mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(sessions): ignore future freshness timestamps
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future `updatedAt` values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.
|
||||
- Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.
|
||||
- WebChat/TTS: persist automatic final-mode TTS audio as a supplemental audio-only transcript update instead of adding a second assistant message with the same visible text. Fixes #72830. Thanks @lhtpluto.
|
||||
- Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as `tsserver` do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.
|
||||
|
||||
@@ -76,8 +76,10 @@ export function evaluateSessionFreshness(params: {
|
||||
now: number;
|
||||
policy: SessionResetPolicy;
|
||||
}): SessionFreshness {
|
||||
const sessionStartedAt = resolveTimestamp(params.sessionStartedAt) ?? params.updatedAt;
|
||||
const lastInteractionAt = resolveTimestamp(params.lastInteractionAt) ?? sessionStartedAt;
|
||||
const updatedAt = resolveTimestamp(params.updatedAt, params.now) ?? 0;
|
||||
const sessionStartedAt = resolveTimestamp(params.sessionStartedAt, params.now) ?? updatedAt;
|
||||
const lastInteractionAt =
|
||||
resolveTimestamp(params.lastInteractionAt, params.now) ?? sessionStartedAt;
|
||||
const dailyResetAt =
|
||||
params.policy.mode === "daily"
|
||||
? resolveDailyResetAtMs(params.now, params.policy.atHour)
|
||||
@@ -95,8 +97,14 @@ export function evaluateSessionFreshness(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTimestamp(value: number | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
function resolveTimestamp(value: number | undefined, now?: number): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof now === "number" && Number.isFinite(now) && value > now) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeResetAtHour(value: number | undefined): number {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { evaluateSessionFreshness, resolveSessionResetPolicy } from "./reset.js"
|
||||
import { resolveAndPersistSessionFile } from "./session-file.js";
|
||||
import { clearSessionStoreCacheForTest, loadSessionStore, updateSessionStore } from "./store.js";
|
||||
import { useTempSessionsFixture } from "./test-helpers.js";
|
||||
import { mergeSessionEntry, type SessionEntry } from "./types.js";
|
||||
import { mergeSessionEntry, mergeSessionEntryWithPolicy, type SessionEntry } from "./types.js";
|
||||
|
||||
describe("session path safety", () => {
|
||||
it("rejects unsafe session IDs", () => {
|
||||
@@ -202,6 +202,38 @@ describe("resolveSessionResetPolicy", () => {
|
||||
idleExpiresAt: 5 * 60_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let future legacy updatedAt values keep daily sessions fresh", () => {
|
||||
const now = new Date(2026, 3, 25, 12, 0, 0, 0).getTime();
|
||||
const freshness = evaluateSessionFreshness({
|
||||
updatedAt: now + 30 * 24 * 60 * 60_000,
|
||||
now,
|
||||
policy: {
|
||||
mode: "daily",
|
||||
atHour: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(freshness.fresh).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let future legacy updatedAt values keep idle sessions fresh", () => {
|
||||
const now = 60 * 60_000;
|
||||
const freshness = evaluateSessionFreshness({
|
||||
updatedAt: now + 30 * 24 * 60 * 60_000,
|
||||
now,
|
||||
policy: {
|
||||
mode: "idle",
|
||||
atHour: 4,
|
||||
idleMinutes: 5,
|
||||
},
|
||||
});
|
||||
|
||||
expect(freshness).toMatchObject({
|
||||
fresh: false,
|
||||
idleExpiresAt: 5 * 60_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("session lifecycle timestamps", () => {
|
||||
@@ -349,6 +381,36 @@ describe("session store lock (Promise chain mutex)", () => {
|
||||
expect(merged.modelProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("caps future updatedAt values at the session merge boundary", () => {
|
||||
const now = 1_000;
|
||||
const merged = mergeSessionEntryWithPolicy(
|
||||
{
|
||||
sessionId: "sess-future",
|
||||
updatedAt: now + 10_000,
|
||||
},
|
||||
{
|
||||
updatedAt: now + 20_000,
|
||||
},
|
||||
{ now },
|
||||
);
|
||||
|
||||
expect(merged.updatedAt).toBe(now);
|
||||
});
|
||||
|
||||
it("caps future updatedAt values while preserving activity", () => {
|
||||
const now = 1_000;
|
||||
const merged = mergeSessionEntryWithPolicy(
|
||||
{
|
||||
sessionId: "sess-preserve-future",
|
||||
updatedAt: now + 10_000,
|
||||
},
|
||||
{},
|
||||
{ now, policy: "preserve-activity" },
|
||||
);
|
||||
|
||||
expect(merged.updatedAt).toBe(now);
|
||||
});
|
||||
|
||||
it("normalizes orphan modelProvider fields at store write boundary", async () => {
|
||||
const key = "agent:main:orphan-provider";
|
||||
const { storePath } = await makeTmpStore({
|
||||
|
||||
@@ -379,10 +379,20 @@ function resolveMergedUpdatedAt(
|
||||
patch: Partial<SessionEntry>,
|
||||
options?: MergeSessionEntryOptions,
|
||||
): number {
|
||||
const now = options?.now ?? Date.now();
|
||||
const existingUpdatedAt = normalizeMergedUpdatedAt(existing?.updatedAt, now);
|
||||
const patchUpdatedAt = normalizeMergedUpdatedAt(patch.updatedAt, now);
|
||||
if (options?.policy === "preserve-activity" && existing) {
|
||||
return existing.updatedAt ?? patch.updatedAt ?? options.now ?? Date.now();
|
||||
return existingUpdatedAt ?? patchUpdatedAt ?? now;
|
||||
}
|
||||
return Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, options?.now ?? Date.now());
|
||||
return Math.max(existingUpdatedAt ?? 0, patchUpdatedAt ?? 0, now);
|
||||
}
|
||||
|
||||
function normalizeMergedUpdatedAt(value: number | undefined, now: number): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.min(value, now);
|
||||
}
|
||||
|
||||
export function mergeSessionEntryWithPolicy(
|
||||
|
||||
Reference in New Issue
Block a user