From 312dc9d82efd83cc08b57d7af65407f65d9fc6c8 Mon Sep 17 00:00:00 2001 From: jiang Date: Tue, 1 Sep 2026 14:35:11 +0800 Subject: [PATCH] fix(memory): migrate legacy summary timeout --- .../v1.0.7/legacy-memory-model-config.ts | 6 +- .../v1.1.2/0001-upgrade-summary-timeout.ts | 69 +++++++++++++++++++ Migrations/src/registry.ts | 2 + .../0001-upgrade-summary-timeout.test.ts | 69 +++++++++++++++++++ .../tests/legacy-memory-model-config.test.ts | 2 +- Migrations/tests/runner.test.ts | 13 +++- 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 Migrations/src/migrations/v1.1.2/0001-upgrade-summary-timeout.ts create mode 100644 Migrations/tests/0001-upgrade-summary-timeout.test.ts diff --git a/Migrations/src/migrations/v1.0.7/legacy-memory-model-config.ts b/Migrations/src/migrations/v1.0.7/legacy-memory-model-config.ts index 14d11e4cd..a2ebefc12 100644 --- a/Migrations/src/migrations/v1.0.7/legacy-memory-model-config.ts +++ b/Migrations/src/migrations/v1.0.7/legacy-memory-model-config.ts @@ -410,7 +410,11 @@ function migrateConfig(config: JsonObject): { changed: boolean; config: JsonObje const summary = fixedRoleConfig(migratedView, "summary"); const evolution = fixedRoleConfig(migratedView, "evolution"); - if (summary) memmyMemory.summary = structuredClone(summary); + if (summary) { + const migratedSummary = structuredClone(summary); + if (migratedSummary.timeoutMs === 45_000) migratedSummary.timeoutMs = 180_000; + memmyMemory.summary = migratedSummary; + } if (evolution) memmyMemory.evolution = structuredClone(evolution); memmyMemory.embedding = mergeEmbedding(migratedView); migrateUserIds(migrated, memmyMemory, migratedView); diff --git a/Migrations/src/migrations/v1.1.2/0001-upgrade-summary-timeout.ts b/Migrations/src/migrations/v1.1.2/0001-upgrade-summary-timeout.ts new file mode 100644 index 000000000..d2acfb5a0 --- /dev/null +++ b/Migrations/src/migrations/v1.1.2/0001-upgrade-summary-timeout.ts @@ -0,0 +1,69 @@ +import { + mutateRuntimeConfig, + mutateRuntimeConfigLockHeld, + type RuntimeConfigDocument, +} from "../../runtime-config-writer.js"; +import { + MigrationError, + type AgentWorkspaceMigrationContext, + type MigrationDefinition, + type MigrationResult, +} from "../../types.js"; + +const MIGRATION_ID = "v1.1.2/0001-upgrade-summary-timeout"; +const LEGACY_SUMMARY_TIMEOUT_MS = 45_000; +const SUMMARY_TIMEOUT_MS = 180_000; + +function isObject(value: unknown): value is RuntimeConfigDocument { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function upgradeSummaryTimeout(config: RuntimeConfigDocument): void { + const memory = isObject(config.memmyMemory) ? config.memmyMemory : null; + const summary = memory && isObject(memory.summary) ? memory.summary : null; + if (summary?.timeoutMs === LEGACY_SUMMARY_TIMEOUT_MS) { + summary.timeoutMs = SUMMARY_TIMEOUT_MS; + } +} + +function wrapError(error: unknown): never { + if (error instanceof MigrationError) { + throw new MigrationError(error.code, error.message, { + migrationId: MIGRATION_ID, + scope: "runtime-config", + cause: error.cause, + }); + } + throw new MigrationError("migration_config_invalid", "Unable to upgrade the memory summary timeout", { + migrationId: MIGRATION_ID, + scope: "runtime-config", + cause: error, + }); +} + +async function runSummaryTimeoutMigration( + context: AgentWorkspaceMigrationContext, +): Promise { + try { + const options = { createIfMissing: false as const }; + const result = context.runtimeConfigLock + ? await mutateRuntimeConfigLockHeld(context.runtimeConfigLock, upgradeSummaryTimeout, options) + : await mutateRuntimeConfig(context.runtimeConfigFile, upgradeSummaryTimeout, options); + if (!result.sourceExists) { + return { scanned: 0, changed: 0, ignored: 0, deferred: true }; + } + return result.changed + ? { scanned: 1, changed: 1, ignored: 0 } + : { scanned: 1, changed: 0, ignored: 1 }; + } catch (error) { + wrapError(error); + } +} + +export const upgradeSummaryTimeoutV112: MigrationDefinition = { + id: MIGRATION_ID, + introducedIn: "1.1.2", + scope: "runtime-config", + description: "Upgrade the legacy 45-second memory summary timeout to 180 seconds", + up: runSummaryTimeoutMigration, +}; diff --git a/Migrations/src/registry.ts b/Migrations/src/registry.ts index 09653bde4..a481badda 100644 --- a/Migrations/src/registry.ts +++ b/Migrations/src/registry.ts @@ -4,6 +4,7 @@ import { importLegacyAppStateModelConfigV107 } from "./migrations/v1.0.7/0002-im import { normalizeGoalStateV107 } from "./migrations/v1.0.7/0003-normalize-goal-state.js"; import { addGoalDagBoundaryV107 } from "./migrations/v1.0.7/0004-add-goal-dag-boundary.js"; import { repairRuntimeModelCatalogV109 } from "./migrations/v1.0.9/0001-repair-runtime-model-catalog.js"; +import { upgradeSummaryTimeoutV112 } from "./migrations/v1.1.2/0001-upgrade-summary-timeout.js"; import { MigrationError, type MigrationDefinition } from "./types.js"; const STABLE_SEMVER_PATTERN = @@ -24,6 +25,7 @@ export const migrations: readonly MigrationDefinition[] = [ normalizeGoalStateV107, addGoalDagBoundaryV107, repairRuntimeModelCatalogV109, + upgradeSummaryTimeoutV112, ]; function definitionError(message: string, migrationId: string | null = null): never { diff --git a/Migrations/tests/0001-upgrade-summary-timeout.test.ts b/Migrations/tests/0001-upgrade-summary-timeout.test.ts new file mode 100644 index 000000000..0b7973b05 --- /dev/null +++ b/Migrations/tests/0001-upgrade-summary-timeout.test.ts @@ -0,0 +1,69 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { upgradeSummaryTimeoutV112 } from "../src/migrations/v1.1.2/0001-upgrade-summary-timeout.js"; +import type { MigrationLogger } from "../src/types.js"; + +const temporaryDirectories: string[] = []; + +function logger(): MigrationLogger { + return { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; +} + +async function fixture(config: unknown) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "memmy-summary-timeout-migration-")); + temporaryDirectories.push(root); + const configPath = path.join(root, "config.yaml"); + await fs.writeFile(configPath, YAML.stringify(config), "utf8"); + return { root, configPath }; +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((directory) => + fs.rm(directory, { recursive: true, force: true }), + ), + ); +}); + +describe("v1.1.2/0001-upgrade-summary-timeout", () => { + it("upgrades the legacy 45-second summary timeout to 180 seconds", async () => { + const { root, configPath } = await fixture({ + memmyMemory: { + summary: { model: "gpt-main", timeoutMs: 45_000 }, + evolution: { timeoutMs: 45_000 }, + }, + }); + + await expect(upgradeSummaryTimeoutV112.up({ + profileWorkspace: root, + sessionsDir: path.join(root, "sessions"), + runtimeConfigFile: configPath, + sessionDagDir: path.join(root, "session-dag"), + logger: logger(), + })).resolves.toEqual({ scanned: 1, changed: 1, ignored: 0 }); + + const config = YAML.parse(await fs.readFile(configPath, "utf8")); + expect(config.memmyMemory.summary).toEqual({ model: "gpt-main", timeoutMs: 180_000 }); + expect(config.memmyMemory.evolution.timeoutMs).toBe(45_000); + }); + + it("preserves a user-defined summary timeout", async () => { + const { root, configPath } = await fixture({ + memmyMemory: { summary: { timeoutMs: 90_000 } }, + }); + + await expect(upgradeSummaryTimeoutV112.up({ + profileWorkspace: root, + sessionsDir: path.join(root, "sessions"), + runtimeConfigFile: configPath, + sessionDagDir: path.join(root, "session-dag"), + logger: logger(), + })).resolves.toEqual({ scanned: 1, changed: 0, ignored: 1 }); + + const config = YAML.parse(await fs.readFile(configPath, "utf8")); + expect(config.memmyMemory.summary.timeoutMs).toBe(90_000); + }); +}); diff --git a/Migrations/tests/legacy-memory-model-config.test.ts b/Migrations/tests/legacy-memory-model-config.test.ts index 0bb54f8fe..affcdeed9 100644 --- a/Migrations/tests/legacy-memory-model-config.test.ts +++ b/Migrations/tests/legacy-memory-model-config.test.ts @@ -230,7 +230,7 @@ describe("legacy Memory model config normalization", () => { }); expect(config.memmyMemory.summary).toMatchObject({ model: "gpt-main", - timeoutMs: 45_000, + timeoutMs: 180_000, }); expect(config.memmyMemory.evolution).toMatchObject({ model: "claude-fixed", diff --git a/Migrations/tests/runner.test.ts b/Migrations/tests/runner.test.ts index 584abd104..43cb5e3b1 100644 --- a/Migrations/tests/runner.test.ts +++ b/Migrations/tests/runner.test.ts @@ -111,9 +111,10 @@ describe("migration runner", () => { "v1.0.7/0003-normalize-goal-state", "v1.0.7/0004-add-goal-dag-boundary", "v1.0.9/0001-repair-runtime-model-catalog", + "v1.1.2/0001-upgrade-summary-timeout", ]); expect(first.deferred).toEqual(["v1.0.7/0002-import-legacy-app-state-model-config"]); - expect(first.results).toEqual({ scanned: 4, changed: 2, ignored: 2 }); + expect(first.results).toEqual({ scanned: 5, changed: 2, ignored: 3 }); expect(second).toEqual({ applied: [], skipped: [ @@ -122,6 +123,7 @@ describe("migration runner", () => { "v1.0.7/0003-normalize-goal-state", "v1.0.7/0004-add-goal-dag-boundary", "v1.0.9/0001-repair-runtime-model-catalog", + "v1.1.2/0001-upgrade-summary-timeout", ], deferred: ["v1.0.7/0002-import-legacy-app-state-model-config"], results: { scanned: 0, changed: 0, ignored: 0 }, @@ -172,6 +174,15 @@ describe("migration runner", () => { key: runtimeConfigTargetKey(configPath), }, }, + { + id: "v1.1.2/0001-upgrade-summary-timeout", + introducedIn: "1.1.2", + appliedAt: expect.stringMatching(/Z$/), + target: { + type: "runtime-config", + key: runtimeConfigTargetKey(configPath), + }, + }, ]); });