diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index a7b26e0ad3e..852a35e2758 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1050,6 +1050,11 @@ export async function startGatewayServer( chatType: sessionRow.chatType, origin: sessionRow.origin, spawnedBy: sessionRow.spawnedBy, + spawnedWorkspaceDir: sessionRow.spawnedWorkspaceDir, + forkedFromParent: sessionRow.forkedFromParent, + spawnDepth: sessionRow.spawnDepth, + subagentRole: sessionRow.subagentRole, + subagentControlScope: sessionRow.subagentControlScope, 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 6fa6352bba1..7bc3d7b3538 100644 --- a/src/gateway/session-message-events.test.ts +++ b/src/gateway/session-message-events.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { appendAssistantMessageToSessionTranscript } from "../config/sessions/transcript.js"; +import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js"; import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js"; import { testState } from "./test-helpers.mocks.js"; import { @@ -80,6 +81,70 @@ async function expectNoMessageWithin(params: { } describe("session.message websocket events", () => { + test("includes spawned session ownership metadata on lifecycle sessions.changed events", async () => { + const previousMinimalGateway = process.env.OPENCLAW_TEST_MINIMAL_GATEWAY; + delete process.env.OPENCLAW_TEST_MINIMAL_GATEWAY; + try { + const storePath = await createSessionStoreFile(); + await writeSessionStore({ + entries: { + child: { + sessionId: "sess-child", + updatedAt: Date.now(), + spawnedBy: "agent:main:parent", + spawnedWorkspaceDir: "/tmp/subagent-workspace", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", + displayName: "Ops Child", + }, + }, + storePath, + }); + + const harness = await createGatewaySuiteHarness(); + try { + await withOperatorSessionSubscriber(harness, async (ws) => { + const changedEvent = onceMessage( + ws, + (message) => + message.type === "event" && + message.event === "sessions.changed" && + (message.payload as { sessionKey?: string } | undefined)?.sessionKey === + "agent:main:child", + ); + + emitSessionLifecycleEvent({ + sessionKey: "agent:main:child", + reason: "reactivated", + }); + + const event = await changedEvent; + expect(event.payload).toMatchObject({ + sessionKey: "agent:main:child", + reason: "reactivated", + spawnedBy: "agent:main:parent", + spawnedWorkspaceDir: "/tmp/subagent-workspace", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", + displayName: "Ops Child", + }); + }); + } finally { + await harness.close(); + } + } finally { + if (previousMinimalGateway === undefined) { + delete process.env.OPENCLAW_TEST_MINIMAL_GATEWAY; + } else { + process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = previousMinimalGateway; + } + } + }); + test("only sends transcript events to subscribed operator clients", async () => { const storePath = await createSessionStoreFile(); await writeSessionStore({