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:
Fusion
2026-04-15 20:36:42 -07:00
committed by gsxdsm
parent 9bd437e166
commit f2c5ac7b82
13 changed files with 390 additions and 10 deletions

View File

@@ -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", () => {

View File

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

View File

@@ -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", () => {

View File

@@ -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);
}
},
};