diff --git a/hindsight-integrations/openclaw/README.md b/hindsight-integrations/openclaw/README.md
index f2d573789..5c1e12d6a 100644
--- a/hindsight-integrations/openclaw/README.md
+++ b/hindsight-integrations/openclaw/README.md
@@ -95,6 +95,7 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh
| `bankIdPrefix` | — | Prefix for bank IDs (e.g. `"prod"`) |
| `retainTags` | `[]` | Tags applied to every retained document, useful for cross-agent/source labeling (e.g. `source_system:openclaw`, `agent:agentname`). Auto-retain also merges inline per-message tags from `...` or `...` blocks in user messages. |
| `retainSource` | `"openclaw"` | `source` value written into retained document metadata |
+| `retainContext` | built-in OpenClaw guidance | Interpretation guidance sent through the Hindsight retain API `context` field. The default tells the extraction LLM that `[context]` sender/channel/provider values, bank IDs, session keys, source systems, and tags are operational routing metadata, not human names, project names, or organizations. |
| `dynamicBankGranularity` | `["agent", "channel", "user"]` | Fields used to derive bank ID. Options: `agent`, `channel`, `user`, `provider` |
| `excludeProviders` | `["heartbeat"]` | Message providers to skip for recall/retain (e.g. `heartbeat`, `slack`, `telegram`, `discord`) |
| `autoRecall` | `true` | Auto-inject memories before each turn. Set to `false` when the agent has its own recall tool. |
@@ -162,6 +163,8 @@ Glob syntax:
Retained documents use stable session-scoped IDs derived from the OpenClaw `sessionKey`. By default (`retainDocumentScope: 'session'`) every retain in a session shares one document id like `openclaw:agent:agentname:discord:channel:123`, so all turns of the conversation accumulate under a single Hindsight document. Set `retainDocumentScope: 'turn'` to fall back to the per-retain ids (`...:turn:000001`, `...:window:000002` for chunked retention). Either way, retained documents include richer metadata such as `session_key`, `agent_id`, `provider`, `channel_id`, `thread_id`, `sender_id`, `turn_index`, and `retention_scope`. Each message in the retained JSON also carries a structured `timestamp` field (ISO 8601) lifted from OpenClaw's per-message time, so facts are not polluted by inline weekday/date prefixes.
+`retainContext` is sent separately from the transcript content and gives Hindsight's extraction LLM interpretation guidance for the retained document. The default is designed for OpenClaw transcripts: it explains that the optional `[context]` block is routing metadata, that assistant-role first-person statements belong to the AI assistant, and that bank IDs or tags should not be treated as the discussed project. This does not remove or change `includeSenderContext`; set `includeSenderContext: false` only if you want to omit the transcript's `[context]` block itself.
+
## Documentation
For full documentation, configuration options, troubleshooting, and development guide, see:
diff --git a/hindsight-integrations/openclaw/openclaw.plugin.json b/hindsight-integrations/openclaw/openclaw.plugin.json
index 7a67369c4..fc8087007 100644
--- a/hindsight-integrations/openclaw/openclaw.plugin.json
+++ b/hindsight-integrations/openclaw/openclaw.plugin.json
@@ -5,8 +5,14 @@
"configContracts": {
"secretInputs": {
"paths": [
- { "path": "llmApiKey", "expected": "string" },
- { "path": "hindsightApiToken", "expected": "string" }
+ {
+ "path": "llmApiKey",
+ "expected": "string"
+ },
+ {
+ "path": "hindsightApiToken",
+ "expected": "string"
+ }
]
}
},
@@ -112,6 +118,11 @@
"description": "Source value written into retained document metadata. Defaults to 'openclaw'.",
"default": "openclaw"
},
+ "retainContext": {
+ "type": "string",
+ "description": "Interpretation guidance sent via the Hindsight retain API context field. It tells the extraction LLM that OpenClaw sender/channel/provider values, bank IDs, session keys, source systems, and tags are operational routing metadata, not human names, project names, or organizations.",
+ "default": "This content is an AI-assistant conversation transcript from OpenClaw. The [context] block at the beginning of each turn contains routing identifiers: 'sender' is an opaque user ID (not a human name), 'channel' is a chat identifier, 'provider' is the messaging platform name. These are operational routing metadata, not semantic actors or people. Messages with role 'assistant' are from the AI assistant; first-person statements in assistant messages refer to the AI, not the human user. Messages with role 'user' are from the human user. Bank IDs, session keys, agent IDs, thread IDs, source systems, and tags in metadata are also operational routing identifiers, not human names, project names, or organizations."
+ },
"autoRecall": {
"type": "boolean",
"description": "Automatically recall memories on every prompt and inject them as context. Set to false when agent has its own recall tool.",
@@ -283,12 +294,16 @@
},
"ignoreSessionPatterns": {
"type": "array",
- "items": { "type": "string" },
+ "items": {
+ "type": "string"
+ },
"description": "Session key glob patterns to skip entirely (no recall, no retain). E.g. [\"agent:main:**\", \"agent:*:cron:**\"]. * matches non-colon chars, ** matches anything."
},
"statelessSessionPatterns": {
"type": "array",
- "items": { "type": "string" },
+ "items": {
+ "type": "string"
+ },
"description": "Session key glob patterns for read-only sessions: retain is always skipped, recall is skipped when skipStatelessSessions is true. E.g. [\"agent:*:subagent:**\", \"agent:*:heartbeat:**\"]."
},
"skipStatelessSessions": {
@@ -383,6 +398,10 @@
"label": "Retain Source",
"placeholder": "openclaw"
},
+ "retainContext": {
+ "label": "Retain Context",
+ "placeholder": "Guidance for interpreting retained OpenClaw transcripts"
+ },
"autoRecall": {
"label": "Auto-Recall",
"placeholder": "true (inject memories on every prompt)"
diff --git a/hindsight-integrations/openclaw/src/backfill.test.ts b/hindsight-integrations/openclaw/src/backfill.test.ts
index 789e77077..92f922b14 100644
--- a/hindsight-integrations/openclaw/src/backfill.test.ts
+++ b/hindsight-integrations/openclaw/src/backfill.test.ts
@@ -197,8 +197,9 @@ describe("backfill helpers", () => {
it("treats a symlinked bin path as direct execution", async () => {
const { isDirectExecution } = await import("./backfill.js");
const dir = mkdtempSync(join(tmpdir(), "hindsight-backfill-bin-"));
- const modulePath = join(process.cwd(), "dist", "backfill.js");
+ const modulePath = join(dir, "backfill.js");
const symlinkPath = join(dir, "hindsight-openclaw-backfill");
+ writeFileSync(modulePath, "#!/usr/bin/env node\n", "utf8");
symlinkSync(modulePath, symlinkPath);
const moduleUrl = pathToFileURL(modulePath).href;
diff --git a/hindsight-integrations/openclaw/src/backfill.ts b/hindsight-integrations/openclaw/src/backfill.ts
index eea034076..8befd03a6 100644
--- a/hindsight-integrations/openclaw/src/backfill.ts
+++ b/hindsight-integrations/openclaw/src/backfill.ts
@@ -17,7 +17,7 @@ function loadPackageVersion(): string {
}
const USER_AGENT = `hindsight-openclaw/${loadPackageVersion()}`;
-import { detectExternalApi, detectLLMConfig } from "./index.js";
+import { DEFAULT_RETAIN_CONTEXT, detectExternalApi, detectLLMConfig } from "./index.js";
import type { BankStats, PluginConfig } from "./types.js";
import {
buildBackfillPlan,
@@ -547,6 +547,7 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise {
expect(request).toEqual({
content: "hello world",
documentId: "openclaw:agent:main:main",
+ context: DEFAULT_RETAIN_CONTEXT,
metadata: {
retained_at: expect.any(String),
message_count: "2",
@@ -385,6 +399,44 @@ describe("buildRetainRequest", () => {
});
});
+ it("includes the default retain context guidance", () => {
+ const request = buildRetainRequest("hello world", 1, {}, {}, 1700000000000, { turnIndex: 1 });
+
+ expect(request.context).toBe(DEFAULT_RETAIN_CONTEXT);
+ });
+
+ it("describes routing metadata and assistant/user roles in the default retain context", () => {
+ expect(DEFAULT_RETAIN_CONTEXT).toContain("routing identifiers");
+ expect(DEFAULT_RETAIN_CONTEXT).toContain("operational routing identifiers");
+ expect(DEFAULT_RETAIN_CONTEXT).toContain("AI assistant");
+ });
+
+ it("uses a configured retain context when provided", () => {
+ const request = buildRetainRequest(
+ "hello world",
+ 1,
+ {},
+ { retainContext: "Custom extraction guidance." },
+ 1700000000000,
+ { turnIndex: 1 }
+ );
+
+ expect(request.context).toBe("Custom extraction guidance.");
+ });
+
+ it("trims configured retain context before sending it", () => {
+ const request = buildRetainRequest(
+ "hello world",
+ 1,
+ {},
+ { retainContext: " Custom extraction guidance. \n" },
+ 1700000000000,
+ { turnIndex: 1 }
+ );
+
+ expect(request.context).toBe("Custom extraction guidance.");
+ });
+
it("falls back to per-turn doc id when appendSupported is false (older API)", () => {
const request = buildRetainRequest(
"hello world",
@@ -1499,3 +1551,35 @@ describe("getPluginConfig — mission semantics (#1270, #1353)", () => {
expect(cfg.observationsMission).toBeUndefined();
});
});
+
+describe("getPluginConfig — retainContext", () => {
+ it("defaults retainContext to the built-in OpenClaw transcript guidance", () => {
+ const cfg = getPluginConfig(makeApi({}));
+ expect(cfg.retainContext).toBe(DEFAULT_RETAIN_CONTEXT);
+ });
+
+ it("passes through an explicit non-empty retainContext", () => {
+ const cfg = getPluginConfig(makeApi({ retainContext: "Treat IDs as routing metadata." }));
+ expect(cfg.retainContext).toBe("Treat IDs as routing metadata.");
+ });
+
+ it("trims an explicit retainContext", () => {
+ const cfg = getPluginConfig(makeApi({ retainContext: " Treat IDs as routing metadata. \n" }));
+ expect(cfg.retainContext).toBe("Treat IDs as routing metadata.");
+ });
+
+ it("falls back to the default when retainContext is blank or non-string", () => {
+ expect(getPluginConfig(makeApi({ retainContext: "" })).retainContext).toBe(
+ DEFAULT_RETAIN_CONTEXT
+ );
+ expect(getPluginConfig(makeApi({ retainContext: 42 })).retainContext).toBe(
+ DEFAULT_RETAIN_CONTEXT
+ );
+ });
+
+ it("keeps the plugin manifest default in sync with the code default", () => {
+ expect(openclawManifest.configSchema?.properties?.retainContext?.default).toBe(
+ DEFAULT_RETAIN_CONTEXT
+ );
+ });
+});
diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts
index d46fa15ee..3feba9272 100644
--- a/hindsight-integrations/openclaw/src/index.ts
+++ b/hindsight-integrations/openclaw/src/index.ts
@@ -32,6 +32,19 @@ function loadPackageVersion(): string {
const USER_AGENT = `hindsight-openclaw/${loadPackageVersion()}`;
+export const DEFAULT_RETAIN_CONTEXT =
+ "This content is an AI-assistant conversation transcript from OpenClaw. " +
+ "The [context] block at the beginning of each turn contains routing identifiers: " +
+ "'sender' is an opaque user ID (not a human name), 'channel' is a chat identifier, " +
+ "'provider' is the messaging platform name. " +
+ "These are operational routing metadata, not semantic actors or people. " +
+ "Messages with role 'assistant' are from the AI assistant; first-person statements " +
+ "in assistant messages refer to the AI, not the human user. " +
+ "Messages with role 'user' are from the human user. " +
+ "Bank IDs, session keys, agent IDs, thread IDs, source systems, " +
+ "and tags in metadata are also operational routing identifiers, " +
+ "not human names, project names, or organizations.";
+
// Logger adapter that routes the embed wrapper's output through openclaw's
// batched structured logger so messages share the same prefix and respect
// the configured log level.
@@ -114,6 +127,7 @@ function scopeClient(c: HindsightClient, bankId: string): BankScopedClient {
async retain(req) {
await c.retain(bankId, req.content, {
documentId: req.documentId,
+ context: req.context,
metadata: toStringMetadata(req.metadata),
tags: req.tags,
updateMode: req.updateMode,
@@ -284,6 +298,7 @@ async function flushRetainQueue(): Promise {
try {
await client.retain(item.bankId, item.content, {
documentId: item.documentId,
+ context: item.context,
metadata: toStringMetadata(item.metadata),
tags: item.tags,
updateMode: item.updateMode,
@@ -1462,6 +1477,10 @@ export function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
typeof config.retainSource === "string" && config.retainSource.trim().length > 0
? config.retainSource.trim()
: undefined,
+ retainContext:
+ typeof config.retainContext === "string" && config.retainContext.trim().length > 0
+ ? config.retainContext.trim()
+ : DEFAULT_RETAIN_CONTEXT,
excludeProviders: Array.isArray(config.excludeProviders)
? Array.from(
new Set([
@@ -2715,6 +2734,10 @@ export function buildRetainRequest(
return {
content: transcript,
documentId: documentId,
+ context:
+ typeof pluginConfig.retainContext === "string" && pluginConfig.retainContext.trim().length > 0
+ ? pluginConfig.retainContext.trim()
+ : DEFAULT_RETAIN_CONTEXT,
metadata: {
retained_at: new Date(now).toISOString(),
message_count: String(messageCount),
diff --git a/hindsight-integrations/openclaw/src/retain-queue.ts b/hindsight-integrations/openclaw/src/retain-queue.ts
index 33fb69444..c6f8eb971 100644
--- a/hindsight-integrations/openclaw/src/retain-queue.ts
+++ b/hindsight-integrations/openclaw/src/retain-queue.ts
@@ -22,6 +22,7 @@ import { randomBytes } from "crypto";
export interface QueuedRetainPayload {
content: string;
documentId?: string;
+ context?: string;
metadata?: Record;
tags?: string[];
updateMode?: "replace" | "append";
@@ -32,6 +33,7 @@ export interface QueuedRetain {
bankId: string;
content: string;
documentId: string;
+ context?: string;
metadata: Record;
tags?: string[];
updateMode?: "replace" | "append";
@@ -63,6 +65,7 @@ export class RetainQueue {
bankId,
content: request.content,
documentId: request.documentId || "conversation",
+ context: request.context,
metadata: metadata || request.metadata || {},
tags: request.tags,
updateMode: request.updateMode,
diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts
index 46b7bc7bf..0e4b8be26 100644
--- a/hindsight-integrations/openclaw/src/types.ts
+++ b/hindsight-integrations/openclaw/src/types.ts
@@ -96,6 +96,7 @@ export interface PluginConfig {
bankIdPrefix?: string; // Prefix for bank IDs (e.g. 'prod' -> 'prod-slack-C123')
retainTags?: string[]; // Tags applied to all retained documents after trimming and deduplication; auto-retain merges these with inline per-message retain-tag directives (e.g. ['source_system:openclaw', 'agent:agentname'])
retainSource?: string; // Source written into retained document metadata (default: 'openclaw')
+ retainContext?: string; // Interpretation guidance sent via the retain API context field. Defaults to built-in OpenClaw transcript/routing metadata guidance.
excludeProviders?: string[]; // Message providers to exclude from recall/retain (e.g. ['telegram', 'discord'])
autoRecall?: boolean; // Auto-recall memories on every prompt (default: true). Set to false when agent has its own recall tool.
dynamicBankGranularity?: Array<"agent" | "provider" | "channel" | "user">; // Fields for bank ID derivation. Default: ['agent', 'channel', 'user']
@@ -165,6 +166,7 @@ export type {
export interface RetainRequest {
content: string;
documentId?: string;
+ context?: string;
metadata?: Record;
tags?: string[];
/**