mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: keep model-run pruning internal
This commit is contained in:
@@ -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-<uuid>`. 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.
|
||||
|
||||
@@ -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-<uuid>` 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.
|
||||
|
||||
@@ -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-<uuid>` 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.<timestamp>` 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.
|
||||
|
||||
@@ -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-<uuid>` and runs before global stale-entry cleanup/capping when it runs.
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` 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-<uuid>`; 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.
|
||||
|
||||
|
||||
@@ -451,9 +451,9 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig);
|
||||
const channel = (res.config.channels as Record<string, { accounts?: Record<string, unknown> }>)?.[
|
||||
channelId
|
||||
];
|
||||
const channel = (
|
||||
res.config.channels as Record<string, { accounts?: Record<string, unknown> }>
|
||||
)?.[channelId];
|
||||
|
||||
expect(channel?.accounts?.default).toEqual({
|
||||
dmPolicy: "allowlist",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1688,8 +1688,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"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-<uuid>`). 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":
|
||||
|
||||
@@ -850,7 +850,6 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ResolvedSessionMaintenanceConfig, "maxEntries">;
|
||||
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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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-<uuid>`).
|
||||
* 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;
|
||||
/**
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) ?? "", {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<DebugProxyCaptureStoreImpl, "persistPayload"> & {
|
||||
persistPayload(
|
||||
data: Buffer,
|
||||
contentType?: string,
|
||||
): CaptureBlobRecord | SharedCaptureBlobRecord;
|
||||
persistPayload(data: Buffer, contentType?: string): CaptureBlobRecord | SharedCaptureBlobRecord;
|
||||
};
|
||||
|
||||
export type LegacyDebugProxyCaptureStore = Omit<DebugProxyCaptureStoreImpl, "persistPayload"> & {
|
||||
@@ -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 } {
|
||||
|
||||
Reference in New Issue
Block a user