diff --git a/src/agents/subagent-registry-run-manager.ts b/src/agents/subagent-registry-run-manager.ts index 418edc46bf3e..12d1ca60d704 100644 --- a/src/agents/subagent-registry-run-manager.ts +++ b/src/agents/subagent-registry-run-manager.ts @@ -878,7 +878,9 @@ export function createSubagentRunManager(params: { sourceId: runId, ownerKey: requesterSessionKey, scopeKind: "session", - requesterOrigin, + // Detached task runtimes are plugin-replaceable. Isolate their input so + // mutation cannot change the already-persisted registry record. + requesterOrigin: requesterOrigin ? structuredClone(requesterOrigin) : undefined, childSessionKey, runId, label: registerParams.label, @@ -907,7 +909,6 @@ export function createSubagentRunManager(params: { }); } params.ensureListener(); - params.persist(); // Always start sweeper — session-mode runs (no archiveAtMs) also need TTL cleanup. params.startSweeper(); // Wait for subagent completion via gateway RPC (cross-process). diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index 2a335a8175e1..bf5908c68fa2 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -38,6 +38,7 @@ import { SUBAGENT_ENDED_REASON_ERROR, SUBAGENT_ENDED_REASON_KILLED, } from "./subagent-lifecycle-events.js"; +import type { SubagentRunRecord } from "./subagent-registry.types.js"; import { createSessionStore, createSubagentRunParams, @@ -4471,6 +4472,85 @@ describe("subagent registry seam flow", () => { ).toBeUndefined(); }); + it.each([ + { name: "running", queued: false }, + { name: "queued", queued: true }, + ])("persists a $name registration exactly once", ({ queued }) => { + mockGatewayMethods(mocks.callGateway, { + "agent.wait": { status: "pending" }, + }); + + mod.registerSubagentRun({ + runId: `run-single-persist-${queued ? "queued" : "running"}`, + task: "persist one registry snapshot", + queued, + }); + + expect(mocks.persistSubagentRunsToDiskOrThrow).toHaveBeenCalledOnce(); + expect(mocks.persistSubagentRunsToDisk).not.toHaveBeenCalled(); + }); + + it.each([ + { name: "running", queued: false }, + { name: "queued", queued: true }, + ])("isolates a $name registration from task runtime input mutation", ({ queued }) => { + const runId = `run-isolated-origin-${queued ? "queued" : "running"}`; + const expectedRequesterOrigin = { + channel: "discord", + to: "channel:123", + accountId: "acct-1", + threadId: 42, + }; + const requesterOrigin = { + ...expectedRequesterOrigin, + deliveryIntent: { + id: "delivery-1", + kind: "outbound_queue" as const, + queuePolicy: "required" as const, + }, + }; + let persistedEntry: SubagentRunRecord | undefined; + mocks.persistSubagentRunsToDiskOrThrow.mockImplementationOnce((runs) => { + persistedEntry = structuredClone(runs.get(runId)); + }); + mockGatewayMethods(mocks.callGateway, { + "agent.wait": { status: "pending" }, + }); + const defaultRuntime = getDetachedTaskLifecycleRuntime(); + const createMutatingTaskRun = vi.fn( + (taskParams: Parameters[0]) => { + if (!taskParams.requesterOrigin) { + throw new Error("expected requester origin"); + } + Object.assign(taskParams.requesterOrigin, { + channel: "mutated", + to: "mutated", + accountId: "mutated", + threadId: "mutated", + }); + return null; + }, + ); + setDetachedTaskLifecycleRuntime({ + ...defaultRuntime, + createQueuedTaskRun: createMutatingTaskRun, + createRunningTaskRun: createMutatingTaskRun, + }); + + mod.registerSubagentRun({ + runId, + task: "isolate the registry delivery context", + queued, + requesterOrigin, + }); + + expect(createMutatingTaskRun).toHaveBeenCalledOnce(); + expect(findRequesterRun(runId)?.requesterOrigin).toEqual(expectedRequesterOrigin); + expect(persistedEntry?.requesterOrigin).toEqual(expectedRequesterOrigin); + expect(mocks.persistSubagentRunsToDiskOrThrow).toHaveBeenCalledOnce(); + expect(mocks.persistSubagentRunsToDisk).not.toHaveBeenCalled(); + }); + it("retains an already-running replacement when its durable write fails", () => { mocks.callGateway.mockImplementation(async (request: { method?: string }) => request.method === "agent.wait" ? { status: "pending" } : {},