feat(host-service): nudge clients with chat:sessions-changed on /events
Summary
The host-service /events bus now sends chat:sessions-changed whenever a workspace's chat-v3 session listing changes. Remote clients such as Roster can refetch listSessions for that workspace when it arrives, instead of polling each host.
Message
{ type: "chat:sessions-changed"; workspaceId: string; occurredAt: number }
This is a nudge, like agent:bindings-changed and page-watch:changed. It carries no session state, so it cannot disagree with listSessions. The client refetches listSessions({ workspaceId }) for the current state.
When it fires
- The journal writes a change to a session row.
- A session is created, or re-opened with a new epoch.
- Its status, title or queued count changes.
- It is discarded (harness start failed).
closeSessionstops a live agent. That changes liveness without a journal write, socloseSessionreports it after disposal, even if disposal throws.
It does not fire on items, deltas, or session events that leave the listing unchanged.
A host restart sends nothing, because no client is connected while the host is down. A client refetches when its /events socket reconnects.
occurredAt on appends is the journal's own ts, so it matches the row's updatedAt.
Wiring
ChatJournaltakesonSessionChanged.createChatRuntimeandcreateChatV3Mountpass it through, andapp.tsmaps it toEventBus.broadcastChatSessionsChanged.- A listener that throws, or returns a promise that rejects, is caught and logged. A durable write never fails because of an observer.
workspace-clientroutes the message byworkspaceIdand exportsChatSessionsChangedPayload({ occurredAt }).- Stored session status is now typed
SessionStatusall the way through. On the column this is$typeonly, so there is no migration.
Compatibility
The change is additive. No tRPC procedure, input or output changed. Released clients drop message types they have no listener for. Hosts without this change never send the message, so clients should keep a listSessions fallback.
Test plan
- Live, against the real chat-v3 routes and
EventBuswith a realclaude-acpagent. The run used an in-process host-service wired likeapp.ts, under Electron's Node, with chat-v3 called over HTTP:createSession→ nudgestarting→idle→ nudge;listSessionsreturnedidleprompt→ nudge onrunning, then nudge onidle; the agent repliedOKcloseSession→ one nudge;getSession.livebecamefalse- An earlier run hit a harness start failure. The status went to
deadand that also sent a nudge.
-
packages/chat-runtime:bun test --isolate: 193 pass. New tests cover listing-only changes, a throwing listener, andcloseSessionreporting once. -
packages/host-service:bun test --isolate src/events/event-bus.test.ts src/chat-v3: 26 pass. -
packages/workspace-client:bun test src/lib/eventBus.test.ts: 12 pass. - Typecheck:
chat-runtimeandchatare clean.host-serviceandworkspace-clientshow only the existing 22auth/trpcerrors. -
bunx biome check: clean.
Review follow-ups
- SDK removed from this PR, as asked. Roster calls the HTTP endpoints directly. Host chat calls through the relay work since #8159.
open()usesopenEpoch'smintedinstead of reading the row three times.occurredAtcomes from the journal'sts.
Summary by cubic
Emits chat:sessions-changed on the host-service /events bus so remote clients like Roster stop polling listSessions per host. The SDK changes are not part of this PR.
The event is a workspace-wide nudge carrying only { workspaceId, occurredAt }; clients refetch listSessions for that workspace, so the event can never disagree with the listing.
ChatJournalfires on session creation, re-open with a changed status, status/title/queued-count changes, and discard.closeSessionannounces once a live agent stops, since closing changes liveness without a journal write.- A throwing
onSessionChangedlistener is caught and logged so it can't fail a committed append; stored status is typedSessionStatusend to end. workspace-clientroutes the event to listeners scoped by workspace or"*"; released clients ignore it and thelistSessionsfallback stays.setModelis left for a follow-up since the claude-code and codex adapters have no mid-session model-change path.
Summary by CodeRabbit
- New Features
- Chat session changes now trigger workspace-scoped notifications, so connected clients can refresh session lists when sessions are opened, updated, closed, or discarded.
- Bug Fixes
- Closing a live session now notifies clients after shutdown, including when shutdown encounters an error. Repeated close requests do not send duplicate notifications.