From 7fadb4f7ff4614e734ac0e480aa2ceffba254be2 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:21:03 -0500 Subject: [PATCH] fix(regression): preserve subagent session ownership metadata --- src/gateway/server-methods/sessions.ts | 5 ++ ...sessions.gateway-server-sessions-a.test.ts | 58 +++++++++++++++++++ src/gateway/session-utils.test.ts | 10 ++++ src/gateway/session-utils.ts | 5 ++ src/gateway/session-utils.types.ts | 5 ++ 5 files changed, 83 insertions(+) diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 05ca9994f87..ec784ec482f 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -155,6 +155,11 @@ function emitSessionsChanged( 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: sessionRow.label, displayName: sessionRow.displayName, deliveryContext: sessionRow.deliveryContext, diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 26e0238d01b..0949821dbbf 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -705,6 +705,64 @@ describe("gateway server sessions", () => { ); }); + test("sessions.changed mutation events include subagent ownership metadata", async () => { + await createSessionStoreDir(); + await writeSessionStore({ + entries: { + "subagent:child": { + sessionId: "sess-child", + updatedAt: Date.now(), + spawnedBy: "agent:main:main", + spawnedWorkspaceDir: "/tmp/subagent-workspace", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", + }, + }, + }); + + const broadcastToConnIds = vi.fn(); + const respond = vi.fn(); + const sessionsHandlers = await getSessionsHandlers(); + await sessionsHandlers["sessions.patch"]({ + req: {} as never, + params: { + key: "subagent:child", + label: "Child", + }, + respond, + context: { + broadcastToConnIds, + getSessionEventSubscriberConnIds: () => new Set(["conn-1"]), + loadGatewayModelCatalog: async () => ({ providers: [] }), + } as never, + client: null, + isWebchatConnect: () => false, + }); + + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ ok: true, key: "agent:main:subagent:child" }), + undefined, + ); + expect(broadcastToConnIds).toHaveBeenCalledWith( + "sessions.changed", + expect.objectContaining({ + sessionKey: "agent:main:subagent:child", + reason: "patch", + spawnedBy: "agent:main:main", + spawnedWorkspaceDir: "/tmp/subagent-workspace", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", + }), + new Set(["conn-1"]), + { dropIfSlow: true }, + ); + }); + test("lists and patches session store via sessions.* RPC", async () => { const { dir, storePath } = await createSessionStoreDir(); const now = Date.now(); diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 109d110de67..49e297dd246 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -1341,6 +1341,11 @@ describe("listSessionsFromStore subagent metadata", () => { sessionId: "sess-child", updatedAt: now - 1_000, spawnedBy: "agent:main:subagent:parent", + spawnedWorkspaceDir: "/tmp/child-workspace", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", } as SessionEntry, "agent:main:subagent:failed": { sessionId: "sess-failed", @@ -1416,6 +1421,11 @@ describe("listSessionsFromStore subagent metadata", () => { expect(child?.startedAt).toBe(now - 7_500); expect(child?.endedAt).toBe(now - 2_500); expect(child?.runtimeMs).toBe(5_000); + expect(child?.spawnedWorkspaceDir).toBe("/tmp/child-workspace"); + expect(child?.forkedFromParent).toBe(true); + expect(child?.spawnDepth).toBe(2); + expect(child?.subagentRole).toBe("orchestrator"); + expect(child?.subagentControlScope).toBe("children"); expect(child?.childSessions).toBeUndefined(); const failed = result.sessions.find((session) => session.key === "agent:main:subagent:failed"); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index a589ac9993f..837f19bde3d 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1194,6 +1194,11 @@ export function buildGatewaySessionRow(params: { return { key, spawnedBy: subagentOwner || entry?.spawnedBy, + spawnedWorkspaceDir: entry?.spawnedWorkspaceDir, + forkedFromParent: entry?.forkedFromParent, + spawnDepth: entry?.spawnDepth, + subagentRole: entry?.subagentRole, + subagentControlScope: entry?.subagentControlScope, kind: classifySessionKey(key, entry), label: entry?.label, displayName, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 243ed42a4b4..5c1d255edee 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -18,6 +18,11 @@ export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeo export type GatewaySessionRow = { key: string; spawnedBy?: string; + spawnedWorkspaceDir?: string; + forkedFromParent?: boolean; + spawnDepth?: number; + subagentRole?: SessionEntry["subagentRole"]; + subagentControlScope?: SessionEntry["subagentControlScope"]; kind: "direct" | "group" | "global" | "unknown"; label?: string; displayName?: string;