fix: normalize subagent registry session keys

This commit is contained in:
Tak Hoffman
2026-04-10 19:29:05 -05:00
parent d369dbe65c
commit a77f76b4d0
3 changed files with 100 additions and 11 deletions

View File

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

View File

@@ -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",

View File

@@ -89,6 +89,13 @@ export function loadSubagentRegistryFromDisk(): Map<string, SubagentRunRecord> {
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<string, SubagentRunRecord> {
} = typed;
out.set(runId, {
...rest,
childSessionKey,
requesterSessionKey,
controllerSessionKey,
requesterOrigin,
cleanupCompletedAt,
cleanupHandled,