feat(FN-1737): auto-delete child/task-worker agents and hide system agents by default
- Auto-delete spawned child agents when their parent task terminates (reportsTo cleanup) - Auto-delete task-worker agents when their owned task completes - Add includeSystem filter to AgentStore.list() and REST API - Hide system agents by default on the agents page (show only user-facing agents) - Wire includeSystem toggle through the API layer and AgentsView component - Add changeset for @gsxdsm/fusion patch release - Add comprehensive tests for agent cleanup and includeSystem filtering
This commit is contained in:
@@ -9168,6 +9168,46 @@ describe("Agent Spawning - Child Termination", () => {
|
||||
expect(mockSession.dispose).toHaveBeenCalled();
|
||||
expect(internals.totalSpawnedCount).toBe(0);
|
||||
});
|
||||
|
||||
it("terminateChildAgent auto-deletes agent after 5 second delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const agentStore = createMockAgentStore() as any;
|
||||
// Add deleteAgent mock to the agent store
|
||||
agentStore.deleteAgent = vi.fn().mockResolvedValue(undefined);
|
||||
const store = createMockStore();
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test", { agentStore } as any);
|
||||
const internals = executor as any;
|
||||
|
||||
const mockSession = { dispose: vi.fn() };
|
||||
const childId = "agent-auto-delete-test";
|
||||
internals.childSessions.set(childId, mockSession);
|
||||
internals.totalSpawnedCount = 1;
|
||||
|
||||
// Terminate the child
|
||||
const terminatePromise = internals.terminateChildAgent(childId);
|
||||
|
||||
// Session should be disposed immediately
|
||||
expect(mockSession.dispose).toHaveBeenCalled();
|
||||
|
||||
// deleteAgent should not be called yet (before 5 seconds)
|
||||
expect(agentStore.deleteAgent).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Now deleteAgent should have been called
|
||||
expect(agentStore.deleteAgent).toHaveBeenCalledTimes(1);
|
||||
expect(agentStore.deleteAgent).toHaveBeenCalledWith(childId);
|
||||
|
||||
// Should not throw even when delete fails
|
||||
await terminatePromise;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent Spawning - runSpawnedChild", () => {
|
||||
|
||||
@@ -3917,6 +3917,12 @@ and show an appropriate message to the user.\`
|
||||
// Agent may not exist in store — that's ok for cleanup
|
||||
}
|
||||
|
||||
// Auto-delete the child agent after a short delay so the UI can observe
|
||||
// the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.options.agentStore?.deleteAgent(childId).catch(() => {});
|
||||
}, 5000);
|
||||
|
||||
this.totalSpawnedCount = Math.max(0, this.totalSpawnedCount - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -558,6 +558,88 @@ describe("InProcessRuntime", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
}, 30000);
|
||||
|
||||
it("auto-deletes task-worker agent on task completion after 5 second delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const deleteAgentSpy = vi.spyOn(store, "deleteAgent").mockResolvedValue(undefined);
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
onComplete?: (task: Task) => void;
|
||||
};
|
||||
expect(executorOptions.onComplete).toBeTypeOf("function");
|
||||
|
||||
// Create a task-worker agent first via onStart
|
||||
executorOptions.onStart?.({ id: "FN-AUTO1" } as Task, join(testDir, "worktree-FN-AUTO1"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents();
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-AUTO1")).toBe(true);
|
||||
});
|
||||
|
||||
// Clear previous calls and trigger onComplete
|
||||
deleteAgentSpy.mockClear();
|
||||
executorOptions.onComplete?.({ id: "FN-AUTO1" } as Task);
|
||||
|
||||
// Verify deleteAgent was not called immediately (before 5 seconds)
|
||||
expect(deleteAgentSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Now deleteAgent should have been called
|
||||
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("auto-deletes task-worker agent on task error after 5 second delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const deleteAgentSpy = vi.spyOn(store, "deleteAgent").mockResolvedValue(undefined);
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onError?: (task: Task, error: Error) => void;
|
||||
};
|
||||
expect(executorOptions.onError).toBeTypeOf("function");
|
||||
|
||||
// Create a task-worker agent first via onStart
|
||||
const onStartOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
};
|
||||
onStartOptions.onStart?.({ id: "FN-AUTO2" } as Task, join(testDir, "worktree-FN-AUTO2"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents();
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-AUTO2")).toBe(true);
|
||||
});
|
||||
|
||||
// Clear previous calls and trigger onError
|
||||
deleteAgentSpy.mockClear();
|
||||
executorOptions.onError?.({ id: "FN-AUTO2" } as Task, new Error("Task failed"));
|
||||
|
||||
// Verify deleteAgent was not called immediately (before 5 seconds)
|
||||
expect(deleteAgentSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Now deleteAgent should have been called
|
||||
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("configuration", () => {
|
||||
|
||||
@@ -304,6 +304,11 @@ export class InProcessRuntime
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
onError: (task, error) => {
|
||||
@@ -331,6 +336,11 @@ export class InProcessRuntime
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user