From 2b6375faf9f86e38a384d888402fb7c95d262eda Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:22:10 -0500 Subject: [PATCH] fix: keep spawned session owners in live events --- src/gateway/server-methods/sessions.ts | 1 + src/gateway/server.impl.ts | 2 + src/gateway/session-message-events.test.ts | 85 ++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 639f229ab53..f286981876e 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -149,6 +149,7 @@ function emitSessionsChanged( sessionId: sessionRow.sessionId, kind: sessionRow.kind, channel: sessionRow.channel, + spawnedBy: sessionRow.spawnedBy, label: sessionRow.label, displayName: sessionRow.displayName, deliveryContext: sessionRow.deliveryContext, diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 4423729e771..b113e214514 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -945,6 +945,7 @@ export async function startGatewayServer( sessionId: sessionRow.sessionId, kind: sessionRow.kind, channel: sessionRow.channel, + spawnedBy: sessionRow.spawnedBy, label: sessionRow.label, displayName: sessionRow.displayName, deliveryContext: sessionRow.deliveryContext, @@ -1026,6 +1027,7 @@ export async function startGatewayServer( sessionId: sessionRow.sessionId, kind: sessionRow.kind, channel: sessionRow.channel, + spawnedBy: sessionRow.spawnedBy, label: event.label ?? sessionRow.label, displayName: event.displayName ?? sessionRow.displayName, deliveryContext: sessionRow.deliveryContext, diff --git a/src/gateway/session-message-events.test.ts b/src/gateway/session-message-events.test.ts index c15ea328d00..89856254651 100644 --- a/src/gateway/session-message-events.test.ts +++ b/src/gateway/session-message-events.test.ts @@ -300,6 +300,91 @@ describe("session.message websocket events", () => { } }); + test("includes spawnedBy metadata on session.message and sessions.changed transcript events", async () => { + const storePath = await createSessionStoreFile(); + const transcriptPath = path.join(path.dirname(storePath), "sess-child.jsonl"); + await writeSessionStore({ + entries: { + child: { + sessionId: "sess-child", + sessionFile: transcriptPath, + updatedAt: Date.now(), + spawnedBy: "agent:main:main", + parentSessionKey: "agent:main:main", + }, + }, + storePath, + }); + const transcriptMessage = { + role: "assistant", + content: [{ type: "text", text: "spawn metadata snapshot" }], + timestamp: Date.now(), + }; + await fs.writeFile( + transcriptPath, + [ + JSON.stringify({ type: "session", version: 1, id: "sess-child" }), + JSON.stringify({ id: "msg-spawn", message: transcriptMessage }), + ].join("\n"), + "utf-8", + ); + + const harness = await createGatewaySuiteHarness(); + try { + const ws = await harness.openWs(); + try { + await connectOk(ws, { scopes: ["operator.read"] }); + await rpcReq(ws, "sessions.subscribe"); + + const messageEventPromise = onceMessage( + ws, + (message) => + message.type === "event" && + message.event === "session.message" && + (message.payload as { sessionKey?: string } | undefined)?.sessionKey === + "agent:main:child", + ); + const changedEventPromise = onceMessage( + ws, + (message) => + message.type === "event" && + message.event === "sessions.changed" && + (message.payload as { phase?: string; sessionKey?: string } | undefined)?.phase === + "message" && + (message.payload as { sessionKey?: string } | undefined)?.sessionKey === + "agent:main:child", + ); + + emitSessionTranscriptUpdate({ + sessionFile: transcriptPath, + sessionKey: "agent:main:child", + message: transcriptMessage, + messageId: "msg-spawn", + }); + + const [messageEvent, changedEvent] = await Promise.all([ + messageEventPromise, + changedEventPromise, + ]); + expect(messageEvent.payload).toMatchObject({ + sessionKey: "agent:main:child", + spawnedBy: "agent:main:main", + parentSessionKey: "agent:main:main", + }); + expect(changedEvent.payload).toMatchObject({ + sessionKey: "agent:main:child", + phase: "message", + spawnedBy: "agent:main:main", + parentSessionKey: "agent:main:main", + }); + } finally { + ws.close(); + } + } finally { + await harness.close(); + } + }); + test("sessions.messages.subscribe only delivers transcript events for the requested session", async () => { const storePath = await createSessionStoreFile(); await writeSessionStore({