feat(coding-agent): auto-deliver async bash job results to chat - #1622
Conversation
Port oh-my-pi's AsyncJobManager (delivery loop, retry, suppression/
acknowledge, job bounding, process-singleton ownership) into
src/core/async/ so background bash jobs (bash({async:true})) propagate
their final output into the conversation as an async-job-result
follow-up (deliverAs: followUp, triggerTurn when idle) instead of
finishing silently until manually polled.
Polling __atomic_bash_job, cancelling, and parent aborts acknowledge
delivery to suppress duplicate follow-ups; results over 12KB inline a
preview and persist full output to the job's temp file. AgentSession
owns the singleton manager, releases it on dispose without clobbering
other live sessions, and the bash tool async branch is extracted to
bash-async-execution.ts.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
…ails registerBashJob can throw after createManagedBashJob has already inserted the job into the module-level map (disposed manager/session mid-flight, or a capacity race), leaving a never-executed zombie entry permanently reported as "running" to __atomic_bash_job polls — TTL cleanup only evicts settled jobs, so it lingered until max-jobs overflow eviction. Discard the managed entry (including any temp output) when registration fails and rethrow the original error. Addresses the P3 review finding from the async-delivery review panel. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Review: auto-deliver async bash job resultsSolid, well-tested port. The delivery/suppression semantics (poll acknowledges, cancel acknowledges, parent abort acknowledges, re-check at the streaming boundary) are carefully thought through, the extraction of Findings, roughly by severity: 1. The new running-job cap is process-global and shared across sessions
2. Zombie "running" job when
|
|
Addressed the known P3 follow-up in 512b687: |
Review: auto-deliver async bash job resultsOverall this is a solid, carefully-adapted port. The delivery/suppression semantics are well thought through (poll/cancel/parent-abort acknowledgement, streaming-boundary staleness recheck, owner-vs-later-session lifecycle), the zombie-job discard fix is a nice catch, and the 20 new tests cover an impressive range of lifecycle edge cases. Docs and changelog are genuinely descriptive. Findings below, roughly by severity. 1. Delivery retries are unbounded and can duplicate the follow-up message
More importantly, retry conflates "delivery failed" with "turn failed". On the idle path, the handler awaits
2. Two jobs completing while idle can race into concurrent
|
Summary
Background bash jobs (
bash({ async: true })) previously finished silently: the job record was updated in a module-level map, and the result only reached the conversation if the model happened to poll__atomic_bash_job <id>. This ports oh-my-pi'sAsyncJobManager(reference:can1357/oh-my-pisrc/async/job-manager.ts+sdk.tswiring) into Atomic so completed job output is automatically delivered back into the chat.Design
src/core/async/module (job-manager.ts,session-manager.ts,format.ts,types.ts): delivery queue + retry loop,isDeliverySuppressed/acknowledgeDeliveries, max-running-job bounding, and upstream's process-singleton ownership semantics (first top-level session owns the manager; in-process sessions share it; dispose releases without clobbering other live sessions).yieldQueue, completions deliver viasendCustomMessage({ customType: "async-job-result", ... }, { deliverAs: "followUp", triggerTurn: true })— queued at the next boundary while streaming, and starting a new turn when idle.__atomic_bash_job <id>, cancelling, and parent aborts acknowledge the delivery so the same completion is not auto-delivered twice.fullOutputPath).bash.tsinto a newbash-async-execution.ts(startAsyncBashCommand), soBashToolOptionsnow threads an optionalasyncJobManager,asyncJobDeliveryHandler, andasyncJobSessionIdthrough to it; existing agent-facing contract (start message, poll, cancel,details.async, TTL/retention) is preserved.registerBashJobfails after a managed job entry was created (disposed manager/session, or a capacity race mid-flight), the entry is now discarded instead of lingering as a permanently"running"zombie visible to__atomic_bash_jobpolls; the triggering tool-call error still surfaces unchanged.docs/tools.md) andCHANGELOG.mdupdated; 20 new tests acrossasync-job-manager.test.tsandagent-session-async-bash.test.ts.Key changes
src/core/async/job-manager.ts—AsyncJobManager: registration, capacity bounding, completion queue, delivery retry loop, suppression/acknowledgement trackingsrc/core/async/session-manager.ts— process-singleton ownership: which session owns the shared manager, shared-manager lifecycle across forked/subagent sessions, safe disposalsrc/core/async/format.ts,types.ts—async-job-resultmessage formatting (inline preview vs. persisted full output) and shared typessrc/core/tools/bash-async-execution.ts— extractedstartAsyncBashCommand, now job-manager-aware (capacity check, registration, delivery handler wiring, abort acknowledgement); discards the managed job entry when registration failssrc/core/tools/bash.ts— async branch now delegates tostartAsyncBashCommand; polling/cancelling a job acknowledges its delivery to suppress duplicate auto-deliverysrc/core/agent-session*.ts— wiring to construct/share theAsyncJobManagerper session and register the delivery handlerpackages/coding-agent/docs/tools.md,CHANGELOG.md— documented the new auto-delivery behavior and the registration-failure fixValidation
Implemented via a 6-loop ralph run with a 3-model review panel; final panel verdict was a unanimous "patch is correct" with
stop_review_loop: true(confidences 0.93 / 0.86 / 0.90):bun run typecheck,bun run lint,bun run check:file-length— passbun run test:unit— 2823 pass / 0 fail (the 2bash-pty-nativefailures reproduce identically on cleanorigin/main; pre-existing native-PTY environment issue)[async-job-result]follow-up auto-delivered the output and triggered a new turnFollow-up addressed in this PR
A P3 edge case flagged in review — a zombie
"running"job entry could linger until overflow eviction ifregisterBashJobthrew aftercreateManagedBashJobinserted into the module map (e.g. session disposed mid-flight) — is now fixed: the managed job entry is discarded when registration fails.🤖 Generated with Atomic (Claude Fable 5)