From a77f76b4d07d3148b23d269308d66c165751f4e4 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:29:05 -0500 Subject: [PATCH] fix: normalize subagent registry session keys --- src/agents/subagent-registry-run-manager.ts | 29 +++++--- .../subagent-registry.persistence.test.ts | 72 +++++++++++++++++++ src/agents/subagent-registry.store.ts | 10 +++ 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/agents/subagent-registry-run-manager.ts b/src/agents/subagent-registry-run-manager.ts index 0c8bbf69b71..58da55eab3f 100644 --- a/src/agents/subagent-registry-run-manager.ts +++ b/src/agents/subagent-registry-run-manager.ts @@ -274,6 +274,14 @@ export function createSubagentRunManager(params: { attachmentsRootDir?: string; retainAttachmentsOnKeep?: boolean; }) => { + const runId = registerParams.runId.trim(); + const childSessionKey = registerParams.childSessionKey.trim(); + const requesterSessionKey = registerParams.requesterSessionKey.trim(); + const controllerSessionKey = + registerParams.controllerSessionKey?.trim() || requesterSessionKey; + if (!runId || !childSessionKey || !requesterSessionKey) { + return; + } const now = Date.now(); const cfg = params.loadConfig(); const archiveAfterMs = resolveArchiveAfterMs(cfg); @@ -287,12 +295,11 @@ export function createSubagentRunManager(params: { const runTimeoutSeconds = registerParams.runTimeoutSeconds ?? 0; const waitTimeoutMs = params.resolveSubagentWaitTimeoutMs(cfg, runTimeoutSeconds); const requesterOrigin = normalizeDeliveryContext(registerParams.requesterOrigin); - params.runs.set(registerParams.runId, { - runId: registerParams.runId, - childSessionKey: registerParams.childSessionKey, - controllerSessionKey: - registerParams.controllerSessionKey ?? registerParams.requesterSessionKey, - requesterSessionKey: registerParams.requesterSessionKey, + params.runs.set(runId, { + runId, + childSessionKey, + controllerSessionKey, + requesterSessionKey, requesterOrigin, requesterDisplayKey: registerParams.requesterDisplayKey, task: registerParams.task, @@ -318,12 +325,12 @@ export function createSubagentRunManager(params: { try { createRunningTaskRun({ runtime: "subagent", - sourceId: registerParams.runId, - ownerKey: registerParams.requesterSessionKey, + sourceId: runId, + ownerKey: requesterSessionKey, scopeKind: "session", requesterOrigin, - childSessionKey: registerParams.childSessionKey, - runId: registerParams.runId, + childSessionKey, + runId, label: registerParams.label, task: registerParams.task, deliveryStatus: @@ -343,7 +350,7 @@ export function createSubagentRunManager(params: { params.startSweeper(); // Wait for subagent completion via gateway RPC (cross-process). // The in-process lifecycle listener is a fallback for embedded runs. - void waitForSubagentCompletion(registerParams.runId, waitTimeoutMs); + void waitForSubagentCompletion(runId, waitTimeoutMs); }; const releaseSubagentRun = (runId: string) => { diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.test.ts index 97ba7d73ffb..b45f95a4244 100644 --- a/src/agents/subagent-registry.persistence.test.ts +++ b/src/agents/subagent-registry.persistence.test.ts @@ -319,6 +319,78 @@ describe("subagent registry persistence", () => { expect(after.version).toBe(2); }); + it("normalizes persisted and newly registered session keys to canonical trimmed values", async () => { + const persisted = { + version: 2, + runs: { + "run-spaced": { + runId: "run-spaced", + childSessionKey: " agent:main:subagent:spaced-child ", + controllerSessionKey: " agent:main:subagent:controller ", + requesterSessionKey: " agent:main:main ", + requesterDisplayKey: "main", + task: "spaced persisted keys", + cleanup: "keep", + createdAt: 1, + startedAt: 1, + }, + }, + }; + await writePersistedRegistry(persisted, { seedChildSessions: false }); + + const restored = loadSubagentRegistryFromDisk(); + const restoredEntry = restored.get("run-spaced"); + expect(restoredEntry).toMatchObject({ + childSessionKey: "agent:main:subagent:spaced-child", + controllerSessionKey: "agent:main:subagent:controller", + requesterSessionKey: "agent:main:main", + }); + + resetSubagentRegistryForTests({ persist: false }); + addSubagentRunForTests(restoredEntry as never); + expect(listSubagentRunsForRequester("agent:main:main")).toEqual([ + expect.objectContaining({ + runId: "run-spaced", + }), + ]); + expect(getSubagentRunByChildSessionKey("agent:main:subagent:spaced-child")).toMatchObject({ + runId: "run-spaced", + }); + + resetSubagentRegistryForTests({ persist: false }); + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-subagent-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + + const mod = await import(`./subagent-registry.ts?t=${Date.now()}`); + vi.mocked(callGateway).mockResolvedValue({ + status: "ok", + startedAt: 111, + endedAt: 222, + }); + + mod.registerSubagentRun({ + runId: " run-live ", + childSessionKey: " agent:main:subagent:live-child ", + controllerSessionKey: " agent:main:subagent:live-controller ", + requesterSessionKey: " agent:main:main ", + requesterDisplayKey: "main", + task: "live spaced keys", + cleanup: "keep", + }); + + expect(mod.listSubagentRunsForRequester("agent:main:main")).toEqual([ + expect.objectContaining({ + runId: "run-live", + childSessionKey: "agent:main:subagent:live-child", + controllerSessionKey: "agent:main:subagent:live-controller", + requesterSessionKey: "agent:main:main", + }), + ]); + expect(mod.getSubagentRunByChildSessionKey("agent:main:subagent:live-child")).toMatchObject({ + runId: "run-live", + }); + }); + it("retries cleanup announce after a failed announce", async () => { const persisted = createPersistedEndedRun({ runId: "run-3", diff --git a/src/agents/subagent-registry.store.ts b/src/agents/subagent-registry.store.ts index 03cc0ded3c5..bf7a5c693a5 100644 --- a/src/agents/subagent-registry.store.ts +++ b/src/agents/subagent-registry.store.ts @@ -89,6 +89,13 @@ export function loadSubagentRegistryFromDisk(): Map { accountId: readStringValue(typed.requesterAccountId), }, ); + const childSessionKey = readStringValue(typed.childSessionKey)?.trim() ?? ""; + const requesterSessionKey = readStringValue(typed.requesterSessionKey)?.trim() ?? ""; + const controllerSessionKey = + readStringValue(typed.controllerSessionKey)?.trim() || requesterSessionKey; + if (!childSessionKey || !requesterSessionKey) { + continue; + } const { announceCompletedAt: _announceCompletedAt, announceHandled: _announceHandled, @@ -98,6 +105,9 @@ export function loadSubagentRegistryFromDisk(): Map { } = typed; out.set(runId, { ...rest, + childSessionKey, + requesterSessionKey, + controllerSessionKey, requesterOrigin, cleanupCompletedAt, cleanupHandled,