diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 0e240309057..a93ec2f6f85 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -1,4 +1,3 @@ -import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { acquireSessionWriteLock } from "../../agents/session-write-lock.js"; @@ -39,6 +38,7 @@ import { import { applySessionStoreMigrations } from "./store-migrations.js"; import { mergeSessionEntry, + mergeSessionEntryPreserveActivity, normalizeSessionRuntimeModelFields, type SessionEntry, } from "./types.js"; @@ -738,14 +738,9 @@ export async function recordSessionMetaFromInbound(params: { return null; } const next = existing - ? normalizeSessionRuntimeModelFields({ - ...existing, - ...patch, - // Inbound metadata updates must not refresh activity timestamps; - // idle reset evaluation relies on updatedAt from actual session turns. - sessionId: existing.sessionId ?? crypto.randomUUID(), - updatedAt: existing.updatedAt ?? Date.now(), - }) + ? // Inbound metadata updates must not refresh activity timestamps; + // idle reset evaluation relies on updatedAt from actual session turns. + mergeSessionEntryPreserveActivity(existing, patch) : mergeSessionEntry(existing, patch); store[resolved.normalizedKey] = next; for (const legacyKey of resolved.legacyKeys) { diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index c62ab8ff966..a8fa15278c6 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -225,12 +225,31 @@ export function setSessionRuntimeModel( return true; } -export function mergeSessionEntry( +export type SessionEntryMergePolicy = "touch-activity" | "preserve-activity"; + +type MergeSessionEntryOptions = { + policy?: SessionEntryMergePolicy; + now?: number; +}; + +function resolveMergedUpdatedAt( existing: SessionEntry | undefined, patch: Partial, + options?: MergeSessionEntryOptions, +): number { + if (options?.policy === "preserve-activity" && existing) { + return existing.updatedAt ?? patch.updatedAt ?? options.now ?? Date.now(); + } + return Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, options?.now ?? Date.now()); +} + +export function mergeSessionEntryWithPolicy( + existing: SessionEntry | undefined, + patch: Partial, + options?: MergeSessionEntryOptions, ): SessionEntry { const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID(); - const updatedAt = Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, Date.now()); + const updatedAt = resolveMergedUpdatedAt(existing, patch, options); if (!existing) { return normalizeSessionRuntimeModelFields({ ...patch, sessionId, updatedAt }); } @@ -248,6 +267,22 @@ export function mergeSessionEntry( return normalizeSessionRuntimeModelFields(next); } +export function mergeSessionEntry( + existing: SessionEntry | undefined, + patch: Partial, +): SessionEntry { + return mergeSessionEntryWithPolicy(existing, patch); +} + +export function mergeSessionEntryPreserveActivity( + existing: SessionEntry | undefined, + patch: Partial, +): SessionEntry { + return mergeSessionEntryWithPolicy(existing, patch, { + policy: "preserve-activity", + }); +} + export function resolveFreshSessionTotalTokens( entry?: Pick | null, ): number | undefined {