Skip to content

Commit 0bae380

Browse files
committed
feat(openclaw): label Current time as UTC, allow disabling per-memory timestamps
Two timestamp-related fixes for the OpenClaw integration: 1. Append " UTC" to the "Current time -" header injected above recalled memories. Without the label the LLM read the timestamp as local time and made wrong recency judgments. This is the same fix that landed for Claude Code in #1568 — the OpenClaw integration was overlooked. 2. New `showTimestamps` boolean in PluginConfig (default true) lets users suppress the per-memory `(YYYY-MM-DDTHH:MM:SS+00:00)` suffix produced by formatMemories. OpenClaw already injects a session-level timestamp (e.g. `[Thu 2026-05-28 00:27 GMT+8]`) into the prompt; the per-memory UTC offsets are redundant, easy to misparse, and waste tokens. The default preserves existing behavior; opt-in suppression mirrors the issue's requested config shape. Closes #1789
1 parent 78c3525 commit 0bae380

4 files changed

Lines changed: 79 additions & 5 deletions

File tree

hindsight-integrations/openclaw/openclaw.plugin.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@
246246
"description": "Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.",
247247
"default": "prepend"
248248
},
249+
"showTimestamps": {
250+
"type": "boolean",
251+
"description": "When true (default), each injected memory line ends with `(mentioned_at)` so the LLM can reason about recency. Set false when the host already injects a session-level timestamp and the per-memory UTC offsets are redundant.",
252+
"default": true
253+
},
249254
"debug": {
250255
"type": "boolean",
251256
"description": "Enable debug logging for Hindsight plugin operations. Equivalent to logLevel: 'debug'.",
@@ -467,6 +472,10 @@
467472
"label": "Recall Injection Position",
468473
"placeholder": "prepend, append, or user"
469474
},
475+
"showTimestamps": {
476+
"label": "Show Per-Memory Timestamps",
477+
"placeholder": "true (default) — disable when host already shows a session timestamp"
478+
},
470479
"debug": {
471480
"label": "Debug"
472481
},

hindsight-integrations/openclaw/src/index.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
22
import {
33
stripMemoryTags,
44
extractRecallQuery,
5+
formatCurrentTimeForRecall,
56
formatMemories,
67
prepareRetentionTranscript,
78
countUserTurns,
@@ -247,6 +248,54 @@ describe("formatMemories", () => {
247248
it("returns empty string for empty memories", () => {
248249
expect(formatMemories([])).toBe("");
249250
});
251+
252+
it("omits per-memory timestamps when showTimestamps is false (#1789)", () => {
253+
const memories: MemoryResult[] = [
254+
makeMemoryResult({
255+
id: "1",
256+
text: "User prefers dark mode",
257+
type: "world",
258+
mentioned_at: "2023-01-01T12:00:00Z",
259+
}),
260+
makeMemoryResult({
261+
id: "2",
262+
text: "User is learning Rust",
263+
type: "experience",
264+
mentioned_at: "2023-02-01T12:00:00Z",
265+
}),
266+
];
267+
const output = formatMemories(memories, { showTimestamps: false });
268+
expect(output).toBe("- User prefers dark mode [world]\n\n- User is learning Rust [experience]");
269+
});
270+
271+
it("keeps per-memory timestamps when showTimestamps is true (explicit default)", () => {
272+
const memories: MemoryResult[] = [
273+
makeMemoryResult({
274+
id: "1",
275+
text: "User prefers dark mode",
276+
type: "world",
277+
mentioned_at: "2023-01-01T12:00:00Z",
278+
}),
279+
];
280+
const output = formatMemories(memories, { showTimestamps: true });
281+
expect(output).toBe("- User prefers dark mode [world] (2023-01-01T12:00:00Z)");
282+
});
283+
});
284+
285+
// ---------------------------------------------------------------------------
286+
// formatCurrentTimeForRecall — UTC label (#1789, mirrors #1568)
287+
// ---------------------------------------------------------------------------
288+
289+
describe("formatCurrentTimeForRecall", () => {
290+
it("renders the UTC suffix so the LLM doesn't misread it as local time", () => {
291+
const fixed = new Date(Date.UTC(2026, 4, 27, 16, 25, 0)); // 2026-05-27 16:25 UTC
292+
expect(formatCurrentTimeForRecall(fixed)).toBe("2026-05-27 16:25 UTC");
293+
});
294+
295+
it("zero-pads month / day / hours / minutes", () => {
296+
const fixed = new Date(Date.UTC(2026, 0, 3, 4, 5, 0));
297+
expect(formatCurrentTimeForRecall(fixed)).toBe("2026-01-03 04:05 UTC");
298+
});
250299
});
251300

252301
// ---------------------------------------------------------------------------

hindsight-integrations/openclaw/src/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,16 @@ async function flushRetainQueue(): Promise<void> {
316316
const DEFAULT_RECALL_PROMPT_PREAMBLE =
317317
"Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:";
318318

319-
function formatCurrentTimeForRecall(date = new Date()): string {
319+
export function formatCurrentTimeForRecall(date = new Date()): string {
320320
const year = date.getUTCFullYear();
321321
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
322322
const day = String(date.getUTCDate()).padStart(2, "0");
323323
const hours = String(date.getUTCHours()).padStart(2, "0");
324324
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
325-
return `${year}-${month}-${day} ${hours}:${minutes}`;
325+
// Suffix with " UTC" so the LLM doesn't misread the timestamp as local
326+
// time and make wrong recency judgments. Mirrors the fix landed for the
327+
// Claude Code integration in #1568. (#1789)
328+
return `${year}-${month}-${day} ${hours}:${minutes} UTC`;
326329
}
327330

328331
/**
@@ -1143,12 +1146,21 @@ export function deriveBankId(
11431146
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-${baseBankId}` : baseBankId;
11441147
}
11451148

1146-
export function formatMemories(results: MemoryResult[]): string {
1149+
export function formatMemories(
1150+
results: MemoryResult[],
1151+
options: { showTimestamps?: boolean } = {}
1152+
): string {
11471153
if (!results || results.length === 0) return "";
1154+
// Default true preserves the existing per-memory timestamp suffix. Hosts
1155+
// that already inject a session-level timestamp (e.g. OpenClaw's
1156+
// `[Thu 2026-05-28 00:27 GMT+8]` header) can set `showTimestamps: false`
1157+
// to drop the redundant `(YYYY-MM-DDTHH:MM:SS+00:00)` suffix on each
1158+
// memory line. (#1789)
1159+
const showTimestamps = options.showTimestamps !== false;
11481160
return results
11491161
.map((r) => {
11501162
const type = r.type ? ` [${r.type}]` : "";
1151-
const date = r.mentioned_at ? ` (${r.mentioned_at})` : "";
1163+
const date = showTimestamps && r.mentioned_at ? ` (${r.mentioned_at})` : "";
11521164
return `- ${r.text}${type}${date}`;
11531165
})
11541166
.join("\n\n");
@@ -1478,6 +1490,7 @@ export function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
14781490
["prepend", "append", "user"].includes(config.recallInjectionPosition)
14791491
? (config.recallInjectionPosition as PluginConfig["recallInjectionPosition"])
14801492
: undefined,
1493+
showTimestamps: config.showTimestamps !== false,
14811494
recallTimeoutMs:
14821495
typeof config.recallTimeoutMs === "number" && config.recallTimeoutMs >= 1000
14831496
? config.recallTimeoutMs
@@ -2176,7 +2189,9 @@ export default function (api: MoltbotPluginAPI) {
21762189
);
21772190

21782191
// Format memories as JSON with all fields from recall
2179-
const memoriesFormatted = formatMemories(results);
2192+
const memoriesFormatted = formatMemories(results, {
2193+
showTimestamps: pluginConfig.showTimestamps,
2194+
});
21802195

21812196
const contextMessage = `<hindsight_memories>
21822197
${pluginConfig.recallPromptPreamble || DEFAULT_RECALL_PROMPT_PREAMBLE}

hindsight-integrations/openclaw/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface PluginConfig {
117117
recallMaxQueryChars?: number; // Max chars for composed recall query. Default: 800
118118
recallPromptPreamble?: string; // Prompt preamble placed above recalled memories. Default: built-in guidance text.
119119
recallInjectionPosition?: "prepend" | "append" | "user"; // Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.
120+
showTimestamps?: boolean; // When true (default), each injected memory line ends with `(mentioned_at)` so the LLM can reason about recency. Set false when the host (e.g. OpenClaw) already injects a session-level timestamp and the per-memory UTC offsets are redundant/noisy. Default: true.
120121
ignoreSessionPatterns?: string[]; // Session key glob patterns to skip entirely (no recall, no retain). E.g. ["agent:main:**", "agent:*:cron:**"]
121122
statelessSessionPatterns?: string[]; // Session key glob patterns for read-only sessions (recall allowed, retain skipped). E.g. ["agent:*:subagent:**"]
122123
skipStatelessSessions?: boolean; // When true (default), stateless sessions also skip recall. When false, they recall but never retain.

0 commit comments

Comments
 (0)