fix(agent): complete saved results after supervisor gap adjudication - #31
Conversation
There was a problem hiding this comment.
Zero Review - 1 finding shown inline.
8,088,836 tokens used (8,030,172 prompt / 58,664 completion; 6,560,384 served from cache, billed ~10%).
Models - prime-root: zero/swe-root (1 agent, 1 task, 40 calls, 2,280,768 tokens), prime-child: zero/balanced (5 agents, 3 tasks, 106 calls, 5,808,068 tokens)
| const signal = session.taskGraph.getAttemptSignal(session.taskId, session.taskActorId); | ||
| const retire = () => { | ||
| session.requestAbort(); | ||
| void this.dispose().catch((error: unknown) => { |
There was a problem hiding this comment.
🟡 Retirement can dispose the replacement session instead of the superseded one
The task-attempt abort callback captures the superseded session for requestAbort(), but then calls this.dispose(). disposeOnce() awaits shutdown work before evaluating this.session.disposeAsync(). During that await, a concurrent switch/new/fork/resume flow can apply a replacement to this._session, so retirement disposes the replacement session instead of the captured superseded session. This can tear down the active replacement while leaving the old session undisposed. Bind disposal to the captured session or serialize retirement with replacement.
Evidence
- The new callback captures the old session only for
requestAbort()and then invokes runtime-leveldispose(). disposeOnce()awaits shutdown steps before dereferencing mutablethis.sessionfor disposal.- Replacement flows can assign a new session during that await and no runtime-level serialization prevents this interleaving.
Zero ReviewSummary
Findings🟡 Preserve blocked state for a root with a saved completion during recovery (packages/coding-agent/src/core/task-graph.ts:1684) Evidence
|
There was a problem hiding this comment.
Zero Review - 1 finding shown inline.
5,037,713 tokens used (4,991,526 prompt / 46,187 completion; 3,668,608 served from cache, billed ~10%).
Models - prime-root: zero/swe-root (1 agent, 1 task, 29 calls, 1,132,706 tokens), prime-child: zero/balanced (3 agents, 2 tasks, 85 calls, 3,905,007 tokens)
| const session = this._session; | ||
| if (!session.taskGraph || !session.taskId || !session.taskActorId || this._metadata.kind !== "subagent") return; | ||
| const task = session.taskGraph.getTask(session.taskId); | ||
| if (["completed", "cancelled", "interrupted"].includes(task.status)) return; |
There was a problem hiding this comment.
🟡 Fence subagent startup against attempt retirement
The new attempt-abort callback starts disposal, but subagent runtime construction is asynchronous. If reassignment happens while createRlmSubagentRuntime() awaits construction, disposal can snapshot and clear the current child map before the late runtime is inserted and published. That hosted session and lease then escape disposal and may continue after ownership changed. The bind path also only installs an abort listener, so an attempt already aborted before binding is missed because AbortSignal does not replay the event. Check signal.aborted, fence publication against disposing, and dispose/release late construction results or track pending creations during disposal.
Evidence
- The changed listener starts abort and disposal on reassignment, while child construction is awaited before the runtime is registered.
- Disposal snapshots registered children; there is no post-await disposal fence or immediate already-aborted check before publication.
There was a problem hiding this comment.
Zero Review - 1 finding shown inline.
5,307,709 tokens used (5,255,766 prompt / 51,943 completion; 4,323,200 served from cache, billed ~10%).
Models - prime-root: zero/swe-root (1 agent, 1 task, 40 calls, 2,323,608 tokens), prime-child: zero/balanced (2 agents, 2 tasks, 71 calls, 2,984,101 tokens)
| if (!session.taskGraph || !session.taskId || !session.taskActorId || this._metadata.kind !== "subagent") return; | ||
| const task = session.taskGraph.getTask(session.taskId); | ||
| if (["completed", "cancelled", "interrupted"].includes(task.status)) return; | ||
| const signal = session.taskGraph.getAttemptSignal(session.taskId, session.taskActorId); |
There was a problem hiding this comment.
🟡 Fence attempts that retired before runtime binding
bindRuntimeHost() registers an abort listener but does not handle an attempt signal that is already aborted. If reassignment happens while createAgentSessionRuntime() is still awaiting, the abort event fires before this listener is added. The newly constructed runtime can then be inserted and published for the superseded task actor because the publication path does not recheck ownership. Check signal.aborted or current ownership immediately after construction and again before publication; dispose the stale runtime and reject the bind.
Evidence
- AbortSignal listeners added after
abort()do not receive the earlier abort event. - The post-construction publication path inserts the runtime and invokes
onSessionPublishedwithout a current-owner check.
Related locations
packages/coding-agent/src/core/agent-session-runtime.ts:400
|
Merged as 3363dc9 for the v0.9.4 release. Addressed retirement cleanup and fresh-child startup races, including terminal tasks during construction/binding. The reported missed-listener reassignment path is fenced by synchronous current-owner validation; fresh publication also checks active task status. Completed-session hydration remains supported. Full CI and |
Problem
No-Ticket: Production Zero regression investigated from nebula-mono PR PrimeIntellect-ai#170 traces; no separate Research ticket was created.
The archived Zero review of nebula-mono PR PrimeIntellect-ai#170 shows an otherwise finished child repeatedly failing completion after the parent accepted a missing Docker E2E check as nonblocking. The parent replaced that owner, and the superseded session continued attempting task actions.
Changes
adjudicatedTaskCompletionpolicy capability. Save a host-valid proposed result against its current attempt while gaps await supervision.No token ceilings or compaction-policy changes. This addresses the observed completion tail, not all of PR PrimeIntellect-ai#170's exploration cost.
Verification
npm run checkpassed, including installer/browser checks and Python inspection checks (one platform skip).Rollout
Includes the lockstep v0.9.4 source/lockfile version bump. Merging triggers the standard stable release workflow. Zero's companion draft must then pin that checksum-verified release before it can merge or deploy. No production changes were made.