From 0324bd40c99cd34f4b7d581d74eb2481275a360f Mon Sep 17 00:00:00 2001 From: wlff123 Date: Tue, 7 Apr 2026 22:45:08 +0800 Subject: [PATCH] add create time in add_message --- examples/openclaw-plugin/client.ts | 19 ++++++++++-- examples/openclaw-plugin/context-engine.ts | 30 +++++++++++++++++-- .../tests/ut/context-engine-afterTurn.test.ts | 21 +++++++++++++ .../ut/plugin-normal-flow-real-server.test.ts | 14 +++++++-- 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/examples/openclaw-plugin/client.ts b/examples/openclaw-plugin/client.ts index 375a68937..5d2bae044 100644 --- a/examples/openclaw-plugin/client.ts +++ b/examples/openclaw-plugin/client.ts @@ -375,7 +375,21 @@ export class OpenVikingClient { ); } - async addSessionMessage(sessionId: string, role: string, content: string, agentId?: string): Promise { + async addSessionMessage( + sessionId: string, + role: string, + content: string, + agentId?: string, + createdAt?: string, + ): Promise { + const body: { + role: string; + content: string; + created_at?: string; + } = { role, content }; + if (createdAt) { + body.created_at = createdAt; + } await this.emitRoutingDebug( "session message POST", { @@ -383,6 +397,7 @@ export class OpenVikingClient { sessionId, role, contentChars: content.length, + created_at: createdAt ?? null, }, agentId, ); @@ -390,7 +405,7 @@ export class OpenVikingClient { `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, { method: "POST", - body: JSON.stringify({ role, content }), + body: JSON.stringify(body), }, agentId, ); diff --git a/examples/openclaw-plugin/context-engine.ts b/examples/openclaw-plugin/context-engine.ts index 244f75ad0..483c691fa 100644 --- a/examples/openclaw-plugin/context-engine.ts +++ b/examples/openclaw-plugin/context-engine.ts @@ -16,6 +16,7 @@ import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js"; type AgentMessage = { role?: string; content?: unknown; + timestamp?: unknown; }; type ContextEngineInfo = { @@ -116,6 +117,29 @@ function msgTokenEstimate(msg: AgentMessage): number { return 1; } +function normalizeTimestamp(value: unknown): string | undefined { + if (typeof value === "number" && Number.isFinite(value)) { + const timestampMs = Math.abs(value) < 100_000_000_000 ? value * 1000 : value; + return new Date(timestampMs).toISOString(); + } + return undefined; +} + +function pickLatestCreatedAt(messages: AgentMessage[]): string | undefined { + for (let i = messages.length - 1; i >= 0; i -= 1) { + const message = messages[i] as Record; + const role = typeof message.role === "string" ? message.role : ""; + if (!role || role === "system") { + continue; + } + const normalized = normalizeTimestamp(message.timestamp); + if (normalized) { + return normalized; + } + } + return undefined; +} + function messageDigest(messages: AgentMessage[], maxCharsPerMsg = 2000): Array<{role: string; content: string; tokens: number; truncated: boolean}> { return messages.map((msg) => { const m = msg as Record; @@ -817,7 +841,8 @@ export function createMemoryOpenVikingContextEngine(params: { return; } - const newMessages = messages.slice(start).filter((m: any) => { + const turnMessages = messages.slice(start) as AgentMessage[]; + const newMessages = turnMessages.filter((m: any) => { const r = (m as Record).role as string; return r === "user" || r === "assistant"; }) as AgentMessage[]; @@ -842,9 +867,10 @@ export function createMemoryOpenVikingContextEngine(params: { const client = await getClient(); const turnText = newTexts.join("\n"); const sanitized = turnText.replace(/[\s\S]*?<\/relevant-memories>/gi, " ").replace(/\s+/g, " ").trim(); + const createdAt = pickLatestCreatedAt(turnMessages); if (sanitized) { - await client.addSessionMessage(OVSessionId, "user", sanitized, agentId); + await client.addSessionMessage(OVSessionId, "user", sanitized, agentId, createdAt); } else { diag("afterTurn_skip", OVSessionId, { reason: "sanitized_empty", diff --git a/examples/openclaw-plugin/tests/ut/context-engine-afterTurn.test.ts b/examples/openclaw-plugin/tests/ut/context-engine-afterTurn.test.ts index 23d9b95c0..768cd560f 100644 --- a/examples/openclaw-plugin/tests/ut/context-engine-afterTurn.test.ts +++ b/examples/openclaw-plugin/tests/ut/context-engine-afterTurn.test.ts @@ -204,6 +204,27 @@ describe("context-engine afterTurn()", () => { expect(storedContent).toContain("hi there"); }); + it("passes the latest non-system message timestamp to addSessionMessage as ISO string", async () => { + const { engine, client } = makeEngine(); + + await engine.afterTurn!({ + sessionId: "s1", + sessionFile: "", + messages: [ + { role: "user", content: "old message", timestamp: 1775037600000 }, + { role: "user", content: "new message", timestamp: 1775037660000 }, + { role: "assistant", content: "new reply", timestamp: 1775037720000 }, + { role: "toolResult", toolName: "bash", content: "exit 0", timestamp: 1775037780000 }, + { role: "system", content: "ignored system message", timestamp: 1775037840000 }, + ], + prePromptMessageCount: 1, + }); + + expect(client.addSessionMessage).toHaveBeenCalledTimes(1); + const createdAt = client.addSessionMessage.mock.calls[0][4] as string; + expect(createdAt).toBe("2026-04-01T10:03:00.000Z"); + }); + it("sanitizes from stored content", async () => { const { engine, client } = makeEngine(); diff --git a/examples/openclaw-plugin/tests/ut/plugin-normal-flow-real-server.test.ts b/examples/openclaw-plugin/tests/ut/plugin-normal-flow-real-server.test.ts index 96544073c..47f2b169d 100644 --- a/examples/openclaw-plugin/tests/ut/plugin-normal-flow-real-server.test.ts +++ b/examples/openclaw-plugin/tests/ut/plugin-normal-flow-real-server.test.ts @@ -237,7 +237,7 @@ describe("plugin normal flow with healthy backend", () => { afterTurn: (params: { sessionId: string; sessionFile: string; - messages: Array<{ role: string; content: unknown }>; + messages: Array<{ role: string; content: unknown; timestamp?: number }>; prePromptMessageCount: number; }) => Promise; }; @@ -260,8 +260,8 @@ describe("plugin normal flow with healthy backend", () => { sessionId: "session-normal", sessionFile: "", messages: [ - { role: "user", content: "Please keep using Rust." }, - { role: "assistant", content: [{ type: "text", text: "Understood." }] }, + { role: "user", content: "Please keep using Rust.", timestamp: Date.parse("2026-04-07T08:00:00Z") }, + { role: "assistant", content: [{ type: "text", text: "Understood." }], timestamp: Date.parse("2026-04-07T08:00:01Z") }, ], prePromptMessageCount: 0, }); @@ -276,6 +276,14 @@ describe("plugin normal flow with healthy backend", () => { expect( requests.some((entry) => entry.method === "POST" && entry.path === "/api/v1/sessions/session-normal/messages"), ).toBe(true); + const addMessageRequest = requests.find( + (entry) => entry.method === "POST" && entry.path === "/api/v1/sessions/session-normal/messages", + ); + expect(addMessageRequest).toBeTruthy(); + expect(JSON.parse(addMessageRequest!.body ?? "{}")).toMatchObject({ + role: "user", + created_at: "2026-04-07T08:00:01.000Z", + }); expect( requests.some((entry) => entry.method === "POST" && entry.path === "/api/v1/sessions/session-normal/commit"), ).toBe(true);