fix(coding-agent): recover from interactive-engine death and stop Escape from killing the engine - #2076
Conversation
Implementation notes (full contents, part 1 of 2)Verbatim contents of the run's implementation-notes artifact Implementation NotesTask: Read the file at /tmp/atomic-repro/engine-death-findings.md first — it contains reproduced evidence, exact file/line references, the user contract, and the required verification for this task. Work in the atomic monorepo (Bun; never node/npm/npx/yarn/pnpm). Fix the interactive-engine interrupt and engine-death handling in packages/coding-agent:
Verification is part of the task, not optional. Add bun:test + node:assert/strict regression tests under test/unit covering: a wedged engine where Escape does not terminate and produces no 'Engine terminated' text; engine-death teardown restoring the editor, focus, and blockingInlineCustomUiDepth 0; child spawn environments free of the ATOMIC_INTERACTIVE_ENGINE_* variables; and submitted text restored on send failure. Then run both live tmux reproductions described in the findings file against the built local CLI and capture tmux output proving typing, Escape, and Ctrl+C all still work and the removed error string never appears. Finish with bun run typecheck, bun run lint, bun run check:file-length, and bun run test:unit all green. Keep every touched file at or under 500 physical lines. Add a packages/coding-agent/CHANGELOG.md entry under [Unreleased] and update the user-facing docs in packages/coding-agent/docs that describe interrupt or engine behavior. Acceptance Matrix
Interface decisions (open points resolved permissively/preservingly):
Running NotesContract amendments receivedNone. No mid-run user steering arrived. Implementation decisions
Reproduced defect found only by live testingThe first clean tmux run of Reproduction A still froze. Tracing the built CLI showed the death teardown ran correctly ( Bun environment caveat (user-relevant)Bun 1.3.14 snapshots the process environment at startup for child spawns that omit Validation
Deferred (out of contract)
Iteration 2 — consolidated findings repairedContract amendments receivedNone. The only mid-run user message inherited through the research artifact was the research instruction itself ("Research whether each unresolved finding still applies and what objective-aligned implementation change would resolve it"), which did not amend the six-item contract. 1. Draft restoration now merges instead of overwriting
Deviation recorded: the draft is exactly what the editor handed to 2. Direct submit branches share the failure handler
Tradeoff recorded: a silent command such as 3. The engine now launches with a clean environmentDeleting the variables cannot satisfy the contract under Bun 1.3.14, so the design changed rather than the scrub. The regression test now uses the omitted-env 4. Ctrl+C rescues a pre-ready hung replacement
5. The env probe publishes atomicallyThe fixture writes 6. Rebased onto origin/main and committed
Iteration 2 validation (all on the rebased tree, after
|
Implementation notes (full contents, part 2 of 2)Continuation: iterations 8-10. Iteration 8 — round-seven findingsContract amendments receivedNone. The research artifact's own "Contract amendments received" section reads Finding 1 — idle Alt+Enter lost the raw draft (fixed)Alt+Enter is an app action, so Fix is the research's one-liner in Checked-in regression: "idle Alt+Enter restores the exact expanded buffer when Finding 3 —
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Too many files changed for review. ( Bypass the limit by tagging |
435f68f to
d40f05b
Compare
… input Escape used to race the engine's cooperative abort against a 250 ms deadline and kill plus restart the engine child whenever it lost, reporting "Engine terminated; … result unknown; inspect side effects before retrying". Escape now only requests the engine's own cancellation and waits for it, and that message no longer exists in any code path. Terminating a wedged engine is an explicit Ctrl+C, armed only when the engine is provably not answering: watchdog-confirmed unresponsive, a cooperative abort unanswered past the same one-second threshold, or a replacement still waiting for readiness past it. That last case has no heartbeat and no watchdog coverage, so it was previously unrecoverable. Ctrl+C reaches the host even while an engine-owned custom UI or overlay owns input, and host-native selectors, dialogs, and forms keep Ctrl+C-as-cancel while the engine is healthy. Engine death is now a host-local lifecycle event. The host closes every remote component from the dead generation, settles its ui.custom() promises, releases widget keys, resets terminal modes, remounts and refocuses the editor, and unwinds the blocking inline custom-UI depth, without waiting for a replacement engine_ready. One automatic replacement attempt follows, with calm status text. The engine child is launched with an environment that never contains the four engine control values; they travel in an owner-only bootstrap file read once and unlinked. Deleting them from process.env afterwards cannot work under Bun, where a child spawned without an explicit env inherits the runtime's launch-time environment. A submission the engine never accepted returns to the editor, ahead of anything typed while the send was pending, on every submit route including /atomic, deferred commands, compaction-time extension commands, streaming steer, bash, /compact, and Alt+Enter. Assistant-model: Claude Opus 5
…ery repeatable Ctrl+C is now always handled by the host while an engine-owned ctx.ui.custom() component or overlay holds input, even with a healthy engine: such a component forwards every key to the child, so one that never resolves trapped the escape key too. Ownership is answered exactly by the remote component controller rather than inferred from overlay presence or inline depth, so native selectors, dialogs, input forms, session pickers, and unrelated native overlays keep Ctrl+C as their own cancel. That path reports "Restarting interactive engine." instead of falsely calling a healthy engine unresponsive. Escape's wait is now genuinely unbounded: abort joined LONG_LIVED_COMMANDS, so a stopped or blocked child no longer turns Escape into a red timeout after 30 seconds while the engine is still working. A replacement that fails on its own now latches Ctrl+C armed, so recovery stays available without Atomic ever retrying on its own. RPC select, confirm, input, and editor dialogs are owned by the generation that opened them: engine death cancels exactly those mounts and suppresses their replies, so a dead generation can no longer leave a dialog on screen or answer through the replacement child. Dialog hides are instance-scoped so stale cleanup cannot dismiss a newer dialog. Remote mounts unwind newest-first, each overlay is hidden through its own handle, and the editor is refocused only when no surviving modal owns input. Draft restoration reads expanded editor text, so a pending large paste survives instead of being reduced to a dead marker. Bootstrap cleanup is ownership-scoped: the child unlinks exactly the file named on the command line, recursive removal requires the handle the host received when it created the directory, and a failed publication removes its own temporary credential file and directory before rethrowing. Assistant-model: Claude Opus 5
…e targeting The newest-first unwind and the exact-overlay-handle close were implemented but only guarded by end-state assertions in the live harness, which passes either way because pi-tui skips unmounted focus targets. The fake host bridge now records the order in which each mount's host close callback runs, so an inline proxy with an overlay stacked above it asserts the overlay closes first. A new suite drives the real showExtensionCustom to assert that an overlay is hidden through its own handle rather than the generic top-overlay call, and that an inline close never takes focus from a surviving overlay. Both fail when the corresponding fix is reverted. Assistant-model: Claude Opus 5
…covery armed Escape and Ctrl+C were classified through the configurable app.clear action, so binding app.clear to Escape sent Escape into the engine stop/restart branch and left Ctrl+C with no host route. Both are now matched by physical key identity through pi-tui's parser, with key-release events filtered, and the editor applies the same fixed Escape guard before its configurable handlers. A focused remote proxy now receives the first Ctrl+C, so extension UIs that bind it keep working; the host takes the next press against the same component. That preserves the workflows prompt-card Skip and stage-chat Close while keeping the escape hatch. Every cooperative-cancellation command is exempt from the generic request deadline, not just abort, so cancelling a running bash no longer produces a red timeout while the engine is still working. A child dying while a replacement is starting latches Ctrl+C instead of being ignored, so the host is never left with no engine and nothing armed. Line widgets are generation-owned, so a dead generation's lines are released while a newer generation's content survives stale cleanup. The editor snapshots its expanded buffer for the dispatch that submits it, so a restored draft is what was typed rather than the trimmed callback argument, and a restored draft no longer also raises a red transport error. Heartbeats moved off the consumptive generic engine-message channel, and the queued writer rejects its in-flight frame so a callback-level EPIPE cannot hang a prompt forever. Assistant-model: Claude Opus 5
…mote UI A remote ctx.ui.custom() component owns every key while it holds input, so a component that never resolves swallowed Ctrl+C. The previous escape needed two presses and then replaced the whole engine, discarding everything else that generation was doing. Ownership is now declared per mount through a new handlesCtrlC option. An unresponsive engine is still terminated on the first press, since a wedged child cannot run a local handler either. A component that declared the option keeps its own Skip, Close, or cancel binding, and is closed if it still owns input on the next press. An undeclared component is closed on the first press through the ordinary close path, so its promise resolves with undefined and the engine keeps running. The bundled workflow surfaces declare it. Engine death is retained rather than transient, so a child that exits between startup returning and the host attaching is still recovered instead of leaving a live TUI bound to a dead engine. Buffered custom-UI frames and extension UI requests are tagged with the generation that produced them and dropped when it dies, so a stale mount frame cannot remount UI that death teardown just closed, or collide with the replacement child's identical component ids. Each submission carries its own raw draft end to end instead of sharing one slot, so two entries that differ only in whitespace can no longer restore each other's text, and submissions still queued behind a failed send come back with it in the order they were entered. Assistant-model: Claude Opus 5
…ror text A submission that the engine never accepted is still the user's text, but the host decided that by matching five message fragments. A dying engine produces write EPIPE, Cannot call write after a stream was destroyed, or write after end, none of which matched, so those submissions were reported as red errors and the typed text was thrown away. Node also documents error.message as free to change in any release. The transport boundary already knows the frame never landed, so it says so. A non-enumerable marker is added to the existing error, leaving its identity, instanceof, code, errno, and syscall untouched, because RpcClient rejections are public. A frozen or non-Error value is wrapped with the original as cause. Marking happens in the queued writer, the child exit, error, and stdin handlers, an explicit stop, a malformed transport, a missing writer, a not-started client, and the request write catch. Request timeouts after a successful write, RPC error responses, provider failures, and anything after agent_start stay unclassified and still surface. Assistant-model: Claude Opus 5
…CP panels Alt+Enter is an app action, so the editor returns before its own pre-trim snapshot runs and the submit handler's only draft is the value the Alt+Enter path hands over. That value was the already-trimmed text, so an idle follow-up the engine never accepted came back without the whitespace the user typed. The raw expanded buffer is passed instead; the handler still trims it for delivery. The /mcp, /mcp setup, and MCP OAuth panels bind Ctrl+C for their own cancel and cleanup, so they now declare handlesCtrlC. Without it the host would close them on the first press and skip their handlers. The changelog gains a Breaking Changes bullet for the migration existing ctx.ui.custom components need: an extension that consumes Ctrl+C keeps that binding only by declaring handlesCtrlC. The runtime default is unchanged, since forwarding an undeclared first press is exactly the trap this work removed. Assistant-model: Claude Opus 5
…e disposal Ownership of a failed submission was inferred from the first byte of output, so a command that changes the working tree and prints nothing looked exactly like one the child never received. Killing the engine during `!touch marker && sleep 400` put that line back in the editor, inviting a second run. The child now announces ownership of every correlated request and flushes the announcement before its handler can touch the shell, an extension, the queue, or compaction. The host restores a draft only for a transport failure that arrived without an announcement, and reports an accepted failure as an ordinary failure. The exit rejection waits for the dead child's stdout to finish parsing so a queued announcement is never missed, bounded so a descendant holding stdout cannot strand the caller. The protocol version becomes 2, because a child that cannot announce must not bind to a host that assumes it can. Disposal now fences engine recovery. A replacement sits between its own stop and its spawn with no child attached, so a disposal-time stop found nothing to do and returned, and the attempt then started an engine after teardown finished. An explicit stop voids the restart permit, a superseded restart fails as cancelled instead of quietly succeeding, and health shutdown joins the attempt before disposal returns. Assistant-model: Claude Opus 5
…s replaced An engine child announces that it owns a request before it starts the work, but that announcement can still be unparsed in the pipe when the child dies. Because automatic recovery starts in the same turn as the death event, its stop detached the reader, retired the generation, and re-failed those requests with `Agent process stopped` — so work that had already run was reported as never sent and offered back to the user for a second run. Death still publishes immediately: the TUI must never wait on a pipe. The generation now gets a bounded settling window on top of it. Its stdout keeps being read, only its ownership frames are honoured, its requests are classified exactly once with the error of whatever ended it, and the replacement starts only afterwards. An explicit stop claims that error before terminating, so a deliberate stop keeps its own wording while an exit keeps its own. Every terminal cause now shares one path: exit, spawn error, stdin error, malformed transport, and explicit stop. The JSONL reader also reports completion for a stream that ends with nothing buffered, which previously left that wait to expire on its timeout. Assistant-model: Claude Opus 5
…the watchdog latch Three review findings from the recovery branch, plus the leak that surfaced while reproducing them. isEngineSendFailure no longer falls back to matching error text. Every rejection RpcClient raises is already built through rpcTransportError, and an accepted request is re-marked per request, so admission is decisive on its own. The legacy marker list could classify any provider, extension, or command error quoting a phrase like "Agent process stopped" as unsent, restoring a draft the engine had already run and hiding the real failure. The tests that leaned on that fallback now inject typed transport errors, which is what production raises, and a new case pins that identical wording resolves differently based only on admission. recover() clears the unresponsive latch when a replacement attempt starts. The watchdog verdict describes the generation being replaced; a fresh child emits no heartbeat before engine_ready, so the latch kept needsExplicitTermination() true through the whole pre-ready window and the first Ctrl+C would stop a replacement that never misbehaved. The fence stays time-bounded: an overdue replacement arms Ctrl+C again on its own account. The abort-lifetime test now resumes and stops its child in a finally block. It SIGSTOPs the engine, and a failed assertion before the resume leaked a frozen child holding this process's stdio pipes - the same leak class that hung the Windows job. One such orphan was still running from an earlier local run. Assistant-model: Claude Opus 5
Rebasing onto main crossed the pi toolchain-parity change (#2079), which moved the root suites from bun test to vitest under Node and adopted pi's biome rule set. This adapts everything the branch added: - Migrate branch-added tests off bun:test/Bun.* to the vitest API and test/helpers/runtime.js (sleep, spawnProcess, spawnSyncCollect, moduleDir, bunExecutable), including test.serial -> test.sequential. - default-main-driver fixture: spawn through the helpers so the fixture host still runs under Bun while the suite runs under Node. - admission-backlog-engine fixture: a Node host hands the child a non-blocking stdout pipe, so the synchronous admission burst raised EAGAIN and killed the child mid-write; writeAllSync retries until the host drains. - bootstrap publication-failure test: scope the observed temp root, because vitest runs test files in parallel and other files create bootstrap directories in the shared tmpdir during the snapshot window. - Align branch-added sources with the biome config on main, and rewrite the two noAssignInExpressions sites (queued-writer pump loop, engine-health attempt id) without behavior change.
d40f05b to
93fe84e
Compare
…n contracts
The branch changed several InteractiveMode contracts without updating the
coding-agent package suite and one integration helper; CI has been red on
exactly these files since before the rebase. Align them with the intended
behavior:
- Queued prompts and input callbacks now carry InteractiveSubmission
({ text, draft }) instead of bare strings, so a failed send can restore the
exact editor buffer. Updated assertions and stub types in the startup-input,
first-run-onboarding, status-autocomplete, and paused-queued-messages suites.
- runUserPromptTurn subscribes to the session to detect turn start; session
stubs in the deferred-startup, startup-latency, loader-continuity, and
resource-gate suites now provide subscribe().
- restoreFailedSubmissionDraft reads mode.pendingUserInputs; the resource-gate
fake mode now seeds it.
- IsolatedInteractiveRuntime and the engine dialog host observe generation
death; client/runtime stubs in rpc-bash-streaming and
startup-resource-ordering now provide onGenerationEnded().
- The showExtensionCustom focus fence consults ui.hasOverlay() before
restoring editor focus; the shared integration overlay host helper now
declares it.
Also fix three load-sensitive tests the full parallel suite exposed while
landing this change, per the repository's fix-it-where-it-lives policy:
- interactive-engine-generation-lifecycle: tolerate ESRCH when the engine
child loses the startup race and is already gone before the explicit
SIGKILL; an early death is still the generation death under test.
- subagents-async-event-journal: temp-root cleanup races the journal drain
these tests deliberately provoke, so recursive removal can hit ENOTEMPTY
when a final async append lands mid-delete; use rm's bounded retry.
- subagents-foreground-intercom-detach: gate the fake children on a release
file written only after the detach commit is processed, instead of fixed
output delays a loaded event loop can outlive; otherwise the commit finds
the attempt closed, nothing detaches, and the detached-exit wait hangs
until the suite timeout.
…S temp symlinks os.tmpdir() on macOS is a /var -> /private/var symlink and jiti resolves transitive imports to realpaths, so the recorded manifest keyed chain files under /private/var while the test compared literal /var fixture paths and reported them missing. Linux and Windows runners have no such symlink, which is why CI stayed green while every macOS checkout failed these two tests.
…s build npm ci --ignore-scripts skips lifecycle scripts by design and the workspace natives package has no install hook, so a fresh clone never compiles packages/natives/native/*.node; the CLI then silently degrades (pty:true falls back to pipes, native grep/find and tree-sitter block ops fall back to JS) and five coding-agent test files fail. Document the explicit 'npm run build --workspace=@bastani/atomic-natives' step, the required Node and Rust toolchains, the node-run dist path the published bin actually uses, and the current CI shape. Verified end to end on a fresh checkout: install, natives build, 'bun packages/coding-agent/src/cli.ts', and build + 'node dist/cli.js'.
Windows has no POSIX permission bits: writeFileSync's mode option maps only to the read-only attribute and stat reports 0o666 for any writable file, so the 0o600 assertion can never hold there. The record's protection on Windows is the per-user temp directory ACL. Keep the secret-free path and directory assertions on every platform.
Unresolved review findings
Reproduced from the final review-round artifact (
review-round-latest.json,convergence_decision.approved: false). Nothing here has been resolved.1. [P2] Satisfy the literal 500-line cap for every touched file — BLOCKING
required_by_objectivepackages/coding-agent/CHANGELOG.md:44This is the finding the implementer restated as unresolved in six consecutive iterations (5 through 10). Their position, recorded in the notes: the same launch contract requires editing
CHANGELOG.mdanddocs/extensions.md, both of which were already over 500 lines before this work started, and released changelog sections are immutable — so "every touched file ≤ 500 lines" and "update these two files" cannot both hold literally. They implemented the narrowest reading (the repository'sbun run check:file-lengthgate, source-like files only, green at 2,554 files) and flagged that the literal wording needs a user decision. No supervisor or user was reachable viaintercomduring the run.2. [P2] Do not restore admitted work based on plain error text — BLOCKING
consistent_with_objectivepackages/coding-agent/src/modes/interactive/interactive-prompt-restore.ts:30-323. [P2] Clear the dead generation's watchdog latch before recovery — BLOCKING
consistent_with_objectivepackages/coding-agent/src/modes/interactive-engine/engine-health.ts:143-145The approving reviewer (reviewer-a) evaluated this claim independently and declined to treat it as blocking, arguing the stale latch leaves
needsExplicitTermination()true only during the restart window and the worst case is one extra user-requested restart. The two reviewers disagree; the finding is recorded as unresolved.4. [P3] Comment claims an inherited legacy engine variable cannot select engine mode, but it still does — non-blocking
consistent_with_objectivepackages/coding-agent/src/utils/interactive-engine-env.ts:9-10Reviewer split
patch is correct(confidence 0.9)patch is incorrect(confidence 0.98)reviewer-b marked three requirements
contradictedin traceability: verbatim draft restoration on send failure, the every-touched-file 500-line cap, and state invariants across the recovering/replacement-ready transitions.Follow-up: the three unresolved findings are addressed (435f68f)
All three findings above are resolved.
bun run test:unit4569 pass / 1 skip / 0 fail,bun run typecheck,bun run lint, andbun run check:file-length(2,557 files) all clean.[P2] 500-line cap on every touched file — resolved as a contract defect, no code change.
The instruction that produced this finding was mine and it was wrong: it demanded a changelog
and docs update while also requiring every touched file to stay under 500 physical lines. The
repo's own gate covers source-like files by design (
.ts,.tsx,.js,.jsx,.mjs,.cjs,.rs), andcheck:file-lengthpasses. Markdown was never in scope. The reviewer wascorrectly enforcing bad wording for six iterations.
[P2] Text-based classification restores admitted work — fixed.
isEngineSendFailurenow decides on admission alone. Every rejectionRpcClientraises isbuilt through
rpcTransportError, and an accepted request is re-marked per request, so thelegacy marker list was dead weight in production and a live hazard: any provider, extension, or
command error quoting a phrase like
Agent process stoppedafter the engine had already run thesubmission would restore the draft and suppress the real error. Tests that relied on the
fallback now inject typed transport errors, which is what production raises. Removing it
surfaced 12 tests in
interactive-submit-send-failure.test.tsthat passed only because of it.A new case pins that identical wording resolves differently based only on admission.
[P2] Stale watchdog latch across generation death — fixed.
recover()clears theunresponsivelatch when a replacement attempt starts. The verdictbelonged to the generation being replaced, and a fresh child emits no heartbeat before
engine_ready, so the latch keptneedsExplicitTermination()true through the entire pre-readywindow and the first Ctrl+C would stop a replacement that never misbehaved. The fence stays
time-bounded: an overdue replacement arms Ctrl+C again on its own account. The regression test
was confirmed to fail without the fix.
Leaked test child — fixed while reproducing the above.
interactive-engine-abort-lifetime.test.tsSIGSTOPs its engine child and had nofinally, so afailed assertion left a frozen child holding the test process's stdio pipes. That is the same
leak class that hung the Windows job in fix(ci): restore green Windows CI and make timed-out suites diagnosable #2066, and one such orphan was found still running
locally. Teardown is now unconditional.
Live verification against this branch
Run from the branch source with the engine-child environment scrubbed:
kill -STOPthe engine mid-bash, then Escape): the engine isnot terminated,
Engine terminated;appears zero times in the full scrollback, typing stillworks, and Ctrl+C then terminates explicitly with the typed draft preserved in the editor.
ctx.ui.customthat never resolves, then
kill -9the engine child): the stale UI is torn down within 5 s, areplacement engine spawns, the editor returns, and typing works. On released 0.9.11-alpha.7 the
same sequence wedges the TUI until
kill -9from another terminal.Contract amendments received
None. No mid-run user steering amended the objective. Every iteration's notes record "Contract amendments received: None"; the two entries that quote inherited text (iterations 3, 5, 6) quote upstream state reports about commit SHAs, not behavior changes. The six-item contract, the Bun-only rule, the 500-line wording, and the verification requirements were never amended.
Two conflicts were resolved by the implementer under the narrowest-reading rule because no user or supervisor was reachable through
intercom:handlesCtrlCoption gets press 1 and is closed on press 2; an undeclared mount closes on press 1.What the change does
Fixes interactive-engine interrupt and engine-death handling in
packages/coding-agent, against the reproduced evidence in/tmp/atomic-repro/engine-death-findings.md.abortAndRecoverinisolated-runtime.tsdropped the 250 ms race againstclient.abort(); Escape now requests the cooperative abort and waits, unbounded.abortand the other cooperative-cancel commands joinedLONG_LIVED_COMMANDSso the generic 30 s RPC deadline does not silently replace the removed one.Engine terminated; <label> result unknown; inspect side effects before retryingdiagnostic is gone, along with thestartsWith("Engine terminated;")branch ininteractive-mode-base.tsand the old transport-test assertion. Ripgrep finds it only in changelog prose and in two tests that assert it as forbidden text.interactive-key-identity.ts, pi-tuimatchesKey), so rebindingapp.clearcannot break either key.RemoteComponentControllersubscribes to a new host-localInteractiveEngineGenerationEndedevent and tears down newest-first: settlesui.custompromises, releases widget keys, resets terminal modes, remounts the editor, restores focus, unwindsblockingInlineCustomUiDepthto 0. Teardown does not wait for a laterengine_ready.EngineHealthController— one automatic replacement, calm status notices rather than red chat errors, a restart permit fencing shutdown, and a bounded terminal-drain phase so a dying generation's admission frames are not lost.ATOMIC_INTERACTIVE_ENGINE_*variables no longer leak into any child. Deleting them fromprocess.envis not sufficient under Bun 1.3.14 (verified: Bun snapshots the environment at startup for spawns that omitenv), so the engine is now spawned with a scrubbed environment and receives host PID, guard path, and API key through a 0600 bootstrap file passed as a private argv flag and unlinked by the child. Subagent foreground, streaming-background, and detached spawns scrub after all merges.Error: Agent process stopped. Ownership is decided by a newengine_request_acceptedprotocol frame (protocol version 1 → 2), not by output heuristics — this is exactly the boundary finding 2 above says still has a text-matching fallback.Breaking change: third-party
ctx.ui.custom()components that consume Ctrl+C must now declarehandlesCtrlC: true; undeclared components close on the first press. All bundled surfaces (MCP panels, workflow graph) were migrated.Verification claimed by the implementer and confirmed by both reviewers
Both reviewers independently ran the gates at HEAD
6ce8125f5:bun run typecheck— exit 0bun run lint— exit 0bun run check:file-length— exit 0 (2,557 tracked source-like files, max 500)bun run test:unit— 4,567 pass, 0 fail, 598 filesbun run --cwd packages/coding-agent docs:check— 38 pagesBoth reviewers also drove the two required live tmux reproductions themselves against the built
dist/cli.js:ctx.ui.custom: the stuck component is torn down, the editor is remounted,Interactive engine stopped unexpectedly; restarting.is shown as a calm status, and a replacement child starts. Typing, Escape, and Ctrl+C all work afterwards.sleep 400bash tool in flight, enginekill -STOP, then Escape: the engine PID is unchanged and in stateTs— not terminated, not restarted. Typing still works. A subsequent Ctrl+C terminates the wedged child, starts a replacement underInteractive engine is not responding; restarting., and leaves the typed draft intact.Engine terminatedreturned zero hits in both reviewers' captures.ENGINE_ENV_CLEAN;ps ewwon a live engine child showed noATOMIC_INTERACTIVE_*variables at all.Known gaps recorded by the implementer
env(git,rg,fd,tmux,ghprobes) still inherit Bun's startup environment snapshot. Harmless for the engine control values now that the engine never receives them; recorded as deferred rather than claimed fixed.RpcTransportErrorclass replacing the marker; bootstrap directory-path unlink hardening;setStatus/setTitlegeneration ownership;{ text, draft }records fromEarlyInputState.process-errorandstdin-errorterminal causes (unit-tested, sharing onefailGenerationpath); the drain barrier is pinned only by unit test; a real workflow stage-chat prompt card was not driven, only the graph overlay that hosts it.QA end-to-end video
Not applicable and not produced. There is no web or frontend UI in this change; the user-visible surface is the terminal TUI. End-to-end proof is tmux pane captures under
/tmp/atomic-repro/from the implementer plus independent captures from both reviewers, all against the built CLI. Both reviewers inspected the nominal QA video path and confirmed the file does not exist. No video is attached and none was invented.Scope
102 files changed, +6,719 / −561, across 9 commits on top of merge-base
8490bbf61. The branch is behindorigin/mainby unrelated CI commits; all figures are against the merge-base.The full implementation notes follow as a comment on this PR.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.