diff --git a/client.ts b/client.ts index 47d32d1..e900175 100644 --- a/client.ts +++ b/client.ts @@ -6,6 +6,7 @@ import { } from "./lib/validate.js" import { log } from "./logger.ts" import { clampEntityContext } from "./memory.ts" +import { buildAddMemoryMetadata } from "./metadata.ts" export type SearchResult = { id: string @@ -72,14 +73,7 @@ export class SupermemoryClient { const cleaned = sanitizeContent(content) const tag = containerTag ?? this.containerTag - // Always stamp `sm_source` so mono's `document.source` column attributes - // these writes to the OpenClaw plugin. Existing callers can still pass - // extra metadata (e.g. `source: "openclaw_tool"`) and it is preserved - // underneath the canonical `sm_source` key. - const mergedMetadata: Record = { - sm_source: "openclaw", - ...(metadata ?? {}), - } + const mergedMetadata = buildAddMemoryMetadata(metadata) log.debugRequest("add", { contentLength: cleaned.length, diff --git a/hooks/capture.ts b/hooks/capture.ts index d89d949..0fa2f9b 100644 --- a/hooks/capture.ts +++ b/hooks/capture.ts @@ -115,7 +115,7 @@ export function buildCaptureHandler( try { await client.addMemory( content, - { source: "openclaw", timestamp: new Date().toISOString() }, + { source: "openclaw" }, customId, undefined, cfg.entityContext, diff --git a/metadata.test.ts b/metadata.test.ts new file mode 100644 index 0000000..4889678 --- /dev/null +++ b/metadata.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test" +import { buildAddMemoryMetadata } from "./metadata.ts" + +const FIXED = new Date("2026-08-13T15:30:00.000Z") + +describe("buildAddMemoryMetadata", () => { + test("stamps sm_source and captured_at when metadata is omitted", () => { + expect(buildAddMemoryMetadata(undefined, () => FIXED)).toEqual({ + sm_source: "openclaw", + captured_at: "2026-08-13T15:30:00.000Z", + }) + }) + + test("preserves caller fields and fills missing captured_at", () => { + expect( + buildAddMemoryMetadata( + { type: "preference", source: "openclaw_command" }, + () => FIXED, + ), + ).toEqual({ + sm_source: "openclaw", + type: "preference", + source: "openclaw_command", + captured_at: "2026-08-13T15:30:00.000Z", + }) + }) + + test("keeps an explicit captured_at", () => { + expect( + buildAddMemoryMetadata( + { captured_at: "2026-07-01T00:00:00.000Z" }, + () => FIXED, + ), + ).toEqual({ + sm_source: "openclaw", + captured_at: "2026-07-01T00:00:00.000Z", + }) + }) + + test("replaces blank captured_at", () => { + expect(buildAddMemoryMetadata({ captured_at: " " }, () => FIXED)).toEqual({ + sm_source: "openclaw", + captured_at: "2026-08-13T15:30:00.000Z", + }) + }) +}) diff --git a/metadata.ts b/metadata.ts new file mode 100644 index 0000000..aeae7de --- /dev/null +++ b/metadata.ts @@ -0,0 +1,21 @@ +export type MemoryMetadata = Record + +/** Merge caller metadata with OpenClaw defaults for `add()`. */ +export function buildAddMemoryMetadata( + metadata?: MemoryMetadata, + now: () => Date = () => new Date(), +): MemoryMetadata { + const merged: MemoryMetadata = { + sm_source: "openclaw", + ...(metadata ?? {}), + } + + // Give extraction a real capture-time anchor (issue #61). Callers can still + // pass an explicit `captured_at` (e.g. when replaying older events). + const existing = merged.captured_at + if (typeof existing !== "string" || !existing.trim()) { + merged.captured_at = now().toISOString() + } + + return merged +} diff --git a/package.json b/package.json index 4cd2553..97c2275 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "config.ts", "logger.ts", "memory.ts", + "metadata.ts", "runtime.ts", "types", "lib/version-check.ts", @@ -32,6 +33,7 @@ "check-types": "tsc --noEmit", "lint": "bunx @biomejs/biome ci .", "lint:fix": "bunx @biomejs/biome check --write .", + "test": "bun test", "build:lib": "node -e \"const fs=require('node:fs'); fs.accessSync('lib/validate.js'); fs.accessSync('lib/validate.d.ts')\"", "prepack": "npm run build" }, diff --git a/tsconfig.json b/tsconfig.json index 852bfd0..cb687ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "lib/*.ts", "lib/*.d.ts" ], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] }