fix(regression): preserve subagent session ownership metadata

This commit is contained in:
Tak Hoffman
2026-03-27 20:21:03 -05:00
parent 5eb3ea3028
commit 7fadb4f7ff
5 changed files with 83 additions and 0 deletions

View File

@@ -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,

View File

@@ -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();

View File

@@ -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");

View File

@@ -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,

View File

@@ -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;