Skip to content

Commit 15b7d88

Browse files
Merge pull request #1363 from corbitsdev/cl-7824-break-orchestrator-to-tui-dependency-before-coding-agent-can-publish
refactor(subagent): stop importing tui
2 parents 7759d2c + cf57a43 commit 15b7d88

31 files changed

Lines changed: 34 additions & 46 deletions

‎docs/ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ tool call
420420

421421
**Approval log** (`src/permission/approval-log.ts`, CL-5666): every consequential decision the gate makes — auto-mode allow/deny or an interactive prompt's allow-once/allow-with-scope/deny/timeout/abort — is appended as one JSONL record to `approvals.jsonl` in the session dir, carrying the classifier/auto-shell rule name that fired (the existing `auto-shell-policy.ts`/`classify.ts` rule names, plus a small closed set of additional fixed literals the log itself defines for decisions those modules don't otherwise name — `auto-allowed-tool`, `non-interactive`, `mega-chain` — never model- or user-authored text), whether the decision was `auto` or `interactive`, a shell chain's segment count, and queued/displayed/settled timestamps. `displayedAt` is set by `PermissionRequest.markDisplayed`, called from `gate-wire.ts`'s `open()` the moment a request actually reaches the overlay host — distinct from when it was raised, so the gap it exposes is the CL-5664 signal (a queued gate arming its timeout before the operator could see it). No command text, file content, path, credential, or other free text is ever recorded — only tool name, rule, mode, segment count, and timing; a sub-agent's free-text dispatch label is deliberately left out, even though it would enable a per-agent breakdown, because nothing constrains what a model puts in it. A hard size cap on the serialized line is defense in depth against a future field reintroducing free text. Writes are fire-and-forget and swallow their own errors; the log defaults to a no-op so nothing depends on it being wired. `scripts/approval-forensics.ts` aggregates across local sessions the same way `intervention-forensics.ts` does for stop/nudge events: per-tool counts by outcome and mode, duration/display-delay percentiles, mega-chain counts, and a duplicate-rate proxy (sessions that hit the same rule more than once).
422422

423-
**Tool wall-clock budget vs. permission prompts.** Each tool `run()` is wrapped by an outer execution watchdog (`src/tui/tool-execution-watchdog.ts`). The watchdog arms when Settings set `tools.timeoutMs` / `tools.maxTimeoutMs`, when foreground `run_shell` has an effective timeout (120s default or per-call, plus 1000ms slack so this layer cannot beat shell-guard), or for `mcp__*` calls. Fleet wait tools are exempt, regardless of Settings: the generic per-tool budget never aborts a sub-agent run while the parent waits for it. That exemption is unconditional, not because the worker is otherwise bounded — there is no turn budget; `deadlineMs` is opt-in, and there is no no-progress or thrash stop. A stuck worker that never trips stall or deadline still runs until the parent turn's stall budget fires, the operator cancels, or, in eval mode, `--agent-timeout-ms` bounds it. `spawn_agent`, `wait_agents`, `ask_director`, and `run_shell` with `background: true` stay exempt from the per-tool watchdog. On TUI, an in-flight `ask_director` is still bounded by the stall watchdog — including after a tool batch resolves and after compact continuation re-entry — so a typical poll does not pin the turn. Concurrent same-name calls still share one `callIdByName` slot (one id per name); the leftover stays named in the per-id `callNameById` map so the stall watchdog still bounds it after the mapping-owning sibling finishes. `wait_agents` is exec-primary only and is not mounted on TUI. By default (`tools.waitForApproval`, Settings → Tools, **On**), an armed budget freezes while the operator is deciding on a permission prompt, so a late approve still runs the tool and the agent waits for the decision instead of timing out under the modal. When **Off**, the budget keeps ticking during the prompt; if it expires first the tool is skipped and the permission modal is dismissed via the budget AbortSignal (auto-deny with a timeout message). The TUI permission queue (`src/tui/gate-wire.ts`, backed by `src/permission/queue.ts`) attaches that signal so ghost prompts cannot outlive an already-aborted tool.
423+
**Tool wall-clock budget vs. permission prompts.** Each tool `run()` is wrapped by an outer execution watchdog (`src/agent/tool-execution-watchdog.ts`). The watchdog arms when Settings set `tools.timeoutMs` / `tools.maxTimeoutMs`, when foreground `run_shell` has an effective timeout (120s default or per-call, plus 1000ms slack so this layer cannot beat shell-guard), or for `mcp__*` calls. Fleet wait tools are exempt, regardless of Settings: the generic per-tool budget never aborts a sub-agent run while the parent waits for it. That exemption is unconditional, not because the worker is otherwise bounded — there is no turn budget; `deadlineMs` is opt-in, and there is no no-progress or thrash stop. A stuck worker that never trips stall or deadline still runs until the parent turn's stall budget fires, the operator cancels, or, in eval mode, `--agent-timeout-ms` bounds it. `spawn_agent`, `wait_agents`, `ask_director`, and `run_shell` with `background: true` stay exempt from the per-tool watchdog. On TUI, an in-flight `ask_director` is still bounded by the stall watchdog — including after a tool batch resolves and after compact continuation re-entry — so a typical poll does not pin the turn. Concurrent same-name calls still share one `callIdByName` slot (one id per name); the leftover stays named in the per-id `callNameById` map so the stall watchdog still bounds it after the mapping-owning sibling finishes. `wait_agents` is exec-primary only and is not mounted on TUI. By default (`tools.waitForApproval`, Settings → Tools, **On**), an armed budget freezes while the operator is deciding on a permission prompt, so a late approve still runs the tool and the agent waits for the decision instead of timing out under the modal. When **Off**, the budget keeps ticking during the prompt; if it expires first the tool is skipped and the permission modal is dismissed via the budget AbortSignal (auto-deny with a timeout message). The TUI permission queue (`src/tui/gate-wire.ts`, backed by `src/permission/queue.ts`) attaches that signal so ghost prompts cannot outlive an already-aborted tool.
424424

425425
`mcp__*` tool calls are the exception to "arms only when Settings set it": they arm unconditionally with a 5-minute default (`DEFAULT_MCP_TOOL_TIMEOUT_MS`), overridable via `mcp.timeoutMs` and still capped by `tools.maxTimeoutMs` (CL-6895). Nothing else bounds an MCP call — the stall watchdog treats an in-flight tool as activity by design, so a wedged MCP server previously hung a tool call, and the turn, forever. On expiry the call returns a normal tool-error result ("MCP tool `<name>` timed out after `<n>`s — the server may be wedged; retry or continue without it"); the turn is never aborted. The MCP client itself (`src/mcp/client.ts`, wrapping `@modelcontextprotocol/sdk`) multiplexes concurrent requests over one connection by JSON-RPC message id with no serial queue or mutex in our code or in the vendored SDK's `Protocol.request()` — so concurrent calls to the same server are not expected to deadlock each other. Live forensics for CL-6895 showed multi-minute MCP calls that eventually completed successfully, consistent with a slow server response rather than a client-side deadlock.
426426

‎scripts/ci-timings.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"scripts/check-dead-exports.test.ts": 1769,
55
"scripts/test-parallel.test.ts": 2400,
66
"src/plugins/shell-guard-plugin.test.ts": 1883,
7-
"src/tui/tool-execution-watchdog.test.ts": 1853,
7+
"src/agent/tool-execution-watchdog.test.ts": 1853,
88
"src/auth/codex/refresh-lock.test.ts": 930,
99
"src/tui/provider-setup.test.ts": 1562,
1010
"src/tui/url-click.test.ts": 1442,
@@ -341,7 +341,7 @@
341341
"src/subagent/run-audit-store.test.ts": 2,
342342
"src/telemetry/ai-observability.test.ts": 2,
343343
"src/tui/chrome-state.test.ts": 2,
344-
"src/tui/dynamic-tool-runner.test.ts": 2,
344+
"src/agent/dynamic-tool-runner.test.ts": 2,
345345
"src/tui/runner/reconnect-recovery.test.ts": 2,
346346
"src/tui/runtime-notices.test.ts": 2,
347347
"src/tui/session-start.test.ts": 2,
@@ -457,7 +457,7 @@
457457
"src/telemetry/first-run.test.ts": 1,
458458
"src/tools/html-convert.test.ts": 1,
459459
"src/tools/ssrf-guard.test.ts": 1,
460-
"src/tui/agent-progress.test.ts": 1,
460+
"src/subagent/agent-progress.test.ts": 1,
461461
"src/tui/command-catalog.test.ts": 1,
462462
"src/tui/command-display.test.ts": 1,
463463
"src/tui/command-registry-setup.test.ts": 1,
@@ -551,7 +551,7 @@
551551
"src/tui/provider-failure-attempt.test.ts": 0,
552552
"src/tui/quota-retry.test.ts": 0,
553553
"src/tui/ramp.test.ts": 0,
554-
"src/tui/resolve-registered-tool-name.test.ts": 0,
554+
"src/agent/resolve-registered-tool-name.test.ts": 0,
555555
"src/tui/resume-seed.test.ts": 0,
556556
"src/tui/runner-exit-code.test.ts": 0,
557557
"src/tui/runner/send-failure-message.test.ts": 0,

‎src/agent/canonical-tool-name.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe("tool-name comparison regression guard", () => {
6262
"src/tui/mcp-view.ts",
6363
"src/tui/runtime-bridge.ts",
6464
"src/tui/stall-watchdog.ts",
65-
"src/tui/tool-execution-watchdog.ts",
65+
"src/agent/tool-execution-watchdog.ts",
6666
"src/tui/turns-to-blocks.ts",
6767
];
6868

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, test, expect } from "bun:test";
22
import type { AgentTool } from "@intx/agent";
33
import { createDynamicToolRunner } from "./dynamic-tool-runner.js";
4-
import { advertisedTools } from "../agent/tool-search.js";
4+
import { advertisedTools } from "./tool-search.js";
55

66
const stringTool = (name: string, reply: string): AgentTool => ({
77
kind: "string",
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
} from "./tool-execution-watchdog.js";
1313
import { resolveRegisteredToolName } from "./resolve-registered-tool-name.js";
1414
import { stripTerminalControlSequences } from "../util/control-char-strip.js";
15-
import { prepareDispatchedToolCall } from "../agent/tool-aliases.js";
16-
import { UNADVERTISED_MOUNTED_BUILTINS } from "../agent/tool-search.js";
15+
import { prepareDispatchedToolCall } from "./tool-aliases.js";
16+
import { UNADVERTISED_MOUNTED_BUILTINS } from "./tool-search.js";
1717

1818
// A tool runner whose set of tools can grow after construction. The static
1919
// createToolRunner freezes its name map at build time, which cannot accommodate

‎src/agent/live-tool-dispatch.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test";
22

3-
import { createDynamicToolRunner } from "../tui/dynamic-tool-runner.js";
3+
import { createDynamicToolRunner } from "./dynamic-tool-runner.js";
44
import {
55
fallbackLiveToolBundle,
66
isLiveToolBundle,

src/tui/resolve-registered-tool-name.test.ts renamed to src/agent/resolve-registered-tool-name.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test";
2-
import { canonicalToolName } from "../agent/canonical-tool-name.js";
2+
import { canonicalToolName } from "./canonical-tool-name.js";
33
import { resolveRegisteredToolName } from "./resolve-registered-tool-name.js";
44

55
const catalog = "mcp__linear__get_release";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { canonicalToolName } from "../agent/canonical-tool-name.js";
1+
import { canonicalToolName } from "./canonical-tool-name.js";
22

33
// Muse Spark emits `default.mcp__*` and duplicated `name.name`; catalog keys
44
// are the unprefixed names. Lookup-only — do not register aliases on the wire.

‎src/agent/tool-aliases.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test";
22
import type { ToolCall, ToolDefinition } from "@intx/types/runtime";
3-
import { createDynamicToolRunner } from "../tui/dynamic-tool-runner.js";
3+
import { createDynamicToolRunner } from "./dynamic-tool-runner.js";
44
import { advertisedTools } from "./tool-search.js";
55
import { canonicalToolName } from "./canonical-tool-name.js";
66
import {

0 commit comments

Comments
 (0)