- Only bridge heartbeats (`agent()`/`llm()`) now re-arm the watchdog; compute, stdout, `log()`/`phase()`, and ordinary tool calls count against the budget.
- Emitted an immediate heartbeat at bridge call start to avoid early abort near budget edge.
- Removed `idle` flag and "of inactivity" suffix from timeout annotation strings.
- Updated docs, prompts, and comments to reflect the new wall-clock semantics.
The per-cell `timeout` is an inactivity budget that only re-arms on status
events, but host-side bridge calls can run long stretches with no
intermediate status (a subagent's time-to-first-token on a reasoning
model, a long quiet nested tool, or an entire oneshot llm() request).
The watchdog mistook that for a stall and aborted working subagents
mid-flight.
Pump a lightweight heartbeat while a bridge call awaits, re-arming the
watchdog through the existing emitStatus -> onStatus channel. The
heartbeat is a pure keepalive: forwarded to bump the timer but never
stored or rendered, so a genuinely stalled cell is still interrupted
once the call settles.
- eval/heartbeat.ts: withBridgeHeartbeat() + EVAL_HEARTBEAT_OP
- agent-bridge/llm-bridge: wrap runSubprocess / completeSimple
- js+py executors: forward heartbeat to onStatus, drop from displayOutputs
- tools/eval.ts: bump on heartbeat, skip persist/render
- Tool execution now renders every 16ms and advances spinner glyphs only every 80ms using a tracked last-advance timestamp.
- Output block shimmer ticks were reduced to 16ms so border-frame updates align with the 60fps cadence for smoother animation.
- Added an early `viewportRepaint` path when `nativeViewportAtBottom` is unavailable and `newLines.length <= height`.
- Marked native scrollback dirty before returning that repaint so cleanup is deferred to the next checkpoint.
- Kept the `historyRebuild` fallback for shrinks beyond the padded viewport top to avoid unnecessary repainting.
- Introduced helper `createEmptyWorkspaceTree` to reduce duplication.
- Populated missing `workspaceTree` field in test contexts for `buildSystemPrompt`.
- Added `TokenTaskBudget` type and `taskBudget` option to `StreamOptions`.
- Forwarded `taskBudget` as `output_config.task_budget` with the `task-budgets-2026-03-13` beta header.
- Fixed `disableThinkingIfToolChoiceForced` to preserve `task_budget` when clearing `effort`.
- Accepted `output_config.task_budget` from Anthropic gateway requests.
When omp runs under Windows Terminal on native Windows, ConPTY routes the
console through a pseudo-console whose GetConsoleScreenBufferInfo answer
is always pinned to the buffer tail; it does not reflect the user's
scroll position in the WT pane. The renderer's planRender used that
answer to authorize the shrink-across-viewport historyRebuild intent,
emitting the destructive \x1b[2J\x1b[H\x1b[3J on every full redraw and
yanking a scrolled-up reader to the top of WT's scrollback.
ProcessTerminal.isNativeViewportAtBottom now returns undefined whenever
WT_SESSION is set, matching the POSIX fallback. The renderer's existing
unknown-viewport deferral keeps streaming-time mutations non-destructive
(viewportRepaint / deferredShrink) and reconciles native history at the
next prompt-submit checkpoint via refreshNativeScrollbackIfDirty({
allowUnknownViewport: true }), where the user is provably at the bottom.
The probe gate is factored into a pure shouldTrustNativeViewportProbe
helper so the contract is unit-testable without spying on process state.
Fixes#1635
- Extended ResolveContext / WriteContext with localProtocolOptions so the
internal-URL router can thread the calling session's local-root mapping
through to handlers.
- LocalProtocolHandler.resolveOptions now prefers context.localProtocolOptions
before consulting the process-global override or the first main-kind session
in AgentRegistry, fixing multi-session ACP hosts (cmux) where reads of
local://PLAN.md were routing to a sibling session's artifacts dir even
though plan-mode writes succeeded against the calling session.
- read, find, ast_grep, ast_edit, and search now thread
this.session.localProtocolOptions into the router so local://, memory://,
agent://, and other handlers see the right caller.
- Added regression tests covering the override-vs-context priority and the
ENOENT-against-caller-root path.
Fixes#1608
The Anthropic OAuth login requested an outdated scope set (`org:create_api_key user:profile user:inference`). Recent Claude Code versions request `user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload`, so omp's consent screen omitted the Claude Code session, connector (MCP), and file-upload grants the current client carries.
omp uses the OAuth access token (sk-ant-oat...) directly for inference and never mints an API key, so `org:create_api_key` was unused. Drop it and align with the current Claude Code scope set.
Guarded find renderer path summaries so raw pre-validation string paths render instead of throwing. Added coverage for pending, fallback, empty, and detailed result render paths.\n\nFixes #1622
The catch around the subagent yield-reminder prompt previously logged
every exception at ERROR. User cancel (^C) and compaction-driven aborts
both surface as ToolAbortError through awaitAbortable, so benign control
flow generated 9 spurious 'Subagent prompt failed' errors in 2 days on
the reporter's instance.
Gate the ERROR branch on '!abortSignal.aborted && !(err instanceof
ToolAbortError)' and route the abort path to logger.debug. The outer
catch + finally still mark the run aborted, so observable behaviour is
unchanged.
Fixes#1623
Enable eager native scrollback rebuild mode while assistant text is actively streaming so reflowed Markdown rows do not leave stale duplicated tails in WSL/Windows Terminal scrollback.\n\nFixes #1615
The first cut at the subprocess isolation swallowed every signal exit (`exitCode === null`) on the assumption it was the intentional SIGKILL from `terminate()`. That misclassifies real worker deaths — SIGSEGV from a native crash, SIGKILL from the OOM killer, an operator `kill -9` — so any in-flight title/completion/download promise would await forever while `#worker` still pointed at a dead process.
Added an `intentionalExit` flag flipped by `wrapSubprocess.terminate()` right before its SIGKILL. `onExit` swallows only the flagged exit; every other signal exit now fires the `errors` channel with a "signal SIGFOO" message so `TinyTitleClient.#handleWorkerError` clears `#pending` and dumps the dead worker handle. Added two regression tests pinning both branches.
Reported by chatgpt-codex-connector on #1607.
Moved the tiny title/memory worker from a Bun Worker thread into a child process spawned via Bun.spawn IPC. The agent CLI gains a hidden --tiny-worker dispatch the parent invokes through process.execPath; the parent SIGKILLs the child on dispose so onnxruntime-node's NAPI finalizer never runs in any address space the agent owns. On Windows that finalizer was segfaulting Bun at shutdown after the tiny title model loaded (issue #1606). Drops the now-dead 'close'/'closed' handshake and the unused parentPort bootstrap, and removes tiny/worker.ts from --compile worker entries in both build scripts plus the regression test that pinned them.
Fixes#1606
openai-codex-responses already owns its first-event/idle watchdog through getOpenAIStreamFirstEventTimeoutMs, so the outer lazy wrapper must skip the generic PI_STREAM_FIRST_EVENT_TIMEOUT_MS race. Without this the Codex SSE path still aborts at the lower generic budget for the same PI_OPENAI_STREAM_IDLE_TIMEOUT_MS > PI_STREAM_FIRST_EVENT_TIMEOUT_MS combination that motivated this PR.
Fixes#1603
Always route OpenAI-family providers through getOpenAIStreamFirstEventTimeoutMs so PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS wins even when callers pass per-call streamIdleTimeoutMs. The OpenAI helper now floors the first-event budget at the caller-resolved idle (which already encompasses per-call streamIdleTimeoutMs or PI_OPENAI_STREAM_IDLE_TIMEOUT_MS upstream), and explicit env disables ("0") on either knob continue to drop the watchdog.
Fixes#1603
OpenAI-compatible local servers can spend longer than the generic first-event budget processing large prompts before they emit response headers or SSE frames. The OpenAI-specific idle timeout now also acts as the OpenAI-family first-event floor unless an explicit OpenAI first-event timeout is configured.
Added regression coverage for OpenAI Responses request setup so a lower generic first-event watchdog no longer undercuts PI_OPENAI_STREAM_IDLE_TIMEOUT_MS.
Fixes#1603
- Added `repairDoubleEncodedJsonString` to unescape fields double-encoded by the model (e.g. literal `\n`, `\"`, `\uXXXX` in `context`/`assignment`/`description`).
- Scoped repair to natural-language fields only, leaving code-bearing tools untouched.
- Applied repair on both render and execution paths in `TaskTool`.
- Converted [SECTION]...[/SECTION] markers to "SECTION\n===" format in system prompt templates.
- Updated system conventions doc to reference the new marker style.
- Updated tests to match against the new header pattern.
Included the reviews field in comments-enabled PR view fetches so pr:// output can show formal review submissions and approvals.
Added protocol coverage that emulates gh --json field selection before asserting rendered approval output.
Fixes#1600
Codex review on #1599: the blank-viewport guard must compare the new transcript length against the viewport top used by the padded repaint, not #scrollbackHighWater. Prior unknown-POSIX viewport repaints can commit a long logical frame without advancing the high-water mark, so the stale high-water mark still lets all-blank deferred shrinks through.\n\nCompute paddedViewportTop from #previousLines.length - height and rebuild when the new tail cannot reach it. Add a regression for an offscreen POSIX viewport repaint that grows the committed frame to 120 rows while high-water remains at the original 20-row overflow, then shrinks to 15 rows.
Codex review on #1599: when a bottom-anchored shrink lands on an unknown POSIX viewport and newLines.length <= scrollbackHighWater, the padded deferredShrink draws the viewport entirely past the end of the new transcript — every viewport row renders as blank, hiding the prompt until the next checkpoint.\n\nFall through to historyRebuild for that case. The yank is the lesser evil vs. a blank viewport that the user cannot interact with to trigger a checkpoint. Small shrinks (where some new content still sits above the scrollback boundary) keep the deferred behavior added in 0887d28c7.
A shrink across the viewport boundary on POSIX terminals that cannot report scrollback position (kitty, plain xterm) fell through to viewportRepaint. Repainting bottom-anchored newLines at newLength - height left rows newLength - height .. prevLength - height - 1 already committed to native scrollback, so they reappeared at the viewport top — two duplicated rows at the scrollback/viewport boundary in bjin's trace.\n\nMark scrollback dirty and emit deferredShrink instead (padding to the previous row count) so no native rows are re-emitted; the next checkpoint rebuild (e.g. prompt submit -> refreshNativeScrollbackIfDirty) cleans up.\n\nFixes #1566
Leaving the dead connection in `#connections` made `getConnectionStatus` report `connected` and `waitForConnection` hand a closed transport to callers after the breaker had explicitly suspended the server. Mirror `#doReconnect`'s teardown: detach `onClose`, fire-and-forget `transport.close()`, and drop the entry from `#connections` (plus its in-flight slots in `#pendingConnections`/`#pendingToolLoads`). Tools stay registered in `#tools` so the user can recover with `/mcp reconnect`.
Test asserts `getConnectionStatus("crashy") === "disconnected"` after the burst.
Refs #1592
A stdio MCP server that completes the initialize + tools/list handshake and then exits cleanly will fire `transport.onClose` on every clean exit, and the old `MCPManager.reconnectServer` path spawned again unconditionally. A misconfigured PHP-shebang MCP (e.g. Laravel Boost in a non-Laravel project) hit this loop and forked 66 487 `php84` processes parented directly to the agent's `bun` PID until macOS force-rebooted.
Add a per-server sliding-window circuit breaker: at most 5 reconnect attempts per 30 s window. The transport `onClose` callback and the per-tool-call retry in `tool-bridge` are subject to the breaker; `/mcp reconnect` passes `{ manual: true }` to reset the window so users can recover after fixing the underlying misconfiguration. Stale `onClose` is detached when the breaker trips so a late EOF event cannot re-arm the loop.
Defended by `mcp-reconnect-storm.test.ts`: a Bun stdio fixture answers the handshake and exits, then asserts the spawn count stays at ≤ 10 (was 127 without the fix).
Fixes#1592
Send the LSP exit notification after a successful shutdown response before falling back to process termination. Add a regression test that fails when a server receives shutdown but not exit.\n\nFixes #1593