How we run Hindsight as the durable memory layer for OpenClaw — transport, tagging, provenance, and the failure modes that cost us real time.
Verified against a live deployment on 2026-10-07.
Plugin @vectorize-io/hindsight-openclaw 0.13.0 · Hindsight server API 0.10.2
Two pieces, and keeping them separate is the whole trick:
:8889). It owns extraction,
consolidation, the vector store, and the SQL state.@vectorize-io/hindsight-openclaw) — the glue. It
hooks OpenClaw's lifecycle: auto-retain after turns, auto-recall before
the prompt builds, plus the agent_knowledge_* tool surface.The plugin talks to the server over HTTP. Point hindsightApiUrl at the root
of the server — not /v1/....
# 1. Install the plugin (verify the install actually succeeded before touching config)
openclaw plugins install @vectorize-io/hindsight-openclaw
# 2. Run the interactive setup wizard
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup
# 3. Validate and start
openclaw config validate
openclaw gateway
The wizard offers three modes:
hindsight-embed daemon; needs an LLM
provider + key.The wizard stores credentials inline in openclaw.json for convenience. For
anything production-shaped, use a SecretRef instead:
openclaw config set plugins.entries.hindsight-openclaw.config.hindsightApiToken \
--ref-source env --ref-id HINDSIGHT_API_TOKEN
| Thing | Value |
|---|---|
| Plugin | @vectorize-io/hindsight-openclaw 0.13.0 |
| Server API | 0.10.2 |
| Server features on | observations, mcp, worker, bank_config_api, file_upload_api |
| Bank layout | one shared default bank, dynamicBankId: false |
| Fact units in bank | ~51,500 |
| Recall / retain | retain on every turn; auto-recall injection off, knowledge tools on |
| Embeddings | omniroute provider, openrouter/qwen/qwen3-embedding-4b (2560-dim) |
Lives at plugins.entries.hindsight-openclaw.config in ~/.openclaw/openclaw.json.
{
"enabled": true,
"hooks": { "allowConversationAccess": true },
"config": {
"hindsightApiUrl": "http://YOUR-HINDSIGHT-HOST:8889",
"bankId": "default",
"dynamicBankId": false,
"enableKnowledgeTools": true,
"autoRetain": true,
"retainRoles": ["user", "assistant"],
"retainFormat": "json",
"retainToolCalls": true,
"retainEveryNTurns": 1,
"retainOverlapTurns": 0,
"excludeProviders": ["heartbeat"],
"skipStatelessSessions": true,
"retainTags": [
"origin_scope:session",
"source_system:openclaw",
"host:primary",
"source:chat"
],
"retainSource": "openclaw",
"retainContext": "This content is an AI-assistant conversation transcript from OpenClaw. Retain request metadata may include routing identifiers such as 'sender_id' (an opaque user ID, not a human name), 'channel_id' (a chat identifier), and 'provider' (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. Starting directory, host and source tags are provenance only. The bank classifies semantic project and scope per fact. Preserve original message dates.",
"autoRecall": false,
"recallBudget": "mid",
"recallMaxTokens": 1024,
"recallTypes": ["observation"],
"preferObservations": false,
"recallRoles": ["user", "assistant"],
"recallContextTurns": 1,
"recallMaxQueryChars": 800,
"recallTimeoutMs": 10000,
"recallInjectionPosition": "user",
"recallPromptPreamble": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:",
"dynamicBankGranularity": ["agent", "channel", "user"],
"retainQueueMaxAgeMs": -1,
"retainQueueFlushIntervalMs": 60000,
"logSummaryIntervalMs": 300000,
"logLevel": "info",
"debug": false
}
}
autoRecall: false + knowledge tools onThis is our biggest deviation from defaults. By default the plugin silently injects recalled snippets into the prompt every turn. We turned automatic injection off and use the explicit tools instead:
agent_knowledge_recall — retrieve specific factsagent_knowledge_reflect — synthesized answer from the bankagent_knowledge_get_page / agent_knowledge_list_pages — read knowledge pagesagent_knowledge_ingest — upload a documentWhy: silent injection burns tokens on irrelevant hits and muddies the transcript. Explicit tools let the agent decide when to reach for memory.
If you want the "it just magically remembers" feel, leave autoRecall on — but
tune recallBudget and recallMaxTokens down or it gets chatty.
recallTypes: ["observation"] means the server's consolidation layer hands back
synthesized observations instead of raw transcript chunks. That is the
difference between "we talked about X once" and "here is what we concluded about X."
Also set a finite reflect_source_facts_max_tokens at the bank level
(4096–8192). Never leave reflection source facts unbounded.
dynamicBankId: false keeps everything in default; project and scope get
classified semantically per fact.
The alternative — a bank per agent/channel/user — gives hard isolation, but every new bank cold-starts with server defaults and you end up stamping the same contract N times.
Our call: tags route; banks isolate. Flip to per-bank only when you genuinely need data isolation (different people, different tenants), not just organization.
excludeProviders: ["heartbeat"] — no cron pingsskipStatelessSessions: true — no stateless sessionsCheap, and it makes everything downstream better.
retainContext earns its keepWithout it, extraction reads host, channel_id, session_key, bank_id as if
they were real entities, and reads assistant first-person text as if the user
said it.
That paragraph tells the extractor which tokens are operational metadata
versus real facts, and instructs the bank to classify project/scope
semantically. If your entity graph is polluted with fake entities named primary,
a model name, or a UUID — this paragraph is the fix.
Tags reach a document through three separate mechanisms. The plugin only owns one of them. Mixing these up causes most confusion.
Every document the plugin retains gets retainTags stamped automatically:
origin_scope:session source_system:openclaw host:primary source:chat
Flat key:value strings. host: is per-host — yours will differ.
agent_knowledge_ingest accepts only title and content — there is no tags
parameter. (Verified against the live tool schema.)
Tags embedded as YAML frontmatter in the content are NOT auto-extracted.
Probed live 2026-10-07 on Hindsight API 0.10.2: a document ingested with a
frontmatter tags: block came back tags: [] from the API, while every
plugin-retained conversation still carried its retainTags. Frontmatter prose
is useful for a human reader, but the server does not lift it into document
tags.
The working path for a hand-written page is ingest, then PATCH:
# 1. Ingest (title becomes the document ID)
# agent_knowledge_ingest { title, content }
# 2. Apply tags via the API
curl -X PATCH \
"$HINDSIGHT/v1/default/banks/default/documents/$DOC_ID" \
-H 'Content-Type: application/json' \
-d '{"tags":["scope:global","source:live-runtime","domain:openclaw",
"knowledge:procedure","host:primary","project:openclaw"]}'
# 3. Verify — the PATCH body is NOT authoritative (see 5.3)
curl "$HINDSIGHT/v1/default/banks/default/documents/$DOC_ID"
The title becomes the document ID, so re-ingesting with the same title replaces the document. That is your update path.
PATCH /v1/default/banks/{bank_id}/documents/{document_id}
This is how we retro-tagged an existing corpus.
Gotcha: PATCH normalizes asynchronously. The PATCH response body is not authoritative — the server rewrites the tag set after writeback and can drop tags. Always confirm with a follow-up
GET.
Namespace-first, key:value, lowercase, colon separators. Counts are from a real
1,285-document corpus.
| Namespace | Values | Meaning |
|---|---|---|
source: |
chat · session-reflection · sleep_consolidation · mcp · upload · external · correction · live-runtime · preference |
how the fact entered the bank |
source_system: |
openclaw · codex · repository-inspection |
which tool produced it |
scope: |
global · session · repository · unclassified |
the primary routing key |
domain: |
hindsight · security · storage · infrastructure · operator |
subject area |
knowledge: |
decision · failure · incident · component · recovery · feature-work |
fact kind |
host: |
primary · dev |
provenance (machine) |
harness: |
codex |
runtime that produced it |
project: |
openclaw · omniroute · repo slugs |
repository binding |
project_scope: |
none |
explicitly not repo-specific |
lifecycle: |
time-bounded · superseded · current |
staleness control |
freshness: |
verify-at-source |
must re-check upstream before trusting |
authority: |
nist · openzfs · truenas · cis · cisa |
external issuing body |
platform: |
openzfs · truenas-scale · docker |
tech platform |
document_type: |
guidance · reference · benchmark · framework · standard · tag-registry |
document shape |
correction: |
operator-confirmed |
a human explicitly corrected this |
scope: — the routing key. scope:unclassified is a legitimate value
for documents the classifier genuinely evaluated and could not place. It is
not a fallback for failures. That distinction matters — see §7.lifecycle:superseded — when a fact is replaced (e.g. a port layout
changed), you do not delete the old document; you tag it superseded. Old
facts stay auditable; recall deprioritizes them.Malformed or failed model output → skip the document, record the error, retry later. Never convert a failure into a fallback tag.
The unclassified tag is only for documents the classifier genuinely evaluated
and could not classify. The moment a failure silently becomes "unclassified," you
have poisoned your routing key with garbage that looks legitimate.
Our tag-repair script stamped wrong tags because it self-set applyApproved: true
on a clean dry-run, and a later run inherited it.
The apply gate lives in deployment source (APPLY_ENABLED constant), not
runtime state. Same principle for any mutating automation.
The pattern that made retro-tagging tractable: a batch LLM sweep that reviews, compresses, and commits in one pass — 10 documents at a time, with a cursor file, resumable, and a permanent-skip list carrying recorded reasons.
Our 1,288-document retro-tag ended at exactly 1 untagged (CONTRIBUTING.md —
ambiguous provenance, deliberately skipped with the reason logged). That is the
shape to copy.
mission + disposition — server-side, per bank. disposition is
{skepticism, literalism, empathy} (we run 4/3/3). It is the persona of the
extractor/reflector.retainContext — the metadata contract from §4.5.dynamicBankGranularity — ["agent","channel","user"]. Only active if you
flip dynamicBankId: true. This is the isolation dial.recallBudget (low/mid/high) — recall effort; higher uses more
retrieval strategies.reflect_source_facts_max_tokens — keep it finite (4096–8192).PATCH /banks/{id}/config as entity_labels on first bank use.excludeProviders + skipStatelessSessions — see §4.4.The loop we teach the agent:
agent_knowledge_recallagent_knowledge_ingest (full content, never pre-summarized)agent_knowledge_get_page or recall againagent_knowledge_reflect is for a synthesized answer rather than a document
dump. It defaults to budget: "low", max_tokens: 1024, fact types
world/experience/observation.
retainQueueFlushIntervalMs: 60000). Verify with a
follow-up read, not the POST response.GET.agent_knowledge_ingest wants the
full raw content — the server does chunking and fact extraction.tail and let a config rewire run on a failed install.
Order that works: run the mutator → check its exit/result → write config
→ openclaw config validate → restart.agent_knowledge_ingest has no tags parameter, and frontmatter tags
are not auto-extracted. Ingest first, then PATCH the document's tags
via the API (§5.2, §5.3), and re-GET to verify.failed rows (409); terminal-delete the row
with DELETE /v1/default/banks/{bank}/operations/{id}/delete to stop the
loop.| Method | Path |
|---|---|
GET |
/health, /version, /openapi.json |
POST |
/v1/default/banks/{bank}/memories — retain |
POST |
/v1/default/banks/{bank}/memories/recall |
GET |
/v1/default/banks/{bank}/memories/list |
POST |
/v1/default/banks/{bank}/memories/dry-run-extract |
GET/PATCH/DELETE |
/v1/default/banks/{bank}/documents/{id} |
POST |
/v1/default/banks/{bank}/documents/{id}/reprocess |
GET/PATCH |
/v1/default/banks/{bank}/config |
GET |
/v1/default/banks |
Package assembled from a live Hindsight + OpenClaw deployment. All versions, paths, and config values were read from the running system, not from memory.