fix(kap-server): carry the live subagent roster in the session snapshot#1719
Conversation
🦋 Changeset detectedLatest commit: 50da3ea The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ff7fd710f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // and becomes the restore source. A subagent's own `turn.ended` flows | ||
| // through the same session queue and must not drop the roster mid-swarm. | ||
| if (event.agentId === MAIN_AGENT_ID) { | ||
| this.bySession.delete(sessionId); |
There was a problem hiding this comment.
Don't clear the roster before the transcript is durable
When a user refreshes immediately after the main turn.ended is broadcast but before the async wire append has flushed the final AgentSwarm tool result, this deletion makes /snapshot return neither the live roster nor the transcript result. The default SnapshotReader reads wire.jsonl directly in packages/kap-server/src/services/snapshot/snapshotReader.ts:100-103/:302-303, while the append log only queues a flush in packages/agent-core-v2/src/persistence/backends/node-fs/appendLogStore.ts:50-56; since the client then subscribes at the turn.ended seq, the earlier subagent.spawned events are not replayed and the swarm member list can still disappear in that race window.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b280c94f43
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| roster.set(event.subagentId, { | ||
| id: event.subagentId, |
There was a problem hiding this comment.
Exclude background subagents from the snapshot roster
When an Agent call uses run_in_background=true, the core emits subagent.spawned with subagentId set to the child agent id, then registers a separate background task whose /tasks id is generated by AgentTaskService.registerTask. Storing those background spawns in the snapshot roster means a refreshed web client gets one synthetic task keyed by the agent id from snap.subagents and another real task keyed by the task id from REST, so the subagent dock can show duplicate rows and cancellation/detail actions can target the non-existent synthetic task. The roster should be limited to foreground/live-only subagents that REST cannot return, or keyed to the real task id for background runs.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9614dc02fc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| entry.output_preview = event.error; | ||
| return; | ||
| } | ||
| case 'turn.started': { |
There was a problem hiding this comment.
Finalize roster entries when the main turn aborts
When the main turn is cancelled/failed/blocked before spawned foreground subagents finish, this tracker keeps their last running entries until a future main turn.started. In that abort path mirrorAgentRun suppresses subagent.failed for aborts (packages/agent-core-v2/src/session/subagent/mirrorAgentRun.ts:135), and the broadcaster maps those main turn.ended reasons to an aborted session, so a page refresh after pressing Stop seeds the snapshot with still-running subagents even though no later lifecycle event may correct them. Please mark or clear the roster on terminal main turn.ended reasons instead of waiting only for the next turn.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 604977c6e9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // task id) — tracking them here too would duplicate the row (keyed by | ||
| // agent id) and mis-target cancel/detail actions. The roster exists | ||
| // for the foreground/live-only subagents REST cannot serve. | ||
| if (event.runInBackground === true) return; |
There was a problem hiding this comment.
Drop roster entries once foreground subagents detach
This only excludes subagents that were launched with run_in_background=true, but a foreground Agent run can later be detached (Ctrl+B/timeout): AgentTool.execution waits for waitForForegroundRelease, and TaskService.detachEntry re-emits it as a detached task that /tasks serves under the generated task id. Until the next main turn starts, the snapshot still carries the original agent-id roster entry, so refreshing in that detached-but-same-turn window seeds both the roster item and the REST task, duplicating the subagent and mis-targeting detail/cancel actions; remove or update the roster when the matching detached task.started event arrives.
Useful? React with 👍 / 👎.
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dec5278f67
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (event.agentId === MAIN_AGENT_ID) { | ||
| this.bySession.delete(sessionId); |
There was a problem hiding this comment.
Preserve aborted swarm roster past next turn
When the previous main turn ended as cancelled, failed, or blocked, the block above synthesizes failed subagent entries because the member subagent.failed events may never arrive/replay. Deleting the entire session roster on the next main turn.started drops that only snapshot source, so if the user starts another prompt and then refreshes, the snapshot contains no subagents and the interrupted swarm card loses its member rows/states again.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e709de6d3a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // neither the live roster nor the transcript result. A subagent's own | ||
| // turn boundaries must never drop the roster mid-swarm. | ||
| if (event.agentId === MAIN_AGENT_ID) { | ||
| this.bySession.delete(sessionId); |
There was a problem hiding this comment.
Keep the roster until the transcript is durable
When a reconnect/resync happens just after the main agent starts a follow-up turn, this delete can make the snapshot contain neither the live roster nor the previous swarm result. SnapshotReader reads wire.jsonl directly and in parallel with getSnapshotState, while the agent wire append path is asynchronous, so there is no guarantee that the <agent_swarm_result> this comment relies on has reached disk before the roster is cleared. Keep the roster until the disk transcript has observed the result (or make the snapshot reader flush/read through the append log) to avoid losing the member list in that window.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fair point in the absolute sense — turn.started does not establish a happens-before with the async append flush. We accept the trade-off deliberately:
- The result record is queued for the append before
turn.ended, so the residual race is limited to immediately-followed turns (queued/cron prompts) — a ms-scale window, versus the minutes-long member-list loss this tracker exists to fix. - The failure inside that window is transient: once the flush lands, the next refresh restores the member list from the transcript.
Fully closing it needs the snapshot reader to read through (or flush) the agent append log, which spans agent-core-v2 persistence and the kap-server reader — intentionally out of scope for this PR. Also qualified the over-strong 'durable' claim in the tracker comment in 50da3ea to say exactly this.
Related Issue
No issue — this ports the already-merged v1 fix #1589 to the v2 server.
Problem
On the v2 (kap-server) backend, a mid-swarm page refresh loses the swarm card's member list: the session snapshot does not carry the live subagent roster, and earlier
subagent.spawnedevents (the only carriers of the swarm identity metadata) are never replayed to a client that resubscribes at the snapshot watermark. The v1 server got the same fix in #1589.What changed
SubagentRosterTracker(ported from v1, with one adaptation: the roster is dropped only on the MAIN agent'sturn.ended, so a swarm member's own turn end cannot wipe the roster mid-swarm).InFlightTurnTracker) and exposes it viagetSnapshotState; cold (non-live) sessions report an empty roster.subagentsfield (protocol type shipped in fix(web): restore swarm member list after page refresh #1589).Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.