From 070996e5c334aa5e496b86d9b072e440394de2c8 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Wed, 24 Jun 2026 10:43:51 -0700 Subject: [PATCH] fix: keep model-run pruning internal --- docs/cli/sessions.md | 1 + docs/concepts/session.md | 8 +++ docs/gateway/config-agents.md | 1 + .../session-management-compaction.md | 8 ++- .../doctor-legacy-config.migrations.test.ts | 6 +- src/commands/sessions-cleanup.test.ts | 3 +- src/config/schema.help.ts | 2 - src/config/schema.labels.ts | 1 - src/config/sessions/runtime-types.ts | 3 +- src/config/sessions/store-maintenance.ts | 41 ++---------- .../store.pruning.integration.test.ts | 8 +-- src/config/sessions/store.pruning.test.ts | 45 +------------- src/config/types.base.ts | 6 -- ...ema.session-maintenance-extensions.test.ts | 21 +------ src/config/zod-schema.session.ts | 14 ----- src/plugin-sdk/session-store-runtime.test.ts | 62 +------------------ src/proxy-capture/store.sqlite.ts | 25 ++++---- 17 files changed, 45 insertions(+), 210 deletions(-) diff --git a/docs/cli/sessions.md b/docs/cli/sessions.md index 4e2b6600490e..556e1416f7ee 100644 --- a/docs/cli/sessions.md +++ b/docs/cli/sessions.md @@ -120,6 +120,7 @@ openclaw sessions cleanup --json - Scope note: `openclaw sessions cleanup` maintains session stores, transcripts, and trajectory sidecars. It does not prune cron run history, which is managed by `cron.runLog.keepLines` in [Cron configuration](/automation/cron-jobs#configuration) and explained in [Cron maintenance](/automation/cron-jobs#maintenance). - Cleanup also prunes unreferenced primary transcripts, compaction checkpoints, and trajectory sidecars older than `session.maintenance.pruneAfter`; files still referenced by `sessions.json` are preserved. +- Cleanup reports short-lived gateway model-run probe cleanup separately as `modelRunPruned`. This only matches strict explicit keys shaped like `agent:*:explicit:model-run-`. The fixed retention is `24h`, but it is pressure-gated: it only removes stale probe rows when session-entry maintenance/cap pressure is reached. When it runs, model-run cleanup happens before global stale cleanup and capping. - `--dry-run`: preview how many entries would be pruned/capped without writing. - In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) plus a summary grouped by session label so you can see what would be kept vs removed. diff --git a/docs/concepts/session.md b/docs/concepts/session.md index d49b56cd4733..80013e703c20 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -127,6 +127,14 @@ in `enforce` mode and applies cleanup during maintenance. Set For production-sized `maxEntries` limits, Gateway runtime writes use a small high-water buffer and clean back down to the configured cap in batches. Session store reads do not prune or cap entries during Gateway startup. This avoids running full store cleanup on every startup or isolated cron session. `openclaw sessions cleanup --enforce` applies the cap immediately. +Gateway model-run probe sessions are short-lived by default. Matching rows with +strict explicit keys like `agent:*:explicit:model-run-` use fixed `24h` +retention, but cleanup is pressure-gated: it only removes stale probe rows when +session-entry maintenance/cap pressure is reached. When model-run cleanup runs, +it runs before the broader stale-entry age cutoff and entry cap. Normal direct, +group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit +this 24h retention. + Maintenance preserves durable external conversation pointers, including group sessions and thread-scoped chat sessions, while still allowing synthetic cron, hook, heartbeat, ACP, and sub-agent entries to age out. diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index c257634e93df..4d793a0359aa 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -1316,6 +1316,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden - `mode`: `enforce` applies cleanup and is the default; `warn` emits warnings only. - `pruneAfter`: age cutoff for stale entries (default `30d`). - `maxEntries`: maximum number of entries in `sessions.json` (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately. + - Short-lived gateway model-run probe sessions use fixed `24h` retention, but cleanup is pressure-gated: it only removes stale strict model-run probe rows when session-entry maintenance/cap pressure is reached. Only strict explicit probe keys matching `agent:*:explicit:model-run-` are eligible; normal direct, group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit this 24h retention. When model-run cleanup runs, it runs before the broader `pruneAfter` stale-entry cleanup and `maxEntries` cap. - `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs. - `resetArchiveRetention`: retention for `*.reset.` transcript archives. Defaults to `pruneAfter`; set `false` to disable. - `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index 4c004f229a58..a64c16909f63 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -81,6 +81,7 @@ Session persistence has automatic maintenance controls (`session.maintenance`) f - `mode`: `enforce` (default) or `warn` - `pruneAfter`: stale-entry age cutoff (default `30d`) - `maxEntries`: cap entries in `sessions.json` (default `500`) +- Short-lived gateway model-run probe retention is fixed at `24h`, but it is pressure-gated: it only removes stale strict probe rows when session-entry maintenance/cap pressure is reached. This applies only to strict explicit probe keys matching `agent:*:explicit:model-run-` and runs before global stale-entry cleanup/capping when it runs. - `resetArchiveRetention`: retention for `*.reset.` transcript archives (default: same as `pruneAfter`; `false` disables cleanup) - `maxDiskBytes`: optional sessions-directory budget - `highWaterBytes`: optional target after cleanup (default `80%` of `maxDiskBytes`) @@ -90,7 +91,12 @@ Normal Gateway writes flow through a per-store session writer that serializes in Maintenance keeps durable external conversation pointers such as group sessions and thread-scoped chat sessions, but synthetic runtime entries for cron, hooks, heartbeat, ACP, and sub-agents can still be removed when they exceed the -configured age, count, or disk budget. +configured age, count, or disk budget. Gateway model-run probe sessions use the +separate `24h` model-run retention only when their key exactly matches +`agent:*:explicit:model-run-`; other explicit sessions are not part of +that retention. The model-run cleanup is applied only under session-entry cap +pressure. Isolated cron runs keep their own `cron.sessionRetention` control, +independent of model-run probe retention. OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The legacy `session.maintenance.rotateBytes` key is ignored and `openclaw doctor --fix` removes it from older configs. diff --git a/src/commands/doctor-legacy-config.migrations.test.ts b/src/commands/doctor-legacy-config.migrations.test.ts index 7ef117f2b5c2..71d4c6f238a1 100644 --- a/src/commands/doctor-legacy-config.migrations.test.ts +++ b/src/commands/doctor-legacy-config.migrations.test.ts @@ -451,9 +451,9 @@ describe("normalizeCompatibilityConfigValues", () => { }, }, } as unknown as OpenClawConfig); - const channel = (res.config.channels as Record }>)?.[ - channelId - ]; + const channel = ( + res.config.channels as Record }> + )?.[channelId]; expect(channel?.accounts?.default).toEqual({ dmPolicy: "allowlist", diff --git a/src/commands/sessions-cleanup.test.ts b/src/commands/sessions-cleanup.test.ts index 3541003eb4b8..47a9cb0672e6 100644 --- a/src/commands/sessions-cleanup.test.ts +++ b/src/commands/sessions-cleanup.test.ts @@ -91,8 +91,7 @@ describe("sessionsCleanupCommand", () => { mocks.resolveMaintenanceConfig.mockReturnValue({ mode: "warn", pruneAfterMs: 7 * 24 * 60 * 60 * 1000, - modelRunPruneAfterMs: null, - modelRunPruneAfterConfigured: true, + modelRunPruneAfterMs: 24 * 60 * 60 * 1000, maxEntries: 500, resetArchiveRetentionMs: 7 * 24 * 60 * 60 * 1000, maxDiskBytes: null, diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 6c9c1b90a8d0..613d6832c4f3 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1688,8 +1688,6 @@ export const FIELD_HELP: Record = { "Deprecated age-retention field kept for compatibility with legacy configs using day counts. Use session.maintenance.pruneAfter instead so duration syntax and behavior are consistent.", "session.maintenance.maxEntries": "Caps total session entry count retained in the store to prevent unbounded growth over time. Use lower limits for constrained environments, or higher limits when longer history is required.", - "session.maintenance.modelRunPruneAfter": - "Retention for short-lived gateway model-run probe sessions (`agent:*:explicit:model-run-`). Default: `24h`, but the unset default only runs under session-entry cap pressure. Set a duration such as `24h` or `2d` for normal age-based cleanup, or `false` to disable.", "session.maintenance.rotateBytes": 'Deprecated and ignored. Do not use for `sessions.json` growth control; OpenClaw no longer creates automatic rotation backups, and "openclaw doctor --fix" removes this key.', "session.maintenance.resetArchiveRetention": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 6e231b04f2a4..566c82fa46f1 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -850,7 +850,6 @@ export const FIELD_LABELS: Record = { "session.maintenance.pruneAfter": "Session Prune After", "session.maintenance.pruneDays": "Session Prune Days (Deprecated)", "session.maintenance.maxEntries": "Session Max Entries", - "session.maintenance.modelRunPruneAfter": "Session Model-run Prune After", "session.maintenance.rotateBytes": "Deprecated Session Rotate Size", "session.maintenance.resetArchiveRetention": "Session Reset Archive Retention", "session.maintenance.maxDiskBytes": "Session Max Disk Budget", diff --git a/src/config/sessions/runtime-types.ts b/src/config/sessions/runtime-types.ts index 90844d6fb03d..300328c25d84 100644 --- a/src/config/sessions/runtime-types.ts +++ b/src/config/sessions/runtime-types.ts @@ -25,8 +25,7 @@ export type ResolvedSessionMaintenanceConfigRuntime = { mode: SessionMaintenanceMode; pruneAfterMs: number; maxEntries: number; - modelRunPruneAfterMs?: number | null; - modelRunPruneAfterConfigured?: boolean; + modelRunPruneAfterMs: number; resetArchiveRetentionMs: number | null; maxDiskBytes: number | null; highWaterBytes: number | null; diff --git a/src/config/sessions/store-maintenance.ts b/src/config/sessions/store-maintenance.ts index 564685326604..83861300633a 100644 --- a/src/config/sessions/store-maintenance.ts +++ b/src/config/sessions/store-maintenance.ts @@ -41,12 +41,7 @@ export type ResolvedSessionMaintenanceConfig = { mode: SessionMaintenanceMode; pruneAfterMs: number; maxEntries: number; - // Optional so external plugin callers that construct a pre-#88632 maintenanceConfig - // (without the model-run fields) still compile; the resolver always sets both, and all - // internal readers treat an absent value as "unset" (model-run pruning disabled). - modelRunPruneAfterMs?: number | null; - /** True when modelRunPruneAfter was explicitly set instead of using the pressure-gated default. */ - modelRunPruneAfterConfigured?: boolean; + modelRunPruneAfterMs: number; resetArchiveRetentionMs: number | null; maxDiskBytes: number | null; highWaterBytes: number | null; @@ -84,24 +79,6 @@ function resolveResetArchiveRetentionMs( } } -function resolveModelRunPruneAfterMs(maintenance?: SessionMaintenanceConfig): number | null { - const raw = maintenance?.modelRunPruneAfter; - if (raw === false) { - return null; - } - const normalized = normalizeStringifiedOptionalString(raw); - if (!normalized) { - return DEFAULT_MODEL_RUN_PRUNE_AFTER_MS; - } - try { - return parseDurationMs(normalized, { defaultUnit: "d" }); - } catch { - // The schema rejects invalid explicit values. Keep direct resolver callers fail-closed - // rather than silently enabling the default 24h model-run cleanup. - return null; - } -} - function resolveMaxDiskBytes(maintenance?: SessionMaintenanceConfig): number | null { const raw = maintenance?.maxDiskBytes; const normalized = normalizeStringifiedOptionalString(raw); @@ -163,8 +140,7 @@ export function resolveMaintenanceConfigFromInput( mode: maintenance?.mode ?? DEFAULT_SESSION_MAINTENANCE_MODE, pruneAfterMs, maxEntries: maintenance?.maxEntries ?? DEFAULT_SESSION_MAX_ENTRIES, - modelRunPruneAfterMs: resolveModelRunPruneAfterMs(maintenance), - modelRunPruneAfterConfigured: maintenance?.modelRunPruneAfter !== undefined, + modelRunPruneAfterMs: DEFAULT_MODEL_RUN_PRUNE_AFTER_MS, resetArchiveRetentionMs: resolveResetArchiveRetentionMs(maintenance, pruneAfterMs), maxDiskBytes, highWaterBytes: resolveHighWaterBytes(maintenance, maxDiskBytes), @@ -198,10 +174,7 @@ export function shouldRunSessionEntryMaintenance(params: { } export function shouldRunModelRunPrune(params: { - maintenance: Pick< - ResolvedSessionMaintenanceConfig, - "maxEntries" | "modelRunPruneAfterConfigured" | "modelRunPruneAfterMs" - >; + maintenance: Pick; entryCount: number; /** * True when the caller caps immediately to `maxEntries` in the same pass (forced @@ -209,13 +182,7 @@ export function shouldRunModelRunPrune(params: { */ force?: boolean; }): boolean { - if (params.maintenance.modelRunPruneAfterMs == null) { - return false; - } - if (params.maintenance.modelRunPruneAfterConfigured) { - return true; - } - // Unset default is pressure-gated, and must align with whichever cap step runs alongside it. + // Model-run cleanup is pressure-gated, and must align with whichever cap step runs alongside it. // Forced maintenance caps immediately down to `maxEntries`, so prune stale probes first whenever // that cap would actually evict; otherwise stale probes would survive while real sessions get // capped (the inverse of #88632). Batched runtime writes instead use the high-water trigger. diff --git a/src/config/sessions/store.pruning.integration.test.ts b/src/config/sessions/store.pruning.integration.test.ts index 93494d25d34a..d99872bf694e 100644 --- a/src/config/sessions/store.pruning.integration.test.ts +++ b/src/config/sessions/store.pruning.integration.test.ts @@ -34,7 +34,6 @@ const ENFORCED_MAINTENANCE_OVERRIDE = { pruneAfterMs: 7 * DAY_MS, maxEntries: 500, modelRunPruneAfterMs: DAY_MS, - modelRunPruneAfterConfigured: true, resetArchiveRetentionMs: 7 * DAY_MS, maxDiskBytes: null, highWaterBytes: null, @@ -135,7 +134,7 @@ describe("Integration: saveSessionStore with pruning", () => { } }); - it("saveSessionStore prunes stale model-run probes before global retention", async () => { + it("saveSessionStore prunes stale model-run probes before capping real sessions", async () => { const now = Date.now(); const staleModelRun = "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174000"; const recentModelRun = "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174001"; @@ -150,7 +149,7 @@ describe("Integration: saveSessionStore with pruning", () => { maintenanceOverride: { ...ENFORCED_MAINTENANCE_OVERRIDE, pruneAfterMs: 30 * DAY_MS, - maxEntries: 500, + maxEntries: 2, }, }); @@ -194,8 +193,7 @@ describe("Integration: saveSessionStore with pruning", () => { maintenance: { mode: "enforce", pruneAfter: "30d", - maxEntries: 500, - modelRunPruneAfter: "24h", + maxEntries: 1, }, }, }); diff --git a/src/config/sessions/store.pruning.test.ts b/src/config/sessions/store.pruning.test.ts index 4d135f28cfea..f8ad432dc3cf 100644 --- a/src/config/sessions/store.pruning.test.ts +++ b/src/config/sessions/store.pruning.test.ts @@ -184,8 +184,7 @@ describe("applyFileBackedSessionStoreMaintenance", () => { mode: "enforce", pruneAfterMs: 7 * DAY_MS, maxEntries: 500, - modelRunPruneAfterMs: null, - modelRunPruneAfterConfigured: true, + modelRunPruneAfterMs: DAY_MS, resetArchiveRetentionMs: null, maxDiskBytes: null, highWaterBytes: null, @@ -242,7 +241,6 @@ describe("applyFileBackedSessionStoreMaintenance", () => { pruneAfterMs: 7 * DAY_MS, maxEntries: 50, modelRunPruneAfterMs: DAY_MS, - modelRunPruneAfterConfigured: false, resetArchiveRetentionMs: null, maxDiskBytes: null, highWaterBytes: null, @@ -570,27 +568,8 @@ describe("resolveMaintenanceConfigFromInput", () => { expect(maintenance.mode).toBe("enforce"); }); - it("defaults gateway model-run probes to 24h retention with override and disable support", () => { + it("defaults gateway model-run probes to fixed 24h retention", () => { expect(resolveMaintenanceConfigFromInput().modelRunPruneAfterMs).toBe(DAY_MS); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: "48h" }).modelRunPruneAfterMs, - ).toBe(2 * DAY_MS); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: false }).modelRunPruneAfterMs, - ).toBe(null); - expect(resolveMaintenanceConfigFromInput().modelRunPruneAfterConfigured).toBe(false); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: "48h" }).modelRunPruneAfterConfigured, - ).toBe(true); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: false }).modelRunPruneAfterConfigured, - ).toBe(true); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: "bad" }).modelRunPruneAfterMs, - ).toBe(null); - expect( - resolveMaintenanceConfigFromInput({ modelRunPruneAfter: "bad" }).modelRunPruneAfterConfigured, - ).toBe(true); }); it("force-gates the unset model-run prune default to the cap-eviction threshold", () => { @@ -603,26 +582,6 @@ describe("resolveMaintenanceConfigFromInput", () => { expect( shouldRunModelRunPrune({ maintenance: defaultMaintenance, entryCount: 50, force: true }), ).toBe(false); - expect( - shouldRunModelRunPrune({ - maintenance: resolveMaintenanceConfigFromInput({ - maxEntries: 50, - modelRunPruneAfter: "24h", - }), - entryCount: 1, - force: true, - }), - ).toBe(true); - expect( - shouldRunModelRunPrune({ - maintenance: resolveMaintenanceConfigFromInput({ - maxEntries: 50, - modelRunPruneAfter: false, - }), - entryCount: 60, - force: true, - }), - ).toBe(false); }); it("batches normal entry-count maintenance for production-sized caps", () => { diff --git a/src/config/types.base.ts b/src/config/types.base.ts index dfa98dc1b236..5280f254b51e 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -259,12 +259,6 @@ export type SessionMaintenanceConfig = { pruneDays?: number; /** Maximum number of session entries to keep. Default: 500. */ maxEntries?: number; - /** - * Retention for gateway model-run probe sessions (`agent:*:explicit:model-run-`). - * Unset defaults to "24h" but only runs under session-entry cap pressure; - * setting a duration opts into unconditional age cleanup, and `false` disables. - */ - modelRunPruneAfter?: string | number | false; /** @deprecated Ignored. Run `openclaw doctor --fix` to remove. */ rotateBytes?: number | string; /** diff --git a/src/config/zod-schema.session-maintenance-extensions.test.ts b/src/config/zod-schema.session-maintenance-extensions.test.ts index 14f0bdd263f1..9bcca8b45854 100644 --- a/src/config/zod-schema.session-maintenance-extensions.test.ts +++ b/src/config/zod-schema.session-maintenance-extensions.test.ts @@ -43,7 +43,6 @@ describe("SessionSchema maintenance extensions", () => { it("accepts valid maintenance extensions", () => { const result = SessionSchema.safeParse({ maintenance: { - modelRunPruneAfter: "24h", resetArchiveRetention: "14d", maxDiskBytes: "500mb", highWaterBytes: "350mb", @@ -52,34 +51,16 @@ describe("SessionSchema maintenance extensions", () => { expect(result.success).toBe(true); }); - it("accepts disabling model-run and reset archive cleanup", () => { + it("accepts disabling reset archive cleanup", () => { const result = SessionSchema.safeParse({ maintenance: { - modelRunPruneAfter: false, resetArchiveRetention: false, }, }); expect(result.success).toBe(true); }); - it("accepts numeric model-run retention with day default units", () => { - const result = SessionSchema.safeParse({ - maintenance: { - modelRunPruneAfter: 2, - }, - }); - expect(result.success).toBe(true); - }); - it("rejects invalid maintenance extension values", () => { - expect(() => - SessionSchema.parse({ - maintenance: { - modelRunPruneAfter: "never", - }, - }), - ).toThrow(/modelRunPruneAfter|duration/i); - expect(() => SessionSchema.parse({ maintenance: { diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index df1d54963b05..f3006ca8959f 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -88,7 +88,6 @@ export const SessionSchema = z /** @deprecated Use pruneAfter instead. */ pruneDays: z.number().int().positive().optional(), maxEntries: z.number().int().positive().optional(), - modelRunPruneAfter: z.union([z.string(), z.number(), z.literal(false)]).optional(), rotateBytes: z.union([z.string(), z.number()]).optional(), resetArchiveRetention: z.union([z.string(), z.number(), z.literal(false)]).optional(), maxDiskBytes: z.union([z.string(), z.number()]).optional(), @@ -109,19 +108,6 @@ export const SessionSchema = z }); } } - if (val.modelRunPruneAfter !== undefined && val.modelRunPruneAfter !== false) { - try { - parseDurationMs(normalizeStringifiedOptionalString(val.modelRunPruneAfter) ?? "", { - defaultUnit: "d", - }); - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["modelRunPruneAfter"], - message: "invalid duration (use ms, s, m, h, d)", - }); - } - } if (val.resetArchiveRetention !== undefined && val.resetArchiveRetention !== false) { try { parseDurationMs(normalizeStringifiedOptionalString(val.resetArchiveRetention) ?? "", { diff --git a/src/plugin-sdk/session-store-runtime.test.ts b/src/plugin-sdk/session-store-runtime.test.ts index 474cebc666dc..de2ac00a5fb5 100644 --- a/src/plugin-sdk/session-store-runtime.test.ts +++ b/src/plugin-sdk/session-store-runtime.test.ts @@ -145,8 +145,7 @@ describe("session-store-runtime compatibility surface", () => { maintenanceConfig: { mode: "enforce", pruneAfterMs: 7 * DAY_MS, - modelRunPruneAfterMs: null, - modelRunPruneAfterConfigured: true, + modelRunPruneAfterMs: DAY_MS, maxEntries: 1, resetArchiveRetentionMs: 7 * DAY_MS, maxDiskBytes: null, @@ -166,65 +165,6 @@ describe("session-store-runtime compatibility surface", () => { expect(getSessionEntry({ sessionKey: staleSessionKey, storePath })).toBeUndefined(); }); - it("accepts a pre-#88632 maintenanceConfig without the model-run fields", async () => { - // Backward-compatibility guard: external plugin callers built `maintenanceConfig` - // before `modelRunPruneAfterMs` / `modelRunPruneAfterConfigured` existed. That old - // shape must still compile (the fields are optional) and behave as before — no - // model-run-specific pruning. - const staleModelRunKey = "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174000"; - const activeSessionKey = "agent:main:active"; - const now = Date.now(); - await saveSessionStore( - storePath, - { - // A gateway model-run probe older than the 24h model-run window but well within the - // 7d generic pruneAfter cutoff, so only model-run-specific pruning could remove it. - [staleModelRunKey]: { - sessionId: "session-probe", - updatedAt: now - 2 * DAY_MS, - }, - [activeSessionKey]: { - sessionId: "session-active", - updatedAt: now, - }, - }, - { skipMaintenance: true }, - ); - - // Note: intentionally NO modelRunPruneAfterMs / modelRunPruneAfterConfigured (old shape), - // and maxEntries high enough that there is NO cap pressure. - const legacyMaintenanceConfig = { - mode: "enforce" as const, - pruneAfterMs: 7 * DAY_MS, - maxEntries: 500, - resetArchiveRetentionMs: 7 * DAY_MS, - maxDiskBytes: null, - highWaterBytes: null, - }; - - await expect( - patchSessionEntry({ - sessionKey: activeSessionKey, - storePath, - maintenanceConfig: legacyMaintenanceConfig, - update: () => ({ model: "gpt-5.5" }), - }), - ).resolves.toMatchObject({ - model: "gpt-5.5", - sessionId: "session-active", - }); - - // The old-shape config disables model-run pruning, so the stale probe survives - // (no model-run field => disabled; no cap pressure => nothing capped). - expect(getSessionEntry({ sessionKey: staleModelRunKey, storePath })).toMatchObject({ - sessionId: "session-probe", - }); - expect(getSessionEntry({ sessionKey: activeSessionKey, storePath })).toMatchObject({ - model: "gpt-5.5", - sessionId: "session-active", - }); - }); - it("keeps deprecated whole-store mutations grouped as one compatibility operation", async () => { const firstSessionKey = "agent:main:first"; const secondSessionKey = "agent:main:second"; diff --git a/src/proxy-capture/store.sqlite.ts b/src/proxy-capture/store.sqlite.ts index ad486370476c..99b6e14ce6b4 100644 --- a/src/proxy-capture/store.sqlite.ts +++ b/src/proxy-capture/store.sqlite.ts @@ -743,10 +743,12 @@ class DebugProxyCaptureStoreImpl { ) .get(...sessionIds) as { count: number } ).count ?? 0; - this.db.prepare(`DELETE FROM capture_events WHERE session_id IN (${placeholders})`).run( - ...sessionIds, - ); - this.db.prepare(`DELETE FROM capture_sessions WHERE id IN (${placeholders})`).run(...sessionIds); + this.db + .prepare(`DELETE FROM capture_events WHERE session_id IN (${placeholders})`) + .run(...sessionIds); + this.db + .prepare(`DELETE FROM capture_sessions WHERE id IN (${placeholders})`) + .run(...sessionIds); const candidateBlobIds = blobRows .map((row) => row.blobId?.trim()) .filter((blobId): blobId is string => Boolean(blobId)); @@ -783,10 +785,7 @@ class DebugProxyCaptureStoreImpl { } export type DebugProxyCaptureStore = Omit & { - persistPayload( - data: Buffer, - contentType?: string, - ): CaptureBlobRecord | SharedCaptureBlobRecord; + persistPayload(data: Buffer, contentType?: string): CaptureBlobRecord | SharedCaptureBlobRecord; }; export type LegacyDebugProxyCaptureStore = Omit & { @@ -860,7 +859,10 @@ export function closeDebugProxyCaptureStore(): void { // Lease API keeps one cached capture-store wrapper alive across related // operations, then releases it without closing the shared state database. -export function acquireDebugProxyCaptureStore(dbPath: string, blobDir: string): { +export function acquireDebugProxyCaptureStore( + dbPath: string, + blobDir: string, +): { store: LegacyDebugProxyCaptureStore; release: () => void; }; @@ -905,10 +907,7 @@ export function acquireDebugProxyCaptureStore( export function persistEventPayload( store: { - persistPayload( - data: Buffer, - contentType?: string, - ): CaptureBlobRecord | SharedCaptureBlobRecord; + persistPayload(data: Buffer, contentType?: string): CaptureBlobRecord | SharedCaptureBlobRecord; }, params: { data?: Buffer | string | null; contentType?: string; previewLimit?: number }, ): { dataText?: string; dataBlobId?: string; dataSha256?: string } {