From 780c81114674f94b4ec1ecdaca08907e70883bdf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 04:24:44 +0000 Subject: [PATCH] refactor: migrate subagent registry store v2 Co-authored-by: adam91holt --- .../subagent-registry.persistence.test.ts | 7 +++- src/agents/subagent-registry.store.ts | 37 +++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.test.ts index c3c7e43583..7e0d4dd560 100644 --- a/src/agents/subagent-registry.persistence.test.ts +++ b/src/agents/subagent-registry.persistence.test.ts @@ -107,7 +107,7 @@ describe("subagent registry persistence", () => { const registryPath = path.join(tempStateDir, "subagents", "runs.json"); const persisted = { - version: 1, + version: 2, runs: { "run-2": { runId: "run-2", @@ -177,6 +177,9 @@ describe("subagent registry persistence", () => { expect(entry?.cleanupCompletedAt).toBe(9); expect(entry?.requesterOrigin?.channel).toBe("whatsapp"); expect(entry?.requesterOrigin?.accountId).toBe("legacy-account"); + + const after = JSON.parse(await fs.readFile(registryPath, "utf8")) as { version?: number }; + expect(after.version).toBe(2); }); it("retries cleanup announce after a failed announce", async () => { @@ -185,7 +188,7 @@ describe("subagent registry persistence", () => { const registryPath = path.join(tempStateDir, "subagents", "runs.json"); const persisted = { - version: 1, + version: 2, runs: { "run-3": { runId: "run-3", diff --git a/src/agents/subagent-registry.store.ts b/src/agents/subagent-registry.store.ts index d433deaa3f..11517942ac 100644 --- a/src/agents/subagent-registry.store.ts +++ b/src/agents/subagent-registry.store.ts @@ -5,14 +5,21 @@ import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; import { normalizeDeliveryContext } from "../utils/delivery-context.js"; import type { SubagentRunRecord } from "./subagent-registry.js"; -export type PersistedSubagentRegistryVersion = 1; +export type PersistedSubagentRegistryVersion = 1 | 2; -type PersistedSubagentRegistry = { +type PersistedSubagentRegistryV1 = { version: 1; + runs: Record; +}; + +type PersistedSubagentRegistryV2 = { + version: 2; runs: Record; }; -const REGISTRY_VERSION = 1 as const; +type PersistedSubagentRegistry = PersistedSubagentRegistryV1 | PersistedSubagentRegistryV2; + +const REGISTRY_VERSION = 2 as const; type PersistedSubagentRunRecord = SubagentRunRecord; @@ -32,22 +39,30 @@ export function loadSubagentRegistryFromDisk(): Map { const raw = loadJsonFile(pathname); if (!raw || typeof raw !== "object") return new Map(); const record = raw as Partial; - if (record.version !== REGISTRY_VERSION) return new Map(); + if (record.version !== 1 && record.version !== 2) return new Map(); const runsRaw = record.runs; if (!runsRaw || typeof runsRaw !== "object") return new Map(); const out = new Map(); + const isLegacy = record.version === 1; + let migrated = false; for (const [runId, entry] of Object.entries(runsRaw)) { if (!entry || typeof entry !== "object") continue; const typed = entry as LegacySubagentRunRecord; if (!typed.runId || typeof typed.runId !== "string") continue; const legacyCompletedAt = - typeof typed.announceCompletedAt === "number" ? typed.announceCompletedAt : undefined; + isLegacy && typeof typed.announceCompletedAt === "number" + ? typed.announceCompletedAt + : undefined; const cleanupCompletedAt = - typeof typed.cleanupCompletedAt === "number" ? typed.cleanupCompletedAt : legacyCompletedAt; + typeof typed.cleanupCompletedAt === "number" + ? typed.cleanupCompletedAt + : legacyCompletedAt; const cleanupHandled = typeof typed.cleanupHandled === "boolean" ? typed.cleanupHandled - : Boolean(typed.announceHandled ?? cleanupCompletedAt); + : isLegacy + ? Boolean(typed.announceHandled ?? cleanupCompletedAt) + : undefined; const requesterOrigin = normalizeDeliveryContext( typed.requesterOrigin ?? { channel: typeof typed.requesterChannel === "string" ? typed.requesterChannel : undefined, @@ -68,6 +83,14 @@ export function loadSubagentRegistryFromDisk(): Map { cleanupCompletedAt, cleanupHandled, }); + if (isLegacy) migrated = true; + } + if (migrated) { + try { + saveSubagentRegistryToDisk(out); + } catch { + // ignore migration write failures + } } return out; }