feat(exec): track spawnedBySession for hierarchical process visualization
- Add sessionKey field to FinishedSession interface - Preserve sessionKey when moving process sessions to finished state - Expose spawnedBySession in process list action for both running and finished sessions - Add comprehensive tests for spawnedBySession tracking This enables hierarchical visualization of sessions containing their child processes, complementing the existing spawnedBy tracking for sub-agent sessions.
This commit is contained in:
parent
b3de488f32
commit
dc10dd2e06
@ -55,6 +55,7 @@ export interface FinishedSession {
|
|||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
scopeKey?: string;
|
scopeKey?: string;
|
||||||
|
sessionKey?: string;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
endedAt: number;
|
endedAt: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@ -162,6 +163,7 @@ function moveToFinished(session: ProcessSession, status: ProcessStatus) {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
command: session.command,
|
command: session.command,
|
||||||
scopeKey: session.scopeKey,
|
scopeKey: session.scopeKey,
|
||||||
|
sessionKey: session.sessionKey,
|
||||||
startedAt: session.startedAt,
|
startedAt: session.startedAt,
|
||||||
endedAt: Date.now(),
|
endedAt: Date.now(),
|
||||||
cwd: session.cwd,
|
cwd: session.cwd,
|
||||||
|
|||||||
201
src/agents/bash-tools.process.spawned-by-session.test.ts
Normal file
201
src/agents/bash-tools.process.spawned-by-session.test.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import type { ProcessSession } from "./bash-process-registry.js";
|
||||||
|
import {
|
||||||
|
addSession,
|
||||||
|
listFinishedSessions,
|
||||||
|
listRunningSessions,
|
||||||
|
markBackgrounded,
|
||||||
|
markExited,
|
||||||
|
resetProcessRegistryForTests,
|
||||||
|
} from "./bash-process-registry.js";
|
||||||
|
import { createProcessTool } from "./bash-tools.process.js";
|
||||||
|
|
||||||
|
describe("process spawnedBySession tracking", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetProcessRegistryForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores sessionKey in ProcessSession when provided", () => {
|
||||||
|
const session: ProcessSession = {
|
||||||
|
id: "sess-1",
|
||||||
|
command: "echo test",
|
||||||
|
sessionKey: "agent:main:whatsapp:user:12345",
|
||||||
|
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
maxOutputChars: 10000,
|
||||||
|
pendingMaxOutputChars: 30_000,
|
||||||
|
totalOutputChars: 0,
|
||||||
|
pendingStdout: [],
|
||||||
|
pendingStderr: [],
|
||||||
|
pendingStdoutChars: 0,
|
||||||
|
pendingStderrChars: 0,
|
||||||
|
aggregated: "",
|
||||||
|
tail: "",
|
||||||
|
exited: false,
|
||||||
|
exitCode: undefined,
|
||||||
|
exitSignal: undefined,
|
||||||
|
truncated: false,
|
||||||
|
backgrounded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
addSession(session);
|
||||||
|
const running = listRunningSessions();
|
||||||
|
|
||||||
|
expect(running).toHaveLength(1);
|
||||||
|
expect(running[0].sessionKey).toBe("agent:main:whatsapp:user:12345");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves sessionKey in finished sessions", () => {
|
||||||
|
const session: ProcessSession = {
|
||||||
|
id: "sess-2",
|
||||||
|
command: "echo finished",
|
||||||
|
sessionKey: "agent:main:subagent:abc123",
|
||||||
|
child: { pid: 456 } as ChildProcessWithoutNullStreams,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
maxOutputChars: 10000,
|
||||||
|
pendingMaxOutputChars: 30_000,
|
||||||
|
totalOutputChars: 0,
|
||||||
|
pendingStdout: [],
|
||||||
|
pendingStderr: [],
|
||||||
|
pendingStdoutChars: 0,
|
||||||
|
pendingStderrChars: 0,
|
||||||
|
aggregated: "",
|
||||||
|
tail: "",
|
||||||
|
exited: false,
|
||||||
|
exitCode: undefined,
|
||||||
|
exitSignal: undefined,
|
||||||
|
truncated: false,
|
||||||
|
backgrounded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
addSession(session);
|
||||||
|
markExited(session, 0, null, "completed");
|
||||||
|
|
||||||
|
const finished = listFinishedSessions();
|
||||||
|
expect(finished).toHaveLength(1);
|
||||||
|
expect(finished[0].sessionKey).toBe("agent:main:subagent:abc123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("process list action returns spawnedBySession for running sessions", async () => {
|
||||||
|
const session: ProcessSession = {
|
||||||
|
id: "sess-3",
|
||||||
|
command: "sleep 60",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
child: { pid: 789 } as ChildProcessWithoutNullStreams,
|
||||||
|
pid: 789,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
maxOutputChars: 10000,
|
||||||
|
pendingMaxOutputChars: 30_000,
|
||||||
|
totalOutputChars: 0,
|
||||||
|
pendingStdout: [],
|
||||||
|
pendingStderr: [],
|
||||||
|
pendingStdoutChars: 0,
|
||||||
|
pendingStderrChars: 0,
|
||||||
|
aggregated: "",
|
||||||
|
tail: "",
|
||||||
|
exited: false,
|
||||||
|
exitCode: undefined,
|
||||||
|
exitSignal: undefined,
|
||||||
|
truncated: false,
|
||||||
|
backgrounded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
addSession(session);
|
||||||
|
|
||||||
|
const tool = createProcessTool();
|
||||||
|
const result = await tool.execute("call-1", { action: "list" });
|
||||||
|
|
||||||
|
expect(result.details).toBeDefined();
|
||||||
|
const details = result.details as {
|
||||||
|
sessions: Array<{ sessionId: string; spawnedBySession?: string }>;
|
||||||
|
};
|
||||||
|
expect(details.sessions).toHaveLength(1);
|
||||||
|
expect(details.sessions[0].sessionId).toBe("sess-3");
|
||||||
|
expect(details.sessions[0].spawnedBySession).toBe("agent:main:main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("process list action returns spawnedBySession for finished sessions", async () => {
|
||||||
|
const session: ProcessSession = {
|
||||||
|
id: "sess-4",
|
||||||
|
command: "echo done",
|
||||||
|
sessionKey: "agent:work:telegram:group:g1",
|
||||||
|
child: { pid: 101 } as ChildProcessWithoutNullStreams,
|
||||||
|
pid: 101,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
maxOutputChars: 10000,
|
||||||
|
pendingMaxOutputChars: 30_000,
|
||||||
|
totalOutputChars: 0,
|
||||||
|
pendingStdout: [],
|
||||||
|
pendingStderr: [],
|
||||||
|
pendingStdoutChars: 0,
|
||||||
|
pendingStderrChars: 0,
|
||||||
|
aggregated: "done\n",
|
||||||
|
tail: "done\n",
|
||||||
|
exited: false,
|
||||||
|
exitCode: undefined,
|
||||||
|
exitSignal: undefined,
|
||||||
|
truncated: false,
|
||||||
|
backgrounded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
addSession(session);
|
||||||
|
markBackgrounded(session);
|
||||||
|
markExited(session, 0, null, "completed");
|
||||||
|
|
||||||
|
const tool = createProcessTool();
|
||||||
|
const result = await tool.execute("call-2", { action: "list" });
|
||||||
|
|
||||||
|
expect(result.details).toBeDefined();
|
||||||
|
const details = result.details as {
|
||||||
|
sessions: Array<{ sessionId: string; spawnedBySession?: string }>;
|
||||||
|
};
|
||||||
|
expect(details.sessions).toHaveLength(1);
|
||||||
|
expect(details.sessions[0].sessionId).toBe("sess-4");
|
||||||
|
expect(details.sessions[0].spawnedBySession).toBe("agent:work:telegram:group:g1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("process list action handles sessions without sessionKey (backward compat)", async () => {
|
||||||
|
const session: ProcessSession = {
|
||||||
|
id: "sess-5",
|
||||||
|
command: "legacy command",
|
||||||
|
// No sessionKey - simulates pre-feature sessions
|
||||||
|
child: { pid: 111 } as ChildProcessWithoutNullStreams,
|
||||||
|
pid: 111,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
maxOutputChars: 10000,
|
||||||
|
pendingMaxOutputChars: 30_000,
|
||||||
|
totalOutputChars: 0,
|
||||||
|
pendingStdout: [],
|
||||||
|
pendingStderr: [],
|
||||||
|
pendingStdoutChars: 0,
|
||||||
|
pendingStderrChars: 0,
|
||||||
|
aggregated: "",
|
||||||
|
tail: "",
|
||||||
|
exited: false,
|
||||||
|
exitCode: undefined,
|
||||||
|
exitSignal: undefined,
|
||||||
|
truncated: false,
|
||||||
|
backgrounded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
addSession(session);
|
||||||
|
|
||||||
|
const tool = createProcessTool();
|
||||||
|
const result = await tool.execute("call-3", { action: "list" });
|
||||||
|
|
||||||
|
expect(result.details).toBeDefined();
|
||||||
|
const details = result.details as {
|
||||||
|
sessions: Array<{ sessionId: string; spawnedBySession?: string }>;
|
||||||
|
};
|
||||||
|
expect(details.sessions).toHaveLength(1);
|
||||||
|
expect(details.sessions[0].sessionId).toBe("sess-5");
|
||||||
|
// Should be undefined, not break
|
||||||
|
expect(details.sessions[0].spawnedBySession).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -98,6 +98,7 @@ export function createProcessTool(
|
|||||||
name: deriveSessionName(s.command),
|
name: deriveSessionName(s.command),
|
||||||
tail: s.tail,
|
tail: s.tail,
|
||||||
truncated: s.truncated,
|
truncated: s.truncated,
|
||||||
|
spawnedBySession: s.sessionKey ?? undefined,
|
||||||
}));
|
}));
|
||||||
const finished = listFinishedSessions()
|
const finished = listFinishedSessions()
|
||||||
.filter((s) => isInScope(s))
|
.filter((s) => isInScope(s))
|
||||||
@ -114,6 +115,7 @@ export function createProcessTool(
|
|||||||
truncated: s.truncated,
|
truncated: s.truncated,
|
||||||
exitCode: s.exitCode ?? undefined,
|
exitCode: s.exitCode ?? undefined,
|
||||||
exitSignal: s.exitSignal ?? undefined,
|
exitSignal: s.exitSignal ?? undefined,
|
||||||
|
spawnedBySession: s.sessionKey ?? undefined,
|
||||||
}));
|
}));
|
||||||
const lines = [...running, ...finished]
|
const lines = [...running, ...finished]
|
||||||
.sort((a, b) => b.startedAt - a.startedAt)
|
.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user