feat(subagents): Codex-aligned in-process runner with Rust control plane - #2205
Conversation
…ust control plane (#2188) Full Codex alignment resolved with the requester: zero OS processes (async runner deleted, survival traded for cold-identity reload), turn-scoped execution guards, LRU residency with transparent cold reload, persistent canonical child identities, 100ms interrupt grace, no kill-capable timers. Control plane implemented in Rust (crates/atomic-natives, NAPI-RS). Atomic keeps per-agent tool allowlists, depth<=5, model fallback, and its request/response tool surface. Clean break: env bridge, watchdog, exit codes, and the file-claim delivery pipeline are deleted. Fixes #2191 via unified background continuation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…detach in process Adds an explicit test-session seam to the in-process runner so the foreground path can be exercised without an OS child, moves pre-admission model-candidate filtering and cwd validation onto the in-process door, runs the structured-output corrective-retry loop inside the runner, and routes intercom detach through continue_in_background instead of the process-era placeholder.
…rogress errors The in-process foreground path stopped writing _input.md, which spec 5.5 keeps as user-facing artifact naming, and never populated AgentProgress.error on a failed attempt. Rewrites the acceptance and structured-output suites against the typed status contract instead of a spawned fake CLI.
Replaces the detached-runner single path with the in-process continuation seam. async-execution-single.ts is deleted per spec section 10 and its behavior moves to runs/inprocess/background-single.ts, which admits the child, continues it in the background, and returns the canonical child path immediately. Refs #2188
…d executor (#2188) async: true is now a don't-wait request over the same in-process foreground executor. runAsyncPath admits the child, settles the call with a typed continued status and canonical child path, then runs the work on the existing runChainPath / runParallelPath un-awaited. Chain substitution, dynamic fanout, worktrees, structured output, skills, progress files, fail-fast, and per-step model candidates stay on the one code path that already implements and tests them, rather than a second serialized runner. This removes the last two OS spawn sites, satisfying spec G1 (zero OS child processes): async-execution-common.ts spawnRunner and the detached runner in subagent-runner-streaming.ts. Deletes per spec section 10 "Modules removed": async-execution-{chain,common,types}.ts and the async-execution.ts barrel, the 12-file subagent-runner*.ts family, async-event-journal.ts, and top-level-async.ts. Their exclusively-process-era tests (subagents-async-config, subagents-async-event-journal) are removed with their subjects; tests covering kept behavior are re-pointed at the in-process path with typed-status assertions.
#2188) With the last OS spawn site gone, these spec section 10 "Modules removed" entries have no importers left: - runs/shared/attempt-watchdog.ts - the idle/wall watchdog; zero importers - runs/shared/final-drain.ts - the stdout drain grace - runs/shared/pi-spawn.ts - CLI-child spawn resolution - runs/shared/spawn-env.ts - the env bridge builder - shared/post-exit-stdio-guard.ts - process-only; sole importer was the watchdog subagents-final-drain.test.ts and subagents-pi-spawn.test.ts are removed with their subjects. interactive-engine-env-scrub.test.ts keeps its three scrubInteractiveEngineEnv tests, which cover kept coding-agent behavior; only the two buildSubagentSpawnEnv cases are removed with spawn-env.ts.
hydrateActiveJobsDeferred captured the extension ctx into a timer; after session replacement or reload the ctx.cwd getter throws, crashing the host (caught by the installed-package Node smoke test). Capture cwd while the ctx is live and never touch ctx from the timer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Too many files changed for review (228 files, 100 file limit). Bypass the limit by tagging |
tmux evidence 1/2 — live runs on the built CLI (in-process runtime)Built Foreground parallel (3 workers) — live rows, per-agent activity, and the ctrl+o hint (#2191's sibling fix): Chain (2 steps) — running step with activity + hint, next step correctly pending: Zero child OS processes during the parallel run (the only CLI process is the host itself): |
tmux evidence 2/2 — async widget, typed results, and the live-progress fixAsync run ( Single child, typed terminal result — session file + artifacts, no exit codes anywhere: Before the live-progress fix (the bug this branch also repairs: bare header, no rows, no hint, while 3 children were actively running): Root cause: the in-process runner emitted |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
The Rust in-process subagent control plane added five NAPI exports (AdmissionRefusalKind, AgentStatus, NapiSubagentControl, SubagentControl, TerminationCause) that the export contract test never learned about, so the coding-agent suite failed on both linux-x64 and windows-x64. Add the five exports to EXPECTED_NATIVE_EXPORTS. The assertion stays an exact whole-surface equality check.
The bundled subagent extension now reaches the Rust control plane in crates/atomic-natives through a static import, so the extension throws at import time when no binding is present. The suites job carried no Rust toolchain, so on a clean Windows checkout 21 unit suites died during collection and 6 more tests failed on the empty tool list that followed. Build the binding before the unit and integration steps, as agent-suite already does. Measured at 42s Linux and 86s Windows, both inside the existing 8/12 minute caps. The topology contract asserted that suites needed no Rust. Replace that stale claim with the ordering assertions the other native-consuming jobs carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…robe The PowerShell one-liner in subagents-zero-process joined the $parent assignment and Get-CimInstance without a ';', producing a parser error (Unexpected token 'Get-CimInstance') and a nonzero probe exit code on the Windows suites job.
Copilot review on #2220: the entries linked only to #2205, the regression source, so release-note readers could not trace the fixes to the PR that implemented them. All seven Unreleased bullets now lead with #2220 and keep #2205 as the named regression source. No released section is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot review on #2213: the entries linked only to #2205, the regression source, so a reader of the release notes could not trace the fix to the PR that implemented it. Both entries now lead with #2213 and keep #2205 as the named regression source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Separate pre-existing defect, not a #2205 regression: chat-transcript.ts is byte-identical between 0.9.12 and 0.9.13-alpha.1. Found while falsifying the initial hypothesis for the Ctrl+O autoscroll bug, which the layer below this one actually fixes. ScrollableComponentViewport stores its offset as a distance from the bottom and compensated it only when content grew: if (this.scrollFromBottom > 0 && this.lastWidth === width && lineCount > this.lastLineCount) { this.scrollFromBottom += lineCount - this.lastLineCount; } A component that loses rows -- a live subagent widget dropping its current-tool row at the end of a tool call -- left scrollFromBottom unchanged, and clampScroll() then pulled the view toward the bottom against a smaller maxScroll. Compensation is now symmetric in both directions, so a user scrolled up stays anchored whether content grows or shrinks, while a user already at the bottom still sticks to it. Scope note: ScrollableComponentViewport is used by ChatSessionHost, not by the normal interactive chat transcript, so this does not by itself change the reported Ctrl+O behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two coupled defects from e4aa7ec (#2205), neither present in 0.9.12. WORKFLOW STAGES COULD NOT DELEGATE. WORKFLOW_STAGE_SUBAGENT_POLICY set `managementActions: "full"` and `fanoutAuthorized: false` in the same object. The two contradict, and the fanout gate discarded the "full" entirely. docs/workflows.md already promised stages the bundled subagent tool with the normal depth guard. Stages now carry fanoutAuthorized: true. THE TS DEPTH GUARD WAS DEAD CODE. checkSubagentDepth() read ATOMIC_SUBAGENT_DEPTH, which no production code ever wrote: #2205 deleted the env bridge and every OS child process, orphaning getSubagentDepthEnv() with zero production callers. So `blocked` was always false and checkDepthForExecution() could never block, at any depth, in any context. Its test stayed green only by writing the depth into process.env itself and asserting the reader read it back -- it simulated the propagation production no longer performed. The guard is now derived from the live admitted policy rather than an environment variable, via getCurrentSubagentDepth(ctx.subagentPolicy) and getInheritedMaxSubagentDepth(). The env machinery is deleted: SUBAGENT_DEPTH_ENV, getSubagentDepthEnv, hasWorkflowStageSubagentGuard, workflowSessionEnv, workflowSessionEnvFromContext, and the env-reading checkSubagentDepth. The Rust admission door in crates/atomic-natives remains the outer bound; the TS guard is now a real inner check rather than decoration. The depth tests no longer write process.env and instead assert against admitted policy depth, including the five-level limit, a stricter configured limit, the ceiling clamp, and inherited maximums in both directions. docs/subagents.md and docs/workflows.md are corrected to describe the mechanism that actually enforces the limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fourth defect from #2205, observed live: a nested child inside a workflow stage came back with `status ok`, `toolCalls 0`, and an envelope of literally `undefined`, and appeared to lack a working subagent tool even though its stage granted the budget. In-process children resolved resources against the parent rather than their own admitted identity, so a nested child could be constructed without the tool set its policy allowed. Children now resolve their own resources and inherit the depth budget through the admitted policy, matching the guard rewritten in the previous commit. Covered by subagents-inprocess-child-resources.test.ts, plus updates to the runner, child-policy gate, guard-propagation, and workflow-stage bundled-resource suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Changelog entries under [Unreleased] for the four #2205 regressions, plus README and skill updates describing the real depth mechanism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot review on #2220: the entries linked only to #2205, the regression source, so release-note readers could not trace the fixes to the PR that implemented them. All seven Unreleased bullets now lead with #2220 and keep #2205 as the named regression source. No released section is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…guard real (#2220) * fix(subagents): gate fanout on delegation, not management actions The fanout authorization check ran before the management branch, so a child with `fanoutAuthorized: false` was refused every `subagent` action with "Subagent fanout is not authorized for this child." — including read-only management (list/get/status/doctor/interrupt/resume), which performs no delegation. That also made the narrower mutating-management gate unreachable dead code. Move the check to guard only the delegation path, and report the actual execution mode rather than a hardcoded "single". Workflow stages additionally shipped `managementActions: "full"` with `fanoutAuthorized: false`, which contradict. Stages are top-level sessions rather than subagent children, so the stage policy now sets `fanoutAuthorized: true`, restoring the delegation the workflow docs already describe. Nesting stays bounded by the unchanged five-level depth guard. Regression coverage in test/unit/subagents-child-policy-gate.test.ts; 7 of its 10 tests fail on the unfixed source. * test(subagents): cover the stage-policy path through parent tool registration Adds the production door a workflow stage actually traverses: the full subagents extension resolving an executor from `ctx.subagentPolicy`. The existing case only covered the fanout-child registration door. * fix(subagents): replace the dead env depth guard and let stages delegate Two coupled defects from e4aa7ec (#2205), neither present in 0.9.12. WORKFLOW STAGES COULD NOT DELEGATE. WORKFLOW_STAGE_SUBAGENT_POLICY set `managementActions: "full"` and `fanoutAuthorized: false` in the same object. The two contradict, and the fanout gate discarded the "full" entirely. docs/workflows.md already promised stages the bundled subagent tool with the normal depth guard. Stages now carry fanoutAuthorized: true. THE TS DEPTH GUARD WAS DEAD CODE. checkSubagentDepth() read ATOMIC_SUBAGENT_DEPTH, which no production code ever wrote: #2205 deleted the env bridge and every OS child process, orphaning getSubagentDepthEnv() with zero production callers. So `blocked` was always false and checkDepthForExecution() could never block, at any depth, in any context. Its test stayed green only by writing the depth into process.env itself and asserting the reader read it back -- it simulated the propagation production no longer performed. The guard is now derived from the live admitted policy rather than an environment variable, via getCurrentSubagentDepth(ctx.subagentPolicy) and getInheritedMaxSubagentDepth(). The env machinery is deleted: SUBAGENT_DEPTH_ENV, getSubagentDepthEnv, hasWorkflowStageSubagentGuard, workflowSessionEnv, workflowSessionEnvFromContext, and the env-reading checkSubagentDepth. The Rust admission door in crates/atomic-natives remains the outer bound; the TS guard is now a real inner check rather than decoration. The depth tests no longer write process.env and instead assert against admitted policy depth, including the five-level limit, a stricter configured limit, the ceiling clamp, and inherited maximums in both directions. docs/subagents.md and docs/workflows.md are corrected to describe the mechanism that actually enforces the limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(subagents): give in-process children their own resource resolution Fourth defect from #2205, observed live: a nested child inside a workflow stage came back with `status ok`, `toolCalls 0`, and an envelope of literally `undefined`, and appeared to lack a working subagent tool even though its stage granted the budget. In-process children resolved resources against the parent rather than their own admitted identity, so a nested child could be constructed without the tool set its policy allowed. Children now resolve their own resources and inherit the depth budget through the admitted policy, matching the guard rewritten in the previous commit. Covered by subagents-inprocess-child-resources.test.ts, plus updates to the runner, child-policy gate, guard-propagation, and workflow-stage bundled-resource suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(subagents): record the child-policy, depth-guard, and nesting fixes Changelog entries under [Unreleased] for the four #2205 regressions, plus README and skill updates describing the real depth mechanism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changelog): attribute the policy fixes to their own PR Copilot review on #2220: the entries linked only to #2205, the regression source, so release-note readers could not trace the fixes to the PR that implemented them. All seven Unreleased bullets now lead with #2220 and keep #2205 as the named regression source. No released section is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Norin Lavaee <nlavaee@umich.edu> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A Ctrl+O-expanded subagent scrolled the chat to the bottom and wiped terminal scrollback throughout a run. Not present in 0.9.12; introduced by e4aa7ec (#2205). Measured mechanism: pi-tui's TUI.doRender() compares the whole rendered line array and, when the earliest changed row is above previousViewportTop, falls back to fullRender(true), which writes \x1b[2J\x1b[H\x1b[3J -- clear screen, home, clear scrollback. #2205's in-process runner published AgentProgress from a catch-all over every child session event, including message_update streaming deltas, and every publish rewrites the elapsed fields the widget renders. The live widget therefore presented pi-tui with a changed above-fold row continuously rather than when its contents changed. Progress now publishes only for events that change what the widget shows, via an exported progressEmissionFor() table: agent_start, tool_execution_start and tool_execution_end forced, message_end throttled, everything else silent. That is the emission profile foreground subagents had before the in-process runner. The 400 ms throttle, the forced-emit milestones, the depth guard, and the typed status contract are unchanged, and no renderer is touched. Paired live capture against the built CLI under tmux at 20 rows, same task and script, differing only by this change: 73 destructive full redraws (66 of which repainted content the user could not see) drops to 20 (10). Restricted to the subagent's own run, 66 drops to 12, all on genuine content changes. Residual: a genuine above-fold change still costs one pi-tui redraw when rowsBelowWidget + widgetRows > terminalRows. pi-tui 0.83.0 is the newest release and still has this branch; upstream earendil-works/pi#4785 and #7194 track it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot review on #2213: the entries linked only to #2205, the regression source, so a reader of the release notes could not trace the fix to the PR that implemented it. Both entries now lead with #2213 and keep #2205 as the named regression source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es (#2213) * fix(subagents): publish live progress only at widget-visible milestones A Ctrl+O-expanded subagent scrolled the chat to the bottom and wiped terminal scrollback throughout a run. Not present in 0.9.12; introduced by e4aa7ec (#2205). Measured mechanism: pi-tui's TUI.doRender() compares the whole rendered line array and, when the earliest changed row is above previousViewportTop, falls back to fullRender(true), which writes \x1b[2J\x1b[H\x1b[3J -- clear screen, home, clear scrollback. #2205's in-process runner published AgentProgress from a catch-all over every child session event, including message_update streaming deltas, and every publish rewrites the elapsed fields the widget renders. The live widget therefore presented pi-tui with a changed above-fold row continuously rather than when its contents changed. Progress now publishes only for events that change what the widget shows, via an exported progressEmissionFor() table: agent_start, tool_execution_start and tool_execution_end forced, message_end throttled, everything else silent. That is the emission profile foreground subagents had before the in-process runner. The 400 ms throttle, the forced-emit milestones, the depth guard, and the typed status contract are unchanged, and no renderer is touched. Paired live capture against the built CLI under tmux at 20 rows, same task and script, differing only by this change: 73 destructive full redraws (66 of which repainted content the user could not see) drops to 20 (10). Restricted to the subagent's own run, 66 drops to 12, all on genuine content changes. Residual: a genuine above-fold change still costs one pi-tui redraw when rowsBelowWidget + widgetRows > terminalRows. pi-tui 0.83.0 is the newest release and still has this branch; upstream earendil-works/pi#4785 and #7194 track it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changelog): attribute the emission fix to its own PR Copilot review on #2213: the entries linked only to #2205, the regression source, so a reader of the release notes could not trace the fix to the PR that implemented it. Both entries now lead with #2213 and keep #2205 as the named regression source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Norin Lavaee <nlavaee@umich.edu> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Separate pre-existing defect, not a #2205 regression: chat-transcript.ts is byte-identical between 0.9.12 and 0.9.13-alpha.1. Found while falsifying the initial hypothesis for the Ctrl+O autoscroll bug, which the layer below this one actually fixes. ScrollableComponentViewport stores its offset as a distance from the bottom and compensated it only when content grew: if (this.scrollFromBottom > 0 && this.lastWidth === width && lineCount > this.lastLineCount) { this.scrollFromBottom += lineCount - this.lastLineCount; } A component that loses rows -- a live subagent widget dropping its current-tool row at the end of a tool call -- left scrollFromBottom unchanged, and clampScroll() then pulled the view toward the bottom against a smaller maxScroll. Compensation is now symmetric in both directions, so a user scrolled up stays anchored whether content grows or shrinks, while a user already at the bottom still sticks to it. Scope note: ScrollableComponentViewport is used by ChatSessionHost, not by the normal interactive chat transcript, so this does not by itself change the reported Ctrl+O behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The natives globalSetup I added in #2224 turned every Linux `suites` run red, including on main. Each run passes all ~5960 tests on both retry attempts and then dies with `exit code 139` -- SIGSEGV during exit. main was green at ad21551 and red at the very next run, 6546900, which is #2224. The only other commit in that range is #2225, which changes timeout numbers, a contract test and docs, and cannot segfault. The sole new executable code is test/global-setup-natives.ts. Cause: `bindingLoads()` called `createRequire(...)(NATIVE_ENTRY)`, and globalSetup runs in vitest's ORCHESTRATOR -- the process owning the worker pool. That dlopened the NAPI addon into a process that otherwise never touches it; workers load it on demand in their own processes. The addon carries #2205's Rust control plane and its Tokio runtime, so on glibc Linux its destructors run at exit alongside pool teardown and the process dies after the suite has already succeeded. The probe now runs in a child: `node -e "require(<entry>)"`, exit 0 means loadable. That keeps every property the check was chosen for -- a real load attempt rather than a filename scan, so it cannot drift from napi-rs's ~700 lines of platform-arch-libc resolution, and a foreign-platform binding still triggers a rebuild -- while the orchestrator never loads the addon. It costs one short spawn on a path that already spawns a Rust build when the binding is missing. Verified locally on all three paths: warm (2.9s, silent), foreign binding present (reports "present but not loadable here" and rebuilds), and full unit suite 625 files / 5957 tests, exit 0. I could not reproduce the segfault locally because I only ever ran this suite on macOS, which is also why #2224 shipped with it. The proof is this PR's own Linux `suites` job. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Separate pre-existing defect, not a #2205 regression: chat-transcript.ts is byte-identical between 0.9.12 and 0.9.13-alpha.1. Found while falsifying the initial hypothesis for the Ctrl+O autoscroll bug, which the layer below this one actually fixes. ScrollableComponentViewport stores its offset as a distance from the bottom and compensated it only when content grew: if (this.scrollFromBottom > 0 && this.lastWidth === width && lineCount > this.lastLineCount) { this.scrollFromBottom += lineCount - this.lastLineCount; } A component that loses rows -- a live subagent widget dropping its current-tool row at the end of a tool call -- left scrollFromBottom unchanged, and clampScroll() then pulled the view toward the bottom against a smaller maxScroll. Compensation is now symmetric in both directions, so a user scrolled up stays anchored whether content grows or shrinks, while a user already at the bottom still sticks to it. Scope note: ScrollableComponentViewport is used by ChatSessionHost, not by the normal interactive chat transcript, so this does not by itself change the reported Ctrl+O behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#2214) * fix(coding-agent): keep the scroll anchor stable when content shrinks Separate pre-existing defect, not a #2205 regression: chat-transcript.ts is byte-identical between 0.9.12 and 0.9.13-alpha.1. Found while falsifying the initial hypothesis for the Ctrl+O autoscroll bug, which the layer below this one actually fixes. ScrollableComponentViewport stores its offset as a distance from the bottom and compensated it only when content grew: if (this.scrollFromBottom > 0 && this.lastWidth === width && lineCount > this.lastLineCount) { this.scrollFromBottom += lineCount - this.lastLineCount; } A component that loses rows -- a live subagent widget dropping its current-tool row at the end of a tool call -- left scrollFromBottom unchanged, and clampScroll() then pulled the view toward the bottom against a smaller maxScroll. Compensation is now symmetric in both directions, so a user scrolled up stays anchored whether content grows or shrinks, while a user already at the bottom still sticks to it. Scope note: ScrollableComponentViewport is used by ChatSessionHost, not by the normal interactive chat transcript, so this does not by itself change the reported Ctrl+O behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coding-agent): keep the anchor when a component shrinks in its interior greptile on #2214 reproduced a viewport jumping from entry-10..19 to entry-13..22 after three entries above the reader were removed. The finding is correct, and the defect is larger than the report suggests. rowsShiftedAboveAnchor compared one scalar row count per component and broke at the first component whose end passed anchorRow, on the stated assumption that a spanning component's changes happen at or below the anchor. In production that assumption never holds: the chat host renders the ENTIRE transcript as a single component in slot 0 (chat-session-host-rendering.ts:23-25), so every scrolled-up reader's anchor is inside it, the loop broke immediately, and the compensation this PR added returned 0 for all of them. It engaged only when the anchor sat past the transcript, parked on the trailing spacer. A production path deletes from that component's interior: compaction_end -> refreshCompactedTranscript -> replaceMessages, which splices the entry array in place (chat-message-renderer.ts:160-164). Windowed components may now report a row map, `rowSegments(width)`, identifying each run of rows by the entry that produced it. For a spanning component the shift is the distance the anchored segment itself moved, rather than a height delta. Segments are matched by object identity, not by cache key. Keys embed the entry index (chat-session-host-rendering.ts:167-173), so a splice renumbers every survivor; the entry objects move through it unchanged. Known limits, deliberate: - A real compaction_end rebuilds entry objects via chatEntriesFromAgentMessages, so only extraEntries keep identity. A viewer anchored in the rebuilt region falls back to the nearest surviving segment above, then below, and lands close rather than exact. - No cross-component identity; duplicate ids resolve to first occurrence. - One small object per entry per measured frame, unmemoized. - Components without rowSegments keep the previous break semantics. Two tests added, both verified to fail with only the consuming branch reverted to `break;` -- entry-10 -> entry-13 and entry-10 -> entry-16 -- and to pass once restored byte-identically. All six existing anchor tests are unchanged and still pass; the only pre-existing line touched in that file is an import. Also repairs the [Unreleased] changelog structure in this file. My conflict resolutions while rebasing #2213/#2218 concatenated bullets without respecting headings, leaving `### Removed` with no blank line before it and two Fixed bullets underneath it. That malformed section is already on main; all six bullets are preserved and no released section is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coding-agent): let a static transcript report its rows to the anchor greptile raised the same drift a third time after the previous push, this time naming ScrollableChatTranscriptComponent. The thread arrived marked resolved; the code disagreed, and the reviewer was right. That class builds its transcript WITHOUT a cache key (:505), so supportsRowWindow is false, rowSegments returned [] for anything not windowed, and isRowWindowComponent gated the viewport away from asking. The previous commit therefore fixed the ChatSessionHost path -- which does pass a cache key -- and left the exported class exactly as broken. Passing it a cache key would have been the obvious fix and is wrong: chat-message-renderer.test.ts:188-198 pins that a transcript without one reflects entries mutated in place, which is precisely what caching by key would miss. The absent key is the feature. So identifying rows is decoupled from windowing. renderAllRows already walks every entry and knows each block's height, so it records them as it goes -- no extra render, and it cannot go stale against an in-place mutation the way a key can. rowSegments returns those on the static path, guarded by the width they were measured at. The viewport reads segments from static components too, after rendering them, since the recording is a by-product of that render. Two tests added against the exported class: entries removed above the viewer keep the anchored entry, and an entry mutated in place is still reflected -- the second guards the regression the cache-key approach would have caused. Reverting only the static-segment plumbing fails the first with entry-4 -> entry-7 while the other nine pass, so it is specific to this gap. npm run check clean; 626 files / 5968 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Norin Lavaee <nlavaee@umich.edu> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #2188. Fixes #2191.
What this is
Replaces the process-spawn subagent runtime with a Codex-aligned in-process runner: every child is an in-process
AgentSessionsupervised by a Rust control plane incrates/atomic-natives(NAPI-RS), a structural port ofcodex-rs's multi-agent architecture. The design, decision record, and clean-break inventory are committed on this branch:specs/2026-08-04-subagents-inprocess-runner.md, backed by the citedresearch/docs (including a 112-citation codex-rs analysis and a live 3-for-3 idle-kill reproduction that occurred during this spec's own research).Design (spec §4.5, decision-for-decision)
async: truenow means don't-wait on the same runtime; runs no longer survive parent exit (breaking, documented) — cold identities persist as session files and reload on demand.subagent_control:registry.rs,execution.rs,residency.rs,status.rs,control.rs): persistent canonical child identities, RAII spawn reservations, turn-scoped execution guards (4/parent), LRU residency with transparent cold reload, watch-channel statuses, literal 100 ms interrupt grace with flush-before-force.TerminationCausehas no timer variant. Bounds are capacity, depth (≤ 5), and provider timeouts.status: ok|error|skipped|interrupted|continued+SessionStatsreplace the0/1/-1/-2/143exit-code protocol; terminal envelopes are token-bounded on both branches.createAgentSession'sfallbackModels— the exact classification and candidate behavior main chat and workflows converged on in Converge main-chat and workflow model fallback: share the failure classifier, scope the switch to the failing turn, and retry before advancing #2170/fix(fallback): converge main-chat and workflow model fallback #2201.Clean break (spec §10)
~50 modules deleted: spawn machinery (
pi-args,pi-spawn,spawn-env),attempt-watchdog, the detached jiti runner (12 files), the file-claim result pipeline (watcher/claims/dedupe/quarantine/stale-PID reconciler), nested-events file/env routing, theATOMIC_SUBAGENT_*env bridge (~20 vars), with CI grep guards against reappearance.Verification
cargo fmt/clippy -D warnings/cargo test(control-plane unit tests: reservations, guards, residency, watch reduction, no-timer-cause exhaustiveness)npm run check,test:unit,test:integration,test:ci-contracts— all green locally (enforced by pre-push)psproof of zero child processes. Captures posted as comments below.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.