From 8e05d94ff4c04290fcb89a9ca464546079260216 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:31:33 -0700 Subject: [PATCH 1/6] Fix PRODUCT.md's stall failure mode to describe pause-and-resume The doc described a hard abort at 3 idle turns with a specific error string that does not exist in src/. The real mechanism auto-pauses (not aborts) at a model-family-dependent tool-only-turn threshold and resumes on the next operator message, matching what ARCHITECTURE.md's "Main-session loop protection" section already describes correctly. --- docs/PRODUCT.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index cc622fc78..2d9bfadc8 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -90,11 +90,13 @@ Config-driven `postTurn` and `postRun` hooks (TypeScript or shell) run automatic ## Failure Modes and Recovery -### Stall (idle cycles) +### Stall (tool-only turns with no narration) -**What the user sees:** The agent stops producing tool calls. After 3 idle turns the run aborts with `Agent stalled: no tool calls for 3 turns.` +**What the user sees:** The agent runs several turns in a row that are all tool calls with no explanation of what it's doing. After a one-shot nudge to explain itself, if the pattern continues the session **auto-pauses**: it stops issuing new inferences and replies with a message like "Auto-paused after N consecutive tool-only turns... Send a message to resume." The session is not aborted — sending any message resumes it. -**Recovery:** State is saved; inspect `~/.corbits/projects///run.json` (or a legacy in-repo `.agent-state/` tree if not yet migrated), adjust the task or prompt, and start a new run. +The exact turn thresholds are model-family-dependent (tighter for models with observed runaway tool-only behavior); see "Main-session loop protection" in `docs/ARCHITECTURE.md`. + +**Recovery:** Send a message to resume. To inspect state first, see `~/.corbits/projects///run.json` (or a legacy in-repo `.agent-state/` tree if not yet migrated). ### Permission denied (exec) From 53c67b9c9b80a3b13429acbe705c148906009d54 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:31:53 -0700 Subject: [PATCH 2/6] Point ARCHITECTURE.md's permission section at the queued-gate timer gap The section described the permission system as fully wired. Two of the three disconnects an earlier design review found (project-grant cwd matching, grant-driven queue reconciliation) are already fixed and merged, so this only calls out what remains live: queued-gate timers arm at emit time instead of display time, and ask_operator has no timeout/abort safety net, both in gate-wire.ts. --- docs/ARCHITECTURE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 95548271e..172ff41d6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -274,6 +274,8 @@ tool call Approval scopes offered: Allow Once (persist nothing), Allow Always for a file or its directory (file tools), or a command shape (shell). There is intentionally no "all files" rung. +**Known live gap.** A queued gate's display-dependent timers (goal-mode auto-skip, tool-budget pause ceiling) currently arm when the request is *received*, not when it is actually shown to the operator — a request sitting behind others in the queue can burn its whole timeout invisibly. `ask_operator` also has no timeout/abort safety net at all, unlike the permission gate, so a queued operator question behind a stuck overlay can hang a run. Both live in `src/tui-opentui/gate-wire.ts`'s `onPermission`/`onOperator`. This callout should be removed once that fix ships — do not let it become permanent known-issue debt. + ### TUI (`src/tui-opentui/`) OpenTUI (`@opentui/core`) is the shipping shell; the Ink/React tree has been deleted from the repo. The runner (`src/tui/runner.ts`) mounts the host via `mountRunnerHost` (`src/tui-opentui/runner-host.ts`), which mounts `mountProductHost` (`src/tui-opentui/product-host.ts`) over the shell (`src/tui-opentui/shell.ts`). From 2ff802c8a1cf5a35aea820b95a4afd627e301b96 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:32:18 -0700 Subject: [PATCH 3/6] Stop presenting ARCHITECTURE.md's Events table as the full reactor surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three-row table only ever named the two events the turn-boundary guards care about, but its heading and placement read as an enumeration of the reactor's event vocabulary — and IMPLEMENTATION.md kept a second, differently partial list with no cross-reference. Retitle the table, point both docs at PRODUCTION_REACTOR_TYPES in stream-event-map.ts as the one canonical list, and drop IMPLEMENTATION.md's separate enumeration. --- docs/ARCHITECTURE.md | 11 ++++++++++- docs/IMPLEMENTATION.md | 12 +++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 172ff41d6..c4940b6d5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,7 +14,16 @@ The reactor (from `@intx/agent`) drives a single agent turn-by-turn. Each turn i This repeats until the director emits `capabilities.done()`. -### Events +### Events the turn-boundary guards distinguish + +This table is not the full reactor/stream event vocabulary — it names only +the two events `src/agent/reactor-events.ts` exports guards for. The +complete set of reactor and stream event types the TUI maps is +`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:66-80` +(15 types as of this writing, including `message.received`, +`inference.start`, `inference.text.delta`, `inference.tool_call.start` / +`.delta` / `.end`, `tool.start`, and `connector.reply`) — treat that as +canonical rather than this table or any other doc's partial list. | Event | When it fires | |---|---| diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 0d9ea820b..fa34a7c92 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -331,13 +331,11 @@ session; that tree re-write is inherent to git and left as residual cost. ### Event Stream -`agent.stream()` emits `ReactorEmittedEvent` objects, including: -- `inference.tool_call.start` / `inference.tool_call.end` — tool call lifecycle -- `tool.start` / `tool.done` — tool execution (with `isError`) -- `inference.usage` — token usage (faremeter) -- `connector.reply` — model reply content -- `inference.error` / `reactor.error` — parse/inference and fatal errors -- `reactor.done` — loop completion +`agent.stream()` emits `ReactorEmittedEvent` objects. `docs/ARCHITECTURE.md`'s +"Events" section names the two turn-boundary/shutdown events the directors +guard on; the full set of reactor and stream event types is +`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:66-80` — +treat that as the canonical list rather than maintaining a second one here. Mid-run queue/steer/interrupt state is a pure state machine in `src/tui-opentui/session-queue.ts` (interaction contract §3): `enqueue` (kind `"queue"`) and `enqueueSteer` (kind `"steer"`) share one pending pool, drained steer-first, then queue, both FIFO within their class. The prompt hint (`src/tui-opentui/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`. From b75d4f5accbca40474b0fb1fb0066b1c5c623198 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:32:33 -0700 Subject: [PATCH 4/6] Fix the stale docs/ index and add a docs/plans/ pointer IMPLEMENTATION.md's File Structure section listed six of the nine docs/ files, missing TUI.md, TELEMETRY.md, and PERFTRACE.md. Separately, docs/plans/ holds design-decision write-ups (including the current authoritative source on live permission-system gaps) but wasn't mentioned anywhere in AGENTS.md's Reference table, so an agent following AGENTS.md's own "read /docs first" instruction had no way to discover it. --- AGENTS.md | 1 + docs/IMPLEMENTATION.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 83c519ac9..a60de9045 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,3 +90,4 @@ Interchange is the standard library for this repo, consumed as published `@intx/ - `docs/PLUGINS.md` — plugin manifest system and discovery - `docs/TELEMETRY.md` — what usage telemetry is collected and why - `docs/PERFTRACE.md` — local PerfTrace and opt-in OTEL export settings +- `docs/plans/` — working notes and design decisions, not normative like the docs above; worth checking for known-issue write-ups before starting related work diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index fa34a7c92..b78b60a19 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -137,7 +137,8 @@ src/ command-catalog.ts, model-catalog.ts, chrome-state.ts, palette.ts, provider-setup.ts Onboarding provider setup flow docs/ - PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md, HOOKS.md, MCP.md, PLUGINS.md + PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md, TUI.md, HOOKS.md, MCP.md, + PLUGINS.md, TELEMETRY.md, PERFTRACE.md ``` ### Auto Mode From 0f0be73d54e369613ded9d064827d6bc7d61f223 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:35:50 -0700 Subject: [PATCH 5/6] Fix a self-contradiction and a stale citation in the Events table pointer Greybeard and Critique both caught the same defect: the new prose claimed the table names "only the two events reactor-events.ts guards for" while the table itself still had three rows, including tool.done (which reactor-events.ts doesn't guard). Rewrote the prose to say why tool.done is there instead of asserting a count the table contradicts. Also corrected the PRODUCTION_REACTOR_TYPES line citation, carried over wrong from the audit doc (66-80 -> 62-78 in both ARCHITECTURE.md and IMPLEMENTATION.md). --- docs/ARCHITECTURE.md | 23 +++++++++++++---------- docs/IMPLEMENTATION.md | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c4940b6d5..e29c58793 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,16 +14,19 @@ The reactor (from `@intx/agent`) drives a single agent turn-by-turn. Each turn i This repeats until the director emits `capabilities.done()`. -### Events the turn-boundary guards distinguish - -This table is not the full reactor/stream event vocabulary — it names only -the two events `src/agent/reactor-events.ts` exports guards for. The -complete set of reactor and stream event types the TUI maps is -`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:66-80` -(15 types as of this writing, including `message.received`, -`inference.start`, `inference.text.delta`, `inference.tool_call.start` / -`.delta` / `.end`, `tool.start`, and `connector.reply`) — treat that as -canonical rather than this table or any other doc's partial list. +### Events this section talks about + +This table is not the full reactor/stream event vocabulary. `tool.done` is +here because the sub-sections below reference it; `inference.done` and +`reactor.done` are the two events `src/agent/reactor-events.ts` exports +guards for (`onTurnBoundary` / `onReactorShutdown`) — see the paragraph +after the table. The complete set of reactor and stream event types the TUI +maps is `PRODUCTION_REACTOR_TYPES` in +`src/tui-opentui/stream-event-map.ts:62-78` (15 types as of this writing, +including `message.received`, `inference.start`, `inference.text.delta`, +`inference.tool_call.start` / `.delta` / `.end`, `tool.start`, and +`connector.reply`) — treat that as canonical rather than this table or any +other doc's partial list. | Event | When it fires | |---|---| diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index b78b60a19..a8f6f1c1d 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -335,7 +335,7 @@ session; that tree re-write is inherent to git and left as residual cost. `agent.stream()` emits `ReactorEmittedEvent` objects. `docs/ARCHITECTURE.md`'s "Events" section names the two turn-boundary/shutdown events the directors guard on; the full set of reactor and stream event types is -`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:66-80` — +`PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:62-78` — treat that as the canonical list rather than maintaining a second one here. Mid-run queue/steer/interrupt state is a pure state machine in `src/tui-opentui/session-queue.ts` (interaction contract §3): `enqueue` (kind `"queue"`) and `enqueueSteer` (kind `"steer"`) share one pending pool, drained steer-first, then queue, both FIFO within their class. The prompt hint (`src/tui-opentui/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`. From 0430ee6e87fd9e624c5471721e0c0cda1bce5f77 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 12:06:08 -0700 Subject: [PATCH 6/6] Quote the real auto-pause message and drop the drift-prone event count The pause text in PRODUCT.md and ARCHITECTURE.md was a paraphrase presented as a quotation; it never matched director.ts. The hardcoded "15 types" count would drift the same way the lists this PR replaced did. --- AGENTS.md | 2 +- docs/ARCHITECTURE.md | 22 +++++++++++----------- docs/IMPLEMENTATION.md | 7 ++++--- docs/PRODUCT.md | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a60de9045..99437c689 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,4 +90,4 @@ Interchange is the standard library for this repo, consumed as published `@intx/ - `docs/PLUGINS.md` — plugin manifest system and discovery - `docs/TELEMETRY.md` — what usage telemetry is collected and why - `docs/PERFTRACE.md` — local PerfTrace and opt-in OTEL export settings -- `docs/plans/` — working notes and design decisions, not normative like the docs above; worth checking for known-issue write-ups before starting related work +- `docs/plans/` — non-normative working notes and design spikes; check before starting related work diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e29c58793..be796a730 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,19 +14,19 @@ The reactor (from `@intx/agent`) drives a single agent turn-by-turn. Each turn i This repeats until the director emits `capabilities.done()`. -### Events this section talks about +### Reactor Events (Partial) This table is not the full reactor/stream event vocabulary. `tool.done` is here because the sub-sections below reference it; `inference.done` and -`reactor.done` are the two events `src/agent/reactor-events.ts` exports -guards for (`onTurnBoundary` / `onReactorShutdown`) — see the paragraph -after the table. The complete set of reactor and stream event types the TUI -maps is `PRODUCTION_REACTOR_TYPES` in -`src/tui-opentui/stream-event-map.ts:62-78` (15 types as of this writing, -including `message.received`, `inference.start`, `inference.text.delta`, -`inference.tool_call.start` / `.delta` / `.end`, `tool.start`, and -`connector.reply`) — treat that as canonical rather than this table or any -other doc's partial list. +`reactor.done` are the two events that `src/agent/reactor-events.ts` +exports guards for (`onTurnBoundary` / `onReactorShutdown`) — see the +paragraph after the table. The complete set of reactor and stream event +types the TUI maps is `PRODUCTION_REACTOR_TYPES` in +`src/tui-opentui/stream-event-map.ts:62-78` (covering `message.received`, +`inference.start`, `inference.text.delta`, `inference.tool_call.start` / +`.delta` / `.end`, `tool.start`, and `connector.reply`, among others) — +treat that as canonical rather than this table or any other doc's partial +list. | Event | When it fires | |---|---| @@ -127,7 +127,7 @@ Defaults are permissive (12 / 20 turn-only thresholds, 5-minute stall timeout) s #### Main-session loop protection -The ChatDirector counts consecutive assistant turns that contain tool calls and no text (`toolOnlyStreak`), reset by any turn with text and by every fresh operator message. A dismissed `ask_operator` counts as a no-progress, tool-only turn — the decline path does not reset the streak. At `toolOnlyTurnNudgeAt` the director arms a one-shot ephemeral wrap-up nudge; at `toolOnlyTurnPauseAt` it stops issuing infers entirely and replies with a loud, operator-facing pause message ("Auto-paused after N consecutive tool-only turns... Send a message to resume"), using the same `capabilities.reply()` channel the workflow-stall message already uses to reach the TUI — no new director-to-UI channel was needed. Because a turn with pending `tool_call` blocks must be followed by tool results before anything else (a bare nudge turn on top of pending tool calls is a provider-invalid conversation), both the nudge and the pause are applied by rewriting the `infer` action that follows once those pending tools have resolved — the same one-shot rewrite shape as the sub-agent report-forced wiring below. This loop-protection rewrite runs with the **highest precedence** among the terminal/continuation rewrites in `decideInner`: it is checked before the workflow-idle, open-task, and goal-governor continuation nudges, since those exist to keep a session moving — exactly the behavior the pause guards against. Resuming is just the operator sending a new message, which resets the streak and un-pauses through the same reset path as the other nudge budgets. +The ChatDirector counts consecutive assistant turns that contain tool calls and no text (`toolOnlyStreak`), reset by any turn with text and by every fresh operator message. A dismissed `ask_operator` counts as a no-progress, tool-only turn — the decline path does not reset the streak. At `toolOnlyTurnNudgeAt` the director arms a one-shot ephemeral wrap-up nudge; at `toolOnlyTurnPauseAt` it stops issuing infers entirely and replies with a loud, operator-facing pause message ("Auto-paused: the model ran N steps in a row without explaining its progress. Send a message to resume", `src/agent/director.ts:424-426`), using the same `capabilities.reply()` channel the workflow-stall message already uses to reach the TUI — no new director-to-UI channel was needed. Because a turn with pending `tool_call` blocks must be followed by tool results before anything else (a bare nudge turn on top of pending tool calls is a provider-invalid conversation), both the nudge and the pause are applied by rewriting the `infer` action that follows once those pending tools have resolved — the same one-shot rewrite shape as the sub-agent report-forced wiring below. This loop-protection rewrite runs with the **highest precedence** among the terminal/continuation rewrites in `decideInner`: it is checked before the workflow-idle, open-task, and goal-governor continuation nudges, since those exist to keep a session moving — exactly the behavior the pause guards against. Resuming is just the operator sending a new message, which resets the streak and un-pauses through the same reset path as the other nudge budgets. #### Sub-agent stall management diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index a8f6f1c1d..4bde87330 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -333,10 +333,11 @@ session; that tree re-write is inherent to git and left as residual cost. ### Event Stream `agent.stream()` emits `ReactorEmittedEvent` objects. `docs/ARCHITECTURE.md`'s -"Events" section names the two turn-boundary/shutdown events the directors -guard on; the full set of reactor and stream event types is +"Reactor Events (Partial)" section names the two turn-boundary/shutdown events +the directors guard on; the full set of reactor and stream event types is `PRODUCTION_REACTOR_TYPES` in `src/tui-opentui/stream-event-map.ts:62-78` — -treat that as the canonical list rather than maintaining a second one here. +treat that as canonical rather than this section or any other doc's partial +list. Mid-run queue/steer/interrupt state is a pure state machine in `src/tui-opentui/session-queue.ts` (interaction contract §3): `enqueue` (kind `"queue"`) and `enqueueSteer` (kind `"steer"`) share one pending pool, drained steer-first, then queue, both FIFO within their class. The prompt hint (`src/tui-opentui/stream.ts`, `PROMPT_HINT`) reads `Enter queue · Alt+Enter steer · Ctrl+C stop`. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 2d9bfadc8..cf7906754 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -92,7 +92,7 @@ Config-driven `postTurn` and `postRun` hooks (TypeScript or shell) run automatic ### Stall (tool-only turns with no narration) -**What the user sees:** The agent runs several turns in a row that are all tool calls with no explanation of what it's doing. After a one-shot nudge to explain itself, if the pattern continues the session **auto-pauses**: it stops issuing new inferences and replies with a message like "Auto-paused after N consecutive tool-only turns... Send a message to resume." The session is not aborted — sending any message resumes it. +**What the user sees:** The agent runs several turns in a row that are all tool calls with no explanation of what it's doing. After a one-shot nudge to explain itself, if the pattern continues the session **auto-pauses**: it stops issuing new inferences and replies with "Auto-paused: the model ran N steps in a row without explaining its progress. Send a message to resume." The session is not aborted — sending any message resumes it. The exact turn thresholds are model-family-dependent (tighter for models with observed runaway tool-only behavior); see "Main-session loop protection" in `docs/ARCHITECTURE.md`.