惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
爱范儿
爱范儿
月光博客
月光博客
The GitHub Blog
The GitHub Blog
M
MIT News - Artificial intelligence
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog
T
Tailwind CSS Blog
美团技术团队
D
Docker
V
Visual Studio Blog
Martin Fowler
Martin Fowler
博客园 - 聂微东
The Cloudflare Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(agent-core): reject invalid session timestamps · open...
steipete · 2026-05-30 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -0,0 +1,10 @@

1+

import { describe, expect, it } from "vitest";

2+

import { createCustomMessage } from "./messages.js";

3+
4+

describe("harness message timestamps", () => {

5+

it("rejects invalid timestamps before creating context messages", () => {

6+

expect(() => createCustomMessage("note", "content", true, {}, "not-a-date")).toThrow(

7+

"custom message timestamp must be a valid timestamp",

8+

);

9+

});

10+

});

Original file line numberDiff line numberDiff line change

@@ -1,5 +1,6 @@

11

import type { ImageContent, Message, TextContent } from "../llm.js";

22

import type { AgentMessage } from "../types.js";

3+

import { requireSessionTimestampMs } from "./session/timestamps.js";

34
45

export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:

56

@@ -87,7 +88,7 @@ export function createBranchSummaryMessage(

8788

role: "branchSummary",

8889

summary,

8990

fromId,

90-

timestamp: new Date(timestamp).getTime(),

91+

timestamp: requireSessionTimestampMs(timestamp, "branch summary timestamp"),

9192

};

9293

}

9394

@@ -100,7 +101,7 @@ export function createCompactionSummaryMessage(

100101

role: "compactionSummary",

101102

summary,

102103

tokensBefore,

103-

timestamp: new Date(timestamp).getTime(),

104+

timestamp: requireSessionTimestampMs(timestamp, "compaction summary timestamp"),

104105

};

105106

}

106107

@@ -117,7 +118,7 @@ export function createCustomMessage(

117118

content,

118119

display,

119120

details,

120-

timestamp: new Date(timestamp).getTime(),

121+

timestamp: requireSessionTimestampMs(timestamp, "custom message timestamp"),

121122

};

122123

}

123124
Original file line numberDiff line numberDiff line change

@@ -15,6 +15,7 @@ import {

1515

getFileSystemResultOrThrow,

1616

toSession,

1717

} from "./repo-utils.js";

18+

import { parseSessionTimestampMs } from "./timestamps.js";

1819
1920

type JsonlSessionRepoFileSystem = Pick<

2021

FileSystem,

@@ -135,7 +136,11 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi {

135136

}

136137

}

137138

}

138-

sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

139+

sessions.sort(

140+

(a, b) =>

141+

(parseSessionTimestampMs(b.createdAt) ?? Number.NEGATIVE_INFINITY) -

142+

(parseSessionTimestampMs(a.createdAt) ?? Number.NEGATIVE_INFINITY),

143+

);

139144

return sessions;

140145

}

141146
Original file line numberDiff line numberDiff line change

@@ -0,0 +1,57 @@

1+

import { describe, expect, it } from "vitest";

2+

import { ok, type FileSystem } from "../types.js";

3+

import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.js";

4+
5+

type JsonlStorageFs = Pick<

6+

FileSystem,

7+

"readTextFile" | "readTextLines" | "writeFile" | "appendFile"

8+

>;

9+
10+

function createReadOnlyFs(content: string): JsonlStorageFs {

11+

return {

12+

readTextFile: async () => ok(content),

13+

readTextLines: async (_path, options) => ok(content.split("\n").slice(0, options?.maxLines)),

14+

writeFile: async () => ok(undefined),

15+

appendFile: async () => ok(undefined),

16+

};

17+

}

18+
19+

describe("JsonlSessionStorage timestamps", () => {

20+

it("rejects invalid session header timestamps", async () => {

21+

const fs = createReadOnlyFs(

22+

`${JSON.stringify({

23+

type: "session",

24+

version: 3,

25+

id: "session-1",

26+

timestamp: "not-a-date",

27+

cwd: "/repo",

28+

})}\n`,

29+

);

30+
31+

await expect(loadJsonlSessionMetadata(fs, "/sessions/invalid.jsonl")).rejects.toThrow(

32+

"session header has invalid timestamp",

33+

);

34+

});

35+
36+

it("rejects invalid entry timestamps", async () => {

37+

const fs = createReadOnlyFs(

38+

`${JSON.stringify({

39+

type: "session",

40+

version: 3,

41+

id: "session-1",

42+

timestamp: "2026-01-01T00:00:00.000Z",

43+

cwd: "/repo",

44+

})}\n${JSON.stringify({

45+

type: "custom",

46+

id: "entry-1",

47+

parentId: null,

48+

timestamp: "not-a-date",

49+

customType: "note",

50+

})}\n`,

51+

);

52+
53+

await expect(JsonlSessionStorage.open(fs, "/sessions/invalid-entry.jsonl")).rejects.toThrow(

54+

"line 2 has invalid timestamp",

55+

);

56+

});

57+

});

Original file line numberDiff line numberDiff line change

@@ -2,6 +2,7 @@ import type { FileSystem, JsonlSessionMetadata, SessionTreeEntry } from "../type

22

import { SessionError, toError } from "../types.js";

33

import { getFileSystemResultOrThrow } from "./repo-utils.js";

44

import { BaseSessionStorage, leafIdAfterEntry } from "./storage-base.js";

5+

import { parseSessionTimestampMs } from "./timestamps.js";

56
67

type JsonlSessionStorageFileSystem = Pick<

78

FileSystem,

@@ -64,6 +65,9 @@ function parseHeaderLine(line: string, filePath: string): SessionHeader {

6465

if (typeof parsed.timestamp !== "string" || !parsed.timestamp) {

6566

throw invalidSession(filePath, "session header is missing timestamp");

6667

}

68+

if (parseSessionTimestampMs(parsed.timestamp) === undefined) {

69+

throw invalidSession(filePath, "session header has invalid timestamp");

70+

}

6771

if (typeof parsed.cwd !== "string" || !parsed.cwd) {

6872

throw invalidSession(filePath, "session header is missing cwd");

6973

}

@@ -102,6 +106,9 @@ function parseEntryLine(line: string, filePath: string, lineNumber: number): Ses

102106

if (typeof parsed.timestamp !== "string" || !parsed.timestamp) {

103107

throw invalidEntry(filePath, lineNumber, "is missing timestamp");

104108

}

109+

if (parseSessionTimestampMs(parsed.timestamp) === undefined) {

110+

throw invalidEntry(filePath, lineNumber, "has invalid timestamp");

111+

}

105112

if (parsed.type === "leaf" && parsed.targetId !== null && typeof parsed.targetId !== "string") {

106113

throw invalidEntry(filePath, lineNumber, "has invalid targetId");

107114

}

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,15 @@

1+

export function parseSessionTimestampMs(value: unknown): number | undefined {

2+

if (typeof value !== "string" || !value.trim()) {

3+

return undefined;

4+

}

5+

const parsed = Date.parse(value);

6+

return Number.isFinite(parsed) ? parsed : undefined;

7+

}

8+
9+

export function requireSessionTimestampMs(value: string, label: string): number {

10+

const parsed = parseSessionTimestampMs(value);

11+

if (parsed === undefined) {

12+

throw new Error(`${label} must be a valid timestamp`);

13+

}

14+

return parsed;

15+

}