Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions examples/openclaw-plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,22 +375,37 @@ export class OpenVikingClient {
);
}

async addSessionMessage(sessionId: string, role: string, content: string, agentId?: string): Promise<void> {
async addSessionMessage(
sessionId: string,
role: string,
content: string,
agentId?: string,
createdAt?: string,
): Promise<void> {
const body: {
role: string;
content: string;
created_at?: string;
} = { role, content };
if (createdAt) {
body.created_at = createdAt;
}
await this.emitRoutingDebug(
"session message POST",
{
path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
sessionId,
role,
contentChars: content.length,
created_at: createdAt ?? null,
},
agentId,
);
await this.request<{ session_id: string }>(
`/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
{
method: "POST",
body: JSON.stringify({ role, content }),
body: JSON.stringify(body),
},
agentId,
);
Expand Down
30 changes: 28 additions & 2 deletions examples/openclaw-plugin/context-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js";
type AgentMessage = {
role?: string;
content?: unknown;
timestamp?: unknown;
};

type ContextEngineInfo = {
Expand Down Expand Up @@ -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<string, unknown>;
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<string, unknown>;
Expand Down Expand Up @@ -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<string, unknown>).role as string;
return r === "user" || r === "assistant";
}) as AgentMessage[];
Expand All @@ -842,9 +867,10 @@ export function createMemoryOpenVikingContextEngine(params: {
const client = await getClient();
const turnText = newTexts.join("\n");
const sanitized = turnText.replace(/<relevant-memories>[\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",
Expand Down
21 changes: 21 additions & 0 deletions examples/openclaw-plugin/tests/ut/context-engine-afterTurn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <relevant-memories> from stored content", async () => {
const { engine, client } = makeEngine();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
};
Expand All @@ -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,
});
Expand All @@ -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);
Expand Down
Loading