feat(kap-server): broadcast agent.created/agent.disposed session events - #1997
Conversation
- emit durable agent.created / agent.disposed facts from the SessionEventBroadcaster lifecycle callbacks, ahead of the agent's own events, and let them bypass per-subscription agent allowlists - add the agent.created / agent.disposed wire types and zod schemas - stamp disposedAt on the transcript roster entry via TranscriptStore.markDisposed so REST consumers can tell a dead agent from a live one
🦋 Changeset detectedLatest commit: 1c33b65 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: 1c33b6541f
ℹ️ 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".
| agentDisposables.delete(agentId); | ||
| subscribedAgents.delete(agentId); | ||
| projectors.delete(agentId); | ||
| store.markDisposed(agentId, new Date().toISOString()); |
There was a problem hiding this comment.
Persist disposedAt before dropping the live store
When a session is closed or the daemon restarts, TranscriptService.dropSession discards this live TranscriptStore and the REST transcript route falls back to readColdRoster from state.json; this line only stamps the in-memory store and no metadata update is written. In that cold-session path, agents that were disposed during the live run lose disposedAt, so transcript API consumers viewing history cannot distinguish them from non-disposed roster entries.
Useful? React with 👍 / 👎.
| export const agentCreatedEventSchema = z.object({ | ||
| type: z.literal('agent.created'), | ||
| }); | ||
|
|
||
| export const agentDisposedEventSchema = z.object({ | ||
| type: z.literal('agent.disposed'), | ||
| }); |
There was a problem hiding this comment.
Keep the shared protocol catalog in sync
Adding these events only to kap-server's private protocol catalog leaves @moonshot-ai/protocol stale; I verified rg "agent.created|agent.disposed" packages/protocol returns no matches, so packages/protocol/src/events.ts still omits these types from AgentEvent and sessionEventMessageSchema. Any SDK/client/test code validating or typing WS frames through the shared package will reject or fail to model events the server now emits.
Useful? React with 👍 / 👎.
Related Issue
No linked issue — the problem is explained below.
Problem
WebSocket clients of a session (web UI, inspectors, SDK consumers) have no way to learn an agent's lifecycle: they never hear when an agent is created or disposed, so late-joining or filtered subscribers cannot reconcile their agent roster with the server's. Likewise, transcript REST consumers cannot tell a disposed agent apart from a live one — a disposed agent keeps its history in the roster with no marker.
What changed
1. Session-grained agent lifecycle events
Problem: Agent creation/disposal was invisible on the wire; only per-agent stream events existed.
What was done:
agent.created/agent.disposedevents from the agent lifecycle callbacks.agent.createdis enqueued ahead of the agent's own events, and disposal is only reported for agents the session actually attached (creation-failure ghosts stay silent).2. Disposal marker in the transcript roster
Problem: After disposal, an agent's transcript correctly stays readable, but roster consumers had no way to know the agent is gone.
What was done:
disposedAton the agent's roster descriptor at disposal (idempotent, first stamp wins; unknown agents gain no entry).disposedAtfield so REST and fresh-reset consumers can render dead agents accordingly.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.