references/best-practices.md
# Best Practices
Practical guidance for agents and developers integrating Hindsight memory into production systems.
**Contents**
- [Core Concepts](#core-concepts) — Memory banks, taxonomy, memory types
- [Bank Configuration](#bank-configuration) — Missions, dispositions, entity labels
- [Retaining Data](#retaining-data) — Content format, context, document_id, [tags](#tags-naming-conventions), observation scopes
- [Recalling Memories](#recalling-memories) — Budget, tag filtering, entity label filtering, include options, query_timestamp
- [Reflecting](#reflecting) — Recall vs reflect, response_schema, auditing
- [Mental Models](#mental-models) — When to create, tag strategy, refresh
- [Anti-patterns](#anti-patterns)
---
## Core Concepts
### Memory Banks
A **memory bank** is an isolated memory store — the unit of separation between users, agents, or contexts. All operations (retain, recall, reflect) target a single bank. Banks do not share data.
- One bank per user is the most common pattern for multi-user applications
- One bank per agent is common for agent-specific long-term memory
- A shared bank with tags can work for cross-user analysis (see [Tags](#tags-naming-conventions))
Banks are auto-created on first use. Configure them before ingesting data to steer behavior.
---
### Taxonomy
| Operation | What it does | When to call it |
|-----------|-------------|-----------------|
| **Retain** | Ingests raw content (conversations, documents, notes). The LLM extracts facts, entities, and relationships — raw content is never stored verbatim. | After each conversation turn or session ends |
| **Recall** | Retrieves relevant memories using 4 parallel strategies: semantic search, BM25, graph traversal, and temporal ranking. Returns a ranked list of facts. | Before generating a response that benefits from past context |
| **Reflect** | Autonomous reasoning loop: searches memory, synthesizes an answer, and returns it directly. Uses mental models and observations hierarchically. | When you want Hindsight to answer a question, not just retrieve facts |
| **Observations** | Deduplicated, evidence-grounded knowledge consolidated from multiple facts. Each observation tracks its supporting memories with exact quotes and proof counts. Refined — not overwritten — when new evidence supports, contradicts, or extends them. | Triggered automatically after retain — not part of the retain call itself |
| **Mental Models** | Pre-computed reflect responses stored for common queries. Return instantly and consistently. | Create for repeated high-traffic queries or slowly-changing user profiles |
---
### Memory Types
Facts extracted during retain are classified into three types:
| Type | Description | Example |
|------|-------------|---------|
| `world` | General knowledge, external facts | "The Eiffel Tower is in Paris" |
| `experience` | Personal events, user-specific facts | "User moved to Berlin in 2024" |
| `observation` | Consolidated belief grounded in multiple supporting facts; deduplicated and refined over time with tracked evidence | "User consistently prefers async communication (5 supporting memories)" |
Use `types` filtering in recall to target specific memory types.
---
## Bank Configuration
Configure a bank before first use to steer memory behavior for your domain. Misconfigured missions are the single biggest cause of low-quality memories.
### Writing Effective Missions
All three missions accept plain language. Be specific about your domain — vague missions produce vague results.
#### `retain_mission`
Injected into the fact extraction prompt. Tells the LLM what to extract and what to ignore.
| Quality | Example |
|---------|---------|
| **Good** | `Always extract technical decisions, API design choices, architectural trade-offs, blockers, and error messages. Ignore greetings, small talk, and scheduling logistics.` |
| **Good** | `Extract personal preferences, ongoing commitments, deadlines, health info, and relationship details. Ignore filler phrases and pleasantries.` |
| **Bad** | `Extract all information` — too vague, extracts noise |
| **Bad** | `Be helpful` — not an extraction directive |
**Tips:**
- List the fact *types* you want (preferences, decisions, errors, commitments)
- List what to *ignore* — this is as important as what to include
- Match the mission to your actual data type (conversations vs documents vs tickets)
#### `observations_mission`
Steers what patterns are synthesized during consolidation. Runs after retain.
```
Identify evolving preferences, recurring patterns, behavioral shifts, and contradictions
with prior knowledge. Focus on durable patterns — not transient states. Highlight when
user behavior contradicts previous observations.
```
**Tips:**
- Emphasize "durable patterns" to avoid ephemeral observation noise
- Mention contradiction detection explicitly if you need historical tracking
- Match scope to how often you expect patterns to change
#### `reflect_mission`
Sets the agent persona and reasoning frame for `reflect` operations.
| Use Case | Mission |
|----------|---------|
| Coding assistant | `You are a senior developer helping optimize the user's workflow. Always factor in past technical decisions, current project context, and stated preferences. Be direct and opinionated.` |
| Customer support | `You are a support agent with full context of this customer's history. Reference past tickets and resolutions where relevant. Be concise and solution-focused.` |
| Personal assistant | `You are a personal assistant who remembers everything important to the user. Personalize every response using what you know about their preferences, schedule, and ongoing projects.` |
| Medical assistant | `You are a health assistant. Reference the user's history accurately. Always recommend consulting a professional for medical decisions. Do not speculate.` |
---
### Disposition Traits
Dispositions affect `reflect` only (not `recall`). Scale 1–5.
| Trait | 1 | 5 |
|-------|---|---|
| `skepticism` | Trusts all memories at face value | Questions contradictions, flags uncertain info |
| `literalism` | Liberal interpretation, infers intent | Strict literal reading, no inference |
| `empathy` | Clinical, neutral tone | Warm, personal, emotionally aware |
**Common profiles:**
| Agent type | Skepticism | Literalism | Empathy |
|------------|------------|------------|---------|
| Code review | 4 | 5 | 1 |
| Customer support | 2 | 3 | 4 |
| Personal assistant | 2 | 2 | 4 |
| Medical assistant | 5 | 4 | 3 |
| Research assistant | 4 | 4 | 2 |
---
### Entity Labels
Define a controlled vocabulary for classification. The LLM will extract and normalize values to your defined set.
```json
{
"entity_labels": [
{
"key": "tech_stack",
"type": "multi-values",
"values": [
{"value": "python", "description": "Python programming language"},
{"value": "typescript", "description": "TypeScript / Node.js"},
{"value": "react", "description": "React frontend framework"}
]
},
{
"key": "priority",
"type": "value",
"tag": true,
"values": [
{"value": "high", "description": "Urgent or blocking"},
{"value": "low", "description": "Nice to have"}
]
}
]
}
```
- **`type: "value"`** — single value per entity (last write wins)
- **`type: "multi-values"`** — accumulates multiple values
- **`tag: true`** — extracted label values are also added as tags (enables filtering by entity value)
Use entity labels when you need consistent classification — domain-specific terms, status values, priority levels, engagement types.
---
## Retaining Data
### Content Format
Pass the richest representation available. Never pre-summarize.
| Format | Recommendation |
|--------|---------------|
| JSON conversation array | **Preferred** for conversations — preserves structure, roles, and relationships |
| Prefixed plain text | Acceptable — `[ISO-timestamp] role: text` per line |
| Markdown / HTML / raw text | Works for documents and notes |
| Pre-summarized text | **Avoid** — loses entity relationships, temporal markers, structural context |
**Conversation JSON (preferred):**
```json
[
{"role": "user", "content": "I'm using React for the frontend.", "timestamp": "2025-06-01T10:30:00Z"},
{"role": "assistant", "content": "Got it. What state management are you using?"},
{"role": "user", "content": "Zustand. We moved away from Redux last quarter."}
]
```
**Why not pre-summarize:** The LLM extracts facts, entities, and relationships from structure. A summary like "user uses React and Zustand" loses the temporal reference ("last quarter"), the entity relationship (React↔frontend, Redux↔migration), and the causal context (moved away from).
---
### The `context` Field
High-impact on extraction quality. Always set it. Describes the *nature and source* of the content.
```python
# Good — specific, descriptive
context="Customer support ticket #12345 from user Alice about a billing discrepancy"
context="Developer's architecture review session for the payments service"
context="User's onboarding form: stated goals, current tools, and team size"
context="Weekly standup notes: blockers, progress, and upcoming tasks"
# Bad — generic, adds no signal
context="some data"
context="conversation"
# Omitted entirely — extraction uses no context
```
---
### The `document_id` Field
Use for upsert behavior. Same `document_id` = delete previous version and reprocess.
**Rules:**
- Use stable, meaningful IDs (session ID, ticket ID, document UUID)
- Always use the same ID for a growing conversation — retain the full conversation with each new message
- Do NOT use random UUIDs per retain call — this creates duplicates
```python
# Good — stable session ID
client.retain(bank_id="user-alice", items=[{
"content": full_conversation,
"document_id": f"session-{session_id}",
}])
# Bad — new random ID every call = duplicates
client.retain(bank_id="user-alice", items=[{
"content": full_conversation,
"document_id": str(uuid.uuid4()), # ❌ creates a new document each time
}])
```
---
### The `timestamp` Field
Set whenever you have temporal context. Enables temporal retrieval strategies.
- ISO 8601 format: `"2025-06-01T10:32:00Z"`
- For conversations: set to when the conversation *started*
- Omitting it disables temporal ranking entirely
---
### Tags: Naming Conventions
Tags scope visibility. A memory tagged `user:alice` is only returned for recall/reflect calls that include `user:alice` in their `tags` filter (with strict matching).
**Standard naming conventions:**
| Pattern | Example | Use for |
|---------|---------|---------|
| `user:<id>` | `user:alice`, `user:u_123` | Per-user isolation |
| `session:<id>` | `session:s_abc` | Session-scoped memories |
| `team:<name>` | `team:engineering` | Shared team knowledge |
| `topic:<name>` | `topic:billing`, `topic:technical` | Domain filtering |
| `scope:<name>` | `scope:private`, `scope:public` | Visibility tiers |
**Multi-tenant minimum:** Every retain for user data must include at least `user:<id>`. Omitting it makes the memory globally visible.
```python
# Multi-tenant retain — always tag with user ID
items=[{
"content": conversation,
"tags": ["user:alice", "session:s_abc", "topic:billing"],
"document_id": f"session-{session_id}",
}]
```
---
### Metadata Schema
Use for source tracking and downstream linking. **Not filterable** — use tags for filtering.
```python
# Source tracking
metadata={"source": "slack", "channel": "#engineering", "thread_id": "T123456"}
# Ticket linking
metadata={"ticket_id": "JIRA-123", "priority": "high", "reporter": "alice"}
# Document provenance
metadata={"url": "https://...", "section": "pricing-faq", "version": "2025-Q1"}
```
Metadata is returned with every recalled memory — use it to link memories back to source systems for UI display, deep-linking, or audit trails.
---
### Observation Scopes
Controls which tag combinations get their own observation pass.
| Value | Behavior | When to use |
|-------|----------|------------|
| `"combined"` | One pass with all tags together | Default — single-user banks, general use |
| `"per_tag"` | One pass per tag independently | Users should have isolated behavioral observations |
| `"all_combinations"` | All possible subsets of tags | Complex multi-dimensional analysis (expensive) |
| Custom list | Explicit scope list | Precise multi-tenant control |
**Custom scope example (recommended for multi-tenant):**
```python
# Observations scoped to: user-level, team-level, and combined
observation_scopes=[
["user:alice"],
["team:engineering"],
["user:alice", "team:engineering"],
]
```
---
### Sync vs Async
| Mode | When to use |
|------|-------------|
| `async_=False` (default) | When you need confirmation before proceeding |
| `async_=True` | End-of-turn or end-of-session retain; user-facing flows where latency matters |
Do not retain and recall in the same turn — retain is a write operation and the extracted memories will not be available immediately.
---
## Recalling Memories
### Budget Selection
| Budget | Latency | Use when |
|--------|---------|----------|
| `low` | 50–100ms | Simple fact lookups, single-hop questions |
| `mid` | 100–300ms | Multi-hop reasoning, relationship queries *(default)* |
| `high` | 300–500ms | Deep exploration, complex cross-domain patterns |
Default to `mid`. Use `low` for high-frequency agent loops. Reserve `high` for explicit "deep recall" user-triggered flows.
---
### Tag Filtering Modes
| Mode | Includes untagged? | Condition |
|------|-------------------|-----------|
| `any` *(default)* | Yes | At least one tag matches, OR untagged |
| `all` | Yes | All specified tags present, OR untagged |
| `any_strict` | No | At least one tag matches |
| `all_strict` | No | All specified tags present |
**Decision guide:**
- Shared global knowledge + per-user: `tags=["user:alice"], tags_match="any"` — returns Alice's memories and untagged global memories
- Fully partitioned (no leakage): `tags=["user:alice"], tags_match="any_strict"` — Alice's memories only
- Multi-condition AND: `tags=["user:alice", "topic:billing"], tags_match="all_strict"` — only where both tags present
**`tag_groups` for complex filters:**
Tag groups use a tree structure with `and`/`or`/`not` compound nodes and `{"tags": [...], "match": "..."}` leaf nodes.
```python
# Alice's billing memories OR shared billing memories (no user tag)
recall(
query="...",
tag_groups=[
{"or": [
{"tags": ["user:alice", "topic:billing"], "match": "all_strict"},
{"and": [
{"tags": ["topic:billing"], "match": "any_strict"},
{"not": {"tags": ["user:alice"], "match": "any_strict"}},
]},
]}
]
)
```
---
### `include` Options
| Option | Default | Enable when |
|--------|---------|-------------|
| `include.entities` | Enabled | — (leave on; provides entity context for graph traversal) |
| `include.chunks` | Disabled | Agent needs exact wording or source quotation |
| `include.source_facts` | Disabled | Tracing observation provenance for auditing |
---
### `types` Filtering
| Value | Returns |
|-------|---------|
| *(not set)* | All types |
| `["observation"]` | Consolidated patterns only — faster for high-level questions |
| `["world", "experience"]` | Raw facts only — for ground-truth or citation-sensitive queries |
---
### Filtering by Memory Shape with Entity Labels
When a single bank contains semantically similar memories that serve different purposes (e.g., concise operating rules vs. detailed troubleshooting procedures), ranking alone cannot reliably distinguish them — two memories about "entrypoints" will score similarly regardless of whether one is a one-line rule and the other is a multi-step runbook.
Use [entity labels](developer/api/memory-banks.md#entity-labels) with `tag: true` to classify facts at retain time and hard-filter at recall time.
**1. Define a label group on the bank:**
```json
{
"entity_labels": [
{
"key": "memory_type",
"description": "The type of knowledge: 'rule' for concise operating rules and canonical guidance, 'procedure' for step-by-step technical instructions and troubleshooting notes",
"type": "value",
"optional": false,
"tag": true,
"values": [
{ "value": "rule", "description": "Concise operating rule or canonical guidance" },
{ "value": "procedure", "description": "Step-by-step technical instruction or troubleshooting note" }
]
}
]
}
```
**2. Retain normally** — the LLM classifies each fact automatically and writes `memory_type:rule` or `memory_type:procedure` as a tag.
**3. Filter at recall time:**
```python
# Only rules — procedures are excluded at the database level, not post-filtered
result = client.recall(
bank_id="my-bank",
query="which entrypoint should I use?",
tags=["memory_type:rule"],
tags_match="any_strict"
)
```
This is a hard SQL WHERE clause applied across all four retrieval strategies. The unwanted memories never enter the ranking pipeline.
---
### `query_timestamp`
Set for time-sensitive queries. Anchors relative temporal expressions and recency scoring to a specific point in time.
```python
# "What was the team working on in January?"
recall(query="team priorities", query_timestamp="2025-01-31T23:59:59Z")
# Current context (most common)
recall(query="user preferences", query_timestamp=datetime.utcnow().isoformat() + "Z")
```
---
## Reflecting
### Recall vs Reflect
| Use `recall` when | Use `reflect` when |
|-------------------|--------------------|
| Agent will reason over facts itself | You want Hindsight to reason and return an answer |
| You need raw citations | You need a synthesized response |
| You're building a RAG pipeline | You want an autonomous multi-step search loop |
| Latency is critical | Response quality matters more than latency |
| You need precise fact counts | You need a contextual, nuanced answer |
---
### `response_schema`
Use when you need structured output for programmatic consumption.
```python
reflect(
query="What are the user's top 3 technical preferences?",
response_schema={
"type": "object",
"properties": {
"preferences": {
"type": "array",
"items": {"type": "string"},
"maxItems": 3
},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"required": ["preferences", "confidence"]
}
)
# Returns: result.structured_output["preferences"], result.structured_output["confidence"]
```
---
### Auditing and Debugging
| Option | Purpose |
|--------|---------|
| `include.facts=True` | Exposes which memories and mental models were used (for transparency/auditing) |
| `include.tool_calls=True` | Full execution trace of the internal search loop (for debugging) |
Enable `include.facts` in production for audit trails. Enable `include.tool_calls` only during development.
---
## Mental Models
Mental models are pre-computed `reflect` responses stored for common queries. They return instantly and consistently.
### When to Create
- Common repeated queries that should return consistent answers
- High-traffic agents that need sub-100ms responses
- User profiles or personas read on every request
- Knowledge summaries reviewed or approved by humans
- Cross-session state that changes slowly (preferences, skills, background)
### Tag Strategy
Tags on a mental model filter BOTH which memories are used to build it AND which recall/reflect calls can see it.
```python
# Per-user mental model — uses Alice's memories (all_strict applied automatically during refresh)
create_mental_model(
bank_id="shared-bank",
name="Alice's Technical Profile",
source_query="Summarize Alice's technical background, preferred stack, and current projects",
tags=["user:alice"],
)
# Global mental model — uses all memories, visible to everyone
create_mental_model(
bank_id="shared-bank",
name="Team Engineering Standards",
source_query="What are the team's agreed engineering standards and conventions?",
# No tags — reads all memories, visible to all
)
```
### Refresh Strategy
| Trigger | When to use |
|---------|------------|
| Manual via API | After significant data updates or review cycles |
| `trigger={"refresh_after_consolidation": True}` | When observations update frequently and the model should stay current |
Create narrow, scoped models — one per knowledge dimension. A mental model titled "Everything about the user" is as useful as none.
**Model granularity examples for a personal assistant:**
- "User Profile" — demographics, preferences, stated goals
- "Current Projects" — active work, deadlines, blockers
- "Technical Stack" — languages, tools, frameworks used
- "Communication Style" — formality preferences, response length preferences
---
## Anti-patterns
| Anti-pattern | Problem | Fix |
|-------------|---------|-----|
| Pre-summarizing before retain | Loses entity relationships, temporal markers, structural context | Retain raw content; Hindsight extracts facts |
| Using random UUIDs as `document_id` | Creates duplicate documents on every retain | Use stable session/ticket/document IDs |
| Omitting the `context` field | Reduces extraction quality significantly | Always describe what kind of data this is |
| Using `metadata` for filtering | Metadata is not filterable | Use `tags` for anything you'll filter on |
| Vague or generic missions | Generic extraction = noisy, low-value memories | Be specific about domain, data type, what to ignore |
| `tags_match="any"` for multi-tenant banks | Leaks memories across users | Use `any_strict` or `all_strict` for user-partitioned data |
| Retaining and recalling in the same request | Retained memories not yet indexed | Retain end-of-turn; recall at the start of next turn |
| One mental model for everything | Low accuracy, slow refresh, hard to scope | Create one model per knowledge dimension |
| `high` budget for every recall | Expensive, slow, usually unnecessary | Use `low` for simple lookups, `mid` default |
| Missing `timestamp` on retain | Disables temporal retrieval strategies | Always set from actual content timestamps |
references/changelog/integrations/ag2.md
---
hide_table_of_contents: true
---
# ag2 Integration Changelog
Changelog for [`hindsight-ag2`](https://pypi.org/project/hindsight-ag2/).
For the source code, see [`hindsight-integrations/ag2`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/ag2).
← [Back to main changelog](../index.md)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/ag2/v0.1.2)
**Improvements**
- Improved type-checking support for the AG2 integration by shipping PEP 561 type information. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
**Bug Fixes**
- All HTTP requests from the AG2 integration now include an identifying User-Agent header for improved compatibility and observability. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
- Resolved critical and high-severity security vulnerabilities in dependencies. ([`ee4510a7`](https://github.com/vectorize-io/hindsight/commit/ee4510a7))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/ag2/v0.1.1)
**Features**
- Added AG2 framework integration for Hindsight. ([`73123870`](https://github.com/vectorize-io/hindsight/commit/73123870))
references/changelog/integrations/agent-framework.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Microsoft Agent Framework Changelog" subtitle="Hindsight memory integration for Microsoft Agent Framework." />
[← Microsoft Agent Framework integration](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/agent-framework)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/agent-framework/v0.1.0)
**Features**
- Added Hindsight memory integration for the Microsoft Agent Framework (without MCP).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f5a6c300f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f5a6c300f</a>
references/changelog/integrations/agentcore.md
---
hide_table_of_contents: true
---
# Bedrock AgentCore Integration Changelog
Changelog for [`hindsight-agentcore`](https://pypi.org/project/hindsight-agentcore/).
For the source code, see [`hindsight-integrations/agentcore`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/agentcore).
← [Back to main changelog](../index.md)
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/agentcore/v0.1.3)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/agentcore/v0.1.3)
**Improvements**
- Hindsight AgentCore integration requests now identify themselves with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/agentcore/v0.1.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/agentcore/v0.1.2)
**Bug Fixes**
- Reflect recall mode now returns the reflection text instead of an empty memory context.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/071502981" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>071502981</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/agentcore/v0.1.1)
**Features**
- Added integration so Hindsight can use Bedrock AgentCore Runtime for memory operations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c91696f5" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c91696f5</a>
**Improvements**
- Improved the AgentCore integration with an async-native client and better task tracking for more reliable runtime behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b153541e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b153541e</a>
references/changelog/integrations/agno.md
---
hide_table_of_contents: true
---
# Agno Integration Changelog
Changelog for [`hindsight-agno`](https://pypi.org/project/hindsight-agno/).
For the source code, see [`hindsight-integrations/agno`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/agno).
← [Back to main changelog](../index.md)
## [0.4.20](https://github.com/vectorize-io/hindsight/tree/integrations/agno/v0.4.20)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/agno/v0.4.20)
**Improvements**
- Added Python typing metadata so Agno integration users receive type information in supported type checkers and IDEs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d054b8840" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d054b8840</a>
**Bug Fixes**
- Hindsight client requests now include an identifying User-Agent for improved service identification and supportability.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9372462e1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9372462e1</a>
references/changelog/integrations/ai-sdk.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Vercel AI SDK Changelog" subtitle="@vectorize-io/hindsight-ai-sdk — memory integration for Vercel AI SDK." />
[← Vercel AI SDK integration](../../sdks/integrations/ai-sdk.md)
## [0.5.2](https://github.com/vectorize-io/hindsight/tree/integrations/ai-sdk/v0.5.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/ai-sdk/v0.5.2)
**Bug Fixes**
- Recall results now preserve the source chunks requested by callers.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f3fa28166" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f3fa28166</a>
## [0.5.1](https://github.com/vectorize-io/hindsight/tree/integrations/ai-sdk/v0.5.1)
**Bug Fixes**
- Fixed AI-SDK reflection tool type definitions to match the OpenAPI spec for more reliable integration behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3d6b3805" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3d6b3805</a>
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/ai-sdk/v0.5.0)
**Bug Fixes**
- Improved compatibility with Hindsight Client v0.5.2 to prevent integration mismatches when using the AI SDK.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bca87412" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bca87412</a>
## [0.4.20](https://github.com/vectorize-io/hindsight/tree/integrations/ai-sdk/v0.4.20)
**Features**
- Add AI SDK integration for Hindsight. ([`7e339e16`](https://github.com/vectorize-io/hindsight/commit/7e339e16))
- Add Deno compatibility for the TypeScript client. ([`72c25c97`](https://github.com/vectorize-io/hindsight/commit/72c25c97))
**Improvements**
- Improve AI SDK tool support to make integrations more capable and reliable. ([`d06a0259`](https://github.com/vectorize-io/hindsight/commit/d06a0259))
**Bug Fixes**
- Security fixes by resolving dependency vulnerabilities affecting the integration. ([`b6a4f17c`](https://github.com/vectorize-io/hindsight/commit/b6a4f17c))
references/changelog/integrations/aider.md
---
hide_table_of_contents: true
---
# Aider Integration Changelog
Changelog for [`hindsight-aider`](https://pypi.org/project/hindsight-aider/).
For the source code, see [`hindsight-integrations/aider`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/aider).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/aider/v0.1.1)
**Features**
- Added Aider integration to capture and store session memory automatically during Aider runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aab703207" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aab703207</a>
**Bug Fixes**
- Fixed Aider/OpenHands integration to close the client cleanly on exit to prevent lingering connections.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a0af09608" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a0af09608</a>
references/changelog/integrations/autogen.md
---
hide_table_of_contents: true
---
# AutoGen Integration Changelog
Changelog for [`hindsight-autogen`](https://pypi.org/project/hindsight-autogen/).
For the source code, see [`hindsight-integrations/autogen`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/autogen).
← [Back to main changelog](../index.md)
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/autogen/v0.1.3)
**Improvements**
- Autogen integration now defaults to the Cloud backend and adds gated end-to-end testing to improve reliability and test coverage.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0eb40a7c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0eb40a7c</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/autogen/v0.1.2)
**Bug Fixes**
- Ensure all HTTP requests include an identifying User-Agent header for better reliability and service compatibility. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/autogen/v0.1.1)
**Features**
- Added AutoGen integration to connect Hindsight with AutoGen-based agent workflows. ([`a757765a`](https://github.com/vectorize-io/hindsight/commit/a757765a))
references/changelog/integrations/chat.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Vercel Chat SDK Changelog" subtitle="@vectorize-io/hindsight-chat — memory integration for Vercel Chat SDK." />
[← Vercel Chat SDK integration](../../sdks/integrations/chat.md)
## [0.4.21](https://github.com/vectorize-io/hindsight/tree/integrations/chat/v0.4.21)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/chat/v0.4.21)
**Bug Fixes**
- Configured recall tag filters are now forwarded so chat memory searches return only appropriately tagged memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7a526ae8e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7a526ae8e</a>
## [0.4.20](https://github.com/vectorize-io/hindsight/tree/integrations/chat/v0.4.20)
**Features**
- Added Chat SDK integration to enable persistent memory for chat bots. ([`fed987f9`](https://github.com/vectorize-io/hindsight/commit/fed987f9))
- Added Deno compatibility for the TypeScript client. ([`72c25c97`](https://github.com/vectorize-io/hindsight/commit/72c25c97))
**Bug Fixes**
- Resolved security vulnerabilities in dependencies used across integrations. ([`b6a4f17c`](https://github.com/vectorize-io/hindsight/commit/b6a4f17c))
references/changelog/integrations/claude-agent-sdk.md
---
hide_table_of_contents: true
---
# Claude Agent SDK Integration Changelog
Changelog for [`hindsight-claude-agent-sdk`](https://pypi.org/project/hindsight-claude-agent-sdk/).
For the source code, see [`hindsight-integrations/claude-agent-sdk`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/claude-agent-sdk).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-agent-sdk/v0.1.0)
**Features**
- Added Claude Agent SDK integration for Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a933a417c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a933a417c</a>
references/changelog/integrations/claude-code.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Claude Code Changelog" subtitle="hindsight-memory — Hindsight memory plugin for Claude Code." />
[← Claude Code integration](../../sdks/integrations/claude-code.md)
## [0.7.6](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.6)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/claude-code/v0.7.6)
**Features**
- Added Cursor Agent CLI as an LLM provider for the Claude Code integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8bc5ab103" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8bc5ab103</a>
- Added support for GitHub Copilot subscriptions as an LLM provider.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dudujuju828" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dudujuju828</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1d9a6a381" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1d9a6a381</a>
**Bug Fixes**
- Prevented daemon idle timeouts from terminating in-progress requests.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d7d137fa1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d7d137fa1</a>
- Fixed resolution of projects specified with bare hub project names.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1e4583478" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1e4583478</a>
- Updated the LiteLLM requirement to support Python 3.14 environments.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a514d3962" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a514d3962</a>
- Made recall-hook timeouts honor the configured request timeout.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/handnewb" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@handnewb</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d06fdd78c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d06fdd78c</a>
- Selected a compatible Python version when starting daemon processes through uvx.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6c8a92c31" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6c8a92c31</a>
## [0.7.5](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.5)
**Bug Fixes**
- Fixes Claude Code integration to retain session deltas so incremental updates aren’t lost between runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/oldnicke" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@oldnicke</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a910fd8a0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a910fd8a0</a>
## [0.7.4](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.4)
**Bug Fixes**
- Prevents recalling the primary or duplicate memory banks when fetching additional banks, reducing redundant results.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b0fb1111e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b0fb1111e</a>
- Runs the MCP server in an isolated working directory to avoid path-related issues when launched from different locations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/oldnicke" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@oldnicke</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4bf126bf5" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4bf126bf5</a>
## [0.7.3](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.3)
**Features**
- Add recall score floors so memory recall can enforce minimum relevance thresholds.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ishanmalik" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ishanmalik</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e93c56028" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e93c56028</a>
- Add recall tag filters to the Claude Code memory hook to restrict recall to specific memory tags.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/962140eef" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>962140eef</a>
## [0.7.2](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.2)
**Bug Fixes**
- Fix directory-to-memory-bank mapping to correctly handle symlinks, improving reliability of bank resolution.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/365diascollaboration-prog" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@365diascollaboration-prog</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0672fba27" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0672fba27</a>
- Improve Windows support by matching directory-to-memory-bank mappings case-insensitively.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/eskild-boop" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@eskild-boop</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c3bb64764" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c3bb64764</a>
- Fix Windows virtualenv detection so the MCP runner can locate the Python interpreter in the Scripts/ layout.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/eldar702" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@eldar702</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/12851bc7e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>12851bc7e</a>
- Reduce unnecessary daemon restarts by increasing the default health-check timeout from 2s to 10s.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/21Felix04" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@21Felix04</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c5a61db2b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c5a61db2b</a>
## [0.7.1](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.1)
**Bug Fixes**
- Enable knowledge tools by default and keep the MCP server running even when knowledge tools are disabled.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/655435ea" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>655435ea</a>
## [0.7.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.7.0)
**Improvements**
- Claude Code integration now defaults recall types to observations to improve memory recall behavior out of the box.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4b19a0fb" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4b19a0fb</a>
**Bug Fixes**
- Recall context now labels the “Current time” value as UTC to avoid timezone confusion.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/valda" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@valda</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3d6c2ba8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3d6c2ba8</a>
- Windows MCP bootstrap is now idempotent, preventing failures when re-running the setup script.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ottopichlhoefer" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ottopichlhoefer</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/80046797" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>80046797</a>
## [0.6.5](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.5)
**Features**
- Added a configurable timeout for MCP requests in the Claude Code integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rsaulo" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rsaulo</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/55ef7067" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>55ef7067</a>
## [0.6.4](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.4)
**Bug Fixes**
- Fixed Claude Code recall parameter naming to avoid failures when limiting results.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/offendingcommit" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@offendingcommit</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/909a4fd4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>909a4fd4</a>
- Improved page retrieval reliability by returning full page content and handling oversized tool results.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b2a693ab" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b2a693ab</a>
- Corrected the agent knowledge page list tool to fetch metadata-only details, preventing incorrect or overly large responses.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6c6ee73c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6c6ee73c</a>
## [0.6.3](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.3)
**Features**
- Adds explicit directory-to-bank mapping and better support for Git worktrees when using the claude-code integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/691c65ac" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>691c65ac</a>
## [0.6.2](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.2)
**Bug Fixes**
- Fixes Claude Code integration setup by bootstrapping required Python dependencies in an isolated virtual environment under the plugin data directory.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/012c100e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>012c100e</a>
## [0.6.1](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.1)
**Features**
- Claude Code’s create-agent skill now supports and understands SDA project layouts when generating agents.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0231094d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0231094d</a>
## [0.6.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.6.0)
**Features**
- Added Claude Code knowledge tools backed by a Python MCP server, enabling the integration to read/write and use Hindsight knowledge more directly.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6c55dbde" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6c55dbde</a>
- Introduced subagents and a new “create-agent” skill to help users create and manage agent configurations/workflows within Claude Code.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1b9d6d91" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1b9d6d91</a>
## [0.4.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.4.0)
**Features**
- Allows recalling memories from additional banks alongside the primary bank for broader context retrieval.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/starbit-biostar" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@starbit-biostar</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cba2b0d8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cba2b0d8</a>
**Improvements**
- Adds a \{user_id\} template variable for retainTags to better organize memories by user.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/soichisumi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@soichisumi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9181c9a2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9181c9a2</a>
**Bug Fixes**
- Fixes transcript parsing when tool results include list-style content, preventing recall/retain from failing on some sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b67b6886" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b67b6886</a>
- Ensures dynamically generated memory bank IDs preserve raw UTF-8, avoiding incorrect bank selection for non-ASCII identifiers.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Desko77" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Desko77</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/08a75b5b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>08a75b5b</a>
- Prevents memory compaction from overwriting memories that were meant to be retained.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9c9a5a29" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9c9a5a29</a>
- Ensures a session’s final memories are retained on SessionEnd even when retainEveryNTurns is greater than 1.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/starbit-biostar" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@starbit-biostar</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aefc1ebc" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aefc1ebc</a>
- Reads transcript files as UTF-8 to prevent errors or corrupted text when transcripts contain non-ASCII characters.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/tordf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@tordf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/eb9be903" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>eb9be903</a>
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.3.1)
**Bug Fixes**
- All Claude Code integration HTTP requests now include an identifying User-Agent for better compatibility and observability. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.3.0)
**Features**
- Claude Code integration now retains tool calls as structured JSON for more accurate memory and retrieval. ([`8cb8b912`](https://github.com/vectorize-io/hindsight/commit/8cb8b912))
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/claude-code/v0.2.0)
**Features**
- Added a Claude Code integration plugin for capturing and using Hindsight memory in Claude Code. ([`f4390bdc`](https://github.com/vectorize-io/hindsight/commit/f4390bdc))
- Claude Code integration can retain full sessions with document upsert and configurable tagging. ([`2d31b67d`](https://github.com/vectorize-io/hindsight/commit/2d31b67d))
**Improvements**
- Improved Claude Code plugin installation and configuration experience. ([`35b2cbb6`](https://github.com/vectorize-io/hindsight/commit/35b2cbb6))
- Integrations no longer rely on hardcoded default models, allowing model selection to be fully configured. ([`58e68f3e`](https://github.com/vectorize-io/hindsight/commit/58e68f3e))
- Claude Code now starts the Hindsight background daemon automatically at session start for smoother operation. ([`26944e25`](https://github.com/vectorize-io/hindsight/commit/26944e25))
**Bug Fixes**
- Added a supported setup command to register hooks reliably, fixing hook registration issues. ([`22ca6a8d`](https://github.com/vectorize-io/hindsight/commit/22ca6a8d))
- Fixed Claude Code integration compatibility on Windows. ([`a94a90ea`](https://github.com/vectorize-io/hindsight/commit/a94a90ea))
references/changelog/integrations/cline.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Cline Changelog" subtitle="Hindsight memory integration for the Cline coding agent." />
[← Cline integration](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cline)
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/cline/v0.2.0)
**Features**
- Added a pip-installable "hindsight-cline" package with a CLI to install and run the Hindsight Cline integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/36e31c675" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>36e31c675</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/cline/v0.1.0)
**Features**
- Added Cline integration that captures task lifecycle and prompt events and stores them in Hindsight memory via installable hooks.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/66e58a23" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>66e58a23</a>
references/changelog/integrations/cloudflare-oauth-proxy.md
---
hide_table_of_contents: true
---
# cloudflare-oauth-proxy Integration Changelog
Changelog for [`hindsight-cloudflare-oauth-proxy`](https://pypi.org/project/hindsight-cloudflare-oauth-proxy/).
For the source code, see [`hindsight-integrations/cloudflare-oauth-proxy`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cloudflare-oauth-proxy).
← [Back to main changelog](../index.md)
## [1.0.1](https://github.com/vectorize-io/hindsight/tree/integrations/cloudflare-oauth-proxy/v1.0.1)
**Features**
- Added support for using a Cloudflare OAuth proxy with self-hosted Hindsight for authentication. ([`aad07a14`](https://github.com/vectorize-io/hindsight/commit/aad07a14))
references/changelog/integrations/codex.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="OpenAI Codex CLI Changelog" subtitle="Hindsight memory integration for OpenAI Codex CLI." />
[← Codex CLI integration](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/codex)
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.3.1)
**Features**
- Add configurable recall score floors to control which memories qualify for recall.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ishanmalik" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ishanmalik</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e93c56028" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e93c56028</a>
**Bug Fixes**
- Increase the daemon health-check timeout to prevent repeated kill/restart loops under slow conditions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/21Felix04" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@21Felix04</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c5a61db2b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c5a61db2b</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.3.0)
**Improvements**
- Added a configurable timeout for Codex recall to prevent long-running recalls from hanging.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/voarsh2" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@voarsh2</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/eb76510a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>eb76510a</a>
**Bug Fixes**
- Filtered out synthetic AGENTS startup messages so they don’t pollute saved memories or transcripts.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/voarsh2" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@voarsh2</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b41e5e36" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b41e5e36</a>
- Fixed PowerShell-related encoding issues when running the Codex integration on Windows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/jerviscui" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@jerviscui</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b837e66c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b837e66c</a>
- Improved UTF-8 handling when reading transcripts and deriving bank IDs, preventing corrupted non-ASCII memory identifiers/content.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Desko77" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Desko77</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/08a75b5b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>08a75b5b</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.2.1)
**Improvements**
- All Codex integration HTTP requests now include an identifying User-Agent header for better request tracking and compatibility. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.2.0)
**Features**
- Retain structured Codex tool calls from rollout files so they’re preserved in Hindsight memory. ([`3461398b`](https://github.com/vectorize-io/hindsight/commit/3461398b))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.1.1)
**Features**
- Added Hindsight memory integration for the OpenAI Codex CLI, enabling Codex to use and store memories in Hindsight. ([`0b17a67c`](https://github.com/vectorize-io/hindsight/commit/0b17a67c))
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/codex/v0.1.0)
**Features**
- Added Hindsight memory integration for OpenAI Codex CLI with three hook scripts: SessionStart (daemon warm-up), UserPromptSubmit (auto-recall), and Stop (auto-retain). ([`0b17a67c`](https://github.com/vectorize-io/hindsight/commit/0b17a67c))
- Full-session retain with session-level upsert using session ID as document ID. ([`0b17a67c`](https://github.com/vectorize-io/hindsight/commit/0b17a67c))
- Dynamic bank IDs for per-project memory isolation. ([`0b17a67c`](https://github.com/vectorize-io/hindsight/commit/0b17a67c))
- Automatic daemon lifecycle management with background pre-start. ([`0b17a67c`](https://github.com/vectorize-io/hindsight/commit/0b17a67c))
- 57 automated tests covering content processing and end-to-end hook behavior. ([`71125cd9`](https://github.com/vectorize-io/hindsight/commit/71125cd9))
references/changelog/integrations/coding-agents.md
---
hide_table_of_contents: true
---
# Coding Agents Integration Changelog
Changelog for [`@vectorize-io/hindsight-coding-agents`](https://www.npmjs.com/package/@vectorize-io/hindsight-coding-agents).
For the source code, see [`hindsight-integrations/coding-agents`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/coding-agents).
← [Back to main changelog](../index.md)
## [0.8.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.8.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/coding-agents/v0.8.2)
**Bug Fixes**
- Coding-agents now adds only newly created commits to its git-log memory document, preventing duplicate commit history on repeated syncs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5529f9be4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5529f9be4</a>
## [0.8.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.8.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/coding-agents/v0.8.1)
**Features**
- Added WorkBuddy and CodeBuddy coding-agent harness support.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8ea18a10b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8ea18a10b</a>
**Improvements**
- Coding-agent integration requests now identify themselves with a dedicated User-Agent header.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
**Bug Fixes**
- Improved recovery from HTTP failures during memory draining, reflection, paging, probing, and extended reflection operations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b335b7f73" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b335b7f73</a>
- Preserved complete retained session history across Pi runs, failed operations, exits, and side sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d3f0001ed" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d3f0001ed</a>
- Fixed memory injection for long prompts, initial empty recalls, repeated DSH runs, and Grok Build.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/87a9d0366" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>87a9d0366</a>
- Corrected session-start survey and reflection timing, shared-bank page scoping, and initiative marker handling.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/87b3e311d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>87b3e311d</a>
- Fixed Windows hook and process launching behavior, Pi session working directories, and Git log parsing.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cdaf0e5ae" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cdaf0e5ae</a>
- Waited through rate-limit responses when deepening memories instead of dropping history.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/11862f546" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>11862f546</a>
## [0.8.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.8.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/coding-agents/v0.8.0)
**Features**
- Add TraeCode (TRAE CN) as a supported coding-agent harness.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cq-guojia" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cq-guojia</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/30bdc7ea6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>30bdc7ea6</a>
- Present memories as past records and allow teams to add custom guidance.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/df324fd0a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>df324fd0a</a>
- Add kimi-code as a supported hook-based coding-agent harness.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/malzzz" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@malzzz</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/557420190" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>557420190</a>
- Allow users to provide a default bank configuration for banks created by the plugin.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/mvanhorn" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@mvanhorn</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f627de346" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f627de346</a>
- Support per-directory coding-agent configuration overrides using path prefixes.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/altaywtf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@altaywtf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b5fe6bac6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b5fe6bac6</a>
- Allow retained session context to identify the author of each turn.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/0xble" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@0xble</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/70869819e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>70869819e</a>
- Include each page's source query in knowledge-base search results.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0bf42b207" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0bf42b207</a>
**Improvements**
- Allow configuration of the automatic memory-injection timeout.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sealdot" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sealdot</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/35a64caf4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>35a64caf4</a>
**Bug Fixes**
- Ignore non-conversation Codex content when capturing session memory.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/gfreschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@gfreschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/26e1d21ff" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>26e1d21ff</a>
- Keep long-running memory deepening operations protected from overlapping runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f2f24887c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f2f24887c</a>
- Prevent unrelated git survey errors from being shown in the terminal.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5361f23e4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5361f23e4</a>
- Fix installation for the DSH coding-agent integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/972fa8958" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>972fa8958</a>
- Handle gzip tool failures correctly in the coding-agent integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/972fa8958" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>972fa8958</a>
- Preserve git history context when the current checkout is behind the recorded commit.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/L4XB" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@L4XB</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/361f2f17a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>361f2f17a</a>
- Preserve prior session content when appending Codex continuation rollouts.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1238dbdbc" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1238dbdbc</a>
- Avoid installing incompatible Grok and Claude hooks in each other's configurations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/altaywtf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@altaywtf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3a5ad3786" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3a5ad3786</a>
- Only append session content when the target memory bank supports text storage.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/HoneyTyagii" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@HoneyTyagii</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d3633c1c2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d3633c1c2</a>
- Prevent concurrent cold starts from launching multiple coding-agent daemons.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Tonited" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Tonited</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/849cb7ad0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>849cb7ad0</a>
- Correctly infer Codex code-mode actions from tool activity.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/altaywtf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@altaywtf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7299532fa" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7299532fa</a>
- Preserve Pi prompt sections when recalling memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/253642c6e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>253642c6e</a>
- Remove duplicated Markdown content from page API responses.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/JoAdo07" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@JoAdo07</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c3bc8d918" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c3bc8d918</a>
- Bind Cursor's MCP integration to the currently open workspace.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/okitoxo" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@okitoxo</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4fd53a73e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4fd53a73e</a>
- Keep the DSH home-directory patch working after reinstalling the integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b9a5d62da" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b9a5d62da</a>
- Write the required hooks version so Cursor displays installed hooks.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/okitoxo" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@okitoxo</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bfc59de73" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bfc59de73</a>
- Use the process working directory when Devin hooks lack a workspace path.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8affd0581" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8affd0581</a>
- Date backfilled chat memories using their original source timestamps.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/wcy12378" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@wcy12378</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e1153b6ac" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e1153b6ac</a>
- Emit DSH recall sources compatible with V4 memory APIs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6af2055b6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6af2055b6</a>
- Include the skill path when configuring the opencode2 harness.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Yi-111-a" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Yi-111-a</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bfc797b1a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bfc797b1a</a>
## [0.7.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.7.0)
**Features**
- Agents are now required to credit memories they use, rather than treating attribution as optional.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/87f62b1d3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>87f62b1d3</a>
- Knowledge-page settings can now be configured separately for each page.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dd80672be" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dd80672be</a>
- Automatic first-prompt memory injection can now select reflection, knowledge pages, or recall.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9ef0d901c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9ef0d901c</a>
**Improvements**
- Memory extraction is concise by default, can be configured and synchronized, and supports appending cached content.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a0e2583b0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a0e2583b0</a>
- Agents more consistently read relevant stored memory during their work.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/01bb474d6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>01bb474d6</a>
- OpenCode 2 integration now follows the stable package releases.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/oliver-mee" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@oliver-mee</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c5f35d409" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c5f35d409</a>
**Bug Fixes**
- Knowledge pages remain available after a transient 404 response instead of being disabled for the rest of the process.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ce4ca0281" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ce4ca0281</a>
- Codex integration now supports current custom tool-call formats.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/carlosrve" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@carlosrve</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/53e1fd85d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>53e1fd85d</a>
- Codex rollout events now retain their original timestamps.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/carlosrve" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@carlosrve</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/107c256dd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>107c256dd</a>
- The companion skill is now delivered through the plugin-manager installation route.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d73c517b4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d73c517b4</a>
- Host-owned harness diagnostics are created in advance so diagnostics are available reliably.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/heyallencao" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@heyallencao</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d288a91e2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d288a91e2</a>
- DSH users are now prompted to start a new session after installation so the integration takes effect.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/65782f70a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>65782f70a</a>
- Automatic update checks now resolve versions through npm reliably.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f962f4b4d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f962f4b4d</a>
- Automatic reflection is no longer prematurely limited to 20 seconds.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5c0c4ac59" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5c0c4ac59</a>
- Documents with distinct non-ASCII titles now remain distinct instead of colliding.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Shy7777" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Shy7777</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9683b9bba" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9683b9bba</a>
- Background Git command errors are now captured for troubleshooting.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/kyletser" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@kyletser</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3ea5489c3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3ea5489c3</a>
- Transcript appending now continues to work for transcripts larger than 32 MB.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0382a9c7b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0382a9c7b</a>
- Codex user messages are now correctly read from UserMessage events.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bef1e5ff3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bef1e5ff3</a>
## [0.6.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.6.1)
**Improvements**
- Reduced noisy output during fallback reflection by keeping details in diagnostic logs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/daaba6542" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>daaba6542</a>
## [0.6.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.6.0)
**Features**
- Record Hindsight tool usage for coding-agents and store integration logs under ~/.hindsight/coding-agents-logs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/220dcbdd9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>220dcbdd9</a>
## [0.5.5](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.5)
**Improvements**
- When session reflection fails, the integration now falls back to using knowledge pages and then observations to continue generating useful context.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a1ecadcd9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a1ecadcd9</a>
## [0.5.4](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.4)
**Features**
- Warns at session start if the legacy hindsight-memory Claude plugin is still active to prevent conflicts.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f39215592" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f39215592</a>
**Improvements**
- Records the reason when an automatic reflect fails to make troubleshooting easier.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e33debb95" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e33debb95</a>
**Bug Fixes**
- Ensures only one codebase survey runs per memory bank at a time by using a heartbeat lease.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cabfbb9fe" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cabfbb9fe</a>
- Fixes Grok config.toml being left with an unmarked Hindsight block after installation/updates.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/729b1120c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>729b1120c</a>
## [0.5.3](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.3)
**Features**
- Added ZCode as a supported harness for the coding-agents integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1f513a639" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1f513a639</a>
**Improvements**
- Knowledge pages now refresh hourly by default, with staggered scheduling to reduce load spikes.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8bcb4bc52" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8bcb4bc52</a>
## [0.5.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.2)
**Features**
- Add a Factory Droid harness to run and test the coding-agents integration in a dedicated droid-based environment.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/mrwogu" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@mrwogu</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/254bc988d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>254bc988d</a>
**Improvements**
- Reduce page-refresh load spikes by spreading scheduled refreshes using hashed cron fields.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/2643186a8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>2643186a8</a>
**Bug Fixes**
- Prevent transcript loss by retrying failed Retain appends instead of overwriting the existing transcript.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e655d889b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e655d889b</a>
- Fix DSH transcript retrieval by reading transcripts via snapshot events for more reliable history loading.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/leilei3167" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@leilei3167</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/363c1ee50" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>363c1ee50</a>
- Fix knowledge pages scoping so pages are tied to the bank rather than the session working directory.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/441ef3588" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>441ef3588</a>
## [0.5.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.1)
**Bug Fixes**
- Fixed the coding-agents integration so runtime auto-updates trigger correctly even when the registry returns a 406 for the /latest media type.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0eb818d0a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0eb818d0a</a>
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.5.0)
**Features**
- Added first-class support for Pi, sharing the same extension adapter and installer as Prime Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/71a9a7008" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>71a9a7008</a>
- Added per-source observation scoping so memories and observations can be attributed and managed by their originating source.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/shauneccles" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@shauneccles</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/247e9b313" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>247e9b313</a>
- Added support for OpenCode v2 (new plugin API).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c249d4379" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c249d4379</a>
- Added Qwen-Code as a supported hook harness.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1aef228f7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1aef228f7</a>
- Added native DeepAgents Dcode integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4388e0f95" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4388e0f95</a>
**Improvements**
- Installed runtime can now keep itself up to date automatically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4fa110dd5" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4fa110dd5</a>
**Bug Fixes**
- Improved tool safety labeling/annotation for MCP tools.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/altaywtf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@altaywtf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/173ed6a93" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>173ed6a93</a>
- Fixed the directory layout survey prompt to reference “Glob” instead of “Read.”<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/MasterST1337" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@MasterST1337</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5ceca53e9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5ceca53e9</a>
- Fixed failed git probing that could incorrectly fork a worktree into a separate bank.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c23e98856" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c23e98856</a>
- Bank configuration updates now only add settings and no longer overwrite existing config.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0c2f9e7c2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0c2f9e7c2</a>
- Fixed OpenCode installation so it won’t replace JSONC configuration files.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/etnperlong" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@etnperlong</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cd2e5407a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cd2e5407a</a>
- Prevented unsafe git log cleanup behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1977d5804" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1977d5804</a>
- Prevented the daemon idle timeout from killing in-flight requests.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d7d137fa1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d7d137fa1</a>
## [0.4.3](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.4.3)
**Features**
- Prime Agent now includes the companion skill when installed, expanding available agent capabilities.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e04d02ee6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e04d02ee6</a>
**Bug Fixes**
- Claude history import no longer fails when encountering an unexpected file, improving reliability of imports.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e04d02ee6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e04d02ee6</a>
## [0.4.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.4.2)
**Bug Fixes**
- Improved reflection behavior by ensuring knowledge pages are checked first, preventing incorrect or premature reflections.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/altaywtf" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@altaywtf</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3b2438356" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3b2438356</a>
- Fixed session discovery for Claude when project paths contain underscores, improving compatibility with more folder naming schemes.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/pstanton237" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@pstanton237</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b448202bd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b448202bd</a>
- Fixed resolving “bare-hub” project names so integrations correctly identify and connect to those projects.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1e4583478" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1e4583478</a>
- Made coding-agents more resilient to hidden/implicit page triggers so runs don’t fail when these triggers are present.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5bda953a2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5bda953a2</a>
- On Windows, child/daemon subprocesses are now hidden to avoid unwanted extra windows appearing during use.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0429060be" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0429060be</a>
## [0.4.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.4.1)
**Bug Fixes**
- Knowledge pages can now access the observations they request, and initiative markers are no longer incorrectly tagged with a page ID.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/170a07857" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>170a07857</a>
## [0.4.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.4.0)
**Features**
- Make the knowledge-page refresh trigger configurable.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f8b3988cf" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f8b3988cf</a>
**Improvements**
- Improve MCP registrations by including the calling harness name for clearer attribution and debugging.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cbbbb5c87" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cbbbb5c87</a>
- Add timestamps to the aggregated git log document to make generated context easier to interpret.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e11a59ff6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e11a59ff6</a>
**Bug Fixes**
- Prevent crashes when no coding-agent tools are discovered by handling empty tool discovery gracefully.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1e889bd1c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1e889bd1c</a>
- Ensure opt-in settings carry over correctly when working across multiple git worktrees.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/069ed96f3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>069ed96f3</a>
- Keep an initiative aligned with the current plan by re-capturing it when the plan changes.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/30943cba1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>30943cba1</a>
- Respect retainSessions in hook-based harnesses so sessions are preserved or discarded as configured.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/188eaa3dc" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>188eaa3dc</a>
- Keep long-lived host credentials active and report which credential was used to reduce unexpected auth failures.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/39de3974e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>39de3974e</a>
- Allow hindsight_reflect to wait up to the configured duration instead of timing out early.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0de91b8b7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0de91b8b7</a>
- Avoid duplicating observations by consolidating to a single set of observations per memory bank.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/27e4b188d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>27e4b188d</a>
- In non-git directories, keep the memory bank anchored to where the session started to prevent bank switching mid-session.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a04f4eed3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a04f4eed3</a>
- Scope knowledge pages to the repository they refer to to prevent cross-repo knowledge leakage.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6ea34d830" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6ea34d830</a>
- Stop forcing a 300-second idle timeout on the shared daemon so it can remain available as configured.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f97017421" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f97017421</a>
- Start the local daemon for plugin harnesses as well, ensuring coding-agents work consistently in plugin-based setups.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9da3dc9eb" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9da3dc9eb</a>
## [0.3.4](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.3.4)
**Features**
- Added DeepSeek Harness (dsh) support to run coding-agents with DeepSeek via a dedicated harness and installer flow.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/28760f62d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>28760f62d</a>
## [0.3.3](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.3.3)
**Improvements**
- When a turn runs without memory, the system now explicitly reports it to make memory usage clearer during runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/383b1d201" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>383b1d201</a>
**Bug Fixes**
- OpenCode survey now runs under the integration’s own agent (instead of plan mode), improving correctness and consistency of survey behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c43a3ebfa" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c43a3ebfa</a>
## [0.3.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.3.2)
**Bug Fixes**
- Improved rate-limit (HTTP 429) retry handling so backoff respects the caller’s timeout/deadline instead of a fixed constant, preventing excessive waiting.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a4c159334" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a4c159334</a>
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.3.1)
**Features**
- Adds an opt-in-only mode so Hindsight memory runs only for projects that have explicitly opted in.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/77bbb9394" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>77bbb9394</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.3.0)
**Breaking Changes**
- Removed the client-side "retainEveryTurns" batching option; batching is now handled server-side.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4fa25f47d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4fa25f47d</a>
**Features**
- Added Prime Agent as a supported harness for the coding-agents integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/29cd6c6ab" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>29cd6c6ab</a>
**Improvements**
- Improved resilience to rate limiting by handling HTTP 429 responses during both initial requests and polling.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/098362d45" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>098362d45</a>
- Preserve source attribution across all ingestion paths so imported memories consistently retain their origin metadata.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/voarsh2" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@voarsh2</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9032ed9c9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9032ed9c9</a>
**Bug Fixes**
- Fixed a race where the prompt hook could clear the retain cursor by writing retain state atomically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aaa58f247" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aaa58f247</a>
- Fixed failures when the working directory no longer exists by resolving the project correctly even after directory deletion/moves.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d3f97da1e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d3f97da1e</a>
- Prevented runaway automatic reflection by enforcing a maximum reflect budget.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/TheAngryPit" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@TheAngryPit</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/490cc5243" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>490cc5243</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.2.1)
**Bug Fixes**
- Prevent Claude Code compaction summaries from being retained in coding-agent conversation history.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f10394933" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f10394933</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.2.0)
**Features**
- Add support for retaining tags and metadata in stored memories (configurable via HINDSIGHT_RETAIN_TAGS).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a22667dc0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a22667dc0</a>
**Bug Fixes**
- Prevent repeated auto-seeding of bank missions so users' mission choices aren’t overwritten on subsequent runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/056982b4a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>056982b4a</a>
- Ensure the configured MCP harness is correctly reported and attributed in recorded data.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/TheAngryPit" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@TheAngryPit</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d270b124a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d270b124a</a>
- Fix retained-turns behavior by excluding harness transport wrapper messages from the retained transcript.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3b42913a6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3b42913a6</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.1.2)
**Bug Fixes**
- Prevent oversized coding-agent sessions from losing memory by bounding transcript reads so retention can still succeed.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aa0aa7649" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aa0aa7649</a>
- Improve retention reliability by appending only new conversation turns and making writes idempotent to avoid duplicate or corrupted memory updates.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/815f5aaa3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>815f5aaa3</a>
- Fix incorrect project detection when working from a repository root worktree, improving integration behavior in monorepos/worktrees.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6d6177219" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6d6177219</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.1.1)
**Bug Fixes**
- Fixed Coding Agents integration to seed the actual harness configuration instead of the default "opencode" harness.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cbbc86469" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cbbc86469</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.1.0)
**Features**
- Installer now includes a styled interactive UI with arrow-key server selection and requires a Hindsight Cloud token during setup.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/79fe41110" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>79fe41110</a>
**Improvements**
- Improved installation reliability by staging the runtime so running via npx works correctly.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a3298cdf0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a3298cdf0</a>
## [0.0.5](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.0.5)
**Features**
- Added a local daemon mode for the coding-agents integration and allow choosing which Hindsight server to connect to during installation.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e1b3d438e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e1b3d438e</a>
**Improvements**
- Improved Codex plugin migration by carrying over the existing endpoint when switching to the coding-agents integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/18a58ef3a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>18a58ef3a</a>
## [0.0.4](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.0.4)
**Features**
- Added a new --import-conversations option to import past coding-agent conversations into Hindsight as a migration path from per-agent plugins.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/17b7f46ae" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>17b7f46ae</a>
**Bug Fixes**
- Fixed how imported sessions are attributed so they’re matched by recorded working directory (cwd) rather than by session name.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d238d2f7d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d238d2f7d</a>
- Improved Devin transcript importing by using node:sqlite, and the installer now refuses to proceed when the required SQLite support isn’t available (with corrected guidance messaging).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/333812c85" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>333812c85</a>
## [0.0.3](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.0.3)
**Breaking Changes**
- Installation now requires explicitly choosing a target (either “install all” or a specific harness).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/05b55d20f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>05b55d20f</a>
**Bug Fixes**
- Added support for additional harnesses and improved environment-based configuration fallback so installs work more reliably across setups.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/2b1c4989f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>2b1c4989f</a>
## [0.0.2](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.0.2)
**Bug Fixes**
- Fixes re-install behavior so Antigravity and the Claude MCP server are correctly repointed instead of left misconfigured.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/47b679ec2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>47b679ec2</a>
## [0.0.1](https://github.com/vectorize-io/hindsight/tree/integrations/coding-agents/v0.0.1)
**Features**
- Enabled coding agents to use Hindsight’s pluggable long-term memory for persistent context across tasks.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b5d8439c8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b5d8439c8</a>
references/changelog/integrations/composio.md
---
hide_table_of_contents: true
---
# Composio Integration Changelog
Changelog for [`hindsight-composio`](https://pypi.org/project/hindsight-composio/).
For the source code, see [`hindsight-integrations/composio`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/composio).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/composio/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/composio/v0.1.1)
**Improvements**
- Composio integration requests now identify themselves with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/composio/v0.1.0)
**Features**
- Added Composio integration to expose Hindsight memory as custom tools for Composio-enabled workflows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1c9ba0e65" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1c9ba0e65</a>
references/changelog/integrations/continue.md
---
hide_table_of_contents: true
---
# Continue Integration Changelog
Changelog for [`hindsight-continue`](https://pypi.org/project/hindsight-continue/).
For the source code, see [`hindsight-integrations/continue`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/continue).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/continue/v0.1.1)
**Bug Fixes**
- Improved stability for Continue integration by making the adapter thread-safe to prevent concurrency-related issues.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/fcb2c958e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>fcb2c958e</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/continue/v0.1.0)
**Features**
- Added Continue.dev integration via an HTTP context provider.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c05ab910" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c05ab910</a>
references/changelog/integrations/copilot-cli.md
---
hide_table_of_contents: true
---
# GitHub Copilot CLI Integration Changelog
Changelog for [`hindsight-copilot-cli`](https://pypi.org/project/hindsight-copilot-cli/).
For the source code, see [`hindsight-integrations/copilot-cli`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/copilot-cli).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/copilot-cli/v0.1.0)
**Features**
- Added a GitHub Copilot CLI integration with hooks to automatically capture and recall Hindsight memories during Copilot sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ScottGuymer" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ScottGuymer</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6500944c7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6500944c7</a>
references/changelog/integrations/crewai.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="CrewAI Changelog" subtitle="hindsight-crewai — persistent memory for CrewAI agents." />
[← CrewAI integration](../../sdks/integrations/crewai.md)
## [0.4.20](https://github.com/vectorize-io/hindsight/tree/integrations/crewai/v0.4.20)
**Features**
- Added CrewAI integration enabling persistent memory for crews via Hindsight. ([`41db2960`](https://github.com/vectorize-io/hindsight/commit/41db2960))
**Improvements**
- All HTTP requests now include an identifying User-Agent header for better compatibility and observability with upstream services. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
- Improved type-checking support for Python users by shipping PEP 561 type information in all packages. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
- Security updates applied to address known dependency vulnerabilities. ([`300d089b`](https://github.com/vectorize-io/hindsight/commit/300d089b))
references/changelog/integrations/cursor-cli.md
---
hide_table_of_contents: true
---
# Cursor CLI Integration Changelog
Changelog for [`hindsight-cursor-cli`](https://pypi.org/project/hindsight-cursor-cli/).
For the source code, see [`hindsight-integrations/cursor-cli`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cursor-cli).
← [Back to main changelog](../index.md)
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/cursor-cli/v0.3.0)
**Bug Fixes**
- Fixed Cursor 3.x transcripts so role-nested agent conversations are parsed correctly for recall/retention.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/bjornmp" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@bjornmp</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/84e67efbf" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>84e67efbf</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/cursor-cli/v0.2.0)
**Features**
- Cursor CLI integration is now available as a pip-installable package (`hindsight-cursor-cli`) with a Python-based installer/CLI for setup and hook installation.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c6dd08944" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c6dd08944</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/cursor-cli/v0.1.0)
**Features**
- Added a Cursor CLI integration for Hindsight, including install/uninstall scripts, session hooks, and commands to retain and recall memories during Cursor sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Korayem" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Korayem</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dbfe83a2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dbfe83a2</a>
references/changelog/integrations/cursor.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Cursor Changelog" subtitle="hindsight-memory - Hindsight memory plugin for Cursor." />
[← Cursor integration](../../sdks/integrations/cursor.md)
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/cursor/v0.2.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/cursor/v0.2.1)
**Features**
- Cursor Agent CLI can now be used as an LLM provider.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8bc5ab103" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8bc5ab103</a>
**Improvements**
- Cursor and other integrations now identify themselves with integration-specific User-Agent headers.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
- Integrations launched through uvx now select a Python version compatible with the daemon.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6c8a92c31" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6c8a92c31</a>
**Bug Fixes**
- Cursor now accepts BOM-prefixed input and correctly scopes memory banks to workspace roots.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ab27418eb" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ab27418eb</a>
- Cursor reliably installs its hooks and rules and flushes pending memories when a session ends.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/682f77a22" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>682f77a22</a>
- In-flight Cursor integration requests are no longer terminated by the daemon idle timeout.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d7d137fa1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d7d137fa1</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/cursor/v0.2.0)
**Features**
- Added a Hindsight memory plugin integration for Cursor.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/91d767cdc" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>91d767cdc</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cursor)
**Features**
- Added a Hindsight memory plugin for Cursor using Cursor's plugin hooks for automatic recall and retain.
- Added Cursor-native extras including an always-on rule and an on-demand `hindsight-recall` skill.
- Added support for local daemon mode, external Hindsight APIs, and native MCP as an alternative integration path.
- Added automated tests covering config loading, bank derivation, content formatting, and hook behavior.
references/changelog/integrations/devin-desktop.md
---
hide_table_of_contents: true
---
# Devin Desktop Integration Changelog
Changelog for [`hindsight-devin-desktop`](https://pypi.org/project/hindsight-devin-desktop/).
For the source code, see [`hindsight-integrations/devin-desktop`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/devin-desktop).
← [Back to main changelog](../index.md)
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/devin-desktop/v0.2.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/devin-desktop/v0.2.1)
**Improvements**
- Devin Desktop requests now identify the integration with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/devin-desktop/v0.1.0)
**Improvements**
- Renamed the Devin Desktop integration (previously Windsurf) and improved stability when using it concurrently.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/fcb2c958e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>fcb2c958e</a>
references/changelog/integrations/dify.md
---
hide_table_of_contents: true
---
# Dify Integration Changelog
Changelog for [`hindsight-dify`](https://pypi.org/project/hindsight-dify/).
For the source code, see [`hindsight-integrations/dify`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/dify).
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/dify/v0.1.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/dify/v0.1.2)
**Improvements**
- Dify integration requests now use an integration-specific User-Agent for clearer service identification and support diagnostics.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/dify/v0.1.1)
**Features**
- Added Dify integration, enabling Hindsight memory tools to be used within Dify workflows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bc23750b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bc23750b</a>
references/changelog/integrations/eliza.md
---
hide_table_of_contents: true
---
# elizaOS Integration Changelog
Changelog for [`@vectorize-io/hindsight-eliza`](https://www.npmjs.com/package/@vectorize-io/hindsight-eliza).
For the source code, see [`hindsight-integrations/eliza`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/eliza).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/eliza/v0.1.0)
**Features**
- Added a Hindsight long-term memory integration for elizaOS, enabling eliza to store and retrieve persistent memories via Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f36a462d1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f36a462d1</a>
references/changelog/integrations/eve.md
---
hide_table_of_contents: true
---
# Eve Integration Changelog
Changelog for [`@vectorize-io/hindsight-eve`](https://www.npmjs.com/package/@vectorize-io/hindsight-eve).
For the source code, see [`hindsight-integrations/eve`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/eve).
← [Back to main changelog](../index.md)
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.3.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/eve/v0.3.1)
**Improvements**
- Eve integration requests now use a dedicated User-Agent identifier for clearer service identification.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.3.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/eve/v0.3.0)
**Features**
- Adds a Hindsight memory provider for Eve’s first-class memory slots, enabling managed memory retrieval and storage through the integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/295b53fac" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>295b53fac</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.2.1)
**Features**
- Eve integration now retains the assistant’s reply in memory by default.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/29cc1d7fd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>29cc1d7fd</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.2.0)
**Features**
- Added an auto-memory mode that works without model tool-calling, enabling Eve to capture memories automatically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dd7e25245" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dd7e25245</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.1.0)
**Features**
- Added an Eve integration helper to connect Eve agents to Hindsight via MCP.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d0b77f5be" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d0b77f5be</a>
references/changelog/integrations/flowise.md
---
hide_table_of_contents: true
---
# Flowise Integration Changelog
Changelog for [`@vectorize-io/flowise-nodes-hindsight`](https://www.npmjs.com/package/@vectorize-io/flowise-nodes-hindsight).
For the source code, see [`hindsight-integrations/flowise`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/flowise).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/flowise/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/flowise/v0.1.1)
**Improvements**
- Flowise Hindsight Recall, Reflect, and Retain requests now identify the integration with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/flowise/v0.1.0)
**Features**
- Added Flowise integration for Hindsight, enabling use of Hindsight memory tools within Flowise workflows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/41ad2b55" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>41ad2b55</a>
references/changelog/integrations/github-copilot.md
---
hide_table_of_contents: true
---
# GitHub Copilot Integration Changelog
Changelog for [`hindsight-copilot`](https://pypi.org/project/hindsight-copilot/).
For the source code, see [`hindsight-integrations/github-copilot`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/github-copilot).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/github-copilot/v0.1.0)
**Features**
- Added GitHub Copilot (VS Code) integration via MCP.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/91bf32842" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>91bf32842</a>
references/changelog/integrations/google-adk.md
---
hide_table_of_contents: true
---
# Google ADK Integration Changelog
Changelog for [`hindsight-google-adk`](https://pypi.org/project/hindsight-google-adk/).
For the source code, see [`hindsight-integrations/google-adk`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/google-adk).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/google-adk/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/google-adk/v0.1.1)
**Bug Fixes**
- Preserve original timestamps when memories are stored through the Google ADK integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8456a7fdc" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8456a7fdc</a>
- Retry Google ADK memory-bank mission setup after transient failures so memory operations can recover automatically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/81f1c18d0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>81f1c18d0</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/google-adk/v0.1.0)
**Features**
- Added Google ADK integration for Hindsight, enabling Hindsight to work with Google ADK-based agents.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c032a74f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c032a74f</a>
references/changelog/integrations/haystack.md
---
hide_table_of_contents: true
---
# Haystack Integration Changelog
Changelog for [`hindsight-haystack`](https://pypi.org/project/hindsight-haystack/).
For the source code, see [`hindsight-integrations/haystack`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/haystack).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/haystack/v0.1.1)
*This release contains internal maintenance and infrastructure changes only.*
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/haystack/v0.1.0)
**Features**
- Added a Haystack integration for Hindsight, enabling use of Hindsight memory tools within Haystack pipelines.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/394d66e6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>394d66e6</a>
references/changelog/integrations/hermes.md
---
hide_table_of_contents: true
---
# Hermes Agent Integration Changelog
Changelog for [`hindsight`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes).
For the source code, see [`hindsight-integrations/hermes`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes).
← [Back to main changelog](../index.md)
## [1.2.3](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v1.2.3)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/hermes/v1.2.3)
**Bug Fixes**
- Default Hermes observations to the shared scope so memories merge across sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/61213351b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>61213351b</a>
## [1.2.2](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v1.2.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/hermes/v1.2.2)
**Features**
- Hermes reflections can now be labeled with configured reflect tags.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/10b14dd0d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>10b14dd0d</a>
- Hermes now supports per-project routing and routing across multiple memory banks.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/jnbn" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@jnbn</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/08ba570e2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>08ba570e2</a>
- Hermes can now filter recalled memories below configured relevance-score thresholds.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Charles-HL" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Charles-HL</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5c2185c65" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5c2185c65</a>
- Hermes now reads retain context, indicator settings, and synchronous recall settings from environment variables.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vadimcomanescu" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@vadimcomanescu</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cf253a8c4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cf253a8c4</a>
- Hermes integrations can now choose the retain strategy used for storing memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/0xble" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@0xble</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/44ac64eae" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>44ac64eae</a>
**Improvements**
- Integrations now identify themselves with integration-specific User-Agent headers.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
- Updated the default Gemini Flash model to gemini-3.8-flash.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d80b54821" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d80b54821</a>
**Bug Fixes**
- Hermes now skips cron-triggered runs when capturing memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/10b14dd0d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>10b14dd0d</a>
- Hermes now honors the HINDSIGHT_BANK_ID setting when selecting a memory bank.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/10b14dd0d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>10b14dd0d</a>
- Hermes automatically rechecks a Hindsight server after it was previously unreachable.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/10b14dd0d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>10b14dd0d</a>
- Resolved a batch of Hermes plugin issues affecting integration behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/37f0cd1a6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>37f0cd1a6</a>
- Improved local embedded Hindsight daemon startup, restart handling, and Windows launching.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/fd70557c6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>fd70557c6</a>
- Hermes preserves built-in memory writes so facts removed from MEMORY.md are not lost.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/spiky02plateau" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@spiky02plateau</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ce809e934" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ce809e934</a>
- Hermes now safely returns no results when recalling or reflecting on an empty memory bank.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vadimcomanescu" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@vadimcomanescu</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7e9057a77" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7e9057a77</a>
## [1.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v1.2.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/hermes/v1.2.1)
**Improvements**
- Hermes now announces embedded server downloads and reuses its client during startup.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a3c8e2a61" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a3c8e2a61</a>
**Bug Fixes**
- Hermes repair guidance now points to the package manager and avoids duplicate dependency listings.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ethernet8023" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ethernet8023</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/05e7958f6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>05e7958f6</a>
## [1.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v1.2.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/hermes/v1.2.0)
**Features**
- Added usage guidance to the Hermes system prompt block to help agents use Hindsight memory effectively.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dragonkid" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dragonkid</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c296e7c57" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c296e7c57</a>
**Bug Fixes**
- Improved embedded Hermes daemon startup by running it in an isolated subprocess and preventing parent Python environments from interfering.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/42104fff6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>42104fff6</a>
## [1.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v1.1.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/hermes/v1.1.0)
**Breaking Changes**
- Removed the previous Hindsight-Hermes integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cf0537ba7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cf0537ba7</a>
**Features**
- Added the Hindsight memory integration for Hermes Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ef90842f8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ef90842f8</a>
- Added file-based configuration support for the Hermes memory integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0ff36548e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0ff36548e</a>
- Added the Hermes Agent memory plugin to the Hindsight integrations repository.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/acccae56b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>acccae56b</a>
**Improvements**
- Made the Hermes memory plugin available as a releasable installable package.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8e903047e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8e903047e</a>
**Bug Fixes**
- Prevented Hermes Agent from deadlocking its event loop when using Hindsight memory operations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/35dfd3aa0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>35dfd3aa0</a>
- Updated lifecycle hook handling for compatibility with Hermes Agent 0.5.0.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e7c9a6832" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e7c9a6832</a>
- Made the Hermes memory plugin usable independently outside the Hermes source tree.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dc7503886" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dc7503886</a>
- Ported post-handoff Hermes memory plugin fixes and required compatible Hindsight dependencies.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/97ae2248a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>97ae2248a</a>
references/changelog/integrations/langgraph.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="LangGraph Changelog" subtitle="hindsight-langgraph — LangGraph and LangChain memory integration." />
[← LangGraph integration](../../sdks/integrations/langgraph.md)
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/langgraph/v0.3.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/langgraph/v0.3.1)
**Bug Fixes**
- Clear stale recalled memory context when a conversation turn has no new user input, preventing outdated memories from being reused.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7ba944aa4" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7ba944aa4</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/langgraph/v0.3.0)
**Bug Fixes**
- Fixes LangGraph integration to correctly load tool bank IDs from configuration, improving tool selection and execution.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/760bfc744" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>760bfc744</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/langgraph/v0.2.0)
**Breaking Changes**
- Updated the LangGraph integration API by removing the legacy BaseStore-based store and adjusting nodes/tools to use the new memory instructions flow.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b67e813a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b67e813a</a>
**Improvements**
- Improved LangGraph integration reliability by fixing graph nodes and adding expanded end-to-end and flow tests.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b67e813a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b67e813a</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/langgraph/v0.1.2)
**Improvements**
- Improved Python typing support for the Hindsight LangGraph integration (added PEP 561 py.typed marker) so type checkers work correctly. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
- Updated dependencies to address critical/high security vulnerabilities in the Hindsight LangGraph integration. ([`ee4510a7`](https://github.com/vectorize-io/hindsight/commit/ee4510a7))
**Bug Fixes**
- All HTTP requests from the Hindsight LangGraph integration now include a consistent identifying User-Agent header. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/langgraph/v0.1.1)
**Features**
- Added LangGraph integration for Hindsight. ([`b4320254`](https://github.com/vectorize-io/hindsight/commit/b4320254))
references/changelog/integrations/litellm.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="LiteLLM Changelog" subtitle="hindsight-litellm — universal LLM memory integration via LiteLLM." />
[← LiteLLM integration](../../sdks/integrations/litellm.md)
## [0.5.4](https://github.com/vectorize-io/hindsight/tree/integrations/litellm/v0.5.4)
**Bug Fixes**
- Fixes LiteLLM integration reliability by correcting injection mode behavior, ensuring context-manager state is restored properly, and making validation/errors consistent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dfe74b1d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dfe74b1d</a>
## [0.5.3](https://github.com/vectorize-io/hindsight/tree/integrations/litellm/v0.5.3)
*This release contains internal maintenance and infrastructure changes only.*
## [0.5.2](https://github.com/vectorize-io/hindsight/tree/integrations/litellm/v0.5.2)
**Bug Fixes**
- Fix conversation storage when using streaming LiteLLM responses.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ac5181f5" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ac5181f5</a>
## [0.5.1](https://github.com/vectorize-io/hindsight/tree/integrations/litellm/v0.5.1)
**Improvements**
- Add bundled type information for better type-checking support when using the integration in typed Python projects. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
- Improve security and stability by updating and constraining the LiteLLM dependency, including excluding a compromised version. ([`8a2388a4`](https://github.com/vectorize-io/hindsight/commit/8a2388a4))
**Bug Fixes**
- Set an identifying User-Agent header on all HTTP requests to improve compatibility with providers and proxies. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/litellm/v0.5.0)
**Features**
- Add streaming support when using the LiteLLM wrapper integration. ([`665877bb`](https://github.com/vectorize-io/hindsight/commit/665877bb))
- Add async retain and reflect support, along with a cleaned-up LiteLLM integration API. ([`1d4879a2`](https://github.com/vectorize-io/hindsight/commit/1d4879a2))
- Initial release of the Hindsight LiteLLM integration implementation. ([`dfccbf29`](https://github.com/vectorize-io/hindsight/commit/dfccbf29))
**Improvements**
- Support sending tags and mission metadata through the LiteLLM integration to improve memory organization and retrieval. ([`f3c5a9c1`](https://github.com/vectorize-io/hindsight/commit/f3c5a9c1))
**Bug Fixes**
- When no explicit Hindsight query is provided, the integration now uses the most recent user message as the query to avoid missing/empty memory lookups. ([`5e8952c5`](https://github.com/vectorize-io/hindsight/commit/5e8952c5))
- Fix API key handling by passing the configured api_key through to the Hindsight client in the LiteLLM integration. ([`c0ca9b02`](https://github.com/vectorize-io/hindsight/commit/c0ca9b02))
references/changelog/integrations/llamaindex.md
---
hide_table_of_contents: true
---
# LlamaIndex Integration Changelog
Changelog for [`hindsight-llamaindex`](https://pypi.org/project/hindsight-llamaindex/).
For the source code, see [`hindsight-integrations/llamaindex`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/llamaindex).
← [Back to main changelog](../index.md)
## [0.1.5](https://github.com/vectorize-io/hindsight/tree/integrations/llamaindex/v0.1.5)
**Improvements**
- Replaced the deprecated manual test with a gated end-to-end test suite, improving integration reliability without requiring a real LLM for every run.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ed34756c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ed34756c</a>
**Bug Fixes**
- LlamaIndex integration now defaults to using Hindsight Cloud, improving out-of-the-box connectivity and reducing setup issues.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ed34756c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ed34756c</a>
## [0.1.4](https://github.com/vectorize-io/hindsight/tree/integrations/llamaindex/v0.1.4)
**Improvements**
- All HTTP requests now include a consistent identifying User-Agent header. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
- Improved Python type-checking support by shipping a PEP 561 marker file in the package. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
- Updated dependencies to address critical and high-severity security vulnerabilities. ([`ee4510a7`](https://github.com/vectorize-io/hindsight/commit/ee4510a7))
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/llamaindex/v0.1.3)
**Bug Fixes**
- Fixed LlamaIndex integration issues with document IDs, the memory API, and ReAct trace handling to improve reliability and correctness. ([`d93dfea8`](https://github.com/vectorize-io/hindsight/commit/d93dfea8))
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/llamaindex/v0.1.2)
**Features**
- Added LlamaIndex integration for Hindsight. ([`2d787c4f`](https://github.com/vectorize-io/hindsight/commit/2d787c4f))
references/changelog/integrations/meta-muse.md
---
hide_table_of_contents: true
---
# Meta Muse Integration Changelog
Changelog for [`hindsight-meta-muse`](https://pypi.org/project/hindsight-meta-muse/).
For the source code, see [`hindsight-integrations/meta-muse`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/meta-muse).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/meta-muse/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/meta-muse/v0.1.1)
**Features**
- Added Meta Muse integration through MCP for connecting Hindsight memory capabilities.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6aaa1b88f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6aaa1b88f</a>
**Improvements**
- Meta Muse integration requests now identify themselves with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
**Bug Fixes**
- Meta Muse connection setup can now be safely repeated and better supports partially completed setups.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b108dfd5d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b108dfd5d</a>
references/changelog/integrations/n8n.md
---
hide_table_of_contents: true
---
# n8n Integration Changelog
Changelog for [`@vectorize-io/n8n-nodes-hindsight`](https://www.npmjs.com/package/@vectorize-io/n8n-nodes-hindsight).
For the source code, see [`hindsight-integrations/n8n`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/n8n).
## [0.1.4](https://github.com/vectorize-io/hindsight/tree/integrations/n8n/v0.1.4)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/n8n/v0.1.4)
**Improvements**
- The n8n integration now identifies its requests with a dedicated User-Agent, improving service-side request attribution and supportability.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/n8n/v0.1.3)
**Improvements**
- Improved reliability and compatibility of the Hindsight n8n node by removing the runtime dependency on the Hindsight client and making direct HTTP requests instead.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e18ab10a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e18ab10a</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/n8n/v0.1.2)
**Bug Fixes**
- Synced the n8n Hindsight integration with recent configuration changes to keep documentation and node behavior consistent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/23dc07b0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>23dc07b0</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/n8n/v0.1.1)
**Features**
- Added an n8n community node package to integrate Hindsight Memory into n8n workflows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c1eaf711" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c1eaf711</a>
**Bug Fixes**
- Fixed the n8n Hindsight node packaging/authentication so it loads correctly and sends the proper authorization header.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ad4c0e77" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ad4c0e77</a>
references/changelog/integrations/nemoclaw.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="NemoClaw Changelog" subtitle="@vectorize-io/hindsight-nemoclaw — persistent memory for NemoClaw sandboxed agents." />
[← NemoClaw integration](../../sdks/integrations/nemoclaw.md)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/nemoclaw/v0.1.2)
*This release contains internal maintenance and infrastructure changes only.*
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/nemoclaw/v0.1.1)
**Features**
- Added a setup CLI package for the Hindsight nemoclaw integration. ([`d284de28`](https://github.com/vectorize-io/hindsight/commit/d284de28))
references/changelog/integrations/obsidian.md
---
hide_table_of_contents: true
---
# Obsidian Integration Changelog
Changelog for [`@vectorize-io/hindsight-obsidian`](https://www.npmjs.com/package/@vectorize-io/hindsight-obsidian).
For the source code, see [`hindsight-integrations/obsidian`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/obsidian).
← [Back to main changelog](../index.md)
## [0.3.1](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.3.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/obsidian/v0.3.1)
**Improvements**
- The Obsidian integration now identifies itself with a dedicated User-Agent when communicating with Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.3.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/obsidian/v0.3.0)
**Features**
- Adds support for observation scopes in the Obsidian integration, allowing observations to be associated with a configured scope.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/omgbabyweb" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@omgbabyweb</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/57f0b8d10" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>57f0b8d10</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.2.1)
**Bug Fixes**
- Fix sync index scoping so Obsidian syncs/reconciliation are isolated per memory bank and API target, preventing cross-target index conflicts.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ea81cf345" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ea81cf345</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.2.0)
**Features**
- Added a headless CLI tool (hindsight-obsidian-sync) to ingest/sync an Obsidian vault into Hindsight without running the Obsidian app.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/93fa0b016" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>93fa0b016</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.1.2)
**Bug Fixes**
- Fixes an issue in the Obsidian chat view to meet community-store review requirements for v0.1.2.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c06e85b64" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c06e85b64</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.1.1)
**Features**
- Improved chat note viewing with grounded-note citations, collapsed-by-default behavior, persisted layout, and inline depth controls.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/fe404efb2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>fe404efb2</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/obsidian/v0.1.0)
**Features**
- Introduces the Hindsight Obsidian plugin, enabling in-editor Hindsight chat and syncing Obsidian notes/memory with Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b0f86f9c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b0f86f9c</a>
references/changelog/integrations/omo.md
---
hide_table_of_contents: true
---
# OMO Integration Changelog
Changelog for [`hindsight-omo`](https://pypi.org/project/hindsight-omo/).
For the source code, see [`hindsight-integrations/omo`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/omo).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/omo/v0.1.0)
**Features**
- Added a new Oh-My-OpenAgent (OMO) integration for Hindsight, including hooks and scripts to retain and recall memories across sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dcbouius" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dcbouius</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/6dc56498" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>6dc56498</a>
references/changelog/integrations/openai-agents.md
---
hide_table_of_contents: true
---
# openai-agents Integration Changelog
Changelog for [`hindsight-openai-agents`](https://pypi.org/project/hindsight-openai-agents/).
For the source code, see [`hindsight-integrations/openai-agents`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/openai-agents).
← [Back to main changelog](../index.md)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/openai-agents/v0.1.2)
**Improvements**
- Improved OpenAI Agents integration reliability by defaulting to the Cloud backend and adding gated end-to-end tests with real-LLM bucketing.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c01fc12f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c01fc12f</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/openai-agents/v0.1.1)
**Improvements**
- Documentation and test polish: corrected the openai-agents SDK version requirement, added `memory_instructions()` to the README and API reference, added a Production Patterns section to the README (error handling, bank lifecycle, multi-agent workflows), and added a dedicated `test_config.py` covering defaults, configure, env var fallback, and reset.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/52c148c2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>52c148c2</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/openai-agents/v0.1.0)
**Features**
- Added integration with the OpenAI Agents SDK for Hindsight’s AI memory workflows.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b8da88c8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b8da88c8</a>
references/changelog/integrations/openclaw.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="OpenClaw Changelog" subtitle="@vectorize-io/hindsight-openclaw — Hindsight memory plugin for OpenClaw." />
[← OpenClaw integration](../../sdks/integrations/openclaw.md)
## [1.0.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v1.0.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/openclaw/v1.0.0)
**Breaking Changes**
- Removes the agent SDK integration and the agent knowledge tools.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d29b842de" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d29b842de</a>
**Features**
- Adds Hindsight as OpenClaw’s native memory provider.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f50ce785c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f50ce785c</a>
**Bug Fixes**
- Honors configured OpenClaw log levels and summary intervals.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/72b9dd99a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>72b9dd99a</a>
- Captures inbound sender identity so Goals can start from received messages.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9e3e26f9e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9e3e26f9e</a>
- Reads completed session transcripts correctly so end-of-session memories are retained.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/30064c100" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>30064c100</a>
- Retains memories from plugin-managed copies of OpenClaw that were not started by the plugin.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/900599ac3" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>900599ac3</a>
- Preserves existing bank missions instead of overwriting them with a default.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7a4ba3678" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7a4ba3678</a>
## [0.13.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.13.0)
**Features**
- Adds agentBankMap support for assigning separate Hindsight memory banks to individual agents.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a2d95f9ef" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a2d95f9ef</a>
**Bug Fixes**
- Prevents recall work from a previous service generation from affecting current requests.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Tosko4" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Tosko4</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bcf1eb3d1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bcf1eb3d1</a>
- Ensures recall requests honor caller deadlines through the client transport.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Tosko4" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Tosko4</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/54226f62a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>54226f62a</a>
- Makes knowledge-tool response payloads available as details when using Code Mode.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/L4XB" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@L4XB</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c4b73f957" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c4b73f957</a>
- Correctly handles transcript session-end records during memory processing.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/L4XB" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@L4XB</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/567fa1c40" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>567fa1c40</a>
- Makes the OpenClaw smoke test fail whenever Hindsight doctor reports a diagnostic issue.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/945b74144" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>945b74144</a>
## [0.12.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.12.0)
**Features**
- Add configurable minimum recall score thresholds (“score floors”) to improve control over which memories are returned.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/koriyoshi2041" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@koriyoshi2041</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1bbae3f66" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1bbae3f66</a>
**Bug Fixes**
- Fix incorrect agent identity being shown/used for dashboard sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7868fe4bf" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7868fe4bf</a>
- Restore compatibility with OpenClaw 2026.8.1 and newer.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3292c96af" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3292c96af</a>
- Prevent the daemon from timing out while a request is still in progress.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d7d137fa1" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d7d137fa1</a>
## [0.11.1](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.11.1)
**Bug Fixes**
- Fixed conversation history being lost when the plugin runs its own local Hindsight daemon: every restart of the OpenClaw host replaced the documents retained before it, so long-running sessions kept only the most recent stretch of their history. Retains that fail while the daemon is unavailable are now queued and retried instead of dropped.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d97b560e2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d97b560e2</a>
## [0.11.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.11.0)
**Features**
- Added GitHub Copilot as a supported subscription provider for LLM usage.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dudujuju828" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dudujuju828</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1d9a6a381" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1d9a6a381</a>
**Improvements**
- Auto-recalled memories now include document provenance tags (e.g., "[doc:<document_id>]") to make sources clearer.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/xiaoxiaoHe-E" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@xiaoxiaoHe-E</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0eee31b63" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0eee31b63</a>
**Bug Fixes**
- Retain requests are now idempotent when queued, preventing duplicate retains from repeated submissions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3d7f7930b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3d7f7930b</a>
- Recall and retain results no longer include the operator-configured display-name prefix, improving consistent naming in returned memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/chethanuk" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@chethanuk</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e62ce26ef" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e62ce26ef</a>
## [0.10.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.10.0)
**Features**
- Added a recall option to prefer returning observations when retrieving memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d19f54c77" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d19f54c77</a>
**Bug Fixes**
- Fixed recalled memories being injected into the wrong context by default (now goes to user context).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/79c9f4afe" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>79c9f4afe</a>
- Prevented retention appends unless there is stored text to append, avoiding incorrect/empty retention updates.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/xmh1011" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@xmh1011</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5ab6bdc9b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5ab6bdc9b</a>
## [0.9.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.9.0)
**Improvements**
- Remove runtime metadata from stored memory content so saved memories are cleaner and more consistent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/de1tydev" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@de1tydev</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aa308ad20" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aa308ad20</a>
**Bug Fixes**
- Prevent synthetic tool-result messages from affecting how recent conversation turns are sliced and grouped.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/kumaxs" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@kumaxs</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/3b7d18d47" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>3b7d18d47</a>
- Ensure dynamic memory banks correctly inherit and use the configured default settings.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/devinkuhn" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@devinkuhn</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ef2e8ab7f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ef2e8ab7f</a>
- Retained documents are now always scoped to the current session to avoid unintended cross-session carryover.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a5f4d30ea" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a5f4d30ea</a>
## [0.8.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.8.0)
**Improvements**
- Openclaw now defaults recall types to include only observations, reducing unexpected memory recall behavior out of the box.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4b19a0fb" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4b19a0fb</a>
- Injected memory context now labels the “Current time” value as UTC for clearer, consistent time interpretation.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dc41f6a5" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dc41f6a5</a>
**Bug Fixes**
- Ensures any un-retained conversation turns are flushed and saved when a session ends, preventing missing memories.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7a4400e0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7a4400e0</a>
- Fixes dispatch being silently skipped in synthetic-main + static-banking configurations, restoring expected memory processing.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/09c9cecf" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>09c9cecf</a>
## [0.7.7](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.7)
**Features**
- Retained transcripts now include a session-context block at the beginning to preserve important session details.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/kryptt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@kryptt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/be696b0d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>be696b0d</a>
**Bug Fixes**
- Setup wizard now correctly backfills the plugin allowlist to include hindsight-openclaw, preventing the integration from being unintentionally blocked after setup.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/4088af36" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>4088af36</a>
## [0.7.6](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.6)
**Bug Fixes**
- Setup wizard now preserves your existing OpenClaw token and URL when re-running setup, preventing unnecessary reconfiguration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ef600683" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ef600683</a>
## [0.7.5](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.5)
**Bug Fixes**
- Setup wizard now correctly saves the “Allow conversation access” setting for OpenClaw.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/abf84872" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>abf84872</a>
## [0.7.4](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.4)
**Bug Fixes**
- Fixes OpenClaw integration configuration so the knowledge tools toggle is correctly honored.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c95206e0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c95206e0</a>
## [0.7.3](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.3)
**Improvements**
- Added optional performance timing debug output for the OpenClaw integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d37940a9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d37940a9</a>
**Bug Fixes**
- Fixed mission handling semantics and corrected the allowed configuration options (including retainQueue).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/aca03832" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>aca03832</a>
- Prevented duplicate OpenClaw registration across multiple API instances to avoid repeated handlers or side effects.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vernmic" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@vernmic</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0ce9f333" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0ce9f333</a>
## [0.7.2](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.2)
*This release contains internal maintenance and infrastructure changes only.*
## [0.7.1](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.1)
**Bug Fixes**
- Fix OpenClaw integration upgrades by using the published agent-sdk dependency and updating the plugin configuration schema.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c1924e9d" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c1924e9d</a>
## [0.7.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.7.0)
**Features**
- Added initial support for self-driving agents in the OpenClaw integration for Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7f30dcc7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7f30dcc7</a>
## [0.6.6](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.6)
**Bug Fixes**
- Retention is now applied correctly for default agent main:main sessions instead of being silently skipped.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/70677457" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>70677457</a>
## [0.6.5](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.5)
**Bug Fixes**
- Fix per-session memory retention so saving a new turn no longer overwrites earlier turns in the same session.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1f897314" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1f897314</a>
- Improve direct-execution detection by correctly resolving full symlink chains, preventing incorrect behavior when invoked through symlinked paths.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/D2758695161" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@D2758695161</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/7ceaa22a" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>7ceaa22a</a>
## [0.6.4](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.4)
**Features**
- Openclaw now uses a session-scoped document ID and records structured timestamps for each message, improving session consistency and message ordering. ([`33645e08`](https://github.com/vectorize-io/hindsight/commit/33645e08))
## [0.6.3](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.3)
**Features**
- Inline retain tags are now merged with default retain tags, improving consistency of retention behavior without extra configuration. ([`b79ab2b7`](https://github.com/vectorize-io/hindsight/commit/b79ab2b7))
**Improvements**
- All HTTP requests now include an identifying User-Agent, improving request attribution and compatibility with stricter endpoints. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
**Bug Fixes**
- Per-agent banking now respects identity skip-filter configuration, preventing incorrect identity filtering. ([`90a22016`](https://github.com/vectorize-io/hindsight/commit/90a22016))
## [0.6.2](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.2)
**Features**
- OpenClaw conversations are now stored in an Anthropic-style JSON format, preserving tool_use/tool_result blocks for more faithful replay and analysis. ([`adc85129`](https://github.com/vectorize-io/hindsight/commit/adc85129))
**Bug Fixes**
- Improved session consistency and reduced noise by stabilizing session identity and skipping non-user operational turns. ([`2ff805d6`](https://github.com/vectorize-io/hindsight/commit/2ff805d6))
- Fixed intermittent missing behavior by ensuring agent hooks are registered on every plugin invocation. ([`1be5ff33`](https://github.com/vectorize-io/hindsight/commit/1be5ff33))
## [0.6.1](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.1)
**Bug Fixes**
- Openclaw setup wizard now prompts for the actual token value instead of an environment variable name. ([`9679d813`](https://github.com/vectorize-io/hindsight/commit/9679d813))
## [0.6.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.0)
**Breaking Changes**
- Configuration is now read from the plugin configuration instead of environment variables, requiring updates to existing deployments. ([`e22ae05f`](https://github.com/vectorize-io/hindsight/commit/e22ae05f))
**Features**
- Adds an interactive setup wizard with Cloud, API, and Embedded configuration modes. ([`87322396`](https://github.com/vectorize-io/hindsight/commit/87322396))
- Adds a daemon lifecycle package for running the Hindsight "all" daemon. ([`576016f5`](https://github.com/vectorize-io/hindsight/commit/576016f5))
- Adds a configuration-aware CLI to backfill historical data into Hindsight memory. ([`72fd3d59`](https://github.com/vectorize-io/hindsight/commit/72fd3d59))
- Adds session pattern filtering to ignore or treat certain sessions as stateless. ([`5a61ac50`](https://github.com/vectorize-io/hindsight/commit/5a61ac50))
- Adds configurable tags for retained memories. ([`b0e8ac0f`](https://github.com/vectorize-io/hindsight/commit/b0e8ac0f))
- Adds support for bankId when using static banks. ([`0e81d1a2`](https://github.com/vectorize-io/hindsight/commit/0e81d1a2))
**Improvements**
- Improves startup resilience and enriches retained memory metadata. ([`1f1716bd`](https://github.com/vectorize-io/hindsight/commit/1f1716bd))
- Adds a JSONL-backed retain queue to improve reliability when the external API is unavailable. ([`087545cc`](https://github.com/vectorize-io/hindsight/commit/087545cc))
- Reduces CLI startup time by deferring heavy initialization until the service starts. ([`41025c3b`](https://github.com/vectorize-io/hindsight/commit/41025c3b))
**Bug Fixes**
- Avoids misrouting by ignoring ctx.channelId when it contains a provider name. ([`d4b8b354`](https://github.com/vectorize-io/hindsight/commit/d4b8b354))
## [0.5.1](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.5.1)
**Bug Fixes**
- Fixed JSON manifest formatting issues in the OpenClaw plugin to prevent manifest parsing/loading problems. ([`704e41fa`](https://github.com/vectorize-io/hindsight/commit/704e41fa))
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.5.0)
**Breaking Changes**
- Removed hardcoded default model settings from integrations so model/provider must be configured explicitly. ([`58e68f3e`](https://github.com/vectorize-io/hindsight/commit/58e68f3e))
**Features**
- Added configurable, structured logging for the OpenClaw integration. ([`d441ab81`](https://github.com/vectorize-io/hindsight/commit/d441ab81))
- Added an auto-recall toggle and support for excluding specific providers from recall/retention. ([`3f9eb27c`](https://github.com/vectorize-io/hindsight/commit/3f9eb27c))
- Added configuration to skip recall/retention for selected providers. ([`fb7be3ec`](https://github.com/vectorize-io/hindsight/commit/fb7be3ec))
- Added dynamic per-channel memory banks to isolate memory across channels. ([`9a776e9f`](https://github.com/vectorize-io/hindsight/commit/9a776e9f))
- Added support for using an external Hindsight API backend. ([`6b346925`](https://github.com/vectorize-io/hindsight/commit/6b346925))
- Added plugin configuration options to select the LLM provider and model. ([`8564135b`](https://github.com/vectorize-io/hindsight/commit/8564135b))
**Improvements**
- Added control over where recalled memories are injected to better preserve prompt caching. ([`200bab23`](https://github.com/vectorize-io/hindsight/commit/200bab23))
- Improved recall/retention controls and scalability, and added Gemini safety settings support. ([`d425e93c`](https://github.com/vectorize-io/hindsight/commit/d425e93c))
- Memory retention now periodically keeps recent conversation turns (default every 10 turns) to improve continuity. ([`ad1660b3`](https://github.com/vectorize-io/hindsight/commit/ad1660b3))
- Improved OpenClaw and embedding parameters for better integration behavior and configuration. ([`749478d9`](https://github.com/vectorize-io/hindsight/commit/749478d9))
- Improved OpenClaw configuration setup and initialization behavior. ([`27498f99`](https://github.com/vectorize-io/hindsight/commit/27498f99))
**Bug Fixes**
- Added a configurable auto-recall timeout to prevent recalls from hanging or taking too long. ([`cd4d449f`](https://github.com/vectorize-io/hindsight/commit/cd4d449f))
- Recalled memories are now injected as system context for more reliable behavior. ([`b17f338e`](https://github.com/vectorize-io/hindsight/commit/b17f338e))
- Health check requests now include the auth token to avoid unauthorized failures. ([`40b02645`](https://github.com/vectorize-io/hindsight/commit/40b02645))
- Improved stability and safety with better shell handling, HTTP mode support, lazy reinitialization, and per-user memory banks. ([`c4610130`](https://github.com/vectorize-io/hindsight/commit/c4610130))
- Fixed failures when ingesting very large content (E2BIG). ([`6bad6673`](https://github.com/vectorize-io/hindsight/commit/6bad6673))
- Prevented memory retention from recursing indefinitely. ([`4f112101`](https://github.com/vectorize-io/hindsight/commit/4f112101))
- Prevented user memories from being wiped on every new session. ([`981cf605`](https://github.com/vectorize-io/hindsight/commit/981cf605))
- Improved shell argument escaping to prevent command failures with special characters. ([`63e2964a`](https://github.com/vectorize-io/hindsight/commit/63e2964a))
- Renamed the OpenClaw binary to the correct name to avoid invocation/config mismatches. ([`b364bc34`](https://github.com/vectorize-io/hindsight/commit/b364bc34))
references/changelog/integrations/opencode.md
---
hide_table_of_contents: true
---
# OpenCode Integration Changelog
Changelog for [`@vectorize-io/opencode-hindsight`](https://www.npmjs.com/package/@vectorize-io/opencode-hindsight).
For the source code, see [`hindsight-integrations/opencode`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/opencode).
← [Back to main changelog](../index.md)
## [0.2.9](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.9)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/opencode/v0.2.9)
**Features**
- The OpenCode integration now identifies itself with its own User-Agent for service requests.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
**Bug Fixes**
- Fixed project resolution for bare Hub project names in the OpenCode integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/1e4583478" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>1e4583478</a>
## [0.2.8](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.8)
**Bug Fixes**
- Fixes session-start memory recall to base its query on the user’s messages for more relevant results.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/036ba19b6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>036ba19b6</a>
## [0.2.7](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.7)
**Features**
- Adds environment variable overrides for retain/recall behavior in the OpenCode integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/ibousfiha" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@ibousfiha</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/cc45e1690" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>cc45e1690</a>
- Adds support for configuring retain tags via the HINDSIGHT_RETAIN_TAGS environment variable.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/mdbenito" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@mdbenito</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/20da6d760" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>20da6d760</a>
**Bug Fixes**
- Fixes plugin SDK availability by installing it at runtime so the OpenCode integration works reliably in more environments.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Sanderhoff-alt" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Sanderhoff-alt</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ae7099fd0" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ae7099fd0</a>
## [0.2.6](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.6)
**Features**
- Added a new "shared" option for observation scopes to support consolidated observations.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0a046f97" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0a046f97</a>
**Bug Fixes**
- Improved compatibility with legacy plugin loaders by removing utility exports from the OpenCode plugin entry point.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/mdbenito" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@mdbenito</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9500a6bf" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9500a6bf</a>
## [0.2.5](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.5)
**Bug Fixes**
- Ensured recall instructions are included in the initial system prompt section instead of being added as a separate section.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/421cde6de" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>421cde6de</a>
## [0.2.4](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.4)
**Bug Fixes**
- Fixed OpenCode integration logging so logs are correctly emitted.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/102416c42" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>102416c42</a>
## [0.2.3](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.3)
**Bug Fixes**
- Improved opencode integration logging to show configuration-only debug info, log the resolved endpoint, and surface errors more clearly.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/796a9eff9" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>796a9eff9</a>
## [0.2.2](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.2)
**Bug Fixes**
- Fixed the OpenCode integration plugin export to avoid exposing a non-function entry point.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/r266-tech" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@r266-tech</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/e68d32583" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>e68d32583</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.1)
**Bug Fixes**
- Opencode integration now defaults to using Hindsight Cloud, with live end-to-end tests gated to prevent unintended runs.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/06f36b8b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>06f36b8b</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.2.0)
**Features**
- Share a single memory bank across multiple Git worktrees in the same repository.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/isac322" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@isac322</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/78e48e59" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>78e48e59</a>
**Improvements**
- Reduce default memory retention frequency to save less often (default retainEveryNTurns: 3 instead of 10).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/902704df" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>902704df</a>
**Bug Fixes**
- Prevent garbled bank IDs by preserving raw UTF-8 when deriving the memory bank identifier dynamically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/Desko77" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@Desko77</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/08a75b5b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>08a75b5b</a>
## [0.1.4](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.1.4)
**Improvements**
- Reduce noisy error output by logging opencode integration errors only in debug mode. ([`33442f19`](https://github.com/vectorize-io/hindsight/commit/33442f19))
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.1.3)
**Bug Fixes**
- Fixes the OpenCode integration to correctly parse messages, avoid shared-state issues, and retain content after compaction. ([`6076354a`](https://github.com/vectorize-io/hindsight/commit/6076354a))
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/opencode/v0.1.2)
**Features**
- Added configuration options to filter recalls by tags (recallTags) and control tag matching behavior (recallTagsMatch). ([`b57e337f`](https://github.com/vectorize-io/hindsight/commit/b57e337f))
**Bug Fixes**
- Fixed the session messages API to return the correct data shape for the OpenCode plugin. ([`fd87de9c`](https://github.com/vectorize-io/hindsight/commit/fd87de9c))
references/changelog/integrations/openhands.md
---
hide_table_of_contents: true
---
# OpenHands Integration Changelog
Changelog for [`hindsight-openhands`](https://pypi.org/project/hindsight-openhands/).
For the source code, see [`hindsight-integrations/openhands`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/openhands).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/openhands/v0.1.1)
**Bug Fixes**
- Ensure the OpenHands client is properly closed when exiting the CLI to prevent lingering connections/resources.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/a0af09608" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>a0af09608</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/openhands/v0.1.0)
**Features**
- Added OpenHands integration with native MCP configuration and memory recall/retain behavior rules.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/65862c4f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>65862c4f</a>
references/changelog/integrations/paperclip.md
---
hide_table_of_contents: true
---
# Paperclip Integration Changelog
Changelog for [`@vectorize-io/hindsight-paperclip`](https://www.npmjs.com/package/@vectorize-io/hindsight-paperclip).
For the source code, see [`hindsight-integrations/paperclip`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/paperclip).
← [Back to main changelog](../index.md)
## [0.4.2](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.4.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/paperclip/v0.4.2)
**Improvements**
- Paperclip integration requests now include an integration-specific User-Agent for clearer identification and diagnostics.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.4.1](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.4.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/paperclip/v0.4.1)
**Bug Fixes**
- The enabledAgentIds allowlist is now enforced on the hindsight_recall and hindsight_retain tools, not just the automatic recall and retention. An agent left out of the list can no longer reach its memory bank by calling the tools directly.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d4cabadb8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d4cabadb8</a>
- The autoRetain setting is now labelled for what it does. It gates retention of issue comments; it was previously described as retaining agent run output when a run completes, which the plugin does not do.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/d4cabadb8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>d4cabadb8</a>
## [0.4.0](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.4.0)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/paperclip/v0.4.0)
**Improvements**
- The Hindsight request timeout can now be configured for the Paperclip integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/sdfantini-cs" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@sdfantini-cs</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/82dcec054" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>82dcec054</a>
**Bug Fixes**
- Paperclip now resolves the Hindsight Cloud API key correctly from configured secret references.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/qveys" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@qveys</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ac1962e6f" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ac1962e6f</a>
- The hindsight_recall tool now uses the query provided by the agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/sdfantini-cs" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@sdfantini-cs</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/82dcec054" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>82dcec054</a>
## [0.3.0](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.3.0)
**Features**
- Added per-agent enable/disable controls for the Paperclip integration to support staged pilot rollouts.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/parties" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@parties</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/0c6d54dc8" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>0c6d54dc8</a>
**Improvements**
- Added operational metrics exposing async-operation queue depth and consolidation backlog as gauges.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/MattJackson" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@MattJackson</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b1fe23fbe" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b1fe23fbe</a>
## [0.2.3](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.2.3)
**Features**
- Added per-user memory isolation for Paperclip by supporting configurable bank granularity.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/beca4b42" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>beca4b42</a>
## [0.2.2](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.2.2)
**Improvements**
- Updated dependencies to address known security vulnerabilities (npm and pip).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dcbouius" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dcbouius</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/26c5028c" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>26c5028c</a>
**Bug Fixes**
- Fixed the Paperclip integration to correctly handle real Paperclip event payloads, improving reliability of event processing.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/amirhmoradi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@amirhmoradi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/be908d5b" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>be908d5b</a>
## [0.2.1](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.2.1)
**Breaking Changes**
- Replaced the Paperclip integration with the new Paperclip plugin (v0.2.0), changing how the integration is packaged and used.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/c571fac7" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>c571fac7</a>
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.2.0)
**Breaking Changes**
- Rewritten as a proper Paperclip plugin (installed via `pnpm paperclipai plugin install`). No code changes required — memory hooks run automatically via the event system.
- Works with all adapter types (Claude, Codex, Cursor, HTTP, Process). Previously required manual `recall()`/`retain()` calls and only supported HTTP adapter agents.
**Features**
- `agent.run.started` hook: auto-recalls context keyed to issue title + description
- `agent.run.finished` hook: auto-retains agent output with `runId` as document ID
- `hindsight_recall` and `hindsight_retain` agent tools for mid-run memory access
- `onValidateConfig`: live connectivity check when operator saves settings
- Configurable bank granularity (company+agent, company-only, agent-only)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.1.2)
**Improvements**
- Paperclip integration now sends an identifying User-Agent on all HTTP requests for better request tracing and compatibility. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/paperclip/v0.1.1)
**Features**
- Added the Hindsight Paperclip TypeScript integration. ([`81441ee9`](https://github.com/vectorize-io/hindsight/commit/81441ee9))
**Bug Fixes**
- Fixed issues in the Paperclip integration based on review feedback. ([`7863ffeb`](https://github.com/vectorize-io/hindsight/commit/7863ffeb))
references/changelog/integrations/pipecat.md
---
hide_table_of_contents: true
---
# Pipecat Integration Changelog
Changelog for [`hindsight-pipecat`](https://pypi.org/project/hindsight-pipecat/).
For the source code, see [`hindsight-integrations/pipecat`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/pipecat).
← [Back to main changelog](../index.md)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/pipecat/v0.1.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/pipecat/v0.1.2)
**Breaking Changes**
- Migrated the Pipecat integration to the pipecat-ai 1.x release line, requiring compatibility with the newer Pipecat API.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/dcbouius" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@dcbouius</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b95055ba2" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b95055ba2</a>
**Features**
- Pipecat requests now identify the integration with a dedicated User-Agent header.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
**Improvements**
- Updated the Pipecat integration to use pipecat-ai 1.12.0.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/cdbartholomew" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@cdbartholomew</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/ccfe85b48" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>ccfe85b48</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/pipecat/v0.1.1)
**Features**
- Added Pipecat voice AI pipeline integration to store and use memories in Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f7cc9ad6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f7cc9ad6</a>
references/changelog/integrations/pydantic-ai.md
---
hide_table_of_contents: true
---
import PageHero from '@site/src/components/PageHero';
<PageHero title="Pydantic AI Changelog" subtitle="hindsight-pydantic-ai — persistent memory tools for Pydantic AI agents." />
[← Pydantic AI integration](../../sdks/integrations/pydantic-ai.md)
## [0.4.20](https://github.com/vectorize-io/hindsight/tree/integrations/pydantic-ai/v0.4.20)
**Features**
- Added Pydantic AI integration to provide persistent agent memory in Hindsight. ([`cab5a40f`](https://github.com/vectorize-io/hindsight/commit/cab5a40f))
**Improvements**
- Set an identifying User-Agent header on all HTTP requests made by clients. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
- Added a PEP 561 py.typed marker so type checkers recognize bundled type hints. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
- Updated dependencies to address critical and high-severity security vulnerabilities. ([`ee4510a7`](https://github.com/vectorize-io/hindsight/commit/ee4510a7))
references/changelog/integrations/roo-code.md
---
hide_table_of_contents: true
---
# Roo Code Integration Changelog
Changelog for [`hindsight-roo-code`](https://pypi.org/project/hindsight-roo-code/).
For the source code, see [`hindsight-integrations/roo-code`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/roo-code).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/roo-code/v0.1.0)
**Features**
- Added Roo Code integration for Hindsight, including MCP support and memory rules.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/bf6b9026" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>bf6b9026</a>
**Improvements**
- Made the Roo Code integration available as an installable CLI package via PyPI (hindsight-roo-code).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/644e37ac" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>644e37ac</a>
references/changelog/integrations/smolagents.md
---
hide_table_of_contents: true
---
# SmolAgents Integration Changelog
Changelog for [`hindsight-smolagents`](https://pypi.org/project/hindsight-smolagents/).
For the source code, see [`hindsight-integrations/smolagents`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/smolagents).
← [Back to main changelog](../index.md)
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/smolagents/v0.1.2)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/smolagents/v0.1.2)
**Improvements**
- The smolagents integration now identifies its requests with a dedicated User-Agent.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/smolagents/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/smolagents/v0.1.1)
**Bug Fixes**
- Configured default tool settings are now honored by the Smolagents integration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/rudycelekli" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@rudycelekli</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b0bbad192" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b0bbad192</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/smolagents/v0.1.0)
**Features**
- Added a SmolAgents integration that lets agents use Hindsight persistent memory via provided memory tools and configuration.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/8314de5e" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>8314de5e</a>
references/changelog/integrations/strands.md
---
hide_table_of_contents: true
---
# Strands Integration Changelog
Changelog for [`hindsight-strands`](https://pypi.org/project/hindsight-strands/).
For the source code, see [`hindsight-integrations/strands`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/strands).
← [Back to main changelog](../index.md)
## [0.1.3](https://github.com/vectorize-io/hindsight/tree/integrations/strands/v0.1.3)
**Bug Fixes**
- Fixes Strands integration to properly close internally owned Hindsight clients, preventing resource leaks and related stability issues.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/2bfd7747" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>2bfd7747</a>
## [0.1.2](https://github.com/vectorize-io/hindsight/tree/integrations/strands/v0.1.2)
**Improvements**
- Improved Python typing support for the Strands integration by shipping the PEP 561 "py.typed" marker. ([`d054b884`](https://github.com/vectorize-io/hindsight/commit/d054b884))
**Bug Fixes**
- All Strands integration HTTP requests now include a consistent identifying User-Agent for better compatibility and troubleshooting. ([`9372462e`](https://github.com/vectorize-io/hindsight/commit/9372462e))
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/strands/v0.1.1)
**Features**
- Added Strands Agents SDK integration, enabling Hindsight memory tools to be used with Strands agents. ([`7fe773c0`](https://github.com/vectorize-io/hindsight/commit/7fe773c0))
references/changelog/integrations/superagent.md
---
hide_table_of_contents: true
---
# Superagent Integration Changelog
Changelog for [`hindsight-superagent`](https://pypi.org/project/hindsight-superagent/).
For the source code, see [`hindsight-integrations/superagent`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/superagent).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/superagent/v0.1.0)
**Features**
- Added safety middleware for the Superagent integration to enforce safer agent behavior.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b70830218" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b70830218</a>
references/changelog/integrations/vapi.md
---
hide_table_of_contents: true
---
# Vapi Integration Changelog
Changelog for [`hindsight-vapi`](https://pypi.org/project/hindsight-vapi/).
For the source code, see [`hindsight-integrations/vapi`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/vapi).
← [Back to main changelog](../index.md)
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/vapi/v0.1.1)
[Commits in this release →](https://github.com/vectorize-io/hindsight/commits/integrations/vapi/v0.1.1)
**Improvements**
- Vapi integration requests now use a dedicated User-Agent for clearer identification and troubleshooting.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/5d4f5cfcd" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>5d4f5cfcd</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/vapi/v0.1.0)
**Features**
- Added Vapi voice AI webhook integration to capture and store call memories in Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dde133da" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dde133da</a>
references/changelog/integrations/zcode.md
---
hide_table_of_contents: true
---
# ZCode Integration Changelog
Changelog for [`hindsight-zcode`](https://pypi.org/project/hindsight-zcode/).
For the source code, see [`hindsight-integrations/zcode`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/zcode).
← [Back to main changelog](../index.md)
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/zcode/v0.1.0)
**Features**
- Added ZCode integration to sync and use Hindsight long-term memory (retain/recall) during ZCode sessions.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/b11e05332" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>b11e05332</a>
references/changelog/integrations/zed.md
---
hide_table_of_contents: true
---
# Zed Integration Changelog
Changelog for [`hindsight-zed`](https://pypi.org/project/hindsight-zed/).
For the source code, see [`hindsight-integrations/zed`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/zed).
← [Back to main changelog](../index.md)
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/zed/v0.2.0)
**Breaking Changes**
- Switched the Zed integration setup CLI from Python to Node.js, removing the Python dependency (installation/setup flow changes).<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/9dee1c594" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>9dee1c594</a>
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/zed/v0.1.0)
**Features**
- Added Zed editor integration, including an MCP context server plus recall/retain memory rules for Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/DK09876" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}>@DK09876</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/539101af" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>539101af</a>
references/cloud-enterprise/enterprise.md
---
title: Hindsight Enterprise
description: Hindsight Enterprise is a single-tenant Hindsight deployment, in our cloud, your cloud or your data center, with Hindsight Memlake for memory at scale.
---
# Hindsight Enterprise
Hindsight Enterprise is Hindsight for organizations that need their own deployment. Instead of sharing the multi-tenant Hindsight Cloud, you get a **single-tenant** installation that Vectorize sets up and runs with you. It includes everything in Hindsight Cloud, plus the security, compliance, networking and scale features below.
**[Book a call](https://calendly.com/d/dvy5-v3f-jhg?utm_source=oss_docs&utm_campaign=enterprise_docs)** · **[Hindsight Enterprise docs](https://docs.hindsight.vectorize.io/enterprise)**
## Deployment options
| Option | Where it runs | Who runs it |
|---|---|---|
| **Hindsight Cloud with Enterprise features** | Hindsight Cloud (multi-tenant) | Vectorize |
| **Private cloud** | A dedicated deployment in Vectorize's cloud account, in the region you choose | Vectorize |
| **Bring your own cloud (BYOC)** | Your AWS, Google Cloud or Azure account | Vectorize, through access you grant |
| **On-premises** | Your Kubernetes infrastructure, including air-gapped | You, with Vectorize's help |
Private cloud and BYOC can be reached over private networking (AWS PrivateLink, Google Cloud Private Service Connect, Azure Private Link), so traffic never crosses the public internet. See [Choosing a deployment option](https://docs.hindsight.vectorize.io/enterprise/deployment/overview) for a full comparison, including who is responsible for what.
## What Enterprise adds
- **Single-tenant deployment:** its own database, storage, compute and identity service.
- **[Hindsight Memlake](#hindsight-memlake):** memory on object storage, for large and growing banks.
- **Single sign-on and MFA:** Okta, Microsoft Entra ID, Google Workspace or any OIDC or SAML provider.
- **Memory Defense:** detect and redact secrets and sensitive data, and block prompt injection before it is stored.
- **Data privacy (GDPR):** erase, retain and export a person's data across banks.
- **Audit logs, webhooks and SIEM:** a record of who did what, streamed to your own systems.
- **Enterprise support:** a ticket portal, a shared Slack channel and contracted response times.
- **Invoiced billing:** annual or contract billing instead of card payments.
## Hindsight Memlake
Hindsight Memlake is the Enterprise storage engine for agent memory. Open-source Hindsight stores memories in PostgreSQL, which works well for most banks. But agent memory only grows, and as banks reach hundreds of thousands of memories, a relational database gets slower and more expensive to keep fast.
Memlake keeps memories in **object storage** (S3, Google Cloud Storage, Azure Blob Storage, or an S3-compatible store on-premises), with one bucket per organization:
- **Built for scale.** In our tests, 100 million memories were served at under 100 ms p99 recall, on four servers.
- **Fast and steady.** Recall time barely moves as a bank grows, because each bank has its own index, kept warm in a cache on the servers.
- **Lower cost as you grow.** Memories live in object storage, the cheapest durable storage there is, and servers hold only a cache, so you add servers for traffic, not for data.
- **Durable.** A write is in object storage before it is acknowledged, and continuously backed up to a separate bucket.
- **Nothing changes for your agents.** Same API, SDKs and features; banks, settings and background jobs stay in PostgreSQL.
If your banks stay under roughly 50,000 memories each, PostgreSQL is a good fit and Memlake is not needed. Memlake is available on private cloud, BYOC and on-premises deployments.
Read more: [Hindsight Memlake overview](https://docs.hindsight.vectorize.io/enterprise/memlake), [architecture](https://docs.hindsight.vectorize.io/enterprise/memlake/architecture), [performance and cost](https://docs.hindsight.vectorize.io/enterprise/memlake/performance) and [tradeoffs](https://docs.hindsight.vectorize.io/enterprise/memlake/tradeoffs).
## Learn more
- [Hindsight Enterprise documentation](https://docs.hindsight.vectorize.io/enterprise)
- [Ways to run Hindsight](./overview.md): self-hosted, Cloud and Enterprise compared
- [Book a call](https://calendly.com/d/dvy5-v3f-jhg?utm_source=oss_docs&utm_campaign=enterprise_docs) with the Hindsight team
references/cloud-enterprise/hindsight-cloud.md
---
title: Hindsight Cloud
description: Hindsight Cloud is the managed, self-serve Hindsight service. Nothing to deploy, models included.
---
# Hindsight Cloud
Hindsight Cloud is Hindsight run for you. Sign up, create a memory bank and connect your agents, with nothing to deploy or operate. It runs the same engine as open-source Hindsight, so everything in these docs about how memory works applies.
**[Start on Hindsight Cloud](https://ui.hindsight.vectorize.io/signup)** · **[Hindsight Cloud docs](https://docs.hindsight.vectorize.io/cloud)**
## What you get
- **Nothing to run.** No servers, databases or upgrades. The LLM, embedding and reranking models are included.
- **The same API.** Retain, recall, reflect, mental models and knowledge pages, through the same [Python](../sdks/python.md), [TypeScript](../sdks/nodejs.md) and [Go](../sdks/go.md) clients, the [CLI](../sdks/cli.md) and the MCP server. Point them at your Cloud API URL and key.
- **Integrations.** Pre-built integrations with agent frameworks, coding agents and AI tools.
- **A console.** Browse banks, memories, mental models and knowledge pages, and see how a bank is learning.
- **Teams.** Invite your team with role-based access, and manage API keys.
- **Usage analytics.** Dashboards for what each bank uses.
## Pricing
You pay for what you use, from a prepaid credit balance: per token for retain and recall, per call for reflect and for creating or refreshing mental models and knowledge pages, and for storage of memories kept longer than 30 days. See [Billing](https://docs.hindsight.vectorize.io/cloud/billing) for current rates.
## Enterprise features on Cloud
Some features are part of Hindsight Enterprise and can be turned on for a Cloud organization: SSO, enforced MFA, Memory Defense, data privacy (GDPR), audit logs, webhooks and SIEM, and Enterprise support. See [Hindsight Cloud with Enterprise features](https://docs.hindsight.vectorize.io/enterprise/deployment/cloud-enterprise).
Hindsight Cloud is shared and multi-tenant. If you need a dedicated deployment, see [Hindsight Enterprise](./enterprise.md).
## Learn more
- [Getting started](https://docs.hindsight.vectorize.io/cloud/getting-started): your first memory bank, step by step
- [Hindsight Cloud documentation](https://docs.hindsight.vectorize.io/cloud)
- [Ways to run Hindsight](./overview.md): self-hosted, Cloud and Enterprise compared
references/cloud-enterprise/overview.md
---
title: Ways to run Hindsight
sidebar_label: Overview
description: Run open-source Hindsight yourself, use the managed Hindsight Cloud, or get a dedicated Hindsight Enterprise deployment.
---
# Ways to run Hindsight
Hindsight runs three ways. All of them use the same memory engine: the same retain, recall and reflect, the same mental models and knowledge pages, and the same API and SDKs. You can start on one and move to another later.
| | Self-hosted (open source) | Hindsight Cloud | Hindsight Enterprise |
|---|---|---|---|
| **What it is** | The open-source engine, run by you | Managed, self-serve, multi-tenant service | A single-tenant deployment, installed and run with Vectorize |
| **Where it runs** | Anywhere: your laptop, Docker, Kubernetes | Hindsight Cloud | Our cloud, your AWS, Google Cloud or Azure account, or your data center |
| **Who runs it** | You | Vectorize | Vectorize, or you with Vectorize's help on-premises |
| **Memory store** | PostgreSQL (or Oracle) | Managed by Vectorize | PostgreSQL, or [Hindsight Memlake](./enterprise.md#hindsight-memlake) on object storage |
| **Models** | Bring your own LLM and embeddings | Included | Included, or your own (Bedrock, Vertex AI, Azure OpenAI) |
| **Sign-in and access** | API keys you configure | Console, teams and API keys | Plus SSO, enforced MFA and private networking |
| **Pricing** | Free (MIT license) | Pay as you go, from prepaid credits | Contract |
| **Get started** | [Installation](../developer/installation.md) | [Sign up](https://ui.hindsight.vectorize.io/signup) | [Book a call](https://calendly.com/d/dvy5-v3f-jhg?utm_source=oss_docs&utm_campaign=enterprise_docs) |
## How to choose
- **Self-hosted** if you want full control, want to try Hindsight locally, or already run Postgres and your own models. These docs cover it: start with [Installation](../developer/installation.md).
- **[Hindsight Cloud](./hindsight-cloud.md)** if you want memory for your agents in minutes, with nothing to deploy and the models included.
- **[Hindsight Enterprise](./enterprise.md)** if you need your own deployment: single-tenant isolation, a region or cloud account of your choice, private networking, SSO and compliance features, contracted support, or memory at a scale where [Hindsight Memlake](./enterprise.md#hindsight-memlake) pays off.
Hindsight Cloud and Enterprise have their own documentation at [docs.hindsight.vectorize.io](https://docs.hindsight.vectorize.io).
references/developer/admin-cli.md
# Admin CLI
The `hindsight-admin` CLI provides administrative commands for managing your Hindsight deployment, including database migrations, backup, and restore operations.
## Installation
The admin CLI is included with the `hindsight-api` package — installing it puts the `hindsight-admin` executable on your `PATH`:
```bash
pip install hindsight-api
# or
uv add hindsight-api
```
## Running the CLI
`hindsight-admin` connects **directly to PostgreSQL** — it does not call the HTTP API. It reads the **same configuration as the API service** (environment variables, and a `.env` file in the current working directory), so it operates on whatever database `HINDSIGHT_API_DATABASE_URL` points to:
- **Default**: `pg0`, the embedded development database (must be run on the host that owns the pg0 data).
- **Production**: set `HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/hindsight`.
Because it talks to the database directly (binary `COPY`, `TRUNCATE`, etc.), the admin CLI is **PostgreSQL-only** (not supported on Oracle). Run it on the same host/container as your API deployment so it inherits the right configuration and has network access to the database:
```bash
# Bare metal / virtualenv (with the API's env or a .env in the working dir)
hindsight-admin worker-status
# Docker — exec into the API container
docker exec -it hindsight-api hindsight-admin backup /data/backup.zip
# Kubernetes — exec into an API pod
kubectl exec deploy/hindsight-api -- hindsight-admin run-db-migration
```
Use `--schema` to target a specific tenant schema (commands default to the configured base schema). See [Environment Variables](#environment-variables) below.
## Commands
### run-db-migration
Run database migrations to the latest version. By default this migrates the base schema plus all tenant schemas discovered by the tenant extension. Use `--schema` for targeted migration of one schema. This is useful when you want to run migrations separately from API startup (e.g., in CI/CD pipelines or before deploying a new version).
```bash
hindsight-admin run-db-migration [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema to run migrations on. If omitted, migrate the base schema plus all discovered tenant schemas. | All schemas |
| `--embedding-dimension` | Expected embedding dimension to enforce after migrations. Omit to skip the post-migration dimension sync. | Skipped |
| `--skip-extension-reconcile` | Skip the post-migration vector / text-search index reconcile (it only does work when `HINDSIGHT_API_VECTOR_EXTENSION` / `HINDSIGHT_API_TEXT_SEARCH_EXTENSION` differs from a schema's existing indexes). Makes a no-change re-migration across many tenant schemas much faster; only use when the backend is unchanged. | Reconcile runs |
**Examples:**
```bash
# Run migrations on the base schema plus all discovered tenant schemas
hindsight-admin run-db-migration
# Run migrations on a specific tenant schema
hindsight-admin run-db-migration --schema tenant_acme
```
:::tip Disabling Auto-Migrations
To disable automatic migrations on API startup, set `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=false`. This is useful when you want to run migrations as a separate step in your deployment pipeline.
:::
---
### repair-bank
Verify and repair a bank's per-`(bank, fact_type)` vector index coverage.
These partial indexes are normally created when a bank is first created (instant on an empty bank), and PostgreSQL maintains them incrementally as the bank grows. A bank that becomes **populated outside that create-time path** — via a logical restore, a cross-version upgrade, or a vector-extension switch — never gets them, so its bank-scoped recall silently falls back to a global index + post-filter. That fallback is both **slower** and can **under-return** results (the approximate nearest-neighbour search draws its candidates from every bank, then filters to yours afterward).
Run this after any of those events to restore full coverage. It detects **missing or invalid** coverage (an INVALID leftover from an interrupted build, or an index whose type drifted after a backend switch, both count as missing) and rebuilds it with `CREATE INDEX CONCURRENTLY`, so it never blocks live retain/recall/consolidation. It is idempotent and safe to re-run.
```bash
hindsight-admin repair-bank (--bank BANK_ID | --all) [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--bank`, `-b` | Bank id to repair. Mutually exclusive with `--all`. | — |
| `--all` | Repair every bank in the base schema and all discovered tenant schemas. | — |
| `--schema`, `-s` | Limit to a single schema. | All schemas |
| `--dry-run` | Report what would be repaired without creating or dropping any index. | Off |
Exactly one of `--bank` or `--all` is required. No-op for backends that use a single global vector index (AlloyDB ScaNN, Oracle). It is idempotent — safe to re-run and safe to run while the API is serving traffic.
It is also a no-op for a bank whose memories a custom store owns, this time decided
per bank rather than per deployment: such a bank has no rows in `memory_units`, so it
is owed no per-bank index and nothing is built for it. Nothing is dropped either — a
bank that carries indexes from before it moved to the store keeps them, because
shedding them is an operator decision with its own timing, not something a repair
does on your behalf. A store-owned bank therefore reports `0 present, 0 created`
whether or not it still carries indexes; to see what one actually carries, query
the catalog for that bank's own partial indexes:
```sql
SELECT indexname FROM pg_indexes
WHERE schemaname = '<tenant schema>'
AND tablename = 'memory_units'
AND indexname LIKE 'idx_mu_emb_%'
AND strpos(indexdef, $$bank_id = '<bank id>'$$) > 0;
```
Drop the last condition to list every per-bank index in the schema instead.
Those indexes are empty, and Postgres plans against every index on a relation, so on
a deployment with many store-owned banks they are charged to every other query that
touches `memory_units`. Drop them with `DROP INDEX CONCURRENTLY` when you are ready.
**Exit code.** `0` when every schema was reconciled. `1` when an index failed to build
(the names are printed, and a re-run retries them), or when a schema was not fully
reconciled — for example because the memories store could not say which banks it
owns. The command does not guess there, because guessing would rebuild the very
indexes described above.
A schema is named as skipped whether it failed before its first bank or partway
through, so it may already have had some banks reconciled; when it got that far the
per-schema line says how many, and those builds are not undone. Both the failed-index names and the
skipped schemas are reported before the command exits, so one never hides the other.
If the database connection itself is lost mid-sweep, the run stops there rather than
repeating the same error for every remaining schema, and still reports what it did.
**Examples:**
```bash
# See which banks are missing coverage without changing anything
hindsight-admin repair-bank --all --dry-run
# Repair every bank across all schemas (run once after a restore/upgrade)
hindsight-admin repair-bank --all
# Repair a single bank
hindsight-admin repair-bank --bank acme-prod
```
---
### rename-bank
:::tip Consider an alias instead
A rename requires stopping the bank's clients, because the old id stops working the moment it commits. If what you want is to move callers onto a new id, **[bank aliases](./api/memory-banks.md#aliases)** do that with no downtime and no row rewrite: the bank answers to both ids until you are done.
Reach for `rename-bank` when the old id must genuinely stop existing — an alias never removes one.
:::
Change a bank's id in place. Every memory, document, entity, mental model and knowledge page is kept exactly as it is. Nothing is re-extracted or re-embedded, and no LLM calls are made.
```bash
hindsight-admin rename-bank --from OLD_ID --to NEW_ID [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--from` | Current bank id. | — |
| `--to` | New bank id. Must not already exist. | — |
| `--schema`, `-s` | Schema the bank lives in. | Configured base schema |
| `--dry-run` | Run the whole rename, print the rows that would move per table, then roll back. | Off |
**Before you run it:**
- **Stop the bank's clients.** Anything still calling the old id gets a 404, or, if it creates banks on demand, silently starts a new, empty bank under the old id. Point every client, and any API key scoped to the bank, at the new id before restarting them.
- **Let the bank's operations finish.** The command refuses while the bank has pending or processing operations.
A file's storage key spells the bank id, so after the rows move the bank's stored files — document attachments and uploaded originals — are copied to keys under the new id, their rows repointed, and the old id's prefix cleared. That last step also removes any export or import archive left under the old id: their download links stop working at the rename, and the operation can produce them again. Deleting the bank afterwards removes its files with it.
The rename runs in a single transaction and only locks that bank's rows. Other banks keep serving. Its duration grows with the bank's size. Afterwards, the bank's vector indexes are rebuilt with `CREATE INDEX CONCURRENTLY`, and until that finishes, recall on the renamed bank runs without them. Running API servers may keep answering for the old id until their bank-info cache expires. PostgreSQL only.
**Examples:**
```bash
# See what would move, change nothing
hindsight-admin rename-bank --from acme --to acme-prod --dry-run
hindsight-admin rename-bank --from acme --to acme-prod
```
---
### backup
Create a backup of all Hindsight data to a zip file.
```bash
hindsight-admin backup OUTPUT [OPTIONS]
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `OUTPUT` | Output file path (will add `.zip` extension if not present) |
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema to backup | `public` |
**Examples:**
```bash
# Backup to a file
hindsight-admin backup /backups/hindsight-2024-01-15.zip
# Backup a specific tenant schema
hindsight-admin backup /backups/tenant-acme.zip --schema tenant_acme
```
The backup includes:
- Memory banks and their configuration
- Documents and chunks
- Entities and their relationships
- Memory units (facts, experiences, observations)
- Entity cooccurrences and memory links
- Mental models and directives
- Webhooks and file storage
- Internal operational tables (async operations, audit log, graph-maintenance queue, and similar bookkeeping) so a restore reproduces a faithful full-database snapshot
:::note Consistency
Backups are created within a database transaction with `REPEATABLE READ` isolation, ensuring a consistent snapshot across all tables.
:::
---
### restore
Restore data from a backup file. **Warning: This deletes all existing data in the target schema.**
```bash
hindsight-admin restore INPUT [OPTIONS]
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `INPUT` | Input backup file (.zip) |
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema to restore to | `public` |
| `--yes`, `-y` | Skip confirmation prompt | `false` |
**Examples:**
```bash
# Restore with confirmation prompt
hindsight-admin restore /backups/hindsight-2024-01-15.zip
# Restore without confirmation (for scripts)
hindsight-admin restore /backups/hindsight-2024-01-15.zip --yes
# Restore to a specific tenant schema
hindsight-admin restore /backups/tenant-acme.zip --schema tenant_acme --yes
```
:::warning Data Loss
Restore will **delete all existing data** in the target schema before importing the backup. Always verify you have a recent backup before performing a restore.
:::
---
### decommission-worker
Release all tasks owned by a worker, resetting them from "processing" back to "pending" status so they can be picked up by other workers.
```bash
hindsight-admin decommission-worker WORKER_ID [OPTIONS]
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `WORKER_ID` | ID of the worker to decommission |
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema | `public` |
| `--yes`, `-y` | Skip confirmation prompt | `false` |
**Examples:**
```bash
# Before scaling down - release tasks from workers being removed
hindsight-admin decommission-worker hindsight-worker-4
hindsight-admin decommission-worker hindsight-worker-3
# Release tasks from a crashed worker
hindsight-admin decommission-worker worker-2
# For a specific tenant schema
hindsight-admin decommission-worker worker-1 --schema tenant_acme
```
**When to Use:**
- **Scaling down**: Before removing worker replicas in Kubernetes
- **Graceful removal**: When taking a worker offline for maintenance
- **Crash recovery**: If a worker crashed while processing tasks
- **Stuck worker**: When a worker is unresponsive
:::tip Finding Worker IDs
Worker IDs default to the hostname. In Kubernetes StatefulSets, this is the pod name (e.g., `hindsight-worker-0`). You can also set a custom ID with `HINDSIGHT_API_WORKER_ID` or `--worker-id`.
:::
### decommission-workers
Release all currently-processing tasks from every worker, resetting them from "processing" back to "pending" status. Use this when one or more workers have crashed or been removed without graceful shutdown and you don't know which worker IDs to target.
```bash
hindsight-admin decommission-workers [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema | `public` |
| `--yes`, `-y` | Skip confirmation prompt | `false` |
**Examples:**
```bash
# Release all processing tasks across all workers (with confirmation)
hindsight-admin decommission-workers
# Skip the confirmation prompt (useful in scripts)
hindsight-admin decommission-workers --yes
# Release tasks in a specific tenant schema
hindsight-admin decommission-workers --schema tenant_acme
```
**When to Use:**
- **Unknown dead workers**: Multiple workers crashed and you do not know their IDs
- **Fleet-wide recovery**: After an infrastructure event where many workers went down
- **"Just fix everything"**: A quick full-queue drain when per-worker cleanup is overkill
:::warning Disruptive
This releases **every** processing task regardless of worker, including tasks owned by healthy workers. Prefer `decommission-worker <WORKER_ID>` when you know which workers need cleanup.
:::
---
### worker-status
Show all currently-processing tasks grouped by worker, including operation type, bank, how long each task has been running, and when it was last updated. Useful for identifying orphaned tasks before decommissioning.
```bash
hindsight-admin worker-status [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--schema`, `-s` | Database schema | `public` |
**Examples:**
```bash
# Show all processing tasks across all workers
hindsight-admin worker-status
# Show processing tasks for a specific tenant schema
hindsight-admin worker-status --schema tenant_acme
```
**When to Use:**
- **Before decommissioning**: Inspect which workers have stale tasks and how long they have been stuck
- **Debugging throughput**: Diagnose why the queue is not draining (are tasks stuck in processing?)
- **Worker health check**: Spot workers whose `last_update_ago` keeps growing, indicating a dead or unresponsive worker
---
### export-bank
Export an entire bank to a portable ZIP archive — documents, facts, observations, bank configuration, mental models, directives, and webhooks. Embeddings are **never** included; they are regenerated on import. This is the source half of a cross-instance migration (e.g. moving to a different embedding model, vector extension, or text-search backend). PostgreSQL only.
```bash
hindsight-admin export-bank --bank <BANK_ID> --output <FILE.zip> [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--bank`, `-b` | Bank id to export. | (required) |
| `--output`, `-o` | Path to write the `.zip` archive. | (required) |
| `--schema`, `-s` | Schema the bank lives in. | base schema |
| `--include-history` | Also export operational history (`audit_log`, `llm_requests`). | `false` |
**Examples:**
```bash
hindsight-admin export-bank --bank my-bank --output my-bank.zip
# include operational history
hindsight-admin export-bank --bank my-bank --output my-bank.zip --include-history
```
Read-only — safe to run against a live instance.
---
### import-bank
Restore a whole-bank archive (produced by `export-bank`) into **this** instance. Facts are re-embedded with this instance's configured embedding model and links/indexes are rebuilt; bank configuration, mental models, directives, and webhooks are restored exactly. No LLM fact-extraction runs, and because a migration restores state, it does **not** fire webhooks or re-run consolidation. PostgreSQL only.
```bash
hindsight-admin import-bank --archive <FILE.zip> [OPTIONS]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--archive`, `-a` | Path to the `.zip` produced by `export-bank`. | (required) |
| `--schema`, `-s` | Target schema. | base schema |
| `--target-bank` | Override the bank id (defaults to the archive's source bank). | source bank |
| `--include-history` | Also restore history if present in the archive. | `false` |
**Examples:**
```bash
hindsight-admin import-bank --archive my-bank.zip
```
Run this against an instance configured with the **target** embedding model / vector extension / text-search backend — that's what re-embedding uses.
:::warning Target bank must not exist
Import restores a **whole bank** (config, facts, mental models, …) — it is **not a merge**. If a bank with the target id already exists, the command fails. Delete that bank first, or use `--target-bank` to restore under a fresh id.
:::
---
## Migrating a bank to a new instance
Changing a bank's **embedding model** (e.g. a 384-dim encoder → a 1024-dim one), **vector extension** (pgvector / vchord / pgvectorscale), or **text-search backend** can't be done in place on a populated bank — the stored vectors and indexes are tied to those settings. Because every embedding and index is a deterministic function of text already on disk, the supported path is to **move the bank to a fresh instance configured with the new settings and re-derive everything there — with no LLM re-extraction**.
`export-bank` / `import-bank` carry documents, facts, observations, bank config, mental models, directives, and webhooks — but never embeddings, which the target instance regenerates with its own model.
**Blue-green runbook:**
1. Stand up a **new instance** on a fresh database, configured with the new embedding model / vector extension / text-search backend.
2. Quiesce writes to the source bank (maintenance window) and run `hindsight-admin backup` for safety.
3. Export from the source, then import into the target:
```bash
# on the source instance:
hindsight-admin export-bank --bank my-bank --output my-bank.zip
# on the target instance (configured with the new settings):
hindsight-admin import-bank --archive my-bank.zip
```
4. Verify on the target: run representative recall queries and compare results.
5. Cut traffic over to the new instance. The old instance stays as an instant rollback until you're confident.
:::note Why a new instance, not in-place
The embedding model is server-level, and a bank's `memory_units.embedding` column has a single dimension shared across the schema, so a different-dimension or different-backend bank needs its own instance/database. The old vectors are never mutated, which makes rollback trivial.
:::
---
## Recovering stuck or zombie operations
A "zombie" operation is one stuck in `processing` indefinitely because the worker that claimed it is gone. The most common cause is an unstable `HINDSIGHT_API_WORKER_ID`: when it defaults to the container hostname, a Docker restart produces a new container ID, the new worker doesn't recognize the old worker's claims as its own, and those tasks are stranded.
**How to spot them:**
```bash
# List processing tasks grouped by worker — workers with a growing last_update_ago are dead
hindsight-admin worker-status
# Bank-level counters; pending_consolidation that never decreases is the usual symptom
curl -s http://localhost:8888/v1/default/banks/<bank_id>/stats
```
**How to recover:**
```bash
# You know which worker is dead (e.g. from worker-status):
hindsight-admin decommission-worker <old-worker-id>
# You don't know — release every processing task across the fleet:
hindsight-admin decommission-workers
```
Both commands reset `processing` rows back to `pending` so a live worker can claim them on the next poll.
**How to prevent it:**
Set `HINDSIGHT_API_WORKER_ID` to a stable value so worker identity survives restarts:
- **Docker**: pass `-e HINDSIGHT_API_WORKER_ID=hindsight-prod` (or per-replica names if running multiple containers)
- **Kubernetes (Helm)**: the chart's StatefulSet uses the pod name automatically — no extra config needed
- **Bare metal / pip**: pass `--worker-id <name>` or set the env var per process
See [Installation - Docker](./installation.md#docker) and [Configuration - Distributed Workers](./configuration.md#distributed-workers).
---
## Environment Variables
The admin CLI uses the same environment variables as the API service. The most important one is:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
**Example:**
```bash
# Use a specific database
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
hindsight-admin backup /backups/mybackup.zip
```
references/developer/api/bank-templates.md
# Bank Templates
Declarative JSON manifests for creating pre-configured memory banks with a single API call.
{/* Import raw source files */}
## Overview
A bank template is a JSON manifest that describes a bank's full setup: configuration overrides, mental models, directives, and more. Instead of making multiple API calls to configure a bank, you submit one manifest and the API provisions everything.
Templates are useful for:
- **Replication** — stamp out identically-configured banks for multiple users or agents
- **Onboarding** — new users start with a known-good configuration instead of configuring from scratch
- **Sharing** — distribute recommended setups as portable JSON files
- **Framework integrations** — ship a recommended template alongside your integration
Browse the Bank Templates Hub for ready-to-use templates.
## Manifest Schema
```json
{
"version": "1",
"bank": {
"reflect_mission": "...",
"retain_mission": "...",
"retain_extraction_mode": "concise | verbose | custom | verbatim | chunks",
"retain_custom_instructions": "...",
"retain_chunk_size": 2048,
"retain_structured_chunk_size": 8192,
"disposition_skepticism": 3,
"disposition_literalism": 3,
"disposition_empathy": 3,
"enable_observations": true,
"observations_mission": "...",
"entity_labels": [{ "key": "sentiment", "type": "value", "values": [{ "value": "positive" }, { "value": "negative" }] }],
"entities_allow_free_form": true
},
"mental_models": [
{
"id": "unique-lowercase-id",
"name": "Human-Readable Name",
"source_query": "The query that generates this mental model's content",
"tags": ["optional", "tags"],
"max_tokens": 2048,
"trigger": {
"refresh_after_consolidation": false,
"fact_types": ["world", "experience", "observation"],
"exclude_mental_models": false,
"exclude_mental_model_ids": []
}
}
],
"directives": [
{
"name": "directive-name",
"content": "The directive instruction text",
"priority": 0,
"is_active": true,
"tags": ["optional", "tags"]
}
]
}
```
### Fields
| Field | Required | Description |
|-------|----------|-------------|
| `version` | Yes | Schema version. Currently `"1"`. |
| `bank` | No | Bank configuration overrides. Omit to leave config unchanged. |
| `mental_models` | No | Mental models to create or update. Omit to leave unchanged. |
| `directives` | No | Directives to create or update. Omit to leave unchanged. |
All of `bank`, `mental_models`, and `directives` are optional. Omit any section to leave that part of the bank unchanged.
### Bank Config Fields
Every per-bank configuration setting can be carried in a template, so an exported
bank reproduces its full configuration when imported elsewhere. All fields in
`bank` are optional — only the fields you include are set as per-bank overrides,
and everything else inherits from the server/tenant defaults.
The complete, always-current list is the template JSON
Schema; each field means the same thing it does in
[Configuration](../configuration.md). The most commonly used ones:
| Field | Type | Description |
|-------|------|-------------|
| `reflect_mission` | string | Mission/context for reflect operations |
| `retain_mission` | string | Steers what gets extracted during retain |
| `retain_extraction_mode` | string | `concise`, `verbose`, `custom`, `verbatim`, or `chunks` |
| `retain_custom_instructions` | string | Custom extraction prompt (requires `mode=custom`) |
| `retain_chunk_size` | integer | Target max characters per content chunk |
| `retain_structured_chunk_size` | integer | Max characters for a single JSONL line or conversation turn to keep whole; defaults to `retain_chunk_size` when unset |
| `disposition_skepticism` | integer (1-5) | How skeptical the disposition is |
| `disposition_literalism` | integer (1-5) | How literal the disposition is |
| `disposition_empathy` | integer (1-5) | How empathetic the disposition is |
| `enable_observations` | boolean | Toggle observation consolidation |
| `observations_mission` | string | Controls what gets synthesised into observations |
| `entity_labels` | object[] | Controlled vocabulary as label groups — see [Memory Banks → entity_labels](./memory-banks.md#entity-labels) |
| `entities_allow_free_form` | boolean | Allow entities outside the label vocabulary |
### Mental Model Fields
| Field | Required | Description |
|-------|----------|-------------|
| `id` | Yes | Unique ID (lowercase alphanumeric with hyphens). Used to match on re-import. |
| `name` | Yes | Human-readable name |
| `source_query` | Yes | The query that generates this model's content via reflect |
| `tags` | No | Tags for scoped visibility. Default: `[]` |
| `max_tokens` | No | Max tokens for generated content (256-8192). Default: `2048` |
| `trigger` | No | Trigger settings for auto-refresh |
### Directive Fields
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Directive name. Used as the match key on re-import. |
| `content` | Yes | The directive instruction text. |
| `priority` | No | Priority value (higher = more important). Default: `0` |
| `is_active` | No | Whether the directive is active. Default: `true` |
| `tags` | No | Tags for categorization. Default: `[]` |
## Import
Import a manifest into a bank. If the bank doesn't exist, it's created automatically.
### Python
```python
template = {
"version": "1",
"bank": {
"retain_mission": "Extract customer issues, resolutions, and sentiment.",
"enable_observations": True,
"observations_mission": "Track recurring customer pain points.",
},
"mental_models": [
{
"id": "sentiment-overview",
"name": "Customer Sentiment Overview",
"source_query": "What is the overall sentiment trend?",
"trigger": {"refresh_after_consolidation": True},
}
],
"directives": [
{
"name": "Acknowledge frustration",
"content": "Always acknowledge frustration before offering solutions.",
"priority": 10,
}
],
}
response = requests.post(
f"{HINDSIGHT_URL}/v1/default/banks/my-bank/import",
json=template,
)
result = response.json()
print(f"Config applied: {result['config_applied']}")
print(f"Mental models created: {result['mental_models_created']}")
print(f"Directives created: {result['directives_created']}")
```
### Node.js
```javascript
const template = {
version: '1',
bank: {
retain_mission: 'Extract customer issues, resolutions, and sentiment.',
enable_observations: true,
observations_mission: 'Track recurring customer pain points.',
},
mental_models: [
{
id: 'sentiment-overview',
name: 'Customer Sentiment Overview',
source_query: 'What is the overall sentiment trend?',
trigger: { refresh_after_consolidation: true },
},
],
directives: [
{
name: 'Acknowledge frustration',
content: 'Always acknowledge frustration before offering solutions.',
priority: 10,
},
],
};
const importResponse = await fetch(
`${HINDSIGHT_URL}/v1/default/banks/my-bank/import`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(template),
},
);
const result = await importResponse.json();
console.log('Config applied:', result.config_applied);
console.log('Mental models created:', result.mental_models_created);
console.log('Directives created:', result.directives_created);
```
### CLI
```bash
curl -X POST "$HINDSIGHT_URL/v1/default/banks/my-bank/import" \
-H "Content-Type: application/json" \
-d '{
"version": "1",
"bank": {
"retain_mission": "Extract customer issues, resolutions, and sentiment.",
"enable_observations": true,
"observations_mission": "Track recurring customer pain points."
},
"mental_models": [
{
"id": "sentiment-overview",
"name": "Customer Sentiment Overview",
"source_query": "What is the overall sentiment trend?",
"trigger": { "refresh_after_consolidation": true }
}
],
"directives": [
{
"name": "Acknowledge frustration",
"content": "Always acknowledge frustration before offering solutions.",
"priority": 10
}
]
}'
```
### Go
```go
template := map[string]interface{}{
"version": "1",
"bank": map[string]interface{}{
"retain_mission": "Extract customer issues, resolutions, and sentiment.",
"enable_observations": true,
"observations_mission": "Track recurring customer pain points.",
},
"mental_models": []map[string]interface{}{
{
"id": "sentiment-overview",
"name": "Customer Sentiment Overview",
"source_query": "What is the overall sentiment trend?",
"trigger": map[string]interface{}{"refresh_after_consolidation": true},
},
},
"directives": []map[string]interface{}{
{
"name": "Acknowledge frustration",
"content": "Always acknowledge frustration before offering solutions.",
"priority": 10,
},
},
}
body, _ := json.Marshal(template)
resp, _ := http.Post(
apiURL+"/v1/default/banks/my-bank/import",
"application/json",
bytes.NewReader(body),
)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
```
### Behavior
- **Config**: all `bank` fields are applied as per-bank config overrides
- **Mental models**: matched by `id` — new ones are created, existing ones are updated only when their definition differs from the manifest
- **Directives**: matched by `name` — new ones are created, existing ones are updated only when they differ from the manifest
- **Async**: mental model content is generated asynchronously. The response includes `operation_ids` to track progress.
- **Idempotent**: re-importing an unchanged manifest updates nothing and queues no refresh, so applying the same template on every sync costs no LLM calls. Only created or changed mental models are regenerated.
### Dry Run
Validate a manifest without applying changes:
### Python
```python
response = requests.post(
f"{HINDSIGHT_URL}/v1/default/banks/my-bank/import",
params={"dry_run": "true"},
json=template,
)
result = response.json()
print(f"Dry run: {result['dry_run']}")
print(f"Would apply config: {result['config_applied']}")
```
### Node.js
```javascript
const dryRunResponse = await fetch(
`${HINDSIGHT_URL}/v1/default/banks/my-bank/import?dry_run=true`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(template),
},
);
const dryRunResult = await dryRunResponse.json();
console.log('Dry run:', dryRunResult.dry_run);
console.log('Would apply config:', dryRunResult.config_applied);
```
### CLI
```bash
curl -X POST "$HINDSIGHT_URL/v1/default/banks/my-bank/import?dry_run=true" \
-H "Content-Type: application/json" \
-d '{"version": "1", "bank": {"retain_mission": "Dry run test."}}'
```
### Go
```go
resp, _ = http.Post(
apiURL+"/v1/default/banks/my-bank/import?dry_run=true",
"application/json",
bytes.NewReader(body),
)
defer resp.Body.Close()
dryRunBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(dryRunBody))
```
Returns what *would* happen (which config would be applied, which mental models would be created) without making any changes. Returns HTTP 400 with a detailed error message if the manifest is invalid.
## Export
Export a bank's current config overrides, mental models, and directives as a manifest:
### Python
```python
response = requests.get(
f"{HINDSIGHT_URL}/v1/default/banks/my-bank/export"
)
exported = response.json()
print(json.dumps(exported, indent=2))
```
### Node.js
```javascript
const exportResponse = await fetch(
`${HINDSIGHT_URL}/v1/default/banks/my-bank/export`,
);
const exported = await exportResponse.json();
console.log(JSON.stringify(exported, null, 2));
```
### CLI
```bash
curl "$HINDSIGHT_URL/v1/default/banks/my-bank/export"
```
### Go
```go
resp, _ = http.Get(apiURL + "/v1/default/banks/my-bank/export")
defer resp.Body.Close()
exported, _ := io.ReadAll(resp.Body)
fmt.Println(string(exported))
```
The exported manifest only includes config fields that were explicitly set as per-bank overrides — not the fully resolved config (which includes server/tenant defaults). This means the exported manifest is portable: importing it into a new bank only overrides the fields that were intentionally customized.
### Round-trip
Export from one bank and import into another to replicate the setup:
### Python
```python
# Export from source bank
response = requests.get(
f"{HINDSIGHT_URL}/v1/default/banks/source-bank/export"
)
exported = response.json()
# Import into a new bank
response = requests.post(
f"{HINDSIGHT_URL}/v1/default/banks/new-bank/import",
json=exported,
)
```
### Node.js
```javascript
// Export from source bank
const srcResponse = await fetch(
`${HINDSIGHT_URL}/v1/default/banks/source-bank/export`,
);
const srcExported = await srcResponse.json();
// Import into a new bank
await fetch(`${HINDSIGHT_URL}/v1/default/banks/new-bank/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(srcExported),
});
```
### CLI
```bash
# Export from source bank
curl "$HINDSIGHT_URL/v1/default/banks/source-bank/export" > template.json
# Import into a new bank
curl -X POST "$HINDSIGHT_URL/v1/default/banks/new-bank/import" \
-H "Content-Type: application/json" \
-d @template.json
```
### Go
```go
// Export from source bank
resp, _ = http.Get(apiURL + "/v1/default/banks/source-bank/export")
defer resp.Body.Close()
srcExported, _ := io.ReadAll(resp.Body)
// Import into a new bank
resp, _ = http.Post(
apiURL+"/v1/default/banks/new-bank/import",
"application/json",
bytes.NewReader(srcExported),
)
defer resp.Body.Close()
```
## JSON Schema
The manifest format is defined by a JSON Schema. Fetch the live schema from your server:
### Python
```python
response = requests.get(
f"{HINDSIGHT_URL}/v1/bank-template-schema"
)
schema = response.json()
print(json.dumps(schema, indent=2))
```
### Node.js
```javascript
const schemaResponse = await fetch(
`${HINDSIGHT_URL}/v1/bank-template-schema`,
);
const schema = await schemaResponse.json();
console.log(JSON.stringify(schema, null, 2));
```
### CLI
```bash
curl "$HINDSIGHT_URL/v1/bank-template-schema"
```
### Go
```go
resp, _ = http.Get(apiURL + "/v1/bank-template-schema")
defer resp.Body.Close()
schema, _ := io.ReadAll(resp.Body)
fmt.Println(string(schema))
```
The static schema is also available at bank-template-schema.json.
## Control Plane
The control plane bank creation dialog includes an optional "Import from template" toggle. Enable it to paste a manifest JSON and pre-configure the bank on creation.
You can also export any bank's template from the bank Settings page via **Actions → Export Template**, which copies the manifest JSON to your clipboard.
## Versioning
The `version` field enables forward-compatible schema evolution. The current version is `"1"`.
When future versions are released:
- Older manifests are automatically upgraded to the current schema on import
- Export always produces the latest version
- The API rejects manifests with a version newer than what the server supports (with a clear error message suggesting an upgrade)
This means old templates keep working indefinitely — no need to manually update them.
references/developer/api/documents.md
# Documents
Track and manage document sources in your memory bank. Documents provide traceability — knowing where memories came from.
{/* Import raw source files */}
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) and understand [how retain works](./retain.md).
## What Are Documents?
Documents are containers for retained content. They help you:
- **Track sources** — Know which PDF, conversation, or file a memory came from
- **Update content** — Re-retain a document to update its facts
- **Delete in bulk** — Remove all memories from a document at once
- **Organize memories** — Group related facts by source
## Chunks
When you retain content, Hindsight splits it into chunks before extracting facts. These chunks are stored alongside the extracted memories, preserving the original text segments.
**Why chunks matter:**
- **Context preservation** — Chunks contain the raw text that generated facts, useful when you need the exact wording
- **Richer recall** — Including chunks in recall provides surrounding context for matched facts
> **💡 Include Chunks in Recall**
>
Use `include_chunks=True` in your recall calls to get the original text chunks alongside fact results. See [Recall](./recall.md) for details.
## Retain with Document ID
Associate retained content with a document:
### Python
```python
# Retain with document ID
client.retain(
bank_id="my-bank",
content="Alice presented the Q4 roadmap...",
document_id="meeting-2024-03-15"
)
# Batch retain for a document with different sections
client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Item 1: Product launch delayed to Q2", "document_id": "meeting-2024-03-15-section-1"},
{"content": "Item 2: New hiring targets announced", "document_id": "meeting-2024-03-15-section-2"},
{"content": "Item 3: Budget approved for ML team", "document_id": "meeting-2024-03-15-section-3"}
]
)
```
### Node.js
```javascript
// Retain with document ID
await client.retain('my-bank', 'Alice presented the Q4 roadmap...', {
document_id: 'meeting-2024-03-15'
});
// Batch retain for a document with different sections
await client.retainBatch('my-bank', [
{ content: 'Item 1: Product launch delayed to Q2', document_id: 'meeting-2024-03-15-section-1' },
{ content: 'Item 2: New hiring targets announced', document_id: 'meeting-2024-03-15-section-2' },
{ content: 'Item 3: Budget approved for ML team', document_id: 'meeting-2024-03-15-section-3' }
]);
```
### CLI
```bash
# Retain content with document ID
hindsight memory retain "$BANK_ID" "Meeting notes content..." --doc-id notes-2024-03-15
# Batch retain from files
hindsight memory retain-files "$BANK_ID" docs/
```
### Go
```go
// Retain with document ID
docID := "meeting-2024-03-15"
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{
Content: hindsight.TextContent("Alice presented the Q4 roadmap..."),
DocumentId: *hindsight.NewNullableString(&docID),
},
},
}).Execute()
```
## Update Documents
Re-retaining with the same document_id **replaces** the old content:
### Python
```python
# Original
client.retain(
bank_id="my-bank",
content="Project deadline: March 31",
document_id="project-plan"
)
# Update (deletes old facts, creates new ones)
client.retain(
bank_id="my-bank",
content="Project deadline: April 15 (extended)",
document_id="project-plan"
)
```
### Node.js
```javascript
// Original
await client.retain('my-bank', 'Project deadline: March 31', {
document_id: 'project-plan'
});
// Update
await client.retain('my-bank', 'Project deadline: April 15 (extended)', {
document_id: 'project-plan'
});
```
### CLI
```bash
# Original
hindsight memory retain "$BANK_ID" "Project deadline: March 31" --doc-id project-plan
# Update
hindsight memory retain "$BANK_ID" "Project deadline: April 15 (extended)" --doc-id project-plan
```
### Go
```go
// Original
planDoc := "project-plan"
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{
Content: hindsight.TextContent("Project deadline: March 31"),
DocumentId: *hindsight.NewNullableString(&planDoc),
},
},
}).Execute()
// Update (deletes old facts, creates new ones)
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{
Content: hindsight.TextContent("Project deadline: April 15 (extended)"),
DocumentId: *hindsight.NewNullableString(&planDoc),
},
},
}).Execute()
```
## Get Document
Retrieve a document's original text and metadata. This is useful for expanding document context after a recall operation returns memories with document references.
### Python
```python
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
async def get_document_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# Get document to expand context from recall results
doc = await api.get_document(
bank_id="my-bank",
document_id="meeting-2024-03-15"
)
print(f"Document: {doc.id}")
print(f"Original text: {doc.original_text}")
print(f"Memory count: {doc.memory_unit_count}")
print(f"Created: {doc.created_at}")
asyncio.run(get_document_example())
```
### Node.js
```javascript
// Get document to expand context from recall results
const { data: doc, error } = await sdk.getDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' }
});
if (error) {
throw new Error(`Failed to get document: ${JSON.stringify(error)}`);
}
console.log(`Document: ${doc.id}`);
console.log(`Original text: ${doc.original_text}`);
console.log(`Memory count: ${doc.memory_unit_count}`);
console.log(`Created: ${doc.created_at}`);
```
### CLI
```bash
hindsight document get "$BANK_ID" notes-2024-03-15
```
### Go
```go
doc, _, err := client.DocumentsAPI.GetDocument(ctx, "my-bank", "meeting-2024-03-15").Execute()
if err != nil {
log.Fatalf("Failed to get document: %v", err)
}
fmt.Printf("Document ID: %s\n", doc.GetId())
fmt.Printf("Memory units: %d\n", doc.GetMemoryUnitCount())
```
## Update Document
Update mutable fields on an existing document without re-processing the content. Currently supports updating `tags`.
The `tags` array **replaces** the document's tags — it is not merged into them. Send the complete set you want the document to end up with: any tag you leave out is dropped, and an empty array clears them all. To remove a single tag, read the document's current tags, drop the one you want gone, and send the rest. Omitting the field entirely is not an update at all and is rejected with a `422`.
### Python
```python
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
from hindsight_client_api.models import UpdateDocumentRequest
async def update_document_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# Fix tags on a document retained with the wrong scope
result = await api.update_document(
bank_id="my-bank",
document_id="meeting-2024-03-15",
update_document_request=UpdateDocumentRequest(tags=["team-a", "team-b"]),
)
print(f"Updated: {result.success}")
# Remove all tags (make document visible everywhere)
await api.update_document(
bank_id="my-bank",
document_id="meeting-2024-03-15",
update_document_request=UpdateDocumentRequest(tags=[]),
)
asyncio.run(update_document_example())
```
### Node.js
```javascript
// Fix tags on a document retained with the wrong scope
const { data: updateResult, error: updateError } = await sdk.updateDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' },
body: { tags: ['team-a', 'team-b'] }
});
if (updateError) {
throw new Error(`Failed to update tags: ${JSON.stringify(updateError)}`);
}
console.log(`Updated: ${updateResult.success}`);
// Remove all tags (make document visible everywhere)
await sdk.updateDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' },
body: { tags: [] }
});
```
### CLI
```bash
# Replace tags with new values (comma-separated)
hindsight document update "$BANK_ID" notes-2024-03-15 --tags team-a,team-b
# Remove all tags (make document visible everywhere)
hindsight document update "$BANK_ID" notes-2024-03-15 --tags ""
```
### Go
```go
// Replace the document's tags (the full set it should end up with)
updateResult, _, err := client.DocumentsAPI.UpdateDocument(ctx, "my-bank", "meeting-2024-03-15").
UpdateDocumentRequest(hindsight.UpdateDocumentRequest{Tags: []string{"team-a", "team-b"}}).
Execute()
if err != nil {
log.Fatalf("Failed to update document: %v", err)
}
fmt.Printf("Updated: %v\n", updateResult.GetSuccess())
// Remove all tags (make document visible everywhere). Pass an empty slice, not nil:
// a nil slice is left out of the request, and the server rejects an update with no tags.
client.DocumentsAPI.UpdateDocument(ctx, "my-bank", "meeting-2024-03-15").
UpdateDocumentRequest(hindsight.UpdateDocumentRequest{Tags: []string{}}).
Execute()
```
> **ℹ️ Observations are re-consolidated**
>
When tags change, any consolidated observations derived from the document's memories are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.
This is required for correctness rather than incidental: consolidation scopes a memory by its tag set, so an observation built under the old tags is no longer valid, and deleting it would strand every other memory that observation was consolidated from unless those are requeued too. The size of that requeue is the number of memories co-sourced with this document's — on a densely co-sourced bank it can be many times the document's own memory count.
Tags are compared as a **set** against the document's current tags, and an update that leaves the set unchanged — including one that only reorders the array — performs no retag and queues no re-consolidation. A repeatable tag-normalisation sweep therefore only pays the re-consolidation cost on the run that actually changes something.
## Delete Document
Remove a document and all its associated memories:
### Python
```python
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
async def delete_document_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# Delete document and all its memories
result = await api.delete_document(
bank_id="my-bank",
document_id="meeting-2024-03-15"
)
print(f"Deleted {result.memory_units_deleted} memories")
asyncio.run(delete_document_example())
```
### Node.js
```javascript
// Delete document and all its memories
const { data: deleteResult } = await sdk.deleteDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' }
});
console.log(`Deleted ${deleteResult.memory_units_deleted} memories`);
```
### CLI
```bash
hindsight document delete "$BANK_ID" notes-2024-03-15
```
### Go
```go
client.DocumentsAPI.DeleteDocument(ctx, "my-bank", "meeting-2024-03-15").Execute()
```
> **⚠️ Warning**
>
Deleting a document permanently removes all memories extracted from it. This action cannot be undone.
## List Documents
List documents in a bank with optional filtering by ID, tags, and time.
`start_date=` and `end_date=` restrict the results to a half-open range `[start, end)` on the timestamp named by `time_field=` — either `created_at` (when the document first arrived) or `updated_at` (its last write, and the default ordering). `time_field` also becomes the sort order, newest first.
Because filtering and ordering follow the same column, `total` counts only the documents inside the window rather than the whole bank. Omit all three parameters to keep the default listing.
### Python
```python
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
async def list_documents_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# List all documents
result = await api.list_documents(bank_id="my-bank")
print(f"Total documents: {result.total}")
# Filter by document ID substring
result = await api.list_documents(bank_id="my-bank", q="report")
# Filter by tags — only docs tagged with "team-a" (untagged excluded)
result = await api.list_documents(
bank_id="my-bank",
tags=["team-a"],
tags_match="any_strict",
)
# Combine ID search and tags
result = await api.list_documents(
bank_id="my-bank",
q="meeting",
tags=["team-a", "team-b"],
tags_match="all_strict", # must have both tags
)
# Paginate
result = await api.list_documents(bank_id="my-bank", limit=20, offset=40)
print(f"Page items: {len(result.items)}")
import asyncio
asyncio.run(list_documents_example())
```
### Node.js
```javascript
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
// List all documents
const { data: allDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' }
});
console.log(`Total documents: ${allDocs.total}`);
// Filter by document ID substring
const { data: reportDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { q: 'report' }
});
// Filter by tags — only docs tagged with "team-a" (untagged excluded)
const { data: taggedDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { tags: ['team-a'], tags_match: 'any_strict' }
});
// Combine ID search and tags
const { data: filtered } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { q: 'meeting', tags: ['team-a', 'team-b'], tags_match: 'all_strict' }
});
// Paginate
const { data: page } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { limit: 20, offset: 40 }
});
console.log(`Page items: ${page.items.length}`);
```
### CLI
```bash
# List all documents
hindsight document list "$BANK_ID"
# Filter by ID substring
hindsight document list "$BANK_ID" --query notes
```
### Go
```go
// List all documents
docs, _, err := client.DocumentsAPI.ListDocuments(ctx, "my-bank").Execute()
if err != nil {
log.Fatalf("Failed to list documents: %v", err)
}
for _, d := range docs.Items {
fmt.Printf("%s: %d memories\n", d.Id, d.GetMemoryUnitCount())
}
```
### Filtering Options
| Parameter | Description |
|---|---|
| `q` | Case-insensitive substring match on document ID or document text. `report` matches `report-2024`, `annual-report`, or any document whose text contains `report`. Not indexed: meant for browsing and inspecting a bank, not for high-volume or latency-sensitive lookups (use recall for those). |
| `tags` | Filter by document tags. Accepts multiple values. |
| `tags_match` | How to match tags (default: `any_strict`). See below. |
| `limit` / `offset` | Pagination. Default limit is 100. |
When `q` is set, each returned row carries `matched_fields`: `["id"]`, `["original_text"]`, or both, so a caller can tell why a document matched.
A `q` search counts at most 1,000 matches. Past that, `total` is a lower bound, `total_is_estimate` is `true`, and pages stop at the cap. Narrow the search, or add a tag or time filter, to reach the rest. Some storage backends match `q` approximately — `matched_fields` always says what matched.
**`tags_match` modes:**
| Mode | Behaviour |
|---|---|
| `any_strict` *(default)* | Document must have **at least one** of the specified tags. Untagged docs excluded. |
| `any` | Same as `any_strict` but also includes untagged documents. |
| `all_strict` | Document must have **all** specified tags. Untagged docs excluded. |
| `all` | Same as `all_strict` but also includes untagged documents. |
## Document History
Every change to a document is recorded with what it did: each retain, append and reprocess (facts added and removed), each fact edit, invalidation, restore and delete, each per-fact observation clear, each tag change, and each consolidation run (observations created, updated and removed, filed under every document their source facts came from).
Observations are rebuilt rather than versioned, so when a change deletes one, this history is the only place its id is still recorded. Each event stores ids and counts only, never text, and each id list is capped at 100. The control plane shows this as the **History** tab of a document.
The history is rebuilt from the bank's [operations](./operations.md), which carry these events, so two limits apply:
- It reaches back only as far as operations are kept (`HINDSIGHT_API_OPERATION_RETENTION_DAYS`, unlimited by default).
- A change that ran without an operation is not in it: a synchronous retain (`async: false`), or a fact edit, delete or tag change while auto-consolidation is off. A fact edit, delete or tag change is recorded on the consolidation operation it queues, next to the rebuild it causes. Clearing every observation in the bank is not listed.
### Python
```python
async def document_history_example():
api = DocumentsApi(ApiClient(Configuration(host="http://localhost:8888")))
# What each retain, fact edit and consolidation did to this document, newest first
history = await api.get_document_history(bank_id="my-bank", document_id="meeting-2024-03-15", limit=50)
for event in history.items:
print(
f"{event.created_at} {event.kind}: +{event.facts_added}/-{event.facts_removed} facts, "
f"observations removed: {event.observations_removed.ids}"
)
asyncio.run(document_history_example())
```
### Node.js
```javascript
// What each retain, fact edit and consolidation did to this document, newest first
const { data: history, error: historyError } = await sdk.getDocumentHistory({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' },
query: { limit: 50 }
});
if (historyError) {
throw new Error(`Failed to get document history: ${JSON.stringify(historyError)}`);
}
for (const event of history.items) {
console.log(`${event.created_at} ${event.kind}: removed ${event.observations_removed.ids.join(', ')}`);
}
```
### CLI
```bash
# What each retain, fact edit and consolidation did to this document, newest first
hindsight document history "$BANK_ID" notes-2024-03-15 --limit 50
```
### Go
```go
// What each retain, fact edit and consolidation did to this document, newest first
history, _, err := client.DocumentsAPI.GetDocumentHistory(ctx, "my-bank", "meeting-2024-03-15").Limit(50).Execute()
if err != nil {
log.Fatalf("Failed to get document history: %v", err)
}
for _, event := range history.GetItems() {
removed := event.GetObservationsRemoved()
fmt.Printf("%s: +%d facts, observations removed: %v\n", event.GetKind(), event.GetFactsAdded(), removed.GetIds())
}
```
```json
{
"document_id": "call-2026-10-01",
"total": 2,
"limit": 50,
"offset": 0,
"items": [
{
"document_id": "call-2026-10-01",
"kind": "consolidation",
"created_at": "2026-10-19T10:02:11Z",
"operation_id": "ce29121a-…",
"fact_id": null,
"facts_added": 0,
"facts_removed": 0,
"observations_created": { "count": 0, "ids": [] },
"observations_updated": { "count": 2, "ids": ["9545…", "c51c…"] },
"observations_removed": { "count": 0, "ids": [] }
},
{
"document_id": "call-2026-10-01",
"kind": "fact_edited",
"created_at": "2026-10-19T10:01:50Z",
"operation_id": "ce29121a-…",
"fact_id": "41ab…",
"facts_added": 0,
"facts_removed": 0,
"observations_created": { "count": 0, "ids": [] },
"observations_updated": { "count": 0, "ids": [] },
"observations_removed": { "count": 1, "ids": ["2098…"] }
}
]
}
```
## Document Response Format
```json
{
"id": "meeting-2024-03-15",
"bank_id": "my-bank",
"original_text": "Alice presented the Q4 roadmap...",
"content_hash": "abc123def456",
"memory_unit_count": 12,
"nodes_by_fact_type": {
"world": 5,
"experience": 4,
"observation": 3
},
"created_at": "2024-03-15T14:00:00Z",
"updated_at": "2024-03-15T14:00:00Z"
}
```
## Next Steps
- [**Operations**](./operations.md) — Monitor background tasks
- [**Memory Banks**](./memory-banks.md) — Configure bank settings
references/developer/api/knowledge-pages.md
# Knowledge Pages
Living markdown documents, organized in a folder tree, that rewrite themselves as the bank learns.
A page is backed by a [mental model](./mental-models.md) but is configured as a document: it is built from the bank's [observations](../observations.md) only, it never reads other pages, and it refreshes incrementally after each consolidation. See [Knowledge Pages](../knowledge-pages.md) for the concepts behind the API.
{/* Import raw source files */}
All endpoints below are relative to a bank:
```
/v1/default/banks/{bank_id}/knowledge-base
```
---
## Get the Tree
Returns the whole knowledge base as a nested tree of folders and pages. Page bodies are **not** included — fetch a page to read its content.
### Python
```python
# Fetch the whole knowledge base as a nested folder/page tree (no page bodies)
tree = client.get_knowledge_base_tree(BANK_ID)
for root in tree.roots:
print(f"{root.kind}: {root.name}")
for child in root.children:
print(f" {child.kind}: {child.name} (stale: {child.is_stale})")
```
### Node.js
```javascript
// Fetch the whole knowledge base as a nested folder/page tree (no page bodies)
const tree = await client.getKnowledgeBaseTree(BANK_ID);
for (const root of tree.roots) {
console.log(`${root.kind}: ${root.name}`);
for (const child of root.children ?? []) {
console.log(` ${child.kind}: ${child.name} (stale: ${child.is_stale})`);
}
}
```
### CLI
```bash
# Show the folder/page tree (no page bodies)
hindsight knowledge-base tree "$BANK_ID"
```
### Go
```go
// Fetch the whole knowledge base as a nested folder/page tree (no page bodies)
tree, _, _ := client.KnowledgeBaseAPI.GetKnowledgeBaseTree(ctx, kpBankID).Execute()
for _, root := range tree.Roots {
fmt.Printf("%s: %s\n", root.Kind, root.Name)
for _, child := range root.Children {
fmt.Printf(" %s: %s (stale: %v)\n", child.Kind, child.Name, child.GetIsStale())
}
}
```
```json
{
"roots": [
{
"id": "kf-9f2c...",
"kind": "folder",
"name": "Operations",
"parent_id": null,
"mental_model_id": null,
"managed": false,
"description": null,
"tags": [],
"timestamp": "2026-08-01T11:04:02+00:00",
"is_stale": null,
"children": [
{
"id": "kp-2e85...",
"kind": "page",
"name": "Deploying the API",
"parent_id": "kf-9f2c...",
"mental_model_id": "mm-77ab...",
"managed": false,
"description": "How is the API deployed?",
"tags": ["ops"],
"timestamp": "2026-08-03T09:12:44+00:00",
"is_stale": true,
"children": []
}
]
}
]
}
```
| Field | Description |
|---|---|
| `kind` | `folder` or `page` |
| `mental_model_id` | The backing mental model (pages only) |
| `description` | The page's source query — the question that rebuilds it |
| `timestamp` | Last refresh for a page, last update for a folder |
| `is_stale` | Pages only: `true` when a memory in *this page's* scope has been written since the page last read the memories (see below) |
| `last_refresh_failed_at` | Pages only: when this page's last refresh failed, or `null` when it succeeded. While it is set the page does not rebuild itself on its trigger — an explicit refresh still runs, and a successful one clears it |
| `managed` | `true` when the node is flagged as system-owned rather than hand-authored |
### How `is_stale` is decided
Each page is answered against its own scope — its tags and its `fact_types` — using the same [staleness check](./mental-models.md#staleness-gating) that decides whether a scheduled refresh does any work. A flagged page is one a refresh would actually rewrite; an unflagged one is a page a refresh would leave alone. Activity elsewhere in the bank does not flag a page whose own scope is quiet.
The whole tree is answered in one query, so the flag costs the same whether the bank has three pages or three hundred, and [`GET /mental-models/{id}`](./mental-models.md) returns the identical value for the page's backing model.
One thing it does not see: **deletions**. The check asks what has been *written* since the page last read the memories, and deleting an in-scope memory leaves no write behind — a page that cites a deleted fact keeps reporting itself up to date.
---
## Create a Page
Creating a page stores it with placeholder content and schedules the first build in the background. Poll the returned `operation_id` via the [operations API](./operations.md) to know when the content is ready.
### Python
```python
# Create a page — content is generated in the background
page = client.create_knowledge_page(
BANK_ID,
name="Deploying the API",
source_query="How is the API deployed?",
parent_id=folder.id,
tags=["ops", "type:runbook"],
)
# Poll the operation to know when the first build has finished
print(f"Page ID: {page.page_id}, operation: {page.operation_id}")
```
### Node.js
```javascript
// Create a page — content is generated in the background
const page = await client.createKnowledgePage(
BANK_ID,
'Deploying the API',
'How is the API deployed?',
{ parentId: folder.id, tags: ['ops', 'type:runbook'] },
);
// Poll the operation to know when the first build has finished
console.log(`Page ID: ${page.page_id}, operation: ${page.operation_id}`);
```
### CLI
```bash
# Create a page — content is generated in the background
hindsight knowledge-base create-page "$BANK_ID" \
"Deploying the API" \
"How is the API deployed?" \
--parent-id "$FOLDER_ID" \
--tags ops,type:runbook
```
### Go
```go
// Create a page — content is generated in the background
page, _, _ := client.KnowledgeBaseAPI.CreateKnowledgePage(ctx, kpBankID).
CreatePageRequest(hindsight.CreatePageRequest{
Name: "Deploying the API",
SourceQuery: "How is the API deployed?",
ParentId: *hindsight.NewNullableString(&folder.Id),
Tags: []string{"ops", "type:runbook"},
}).Execute()
// Poll the operation to know when the first build has finished
fmt.Printf("Page ID: %s, operation: %s\n", page.PageId, page.GetOperationId())
```
```json
{
"page_id": "kp-2e85...",
"mental_model_id": "mm-77ab...",
"operation_id": "op-1d0f..."
}
```
### Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Page name. Must be unique within its folder, case-insensitively — a duplicate returns `409`. (Enforced on PostgreSQL only.) |
| `source_query` | string | Yes | The question the page answers, re-asked on every refresh. |
| `parent_id` | string | No | Folder to create the page in. `null` (or omitted) creates it at the root. |
| `tags` | list | No | Tags that scope which memories the page is built from — see [Tags Are a Filter](#tags-are-a-filter). A `type:<x>` tag also sets the page's rendered type, and still counts as part of the filter. |
| `max_tokens` | int | No | Content budget. Defaults to `4096` (a plain mental model defaults to `2048`). |
| `trigger` | object | No | Refresh configuration — see below. |
### Tags Are a Filter
`tags` are not labels on the page. They are the scope the page is built from, and a tagged page matches memories with **`all_strict`** by default: a memory must carry **every** one of the page's tags, and untagged memories are **excluded entirely**.
So a page created like this:
```json
{
"name": "Homelab Infrastructure",
"source_query": "NAS, ThinkPad, docker containers, jellyfin",
"tags": ["type:runbook", "homelab", "infrastructure"]
}
```
is built only from memories tagged `type:runbook` **and** `homelab` **and** `infrastructure`. If your memories were retained without those exact tags — which is the usual case when the tags are invented at page-creation time to describe the topic — the page matches nothing and generates as *"I don't have information about this."* A direct [recall](./recall.md) for the same query still returns everything, because recall was not given the same filter.
The `type:<x>` tag makes this easy to trip over: it is documented as setting the page's rendered type, but it narrows retrieval like any other tag.
Three ways to get this right:
| You want | Do this |
|---|---|
| The page built from the whole bank | Omit `tags` (or pass `[]`) |
| The tags to scope the page, but untagged memories still included | Keep `tags`, add `"trigger": {"tags_match": "all"}` |
| Only memories carrying every tag (strict isolation, e.g. per-user pages) | Keep `tags` and the `all_strict` default |
To repair a page that already generated empty, `PATCH` it with `{"tags": []}` or with the widened `tags_match`, then refresh it — the tags are stored on the backing mental model, not baked into the content.
See [tag matching modes](./recall.md#tags) for the full semantics of `any`, `all`, `any_strict`, `all_strict`, and `exact`.
### Default Trigger
When `trigger` is omitted, the page is created with a document-oriented configuration:
```json
{
"mode": "delta",
"fact_types": ["observation"],
"exclude_mental_models": true,
"refresh_after_consolidation": true
}
```
This makes the page a living document built from consolidated observations only, refreshed incrementally whenever consolidation produces new knowledge in its scope, and never influenced by other pages.
| Setting | Why |
|---|---|
| `fact_types: ["observation"]` | The page reads consolidated beliefs, not the raw conversational noise underneath them. Observations are already deduplicated and evidence-backed, so a page reads as a settled document instead of a transcript. Enforced structurally — with only `observation` in scope, the refresh agent isn't given the raw-memory recall tool at all. |
| `exclude_mental_models: true` | A page never reflects on sibling pages. Without this, pages would cite each other and drift into a feedback loop where one wrong claim propagates across the knowledge base. |
| `mode: "delta"` | Each refresh edits the existing document with what is new since the last refresh instead of regenerating it, so hand-tuned structure and wording survive. See [Refresh Mode](./mental-models.md#refresh-mode). |
| `refresh_after_consolidation: true` | The page rewrites itself whenever consolidation produces new knowledge in its scope — gated by the same [staleness check](./mental-models.md#staleness-gating) as any mental model, so unrelated bank activity doesn't trigger rebuilds. |
### Page Lifecycle
1. **Create** — the page is stored with placeholder content and a background refresh is submitted; the call returns immediately with an `operation_id`.
2. **First build** — a full generation, since there is no prior document to edit.
3. **Consolidation** — new memories are retained and consolidated into observations.
4. **Staleness check** — pages whose trigger asks for it are checked against their own scope (tags and `fact_types` both apply).
5. **Delta refresh** — stale pages are rewritten by editing the existing document with the new observations only.
Observations are what a page is *built from*, but it can still inspect the evidence underneath them: the refresh agent can expand a memory to its original chunk or document (unless `store_document_text` is disabled for the bank), and if `HINDSIGHT_API_REFLECT_SOURCE_FACTS_MAX_TOKENS` is enabled — off by default — observation search also returns each observation's grounding facts.
> **ℹ️ Info**
>
A supplied `trigger` is a **patch**: only the fields you actually send are applied, and the rest keep the defaults above. Sending `{"trigger": {"tags_match": "all"}}` widens the tag filter and leaves `mode`, `fact_types`, `exclude_mental_models`, and `refresh_after_consolidation` as they are. The one exception is the two refresh triggers, which stay mutually exclusive: setting `refresh_cron` clears `refresh_after_consolidation`, and vice versa.
Every [mental model trigger setting](./mental-models.md#trigger-settings) is accepted here — including `refresh_cron` for scheduled rebuilds instead of consolidation-driven ones, and `tag_groups` for compound tag scoping.
---
## Create a Folder
### Python
```python
# Create a folder (omit parent_id, or pass None, to create it at the root)
folder = client.create_knowledge_folder(BANK_ID, name="Operations")
print(f"Folder ID: {folder.id}")
```
### Node.js
```javascript
// Create a folder (omit parentId, or pass null, to create it at the root)
const folder = await client.createKnowledgeFolder(BANK_ID, 'Operations');
console.log(`Folder ID: ${folder.id}`);
```
### CLI
```bash
# Create a folder (omit --parent-id to create it at the root)
hindsight knowledge-base create-folder "$BANK_ID" "Operations"
```
### Go
```go
// Create a folder (leave ParentId unset to create it at the root)
folder, _, _ := client.KnowledgeBaseAPI.CreateKnowledgeFolder(ctx, kpBankID).
CreateFolderRequest(hindsight.CreateFolderRequest{Name: "Operations"}).
Execute()
fmt.Printf("Folder ID: %s\n", folder.Id)
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Folder name |
| `parent_id` | string | No | Parent folder. Omit or pass `null` for the root. |
A `parent_id` that does not exist, or that points at a page rather than a folder, returns `400`.
---
## Read a Page
Returns the page rendered as a markdown document.
### Python
```python
# Read a page as a markdown document
document = client.get_knowledge_page(BANK_ID, page.page_id)
print(document.type) # "runbook" — from the type:runbook tag
print(document.body) # the synthesized markdown body
print(document.markdown) # YAML frontmatter + body
```
### Node.js
```javascript
// Read a page as a markdown document
const document = await client.getKnowledgePage(BANK_ID, page.page_id);
console.log(document.type); // "runbook" — from the type:runbook tag
console.log(document.body); // the synthesized markdown body
console.log(document.markdown); // YAML frontmatter + body
```
### CLI
```bash
# Read a page as a markdown document
hindsight knowledge-base get-page "$BANK_ID" "$PAGE_ID"
```
### Go
```go
// Read a page as a markdown document
document, _, _ := client.KnowledgeBaseAPI.GetKnowledgePage(ctx, kpBankID, page.PageId).Execute()
fmt.Println(document.Type) // "runbook" — from the type:runbook tag
fmt.Println(document.GetBody()) // the synthesized markdown body
fmt.Println(document.Markdown) // YAML frontmatter + body
```
```json
{
"id": "kp-2e85...",
"name": "Deploying the API",
"type": "runbook",
"description": "How is the API deployed?",
"tags": ["ops"],
"timestamp": "2026-08-03T09:12:44+00:00",
"body": "# Deploying the API\n\n...",
"markdown": "---\nid: \"kp-2e85...\"\ntype: \"runbook\"\n...\n---\n\n# Deploying the API\n\n..."
}
```
- `body` is the synthesized markdown body on its own.
- `markdown` is the full document: a YAML frontmatter block (`id`, `type`, `title`, `description`, `tags`, `timestamp`) followed by the body.
- `type` comes from a `type:<x>` tag and defaults to `knowledge-page`. The `type:` tag is removed from the returned `tags`.
---
## Search Pages
Document-level hybrid search: a full-text (BM25) match and a vector-similarity match, fused with Reciprocal Rank Fusion. There is no reranking step, which keeps it fast enough to be an agent's first call.
### Python
```python
# Hybrid search (full-text + vector) over whole pages
results = client.search_knowledge_base(BANK_ID, q="how do we deploy", limit=5)
for hit in results.results:
print(f"{hit.score:.3f} {hit.name}: {hit.snippet}")
```
### Node.js
```javascript
// Hybrid search (full-text + vector) over whole pages
const results = await client.searchKnowledgeBase(BANK_ID, 'how do we deploy', { limit: 5 });
for (const hit of results.results) {
console.log(`${hit.score.toFixed(3)} ${hit.name}: ${hit.snippet}`);
}
```
### CLI
```bash
# Hybrid search (full-text + vector) over whole pages
hindsight knowledge-base search "$BANK_ID" "how do we deploy" --limit 5
```
### Go
```go
// Hybrid search (full-text + vector) over whole pages
results, _, _ := client.KnowledgeBaseAPI.SearchKnowledgeBase(ctx, kpBankID).
Q("how do we deploy").Limit(5).Execute()
for _, hit := range results.Results {
fmt.Printf("%.3f %s: %s\n", hit.Score, hit.Name, hit.Snippet)
}
```
```json
{
"results": [
{
"id": "kp-2e85...",
"name": "Deploying the API",
"mental_model_id": "mm-77ab...",
"snippet": "The API is deployed via ...",
"score": 0.032,
"updated_at": "2026-08-03T09:12:44+00:00"
}
],
"total": 1
}
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `q` | string | — | Required. Search query (min length 1). |
| `limit` | int | `10` | Maximum results, 1–50. |
| `tags` | string[] | — | Only pages carrying these tags, matched per `tags_match`. See [Filter by Tags](#filter-by-tags). |
| `tags_match` | string | `any` | `any`, `all`, `any_strict`, `all_strict`, or `exact`. |
| `tag_groups` | string | — | JSON-encoded compound filter, same shape as recall's `tag_groups`. |
This searches whole pages. To search individual memories, use [recall](./recall.md).
### Filter by Tags
The tree and search take the same tag filter as [recall](./recall.md): `tags` with `tags_match`, or a compound `tag_groups` expression (JSON-encoded in the query string, since both are `GET` requests). It matches each page's own tags. Search ranks only among the pages that match, and the tree keeps only matching pages plus the folders above them. A folder with no matching page under it is left out, because its name can reveal as much as its pages.
As in recall, `any` and `all` also return **untagged** pages. Use `any_strict`, `all_strict`, or `exact` when only tagged pages should come back, for example to show a user only their own pages.
### Python
```python
# Narrow the tree and search to pages carrying a tag (recall's tags / tags_match / tag_groups)
tree = client.get_knowledge_base_tree(BANK_ID, tags=["type:runbook"], tags_match="any_strict")
ops_hits = client.search_knowledge_base(BANK_ID, q="how do we deploy", tags=["ops"], tags_match="all_strict")
print(f"{len(tree.roots)} runbook roots, {len(ops_hits.results)} ops hits")
# Compound filters use tag_groups: here, runbooks that are not drafts
not_drafts = client.search_knowledge_base(
BANK_ID,
q="how do we deploy",
tag_groups=[{"and": [{"tags": ["type:runbook"]}, {"not": {"tags": ["draft"]}}]}],
)
print(f"{len(not_drafts.results)} non-draft runbooks")
```
### Node.js
```javascript
// Narrow the tree and search to pages carrying a tag (recall's tags / tags_match / tag_groups)
const runbooks = await client.getKnowledgeBaseTree(BANK_ID, { tags: ['type:runbook'], tagsMatch: 'any_strict' });
const opsHits = await client.searchKnowledgeBase(BANK_ID, 'how do we deploy', { tags: ['ops'], tagsMatch: 'all_strict' });
console.log(`${runbooks.roots.length} runbook roots, ${opsHits.results.length} ops hits`);
// Compound filters use tagGroups: here, runbooks that are not drafts
const notDrafts = await client.searchKnowledgeBase(BANK_ID, 'how do we deploy', {
tagGroups: [{ and: [{ tags: ['type:runbook'] }, { not: { tags: ['draft'] } }] }],
});
console.log(`${notDrafts.results.length} non-draft runbooks`);
```
### CLI
```bash
# Narrow the tree and search to pages carrying a tag (recall's tags / tags_match / tag_groups)
hindsight knowledge-base tree "$BANK_ID" --tags type:runbook --tags-match any_strict
hindsight knowledge-base search "$BANK_ID" "how do we deploy" --tags ops --tags-match all_strict
# Compound filters use --tag-groups: here, runbooks that are not drafts
hindsight knowledge-base search "$BANK_ID" "how do we deploy" \
--tag-groups '[{"and":[{"tags":["type:runbook"]},{"not":{"tags":["draft"]}}]}]'
```
### Go
```go
// Narrow the tree and search to pages carrying a tag (recall's tags / tags_match / tag_groups)
runbooks, _, _ := client.KnowledgeBaseAPI.GetKnowledgeBaseTree(ctx, kpBankID).
Tags([]string{"type:runbook"}).TagsMatch("any_strict").Execute()
fmt.Printf("runbook roots: %d\n", len(runbooks.Roots))
// Compound filters use tag_groups, sent as one JSON-encoded query param
notDrafts, _, _ := client.KnowledgeBaseAPI.SearchKnowledgeBase(ctx, kpBankID).
Q("how do we deploy").
TagGroups(`[{"and":[{"tags":["type:runbook"]},{"not":{"tags":["draft"]}}]}]`).Execute()
fmt.Printf("non-draft hits: %d\n", len(notDrafts.Results))
```
---
## Update or Move a Node
One `PATCH` renames a node, moves it, and/or updates a page's options. Each field applies only when present in the body.
### Python
```python
# Rename a node, move it, and/or update a page's options.
# Changing source_query rebuilds the page against the new question.
client.update_knowledge_node(
BANK_ID,
page.page_id,
name="Deploying the API (v2)",
tags=["ops", "type:runbook", "reviewed"],
)
```
### Node.js
```javascript
// Rename a node, move it, and/or update a page's options.
// Changing sourceQuery rebuilds the page against the new question.
await client.updateKnowledgeNode(BANK_ID, page.page_id, {
name: 'Deploying the API (v2)',
tags: ['ops', 'type:runbook', 'reviewed'],
});
```
### CLI
```bash
# Rename a node, move it, and/or update a page's options.
# Changing --source-query rebuilds the page against the new question.
hindsight knowledge-base update "$BANK_ID" "$PAGE_ID" \
--name "Deploying the API (v2)" \
--tags ops,type:runbook,reviewed
```
### Go
```go
// Rename a node, move it, and/or update a page's options.
// Changing SourceQuery rebuilds the page against the new question.
client.KnowledgeBaseAPI.UpdateKnowledgeNode(ctx, kpBankID, page.PageId).
UpdateNodeRequest(hindsight.UpdateNodeRequest{
Name: *hindsight.NewNullableString(hindsight.PtrString("Deploying the API (v2)")),
Tags: []string{"ops", "type:runbook", "reviewed"},
}).Execute()
```
| Parameter | Type | Applies to | Description |
|---|---|---|---|
| `name` | string | Both | New name |
| `parent_id` | string \| null | Both | New parent folder. Pass `null` explicitly to move to the root. |
| `source_query` | string | Pages | New question. Changing it schedules an async refresh so the page rebuilds against the new question. |
| `tags` | list | Pages | Replaces the page's tags, and so [the scope it is built from](#tags-are-a-filter). Pass `[]` to clear them and rebuild the page from the whole bank. |
| `max_tokens` | int | Pages | New content budget |
| `trigger` | object | Pages | Refresh settings to change, applied as a patch. `{"tags_match": "all"}` keeps the page's tags but stops excluding untagged memories. |
Sending an empty body returns `400`; an unknown node returns `404`.
---
## Delete a Node
Deletes a folder or page **and its entire subtree**. The mental models backing the deleted pages are removed too.
### Python
```python
# Delete a folder or page — deleting a folder removes its whole subtree
client.delete_knowledge_node(BANK_ID, folder.id)
```
### Node.js
```javascript
// Delete a folder or page — deleting a folder removes its whole subtree
await client.deleteKnowledgeNode(BANK_ID, folder.id);
```
### CLI
```bash
# Delete a folder or page — deleting a folder removes its whole subtree
hindsight knowledge-base delete "$BANK_ID" "$FOLDER_ID" -y
```
### Go
```go
// Delete a folder or page — deleting a folder removes its whole subtree
client.KnowledgeBaseAPI.DeleteKnowledgeNode(ctx, kpBankID, folder.Id).Execute()
```
```json
{"status": "deleted"}
```
---
## Export as a Markdown Bundle
Returns the whole knowledge base as a flat set of portable markdown files: a nested `index.md`, one `<page-id>.md` per page, and a `<page-id>.log.md` refresh history for pages that have been rebuilt.
### Python
```python
# Export the knowledge base as a portable markdown bundle
bundle = client.export_knowledge_base(BANK_ID)
for file in bundle.files:
print(file.path) # index.md, <page-id>.md, <page-id>.log.md
```
### Node.js
```javascript
// Export the knowledge base as a portable markdown bundle
const bundle = await client.exportKnowledgeBase(BANK_ID);
for (const file of bundle.files) {
console.log(file.path); // index.md, <page-id>.md, <page-id>.log.md
}
```
### CLI
```bash
# Export the knowledge base as a portable markdown bundle
hindsight knowledge-base export "$BANK_ID"
```
### Go
```go
// Export the knowledge base as a portable markdown bundle
bundle, _, _ := client.KnowledgeBaseAPI.ExportKnowledgeBase(ctx, kpBankID).Execute()
for _, file := range bundle.Files {
fmt.Println(file.Path) // index.md, <page-id>.md, <page-id>.log.md
}
```
```json
{
"files": [
{"path": "index.md", "content": "---\ntype: \"index\"\n..."},
{"path": "kp-2e85....md", "content": "---\nid: \"kp-2e85...\"\n..."},
{"path": "kp-2e85....log.md", "content": "---\ntype: \"log\"\n..."}
]
}
```
> **💡 Mirror it to disk**
>
`hindsight fs mount --bank my-bank` keeps a local folder in sync with this bundle via a background refresh loop, so `ls`, `grep`, `rg`, and your editor work against real files.
---
## Storage
| Table | Holds |
|---|---|
| `knowledge_pages` | The tree: folders and pages, their names, parents, and ordering. A page row references its backing mental model; a folder row has none. |
| `mental_models` | The content: the document body, its source query, tags, token budget, trigger, and refresh history. |
The page layer owns only tree structure — everything about the content lives on the backing mental model, which is why every [mental model](./mental-models.md) capability applies to pages unchanged.
---
## Endpoint Summary
| Method | Path | Description |
|---|---|---|
| `GET` | `/knowledge-base/tree` | Nested folder/page tree with staleness |
| `POST` | `/knowledge-base/folders` | Create a folder |
| `POST` | `/knowledge-base/pages` | Create a page (async first build) |
| `GET` | `/knowledge-base/pages/{page_id}` | Read a page as markdown |
| `GET` | `/knowledge-base/search` | Hybrid search over pages |
| `PATCH` | `/knowledge-base/nodes/{node_id}` | Rename, move, or reconfigure a node |
| `DELETE` | `/knowledge-base/nodes/{node_id}` | Delete a node and its subtree |
| `GET` | `/knowledge-base/export` | Export the whole base as markdown files |
references/developer/api/main-methods.md
# Main Methods
Hindsight provides three core operations: **retain**, **recall**, and **reflect**.
{/* Import raw source files */}
> **💡 Prerequisites**
>
Make sure you've [installed Hindsight](../installation.md) and completed the [Quick Start](./quickstart.md).
## Retain: Store Information
Store conversations, documents, and facts into a memory bank.
### Python
```python
# Store a single fact
client.retain(
bank_id="my-bank",
content="Alice joined Google in March 2024 as a Senior ML Engineer"
)
# Store a conversation
conversation = """
User: What did you work on today?
Assistant: I reviewed the new ML pipeline architecture.
User: How did it look?
Assistant: Promising, but needs better error handling.
"""
client.retain(
bank_id="my-bank",
content=conversation,
context="Daily standup conversation"
)
# Batch retain multiple items
client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Bob prefers Python for data science"},
{"content": "Alice recommends using pytest for testing"},
{"content": "The team uses GitHub for code reviews"}
]
)
```
### Node.js
```javascript
// Store a single fact
await client.retain('my-bank', 'Alice joined Google in March 2024 as a Senior ML Engineer');
// Store a conversation
const conversation = `
User: What did you work on today?
Assistant: I reviewed the new ML pipeline architecture.
User: How did it look?
Assistant: Promising, but needs better error handling.
`;
await client.retain('my-bank', conversation, {
context: 'Daily standup conversation'
});
// Batch retain multiple items
await client.retainBatch('my-bank', [
{ content: 'Bob prefers Python for data science' },
{ content: 'Alice recommends using pytest for testing' },
{ content: 'The team uses GitHub for code reviews' }
]);
```
### CLI
```bash
# Store a single fact
hindsight memory retain "$BANK_ID" "Alice joined Google in March 2024 as a Senior ML Engineer"
# Store from a file
hindsight memory retain-files "$BANK_ID" conversation.txt --context "Daily standup"
# Store multiple files
hindsight memory retain-files "$BANK_ID" docs/
```
### Go
```go
// Store a fact or conversation into a memory bank
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Alice joined Google in March 2024 as a Senior ML Engineer")},
},
}).Execute()
```
**What happens:** Content is processed by an LLM to extract rich facts, identify entities, and build connections in a knowledge graph.
**See:** [Retain Details](./retain.md) for advanced options and parameters.
---
## Recall: Search Memories
Search for relevant memories using multi-strategy retrieval.
### Python
```python
# Basic search
results = client.recall(
bank_id="my-bank",
query="What does Alice do at Google?"
)
for result in results.results:
print(f"- {result.text}")
# Search with options
results = client.recall(
bank_id="my-bank",
query="What happened last spring?",
budget="high", # More thorough graph traversal
max_tokens=8192, # Return more context
types=["world"] # Only world facts
)
# Include source chunks for more context
results = client.recall(
bank_id="my-bank",
query="Tell me about Alice",
include_chunks=True,
max_chunk_tokens=500
)
# Check chunk details (chunks are on response level, keyed by memory ID)
for result in results.results:
print(f"Memory: {result.text}")
if results.chunks and result.id in results.chunks:
chunk = results.chunks[result.id]
print(f" Source: {chunk.text[:100]}...")
```
### Node.js
```javascript
// Basic search
const results = await client.recall('my-bank', 'What does Alice do at Google?');
for (const result of results.results) {
console.log(`- ${result.text}`);
}
// Search with options
const filteredResults = await client.recall('my-bank', 'What happened last spring?', {
budget: 'high',
maxTokens: 8192,
types: ['world']
});
// Include entity information
const entityResults = await client.recall('my-bank', 'Tell me about Alice', {
includeEntities: true,
maxEntityTokens: 500
});
// Check entity details
for (const [entityId, entity] of Object.entries(entityResults.entities || {})) {
console.log(`Entity: ${entity.canonical_name}`);
console.log(`Observations: ${entity.observations}`);
}
```
### CLI
```bash
# Basic search
hindsight memory recall "$BANK_ID" "What does Alice do at Google?"
# Search with options
hindsight memory recall "$BANK_ID" "What happened last spring?" \
--budget high \
--max-tokens 8192 \
--fact-type world,experience
# Verbose output
hindsight memory recall "$BANK_ID" "Tell me about Alice" -v
```
### Go
```go
// Search for memories using a natural language query
resp, _, _ := client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What does Alice do at Google?",
}).Execute()
for _, r := range resp.Results {
fmt.Println(r.Text)
}
```
**What happens:** Four search strategies (semantic, keyword, graph, temporal) run in parallel, results are fused and reranked.
**See:** [Recall Details](./recall.md) for tuning quality vs latency.
---
## Reflect: Reason with Disposition
Generate disposition-aware responses using memories and observations.
### Python
```python
# Basic reflect
response = client.reflect(
bank_id="my-bank",
query="Should we adopt TypeScript for our backend?",
include_facts=True,
)
print(response.text)
print("\nBased on:", len(response.based_on.memories if response.based_on else []), "facts")
# Reflect with options
response = client.reflect(
bank_id="my-bank",
query="What are Alice's strengths for the team lead role?",
budget="high", # More thorough reasoning
include_facts=True,
)
# See which facts influenced the response
for fact in (response.based_on.memories if response.based_on else []):
print(f"- {fact.text}")
```
### Node.js
```javascript
// Basic reflect
const response = await client.reflect('my-bank', 'Should we adopt TypeScript for our backend?');
console.log(response.text);
console.log('\nBased on:', (response.based_on || []).length, 'facts');
// Reflect with options
const detailedResponse = await client.reflect('my-bank', "What are Alice's strengths for the team lead role?", {
budget: 'high'
});
// See which facts influenced the response
for (const fact of detailedResponse.based_on || []) {
console.log(`- ${fact.text}`);
}
```
### CLI
```bash
# Basic reflect
hindsight memory reflect "$BANK_ID" "Should we adopt TypeScript for our backend?"
# With higher reasoning budget
hindsight memory reflect "$BANK_ID" "Analyze our tech stack" --budget high
```
### Go
```go
// Generate a reasoned response using memories and bank disposition
answer, _, _ := client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "Should we adopt TypeScript for our backend?",
}).Execute()
fmt.Println(answer.GetText())
```
**What happens:** Memories and observations are recalled, bank disposition is applied, and the LLM reasons through the evidence to generate a response.
**See:** [Reflect Details](./reflect.md) for disposition configuration.
---
## Comparison
| Feature | Retain | Recall | Reflect |
|---------|--------|--------|---------|
| **Purpose** | Store information | Find information | Reason about information |
| **Input** | Raw text/documents | Search query | Question/prompt |
| **Output** | Memory IDs | Ranked facts + observations | Reasoned response |
| **Uses LLM** | Yes (extraction) | No | Yes (generation) |
| **Uses observations** | No | Yes | Yes |
| **Disposition** | No | No | Yes |
---
## Next Steps
- [**Retain**](./retain.md) — Advanced options for storing memories
- [**Recall**](./recall.md) — Tuning search quality and performance
- [**Reflect**](./reflect.md) — Configuring disposition
- [**Memory Banks**](./memory-banks.md) — Managing memory bank disposition
references/developer/api/memories.md
# Memories
A **memory unit** is the atomic fact Hindsight extracts and stores. This page covers the endpoints for working with individual memory units — reading and listing them, inspecting how a derived observation evolved, and **curating** them (correcting, retiring, or restoring). Ingesting and querying memories is covered separately in [Retain](./retain.md) and [Recall](./recall.md).
{/* Import raw source files */}
## Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/v1/default/banks/{bank}/memories/list` | List/filter memory units in a bank |
| `GET` | `/v1/default/banks/{bank}/memories/{id}` | Fetch a single memory unit |
| `GET` | `/v1/default/banks/{bank}/memories/{id}/history` | Refresh history of a derived observation |
| `PATCH` | `/v1/default/banks/{bank}/memories/{id}` | Curate: edit / invalidate / restore |
| `DELETE` | `/v1/default/banks/{bank}/memories/{id}/observations` | Clear a memory's derived observations |
## List memory units
List the memory units in a bank. The response includes each unit's `fact_type` (`world` | `experience` | `observation`), `state` (`valid` | `invalidated`), metadata, entities, occurred dates, and — for facts a user has edited — an `edited_at` timestamp. Invalidated rows are **included by default** so curation stays auditable; filter with `state=`.
Narrow the results with query parameters: `type=` (fact type), `q=` (case-insensitive substring filter over text and context — not indexed, so it scans the bank; meant for browsing and inspection, use recall for production search), `document_id=` (a single source document), and `entity_id=` (memory units linked to a given entity). The `entity_id` filter is an exact reverse lookup over stored entity links — not a text or semantic match — so you can list an entity's evidence (e.g. its observations, with `type=observation`) without scanning every memory. Because entity links exist only for live units, combining `entity_id` with `state=invalidated` returns nothing.
### Time window
`start_date=` and `end_date=` restrict the results to a half-open range `[start, end)`, and `time_field=` chooses which timestamp they apply to:
| `time_field` | Means |
|---|---|
| `created_at` (default) | When the memory was ingested |
| `updated_at` | When it was last written |
| `mentioned_at` | When the fact was mentioned |
| `occurred_start` / `occurred_end` | When the fact itself occurred |
`time_field` also becomes the sort order (newest first), and **memories with no value on that column are excluded** — so `total` counts only the memories carrying that timestamp, and can be `0` on a bank that is not empty. Omit all three parameters to keep the default listing, which orders by `mentioned_at` then `created_at` and drops nothing.
This filters, unlike recall's `temporal_window`, which only ranks — use it when you need an exhaustive, time-bounded read rather than the best matches.
### Python
```python
# List memory units in a bank. Invalidated rows are included by default.
memories = await client.memory.list_memories(bank_id=BANK_ID)
for unit in memories.items:
print(f"- [{unit.fact_type}] {unit.text}")
# Filter to only the invalidated facts (e.g. to review duplicates).
invalidated = await client.memory.list_memories(bank_id=BANK_ID, state="invalidated")
print(f"{len(invalidated.items)} invalidated fact(s)")
```
### Node.js
```javascript
// List memory units in a bank. Invalidated rows are included by default.
const memories = await client.listMemories(BANK_ID);
for (const unit of memories.items) {
console.log(`- [${unit.fact_type}] ${unit.text}`);
}
// Filter to only the invalidated facts (e.g. to review duplicates).
const invalidated = await client.listMemories(BANK_ID, { state: 'invalidated' });
console.log(`${invalidated.items.length} invalidated fact(s)`);
```
### CLI
```bash
# List memory units in a bank (invalidated rows are included by default)
hindsight memory list "$BANK_ID"
# Filter to only the invalidated facts (e.g. to review duplicates)
curl -s "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/list?state=invalidated"
```
### Go
```go
// List memory units in a bank. Invalidated rows are included by default.
memories, _, _ := client.MemoryAPI.ListMemories(ctx, memBankID).Execute()
for _, unit := range memories.GetItems() {
fmt.Printf("- [%v] %v\n", unit.GetFactType(), unit.GetText())
}
// Filter to only the invalidated facts (e.g. to review duplicates).
invalidated, _, _ := client.MemoryAPI.ListMemories(ctx, memBankID).State("invalidated").Execute()
fmt.Printf("%d invalidated fact(s)\n", len(invalidated.GetItems()))
```
## Fetch a single memory unit
Fetch a memory unit by ID, including its content, metadata, entities, timestamps, tags, and curation state.
### Python
```python
# Fetch a single memory unit (includes metadata, entities, dates, and state).
memory = await client.memory.get_memory(bank_id=BANK_ID, memory_id=memory_id)
print(f"Text: {memory['text']}")
print(f"Type: {memory['type']} Entities: {memory['entities']}")
```
### Node.js
```javascript
// Fetch a single memory unit (metadata, entities, dates, state).
const memory = await (
await fetch(`${HINDSIGHT_URL}/v1/default/banks/${BANK_ID}/memories/${memoryId}`)
).json();
console.log(`Text: ${memory.text}`);
console.log(`Type: ${memory.type} Entities: ${memory.entities}`);
```
### CLI
```bash
# Fetch a single memory unit (metadata, entities, dates, state)
curl -s "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$MEMORY_ID"
```
### Go
```go
// Fetch a single memory unit (metadata, entities, dates, state).
memory, _, _ := client.MemoryAPI.GetMemory(ctx, memBankID, memoryID).Execute()
fmt.Printf("Memory: %v\n", memory)
```
For a **derived observation**, the history endpoint returns how it was refreshed over time as new source facts arrived:
### Python
```python
# Get the refresh history of a derived observation.
history = await client.memory.get_observation_history(
bank_id=BANK_ID, memory_id=observation.id
)
print(f"Observation history entries: {len(history)}")
```
### Node.js
```javascript
// Get the refresh history of a derived observation.
const history = await (
await fetch(`${HINDSIGHT_URL}/v1/default/banks/${BANK_ID}/memories/${observation.id}/history`)
).json();
console.log(`Observation history entries: ${history.length}`);
```
### CLI
```bash
# Get the refresh history of a derived observation
curl -s "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$OBSERVATION_ID/history"
```
### Go
```go
// Get the refresh history of a derived observation.
history, _, _ := client.MemoryAPI.GetObservationHistory(ctx, memBankID, observationID).Execute()
fmt.Printf("Observation history: %v\n", history)
```
## Curation: editing, invalidating & pruning
Memory is append-only by design — but sometimes a stored fact is **wrong**, has gone **stale**, or is a **duplicate**. Curation lets you correct or retire individual memories without losing the audit trail. Retired facts are moved out of the active set, so recall never returns them, while remaining fully recoverable.
### When to reach for what
Not every "bad memory" needs the same tool. Pick by *why* it's bad:
| The memory is… | Use | Why |
|---|---|---|
| **Wrong because the whole bank extracts badly** (e.g. consistently wrong subject) | Fix the bank's `retain_mission` / `observations_mission`, then **reprocess** the document | Systematic problems are best fixed at the source, then replayed — see [Retain](./retain.md) and [Observations](../observations.md). |
| **Wrong as a one-off** (a single misextracted fact) | **Edit** the memory | Corrects the fact and regenerates everything derived from it. |
| **No longer true, with nothing to replace it** (decommissioned server, a tool that was fixed, a role that changed) | **Invalidate** the memory | Nothing in the pipeline knows the world changed, so you tell it explicitly. |
| **A duplicate or superseded fact** | **Invalidate** the memory | Removes the noise from recall while keeping the audit trail. |
| **Superseded by a newer fact you're storing anyway** (e.g. "likes BMW" → "likes Toyota") | Just retain the new fact | Consolidation already reconciles in-stream contradictions into a single observation. |
The rule of thumb: **if Hindsight could have known, let consolidation handle it; if only you know, curate it.**
Only raw **world** and **experience** facts can be curated. Observations are *derived* — they regenerate from their sources, so you curate the underlying facts, not the observation. A `PATCH` on an observation returns `400`.
### Edit a memory
Correct what the LLM extracted. You can change the **text**, **context**, **occurred dates**, **mention time** (`mentioned_at`), **fact type**, and **entities** — anything the extractor could have gotten wrong. Hindsight re-embeds the fact, drops the observations and links derived from the old version, and re-consolidates, so downstream knowledge reflects the correction. Edited facts are marked with an `edited_at` timestamp (surfaced as an **Edited** badge in the control plane).
Correcting a fact never discards the **cause-and-effect relationships** it takes part in: those come from reading the original source, so an edit (or an invalidate/restore round-trip) keeps them intact rather than dropping links nothing could rebuild.
You don't need to rebuild anything yourself: an edit **automatically recomputes the knowledge graph and links** in the background. The fact's entity associations are re-resolved from the new text/entities, its temporal and semantic links are re-derived, and consolidation re-runs — all triggered by the edit. The PATCH returns as soon as the change is committed; the graph/observation rebuild happens asynchronously right after.
### Python
```python
# Correct the fact's text. Re-embeds, drops derived observations/links,
# re-consolidates, and recomputes the graph automatically.
await client.memory.update_memory(
bank_id=BANK_ID,
memory_id=memory_id,
update_memory_request=UpdateMemoryRequest(
text="The user visited Paris in 2023.",
reason="wrong subject",
),
)
```
### Node.js
```javascript
// Correct the fact's text. Re-embeds, drops derived observations/links,
// re-consolidates, and recomputes the graph automatically.
await patchMemory(memoryId, { text: 'The user visited Paris in 2023.', reason: 'wrong subject' });
```
### CLI
```bash
# Correct the fact's text. Re-embeds, drops derived observations/links,
# re-consolidates, and recomputes the graph automatically.
curl -s -X PATCH "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$MEMORY_ID" \
-H "Content-Type: application/json" \
-d '{"text": "The user visited Paris in 2023.", "reason": "wrong subject"}'
```
### Go
```go
// Correct the fact's text. Re-embeds, drops derived observations/links,
// re-consolidates, and recomputes the graph automatically.
client.MemoryAPI.UpdateMemory(ctx, memBankID, memoryID).
UpdateMemoryRequest(hindsight.UpdateMemoryRequest{
Text: *hindsight.NewNullableString(hindsight.PtrString("The user visited Paris in 2023.")),
Reason: *hindsight.NewNullableString(hindsight.PtrString("wrong subject")),
}).Execute()
```
You can correct the dates, fact type, and entities the same way. For `context`, `occurred_start`, `occurred_end`, and `mentioned_at`, an empty string `""` clears the field and omitting it leaves it unchanged. For `entities`, a list **replaces** the fact's entity set and `[]` detaches them all; omitting it leaves them unchanged.
### Resolving entity names
`resolve_entities` controls how the names in `entities` are matched to entities in the bank:
| Value | Behaviour |
| --- | --- |
| `true` (default) | What retain does. Each name is resolved against the bank, so a name close to one already there may resolve to that existing entity instead, based on name similarity plus how strongly it co-occurs with the other names you sent. |
| `false` | The names are taken literally. An existing entity is reused only when its name matches case-insensitively, any other name creates a new entity, and names in the same request are never merged with each other. |
**Pass `false` when you are correcting a fact by hand.** With resolution on, a name that is close to one already in the bank can be matched onto that neighbour rather than the entity you named — `Dr. Waller` onto a `Dr Wall` typo, `Alice Smith` onto `Alice` — and because the edit succeeds normally the substitution is not obvious from the response. Resolution is right for names that came out of extraction, where spelling varies and the bank's existing entity is usually the one meant; it is wrong when you already know which entity you want. The default stays `true` so existing callers are unaffected.
The same flag exists on [retain](./retain.md#resolve_entities) for the entities you supply there.
### Python
```python
# Correct dates, fact type, and entities in one call. "" clears a field;
# entities replaces the set ([] detaches all); omit to leave unchanged.
# resolve_entities=False keeps the entity names you wrote from being matched
# onto a similar entity that already exists.
await client.memory.update_memory(
bank_id=BANK_ID,
memory_id=memory_id,
update_memory_request=UpdateMemoryRequest(
occurred_start="2023-06-01",
fact_type="experience",
entities=["Alice", "Paris"],
resolve_entities=False,
),
)
```
### Node.js
```javascript
// Correct dates, fact type, and entities in one call. "" clears a field;
// entities replaces the set ([] detaches all); omit to leave unchanged.
// resolve_entities: false keeps the entity names you wrote from being matched
// onto a similar entity that already exists.
await patchMemory(memoryId, {
occurred_start: '2023-06-01',
fact_type: 'experience',
entities: ['Alice', 'Paris'],
resolve_entities: false,
});
```
### CLI
```bash
# Correct dates, fact type, and entities in one call. "" clears a field;
# entities replaces the set ([] detaches all); omit to leave unchanged.
# resolve_entities false keeps the entity names you wrote from being matched
# onto a similar entity that already exists.
curl -s -X PATCH "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$MEMORY_ID" \
-H "Content-Type: application/json" \
-d '{"occurred_start": "2023-06-01", "fact_type": "experience", "entities": ["Alice", "Paris"], "resolve_entities": false}'
```
### Go
```go
// Correct dates, fact type, and entities in one call. "" clears a field;
// entities replaces the set ([] detaches all); omit to leave unchanged.
// ResolveEntities false keeps the entity names you wrote from being matched
// onto a similar entity that already exists.
client.MemoryAPI.UpdateMemory(ctx, memBankID, memoryID).
UpdateMemoryRequest(hindsight.UpdateMemoryRequest{
OccurredStart: *hindsight.NewNullableString(hindsight.PtrString("2023-06-01")),
FactType: *hindsight.NewNullableString(hindsight.PtrString("experience")),
Entities: []string{"Alice", "Paris"},
ResolveEntities: hindsight.PtrBool(false),
}).Execute()
```
### Invalidate a memory (reversible)
Soft-retire a fact. An invalidated memory:
- **disappears from recall**, consolidation, and the knowledge graph,
- has its **links pruned** and its **derived observations re-computed** without it (cause-and-effect relationships are kept aside and come back if you restore it),
- **stays in the bank** for audit (visible via the memory and document views), and
- can be **restored** at any time.
### Python
```python
# Soft-retire a fact: removed from recall/consolidation/graph, links pruned,
# derived observations recomputed without it — but kept for audit.
await client.memory.update_memory(
bank_id=BANK_ID,
memory_id=memory_id,
update_memory_request=UpdateMemoryRequest(
state="invalidated",
reason="server decommissioned 2026-06-01",
),
)
```
### Node.js
```javascript
// Soft-retire a fact: removed from recall/consolidation/graph, links pruned,
// derived observations recomputed without it — but kept for audit.
await patchMemory(memoryId, { state: 'invalidated', reason: 'server decommissioned 2026-06-01' });
```
### CLI
```bash
# Soft-retire a fact: removed from recall/consolidation/graph, links pruned,
# derived observations recomputed without it — but kept for audit.
curl -s -X PATCH "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$MEMORY_ID" \
-H "Content-Type: application/json" \
-d '{"state": "invalidated", "reason": "server decommissioned 2026-06-01"}'
```
### Go
```go
// Soft-retire a fact: removed from recall/consolidation/graph, links pruned,
// derived observations recomputed without it — but kept for audit.
client.MemoryAPI.UpdateMemory(ctx, memBankID, memoryID).
UpdateMemoryRequest(hindsight.UpdateMemoryRequest{
State: *hindsight.NewNullableString(hindsight.PtrString("invalidated")),
Reason: *hindsight.NewNullableString(hindsight.PtrString("server decommissioned 2026-06-01")),
}).Execute()
```
Restoring moves the fact back into the active set, brings back the cause-and-effect relationships it took part in, and re-consolidates:
### Python
```python
# Restore a previously invalidated fact.
await client.memory.update_memory(
bank_id=BANK_ID,
memory_id=memory_id,
update_memory_request=UpdateMemoryRequest(state="valid"),
)
```
### Node.js
```javascript
// Restore a previously invalidated fact.
await patchMemory(memoryId, { state: 'valid' });
```
### CLI
```bash
# Restore a previously invalidated fact.
curl -s -X PATCH "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/memories/$MEMORY_ID" \
-H "Content-Type: application/json" \
-d '{"state": "valid"}'
```
### Go
```go
// Restore a previously invalidated fact.
client.MemoryAPI.UpdateMemory(ctx, memBankID, memoryID).
UpdateMemoryRequest(hindsight.UpdateMemoryRequest{
State: *hindsight.NewNullableString(hindsight.PtrString("valid")),
}).Execute()
```
Behind the scenes, invalidating **moves** the row out of the active `memory_units` table into a separate archive, so recall and consolidation never need a "skip invalidated" filter — the rows simply aren't there.
> **📝 Documents are the source of truth**
>
A memory is extracted from a document. Editing or invalidating a memory does **not** change the document it came from — that's deliberate: the document stays as an accurate historical record. As a result, **reprocessing a document resets curation** of the facts it produced (extraction runs fresh from the original text). Fix systematic issues at the mission level and reprocess; use edit/invalidate for the residue.
### A pruning workflow
To clean up duplicates and reclaim noise: cluster duplicates from `memories/list`, then **invalidate** them — recall is clean immediately, and the audit trail is preserved.
references/developer/api/memory-banks.md
# Memory Banks
Memory banks are isolated containers that store all memory-related data for a specific context or use case.
{/* Import raw source files */}
## What is a Memory Bank?
A memory bank is a complete, isolated storage unit containing:
- **Memories** — Facts and information retained from conversations
- **Documents** — Files and content indexed for retrieval
- **Entities** — People, places, concepts extracted from memories
- **Relationships** — Connections between entities in the knowledge graph
- **Directives** — Hard rules the agent must follow during reflect operations
Banks are completely isolated from each other — memories stored in one bank are not visible to another.
You don't need to pre-create a bank. Hindsight will automatically create it with default settings when you first use it.
Only *writes* create a bank — retaining, updating its profile, changing its config. **Reads of a
bank that does not exist return `404`**, so a typo'd, renamed or deleted `bank_id` is reported
rather than answered with empty results. That matters if you are monitoring a bank: `GET .../stats`
on a missing bank fails loudly instead of returning zeroed counters that look like a healthy,
empty bank.
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) to install the client and start the server.
## Creating a Memory Bank
### Python
```python
client.create_bank(bank_id="my-bank")
```
### Node.js
```javascript
await client.createBank('my-bank');
```
### CLI
```bash
hindsight bank create my-bank
```
### Go
```go
client.BanksAPI.CreateOrUpdateBank(ctx, "my-bank").
CreateBankRequest(hindsight.CreateBankRequest{}).Execute()
```
## Aliases {#aliases}
An **alias** is an extra id a bank answers to. Every endpoint that accepts a bank id accepts its aliases too, so an alias is a second way in to the same bank — nothing is copied, moved or duplicated.
This exists because a bank's id is the key on all of its data, so changing it means rewriting every row and cutting all clients over at once (that is [`rename-bank`](../admin-cli.md#rename-bank), and it needs downtime). An alias lets you do the same migration in phases: add the new id, move clients across a few at a time while both ids work, and stop when nothing calls the old one.
```
POST /v1/default/banks/{bank_id}/aliases {"alias": "new-id"}
GET /v1/default/banks/{bank_id}/aliases
PATCH /v1/default/banks/{bank_id}/aliases/{alias} {"primary": true}
DELETE /v1/default/banks/{bank_id}/aliases/{alias}
```
### Showing a bank under an alias
Once callers have moved, the control plane can present the bank under the new id instead of the one it was created with. `PATCH` with `primary: true` promotes an alias; `primary: false` returns the bank to showing its own id without removing the alias.
This is **display only**. `bank_id` remains the bank's identity: authorisation, metering, exports and audit logs all continue to use it, and it is what every response reports. The control plane shows the real id alongside the alias so the two are never confused.
- **Optional, with no default.** A bank's own id is not an alias, so "not shown under an alias" is the normal state.
- **At most one.** Promoting an alias demotes whichever one was shown before, in a single transaction.
- **It cannot outlive the alias.** Deleting the promoted alias returns the bank to showing its own id.
**What to know:**
- **A bank can have several aliases**, so more than one old id can be retired at a time.
- **A name is unique across banks and aliases.** Adding one that already names a bank or another alias returns `409`, so an alias can never reach two banks.
- **An alias never replaces the bank's own id.** The real id keeps working and is what every response reports, including responses to requests made through an alias. Removing a bank's real id is not possible — that is what `rename-bank` is for.
- **Deleting an alias only closes that door.** The bank and its memories are untouched, and callers still using the removed id get exactly what they got before it existed.
- **Deleting a bank removes its aliases**, freeing those names for reuse.
- **Aliases are not carried by export, import or clone.** They describe which ids reach a bank on *this* deployment, so a copy does not inherit them — add them to the target deliberately.
- **A change takes a few seconds to reach every API replica.** See [`HINDSIGHT_API_BANK_ALIAS_CACHE_TTL_SECONDS`](../configuration.md#bank-alias-cache).
## Bank Configuration
Each memory bank can be configured independently per operation. Configuration can be set via the [bank config API](#updating-configuration), the Control Plane UI, or [server-wide environment variables](../configuration.md).
### retain_mission {#retain-configuration}
A plain-language description of what this bank should pay attention to during extraction. The mission is injected into the extraction prompt alongside the built-in rules — it steers focus without replacing the extraction logic.
```
e.g. Always include technical decisions, API design choices, and architectural trade-offs.
Ignore meeting logistics, greetings, and social exchanges.
```
Works alongside any extraction mode. Leave blank for general-purpose extraction.
### retain_extraction_mode
Controls how aggressively facts are extracted:
| Mode | Description |
|------|-------------|
| `concise` *(default)* | Selective — only facts worth remembering long-term |
| `verbose` | Captures more detail per fact; slower and uses more tokens |
| `custom` | Write your own extraction rules via `retain_custom_instructions` |
| `verbatim` | Stores each chunk's original text as one memory; the LLM extracts metadata such as entities and dates |
| `chunks` | Stores each chunk as one memory without an LLM call; only caller-provided entities are available |
### retain_custom_instructions
Only active when `retain_extraction_mode` is `custom`. Replaces the built-in extraction rules entirely with your own instructions.
### retain_chunk_size
Maximum number of characters per chunk when splitting content for fact extraction. Larger chunks mean fewer LLM calls but may reduce extraction quality on long inputs; smaller chunks improve granularity at the cost of more calls.
Default: `3000`
### retain_structured_chunk_size
Maximum number of characters for a single JSONL line or conversation turn to keep whole when it exceeds `retain_chunk_size`. When unset, the limit is exactly `retain_chunk_size`; set a larger value for structured logs or chat transcripts where splitting a single record would lose useful context.
Default: unset, which uses `retain_chunk_size`
See [Retain configuration](../configuration.md#retain) for environment variable names and defaults.
### entity_labels {#entity-labels}
Defines a controlled vocabulary of `key:value` classification labels extracted at retain time and stored as entities. Because labels become entities, they automatically link memories in the knowledge graph (two memories with `pedagogy:scaffolding` are linked), improve semantic and BM25 retrieval, and optionally filter memories via the standard `tags`/`tags_match` API when `tag: true` is set on a group.
Each entry in `entity_labels` is a **label group** — one classification dimension:
```json
{
"entity_labels": [
{
"key": "engagement",
"description": "Student engagement level during the session",
"type": "value",
"optional": true,
"values": [
{ "value": "active", "description": "Student is actively participating" },
{ "value": "passive", "description": "Student is listening but not participating" }
]
},
{
"key": "pedagogy",
"description": "Teaching strategies used",
"type": "multi-values",
"values": [
{ "value": "scaffolding", "description": "Breaking complex tasks into smaller steps" },
{ "value": "direct_instruction", "description": "Explicit explanation by the teacher" },
{ "value": "socratic_questioning", "description": "Guiding through questions rather than answers" }
]
}
]
}
```
| Field | Default | Description |
|-------|---------|-------------|
| `key` | — | Label group identifier. Becomes the prefix in `key:value` entities (or `key:field:value` for `"map"`). |
| `description` | `""` | Shown to the LLM to guide label assignment. |
| `type` | `"value"` | `"value"` → pick one enum value; `"multi-values"` → pick multiple; `"text"` → free-form string; `"multi-text"` → any number of free-form strings; `"map"` → structured group with named fields. |
| `values` | `[]` | Allowed values for `"value"` and `"multi-values"` types. Ignored for `"text"`, `"multi-text"` and `"map"`. |
| `fields` | `{}` | Field definitions for `"map"` types. Each field is itself typed (`"text"`, `"multi-text"`, `"value"`, `"multi-values"`, or nested `"map"`). Ignored for non-map types. |
| `optional` | `true` | When `true` the LLM may skip the label if not applicable. When `false` the LLM must always assign a value. Has no effect on `"multi-values"` or `"multi-text"` groups (always optional). |
| `tag` | `false` | When `true`, extracted `key:value` labels are also written as tags on the memory unit, enabling filtering via `tags`/`tags_match` in recall/reflect. |
**Enum groups** (`type: "value"` or `type: "multi-values"`): the LLM picks from the predefined `values` list; anything outside the list is silently dropped. Vocabulary stays stable and graph links stay tight. Use `"multi-values"` when a fact can belong to several values at once.
**Free-text groups** (`type: "text"`): the LLM writes any string. Use the `description` field to provide examples and guidance. Graph clustering is less reliable than with enum groups because the model may phrase the same concept differently across sessions.
```json
{
"key": "topic",
"description": "Specific subject being discussed. Examples: algebra, quadratic equations, geometry.",
"type": "text",
"optional": true,
"values": []
}
```
**Open-vocabulary groups** (`type: "multi-text"`): like `"text"`, but the LLM writes a *list* of strings instead of one — so a single fact can carry several values that nobody enumerated in the bank config. Use it when the interesting values only exist in the content: the names a thing is known by (canonical name plus abbreviations, acronyms and alternative spellings), ticket references a fact cites, product codes or SKUs. Each value becomes its own `key:value` entity, and with `tag: true` each is written as a tag, so a bank can derive a classification from content and then filter on it at recall without the caller supplying the vocabulary.
```json
{
"key": "name",
"description": "Every name the subject of this fact is known by — canonical name plus abbreviations, acronyms, short forms and alternative spellings.",
"type": "multi-text",
"tag": true
}
```
Retaining *"We deploy services to a Kubernetes cluster on EKS — the team usually just says k8s, or kube"* yields the entities and tags `name:kubernetes`, `name:k8s` and `name:kube`, so `{"tags": ["name:k8s"], "tags_match": "any_strict"}` finds the fact. A `"text"` group would keep only one of the three.
**Map groups** (`type: "map"`): defines a structured entity type with named fields. Each field is itself typed (`"text"`, `"multi-text"`, `"value"`, `"multi-values"`, or nested `"map"`) so you can describe rich entities like a person with name, role, and organization. Each extracted field is stored as a flat `key:field:value` entity string (e.g. `person:name:Alice`), reusing the existing entity storage with no schema changes — so map fields participate in the knowledge graph and retrieval the same way single-value labels do.
```json
{
"key": "person",
"description": "A person mentioned in the text",
"type": "map",
"fields": {
"name": { "type": "text", "description": "Full name of the person" },
"role": { "type": "text", "description": "Job title or role" },
"organization": { "type": "text", "description": "Company or organization" }
}
}
```
**How label entities resolve.** Regular entities resolve fuzzily so close name variants merge ("Alice" / "Alice Chen"). Label entities are different: their canonical names are user-defined, so two similar-looking values (`use:use-001` / `use:use-002`) must stay distinct. They therefore resolve by **exact match only** and are stored with `entity_kind = "label"`, which keeps them out of fuzzy name matching entirely — a free-text label group accumulating thousands of similar values doesn't slow down resolution of the bank's regular entities. The classification is fixed when the entity is first stored; removing a label group later doesn't reclassify its existing entities.
### entities_allow_free_form
By default, entity labels are extracted **alongside** regular named entities (people, places, concepts). Set to `false` to disable free-form extraction so only label entities are stored:
```json
{
"entity_labels": [...],
"entities_allow_free_form": false
}
```
### enable_observations {#observations-configuration}
Toggles observation consolidation on or off. When `false`, no consolidation runs for this bank — neither automatic nor manual. Defaults to `true` when the observations feature is enabled on the server.
### enable_auto_consolidation
Controls whether consolidation runs automatically after retain, delete, and update operations. When `false`, consolidation only runs when explicitly triggered via the [consolidate endpoint](../observations.md#trigger-consolidation). Defaults to `true`.
This is useful when you want full control over consolidation timing — for example, batching many retains before consolidating, or running [targeted consolidation](../observations.md#targeted-consolidation) for specific scopes only.
### default_observation_scopes {#default_observation_scopes}
The [`observation_scopes`](./retain.md#observation_scopes) applied to retained items that send none. Defaults to `combined`, which groups each memory by its full tag set. Set it to `[["topic:*"]]` to group memories by their own `topic:` tags only — e.g. ones added by an [entity label](#entity-labels) with `tag: true` — so memories about the same subject from different agents or sessions consolidate together without changing any client. Stored on each memory at retain, so changing it doesn't re-scope memories already retained — until their document is reprocessed, which applies the current default.
### observations_mission
Defines what this bank should synthesise into durable observations. Replaces the built-in consolidation rules entirely — leave blank to use the server default.
```
e.g. Observations are stable facts about people and projects.
Always include preferences, skills, and recurring patterns.
Ignore one-off events and ephemeral state.
```
### consolidation_llm_batch_size
Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Leave unset to use the server default (`8`).
### consolidation_source_facts_max_tokens
Total token budget for source facts included with observations in the consolidation prompt. Source facts give the LLM evidence to compare new facts against existing observations. `-1` = unlimited. Leave unset to use the server default (`-1`).
### consolidation_source_facts_max_tokens_per_observation
Per-observation token cap for source facts in the consolidation prompt. Each observation independently gets at most this many tokens of source facts, preventing a single observation with many source facts from consuming the entire budget. `-1` = unlimited. Leave unset to use the server default (`256`).
See [Observations configuration](../configuration.md#observations) for environment variable names and defaults.
### reflect_mission
A first-person narrative that provides identity and framing context for `reflect`. The agent uses this to ground its reasoning and apply a consistent perspective.
```
e.g. You are a senior engineering assistant.
Always ground answers in documented decisions and rationale.
Ignore speculation. Be direct and precise.
```
### disposition_skepticism
How skeptical vs trusting the bank is when evaluating claims during `reflect`. Scale 1–5.
### Python
```python
client.create_bank(bank_id="architect-bank")
client.update_bank_config(
"architect-bank",
reflect_mission="You're a senior software architect - keep track of system designs, "
"technology decisions, and architectural patterns. Prefer simplicity over cutting-edge.",
disposition_skepticism=4, # Questions new technologies
disposition_literalism=4, # Focuses on concrete specs
disposition_empathy=2, # Prioritizes technical facts
)
```
### Node.js
```javascript
await client.createBank('architect-bank');
await client.updateBankConfig('architect-bank', {
reflectMission: "You're a senior software architect - keep track of system designs, technology decisions, and architectural patterns.",
dispositionSkepticism: 4, // Questions new technologies
dispositionLiteralism: 4, // Focuses on concrete specs
dispositionEmpathy: 2, // Prioritizes technical facts
});
```
### CLI
```bash
hindsight bank create architect-bank \
--mission "You're a senior software architect - keep track of system designs, technology decisions, and architectural patterns. Prefer simplicity over cutting-edge." \
--skepticism 4 \
--literalism 4 \
--empathy 2
```
### Go
```go
client.BanksAPI.CreateOrUpdateBank(ctx, "architect-bank").
CreateBankRequest(hindsight.CreateBankRequest{
ReflectMission: *hindsight.NewNullableString(hindsight.PtrString(
"You're a senior software architect - keep track of system designs, " +
"technology decisions, and architectural patterns. Prefer simplicity over cutting-edge.",
)),
DispositionSkepticism: *hindsight.NewNullableInt32(hindsight.PtrInt32(4)),
DispositionLiteralism: *hindsight.NewNullableInt32(hindsight.PtrInt32(4)),
DispositionEmpathy: *hindsight.NewNullableInt32(hindsight.PtrInt32(2)),
}).Execute()
```
| Value | Behaviour |
|-------|-----------|
| `1` | Trusting — accepts information at face value |
| `3` *(default)* | Balanced |
| `5` | Skeptical — questions and doubts claims |
### disposition_literalism
How literally to interpret information during `reflect`. Scale 1–5.
| Value | Behaviour |
|-------|-----------|
| `1` | Flexible — reads between the lines, considers context |
| `3` *(default)* | Balanced |
| `5` | Literal — takes things exactly as stated |
### disposition_empathy
How much to weight emotional context when reasoning during `reflect`. Scale 1–5.
| Value | Behaviour |
|-------|-----------|
| `1` | Detached — focuses on facts and logic |
| `3` *(default)* | Balanced |
| `5` | Empathetic — considers emotional context |
> **ℹ️ Info**
>
Disposition traits and `reflect_mission` only affect the `reflect` operation. `retain_mission` and `observations_mission` are separate per-operation settings.
### mcp_enabled_tools
An allowlist of MCP tool names that are enabled for this bank. When set, only the listed tools can be invoked; any tool not in the list returns an error (tools still appear in the MCP tools list for protocol compatibility). Set to `null` (or omit) to allow all tools.
```json
["recall", "reflect"]
```
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`, `get_knowledge_base_tree`, `search_knowledge_base`, `get_knowledge_page`, `create_knowledge_folder`, `create_knowledge_page`, `update_knowledge_node`, `delete_knowledge_node`.
### llm_gemini_safety_settings
Controls content filtering thresholds for Gemini and VertexAI providers. Accepts a list of safety setting objects in the [Google AI safety settings format](https://ai.google.dev/api/generate-content#v1beta.SafetySetting). When `null` (default), Gemini's built-in safety defaults are used.
```json
[
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}
]
```
Only applies when `HINDSIGHT_API_LLM_PROVIDER` is `gemini` or `vertexai`.
### recall_budget_function {#recall-budget-configuration}
Selects how the [`recall` request's `budget` parameter](./recall.md) (`low` / `mid` / `high`) maps to the internal `thinking_budget` integer used by every retrieval method (semantic, BM25, graph, temporal). Two functions are supported:
| Function | Behaviour |
|----------|-----------|
| `fixed` *(default)* | `thinking_budget = recall_budget_fixed_<level>` — independent of `max_tokens`. Preserves legacy behavior. |
| `adaptive` | `thinking_budget = round(max_tokens * recall_budget_adaptive_<level>)`, clamped to `[recall_budget_min, recall_budget_max]`. Retrieval breadth scales with the requested output size. |
```json
{
"recall_budget_function": "adaptive",
"recall_budget_adaptive_low": 0.05,
"recall_budget_adaptive_mid": 0.1,
"recall_budget_adaptive_high": 0.3,
"recall_budget_min": 30,
"recall_budget_max": 1500
}
```
### recall_budget_fixed_low / recall_budget_fixed_mid / recall_budget_fixed_high
When `recall_budget_function` is `fixed` (the default), these positive integers are used directly as the per-method retrieval limit for each `budget` level. Defaults: `100` / `300` / `1000` — exactly matching the legacy hardcoded mapping.
### recall_budget_adaptive_low / recall_budget_adaptive_mid / recall_budget_adaptive_high
When `recall_budget_function` is `adaptive`, these positive ratios multiply the request's `max_tokens` to derive the per-method retrieval limit. Defaults: `0.025` / `0.075` / `0.25` — chosen to roughly match the fixed defaults at `max_tokens = 4096`.
### recall_budget_min / recall_budget_max
Floor and ceiling applied to the result of the adaptive function (after the ratio multiplication). Both must be positive integers and `min ≤ max`. Defaults: `20` / `2000`.
See [Recall budget mapping](../configuration.md#recall-budget-mapping) for environment variable names and full defaults.
### memory_defense {#memory_defense}
Per-bank Memory Defense policy. Defaults to absent (Memory Defense disabled on this bank).
| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | bool | `false` | Master switch. |
| `default_action` | `allow`\|`redact`\|`quarantine`\|`block` | `allow` | Fallback action when no rule matches. |
| `protected_tag_namespaces` | `list[str]` | `[]` | Writes with tags in these namespaces (`ns:*`) are subject to the `protected_key` detector. |
| `immutable_tag_namespaces` | `list[str]` | `[]` | Writes to these namespaces are blocked. |
| `rules` | `list[Rule]` | `[]` | Detector-to-action mappings (see below). |
| `detector_overrides` | `dict` | `{}` | Per-detector tuning (e.g. `size_anomaly.max_size`). |
`Rule` shape:
| Field | Required | Description |
|---|---|---|
| `on` | yes | Detector name (`prompt_injection`, `sensitive_data`, `protected_key`, `immutable_key`, `size_anomaly`) or `*` for any. |
| `action` | yes | One of `allow`, `redact`, `quarantine`, `block`. |
| `min_severity` | no | Minimum severity (`low`, `medium`, `high`, `critical`) for the rule to fire. Defaults to `low`. |
Invalid policies are rejected on PATCH with HTTP 422.
See the [Memory Defense guide](../memory-defense/index.md) for usage examples.
---
## Previewing Prompts
A mission only means something once you can see the prompt it lands in. `POST /v1/default/banks/{bank_id}/prompts/preview` renders the exact messages `retain`, `consolidation` or `reflect` would send for a bank — no LLM call, no reads, nothing stored.
The operation is the whole request. Everything that shapes the prompt is read from the bank — its resolved config, profile and directives — and the runtime data an operation would be given is a fixed bracketed placeholder:
### Python
```python
from hindsight_client_api.models import PromptPreviewRequest
preview = await client.banks.preview_prompt("my-bank", PromptPreviewRequest(operation="retain"))
for message in preview.messages:
print(message.role, len(message.blocks))
```
### Node.js
```javascript
const { data: preview } = await sdk.previewPrompt({
client: apiClient,
path: { bank_id: 'my-bank' },
body: { operation: 'retain' },
});
for (const message of preview.messages) console.log(message.role, message.blocks.length);
```
### CLI
```bash
curl --fail-with-body -X POST "$HINDSIGHT_URL/v1/default/banks/my-bank/prompts/preview" \
-H "Content-Type: application/json" \
-d '{"operation": "retain"}'
```
### Go
```go
preview, _, err := client.BanksAPI.PreviewPrompt(ctx, "my-bank").
PromptPreviewRequest(hindsight.PromptPreviewRequest{Operation: hindsight.PtrString("retain")}).
Execute()
for _, message := range preview.Messages {
fmt.Println(message.Role, len(message.Blocks))
}
```
For `retain`, add `"strategy": "<name>"` to render under one of the bank's named retain strategies. Omit it and the bank's `retain_default_strategy` applies — exactly as it does for a retain that names none — so what you see is what retain would send. The response echoes the strategy that applied in `strategy`, and lists the bank's strategy names in `strategies` so a picker needs no second call.
There is deliberately nothing to override. A preview answers "what does this bank send"; letting a caller pass its own mission or sample text only moves that question somewhere the bank cannot answer it. To try a candidate value, save it and look again — the response says which settings are editable.
The response carries the messages in send order, each broken into the blocks it is built from:
```json
{
"messages": [
{
"role": "system",
"blocks": [
{
"text": "Extract SIGNIFICANT facts from text...",
"source": "builtin",
"field": "retain_extraction_mode",
"section": "",
"heading": "Selectivity",
"active": true,
"value": "concise",
"kind": "choice",
"choices": ["concise", "verbose", "verbatim", "chunks", "custom"],
"editable": true
},
{
"text": "",
"source": "config",
"field": "retain_custom_instructions",
"section": "",
"heading": "",
"active": false,
"kind": "text",
"editable": true
}
]
},
{ "role": "user", "blocks": [] }
],
"response_schema": { "type": "object", "properties": { "facts": {} } }
}
```
- **`messages`** — the request, system first, exactly as the model receives it.
- **`blocks`** — the **active** blocks of a message concatenate back to its text exactly: nothing dropped, duplicated or reordered. `source` says what produced each: `config` for a setting you can change, `builtin` for Hindsight's own wording. The runtime data an operation is given is not a block — it is a hole in the text, marked inline with `«…»`.
- **Inactive blocks** (`active: false`) have no text. They mark a setting that is switched off, at the point where it *would* land — so a mission you have not written yet is still visible where it would go.
- **Identifying a block** — whichever of these applies, in order: `field` is the config field behind it; `section` is a slug for a part no single field owns (`bank_identity`, `disposition`, `directives`); `heading` is the section heading the prompt text itself carries there. All three are machine values. The response carries **no display copy** — what a block is called, and what turning a switched-off one on would do, is for the client to say in the language it is running in.
- **`editable`** — false for server-level fields such as `llm_output_language`, which shape the prompt but cannot be overridden per bank.
**Both messages are returned on purpose.** For `retain` and `consolidation` the mission travels in the *user* message, not the system prompt: keeping the system prompt identical for every bank lets one provider-side prompt cache serve them all, which is a large cost saving on high-volume ingestion. Only `reflect` puts its mission in the system prompt.
### When there is no prompt
`chunks` extraction mode stores each chunk verbatim and never calls an LLM. There is no prompt to show, so `messages` is empty and `skipped_reason` explains why:
```json
{
"operation": "retain",
"messages": [],
"skipped_reason": "Chunks mode stores each chunk verbatim as its own memory and never calls an LLM, ..."
}
```
In the Control Plane, each Mission field on the bank **Configuration** tab has a **Preview prompt** button, and each block's setting can be edited there and saved to the bank.
> **💡 Tip**
>
[Dry-run extraction](memories.md) is the paid counterpart: it spends a real LLM call to show what the same configuration actually *extracts*. It resolves its config the same way, strategy included, so the two agree. The Control Plane pairs them in one prompt tester.
## Updating Configuration
Bank configuration fields (retain mission, extraction mode, observations mission, etc.) are managed via a **separate config API**, not the `create_bank` call. This lets you change operational settings independently from the bank's identity and disposition.
### Setting Configuration Overrides
### Python
```python
client.update_bank_config(
"my-bank",
retain_mission="Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics and social exchanges.",
retain_extraction_mode="verbose",
observations_mission="Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events.",
disposition_skepticism=4,
disposition_literalism=4,
disposition_empathy=2,
)
```
### Node.js
```javascript
await client.updateBankConfig('my-bank', {
retainMission: 'Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics and social exchanges.',
retainExtractionMode: 'verbose',
observationsMission: 'Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events.',
dispositionSkepticism: 4,
dispositionLiteralism: 4,
dispositionEmpathy: 2,
});
```
### CLI
```bash
hindsight bank set-config my-bank \
--retain-mission "Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics and social exchanges." \
--retain-extraction-mode verbose \
--observations-mission "Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events." \
--disposition-skepticism 4 \
--disposition-literalism 4 \
--disposition-empathy 2
```
### Go
```go
client.BanksAPI.UpdateBankConfig(ctx, "my-bank").
BankConfigUpdate(hindsight.BankConfigUpdate{
Updates: map[string]interface{}{
"retain_mission": "Always include technical decisions, API design choices, and architectural trade-offs. " +
"Ignore meeting logistics and social exchanges.",
"retain_extraction_mode": "verbose",
"observations_mission": "Observations are stable facts about people and projects. " +
"Always include preferences, skills, and recurring patterns. Ignore one-off events.",
"disposition_skepticism": 4,
"disposition_literalism": 4,
"disposition_empathy": 2,
},
}).Execute()
```
You can update any subset of fields — only the keys you provide are changed.
### Reading the Current Configuration
### Python
```python
# Returns resolved config (server defaults merged with bank overrides) and the raw overrides
data = client.get_bank_config("my-bank")
# data["config"] — full resolved configuration
# data["overrides"] — only fields overridden at the bank level
```
### Node.js
```javascript
// Returns resolved config (server defaults merged with bank overrides) and the raw overrides
const { config, overrides } = await client.getBankConfig('my-bank');
// config — full resolved configuration
// overrides — only fields overridden at the bank level
```
### CLI
```bash
# Returns resolved config (server defaults merged with bank overrides)
hindsight bank config my-bank
# Show only bank-specific overrides
hindsight bank config my-bank --overrides-only
```
### Go
```go
// Returns resolved config (server defaults merged with bank overrides) and the raw overrides
result, _, _ := client.BanksAPI.GetBankConfig(ctx, "my-bank").Execute()
// result.Config — full resolved configuration
// result.Overrides — only fields overridden at the bank level
fmt.Println("Config keys:", len(result.GetConfig()))
```
The response distinguishes:
- **`config`** — the fully resolved configuration (server defaults merged with bank overrides)
- **`overrides`** — only the fields explicitly overridden for this bank
### Resetting to Defaults
### Python
```python
# Remove all bank-level overrides, reverting to server defaults
client.reset_bank_config("my-bank")
```
### Node.js
```javascript
// Remove all bank-level overrides, reverting to server defaults
await client.resetBankConfig('my-bank');
```
### CLI
```bash
# Remove all bank-level overrides, reverting to server defaults
hindsight bank reset-config my-bank -y
```
### Go
```go
// Remove all bank-level overrides, reverting to server defaults
client.BanksAPI.ResetBankConfig(ctx, "my-bank").Execute()
```
This removes all bank-level overrides. The bank reverts to server-wide defaults (set via environment variables).
You can also update configuration directly from the Control Plane UI — navigate to a bank and open the **Configuration** tab.
---
## Directives
Directives are hard rules that the agent must follow during [reflect](./reflect.md) operations. Unlike disposition traits which influence *how* the agent reasons, directives are explicit instructions that are enforced whenever they are in scope (see [Directive Scope and Tags](#directive-scope-and-tags)).
> **ℹ️ Info**
>
Directives only affect the `reflect` operation. They are injected into prompts and the agent is required to comply with them in all responses.
### When to Use Directives
Use directives for rules that must never be violated:
- **Language/style constraints**: "Always respond in formal English"
- **Privacy rules**: "Never share personal data with third parties"
- **Domain constraints**: "Prefer conservative investment recommendations"
- **Behavioral guardrails**: "Always cite sources when making claims"
### Directive Scope and Tags
Directives can carry `tags`, and those tags scope **when** a directive is applied during `reflect` — mirroring how tags scope memories:
- **Untagged directives always apply**, on every `reflect`.
- **Tagged directives apply only when the `reflect` request includes matching tags** (using the request's `tags_match` mode). A `reflect` call with no tags applies only the untagged directives.
To apply **every** active directive regardless of tags, set `apply_all_directives: true` on the `reflect` request. This ignores tag scope for directives (untagged and tagged alike are enforced) and is useful when an operator keeps tagged directives for organization but wants all of them enforced on an untagged reflection.
### Creating Directives
### Python
```python
# Create a directive (hard rule for reflect)
directive = client.create_directive(
bank_id=BANK_ID,
name="Formal Language",
content="Always respond in formal English, avoiding slang and colloquialisms."
)
print(f"Created directive: {directive.id}")
```
### Node.js
```javascript
// Create a directive (hard rule for reflect)
const directive = await client.createDirective(
BANK_ID,
'Formal Language',
'Always respond in formal English, avoiding slang and colloquialisms.'
);
console.log(`Created directive: ${directive.id}`);
```
### CLI
```bash
# Create a directive (hard rule for reflect)
hindsight directive create "$BANK_ID" \
"Formal Language" \
"Always respond in formal English, avoiding slang and colloquialisms."
```
### Go
```go
// Create a directive (hard rule for reflect)
directive, _, _ := client.DirectivesAPI.CreateDirective(ctx, bankID).
CreateDirectiveRequest(hindsight.CreateDirectiveRequest{
Name: "Formal Language",
Content: "Always respond in formal English, avoiding slang and colloquialisms.",
}).Execute()
fmt.Printf("Created directive: %s\n", directive.GetId())
```
### Listing Directives
### Python
```python
# List all directives in a bank
directives = client.list_directives(bank_id=BANK_ID)
for d in directives.items:
print(f"- {d.name}: {d.content[:50]}...")
```
### Node.js
```javascript
// List all directives in a bank
const directives = await client.listDirectives(BANK_ID);
for (const d of directives.items) {
console.log(`- ${d.name}: ${d.content.slice(0, 50)}...`);
}
```
### CLI
```bash
# List all directives in a bank
hindsight directive list "$BANK_ID"
```
### Go
```go
// List all directives in a bank
directives, _, _ := client.DirectivesAPI.ListDirectives(ctx, bankID).Execute()
for _, d := range directives.GetItems() {
content := d.GetContent()
if len(content) > 50 {
content = content[:50]
}
fmt.Printf("- %s: %s...\n", d.GetName(), content)
}
```
### Updating Directives
### Python
```python
# Update a directive (e.g., disable without deleting)
updated = client.update_directive(
bank_id=BANK_ID,
directive_id=directive_id,
is_active=False
)
print(f"Directive active: {updated.is_active}")
```
### Node.js
```javascript
// Update a directive (e.g., disable without deleting)
const updated = await client.updateDirective(BANK_ID, directiveId, {
isActive: false
});
console.log(`Directive active: ${updated.is_active}`);
```
### CLI
```bash
# Update a directive (e.g., disable without deleting)
hindsight directive update "$BANK_ID" "$DIRECTIVE_ID" --is-active false
```
### Go
```go
// Update a directive (e.g., disable without deleting)
isActiveFalse := false
updated, _, _ := client.DirectivesAPI.UpdateDirective(ctx, bankID, directiveID).
UpdateDirectiveRequest(hindsight.UpdateDirectiveRequest{
IsActive: *hindsight.NewNullableBool(&isActiveFalse),
}).Execute()
fmt.Printf("Directive active: %v\n", updated.GetIsActive())
```
### Deleting Directives
### Python
```python
# Delete a directive
client.delete_directive(
bank_id=BANK_ID,
directive_id=directive_id
)
```
### Node.js
```javascript
// Delete a directive
await client.deleteDirective(BANK_ID, directiveId);
```
### CLI
```bash
# Delete a directive
hindsight directive delete "$BANK_ID" "$DIRECTIVE_ID" -y
```
### Go
```go
// Delete a directive
client.DirectivesAPI.DeleteDirective(ctx, bankID, directiveID).Execute()
```
### Directives vs Disposition
| Aspect | Directives | Disposition |
|--------|------------|-------------|
| **Nature** | Hard rules, must be followed | Soft influence on reasoning style |
| **Enforcement** | Strict — responses are rejected if violated | Flexible — shapes interpretation |
| **Use case** | Compliance, guardrails, constraints | Personality, character, tone |
| **Example** | "Never recommend specific stocks" | High skepticism: questions claims |
---
## Bank transfer (export & import)
Move a bank — or just its documents — between banks and instances **without re-running the LLM**. One archive format, one pair of endpoints, and three flags that decide what travels:
| Flag | Default | What it carries |
|------|---------|-----------------|
| `include_data` | `true` | Documents, raw chunks, extracted facts, consolidated observations, entities and links, attachments **and their bytes**, the curation archive of invalidated facts, the operations log, the maintenance queues — and what the bank synthesized from all of it: mental models, their refresh history, and the knowledge-page tree over them. |
| `include_bank_config` | `true` | How the bank is set up: its config overrides, directives, and webhooks. |
| `include_history` | `false` | `audit_log` and `llm_requests`. |
Mental models and knowledge pages count as **data**, not configuration. A mental model is a reading of the bank's facts and cites them by id in its `based_on` evidence, so carrying it without them would restore a synthesis whose grounding resolves to nothing. Their refresh history follows them for the same reason.
Embeddings and database ids are never carried: facts are re-embedded with the *target* bank's model and entities are re-resolved against it, so an archive moves cleanly to an instance configured with a different embedding model, vector extension, or text-search backend.
Webhooks travel with `include_bank_config`. When copying a bank whose webhooks point at a per-bank consumer, export with `include_bank_config=false`, or delete them on the copy.
### Export
`POST /v1/default/banks/{bank_id}/transfer/export` — runs as a **background operation** (a whole-bank export loads every unit and compresses a large archive). Returns `202` with an `operation_id`; poll the bank's operations endpoint, then download the archive from the `download_url` in `result_metadata`.
### Python
```python
# Whole bank, memories + config, no history.
# Submits the export, polls the operation, downloads the ZIP.
archive = await client.aexport_bank("transfer-py")
# Just the memories
memories_only = await client.aexport_bank("transfer-py", include_bank_config=False)
# Specific documents (a document subset carries no bank-level sections).
# The low-level call only submits; poll the returned operation yourself.
submission = await client.bank_transfer.export_bank_transfer(
"transfer-py", document_id=["doc-1", "doc-2"], include_bank_config=False
)
```
### Node.js
```javascript
// Whole bank, memories + config, no history.
// Submits the export, polls the operation, downloads the ZIP.
const archive = await client.exportBank('transfer-js');
// Just the memories
const memoriesOnly = await client.exportBank('transfer-js', { includeBankConfig: false });
// Specific documents (a document subset carries no bank-level sections).
// The low-level call only submits; poll the returned operation yourself.
const { data: subset } = await sdk.exportBankTransfer({
client: apiClient,
path: { bank_id: 'transfer-js' },
query: { document_id: ['doc-1', 'doc-2'], include_bank_config: false },
});
```
### CLI
```bash
# Whole bank, memories + config, no history
curl --fail-with-body -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/transfer/export"
# Just the memories
curl --fail-with-body -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/transfer/export?include_bank_config=false"
# Specific documents (a document subset carries no bank-level sections)
curl --fail-with-body -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/transfer/export?document_id=doc-1&document_id=doc-2&include_bank_config=false"
```
### Go
```go
// Whole bank, memories + config, no history
whole, _, err := client.BankTransferAPI.ExportBankTransfer(ctx, "transfer-go").Execute()
// Just the memories
memoriesOnly, _, err := client.BankTransferAPI.ExportBankTransfer(ctx, "transfer-go").
IncludeBankConfig(false).Execute()
// Specific documents (a document subset carries no bank-level sections)
subset, _, err := client.BankTransferAPI.ExportBankTransfer(ctx, "transfer-go").
DocumentId([]string{"doc-1", "doc-2"}).IncludeBankConfig(false).Execute()
// Each returns an operation id: poll it, then download result_metadata["storage_key"]
fmt.Println(whole.OperationId, memoriesOnly.OperationId, subset.OperationId)
```
A document subset must also pass `include_bank_config=false` (and leave `include_history` off). Bank-level sections cannot be scoped to documents, so the server rejects that combination with `400`.
### Import
`POST /v1/default/banks/{bank_id}/transfer/import` — multipart upload (`file` = the ZIP), also a background operation.
| Mode | Behaviour |
|------|-----------|
| `restore` (default) | Writes a whole bank into `target_bank_id`, which **must not already exist**. This is how a bank is moved to another instance, or copied under a new id. |
| `merge` | Folds the archive's documents into `{bank_id}`. `document_conflict` decides what happens to document ids that already exist: `skip` (default), `replace`, `new-id`. |
### Python
```python
# Restore a bank under a new id
operation_id = await client.aimport_bank("transfer-py", archive, target_bank_id="transfer-py-copy")
# The restore is recorded against the bank in the URL — poll it there
status = await client.operations.get_operation_status("transfer-py", operation_id)
# Merge an archive's documents into an existing bank
submission = await client.bank_transfer.import_bank_transfer(
"transfer-py-other",
("transfer-py.zip", archive),
mode="merge",
document_conflict="replace",
)
```
### Node.js
```javascript
// Restore a bank under a new id
const restoreId = await client.importBank('transfer-js', new Blob([archive]), {
targetBankId: 'transfer-js-copy',
});
// The restore is recorded against the bank in the URL — poll it there
const { data: restoreStatus } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'transfer-js', operation_id: restoreId },
});
// Merge an archive's documents into an existing bank
const { data: merge } = await sdk.importBankTransfer({
client: apiClient,
path: { bank_id: 'transfer-js-other' },
query: { mode: 'merge', document_conflict: 'replace' },
body: { file: new Blob([archive]) },
});
```
### CLI
```bash
# Restore a bank under a new id
curl --fail-with-body -H "Authorization: Bearer $API_KEY" -F "file=@transfer-bank.zip" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/transfer/import?target_bank_id=transfer-bank-copy"
# Merge an archive's documents into an existing bank
curl --fail-with-body -H "Authorization: Bearer $API_KEY" -F "file=@transfer-bank.zip" \
"$HINDSIGHT_URL/v1/default/banks/transfer-other-bank/transfer/import?mode=merge&document_conflict=replace"
```
### Go
```go
// Restore a bank under a new id
file, _ := os.Open(archivePath) // the ZIP downloaded from the export
restore, _, err := client.BankTransferAPI.ImportBankTransfer(ctx, "transfer-go").
File(file).TargetBankId("transfer-go-copy").Execute()
// The restore is recorded against the bank in the URL — poll it there
status, _, err := client.OperationsAPI.GetOperationStatus(ctx, "transfer-go", restore.OperationId).Execute()
// Merge an archive's documents into an existing bank
file, _ = os.Open(archivePath)
merge, _, err := client.BankTransferAPI.ImportBankTransfer(ctx, "transfer-go-other").
File(file).Mode("merge").DocumentConflict("replace").Execute()
```
The include flags apply here too, and can only narrow: they restore a subset of what the archive holds, never more.
In `restore` mode the operation is recorded against `{bank_id}` — the bank in the URL — because the target bank does not exist yet. Poll that bank's operations endpoint for status and the per-component counts.
A restore carries the operations log as history, not as work: anything still in flight when the bank was exported is left behind, so a copied bank never re-runs the original's queued retains or re-fires its webhooks. In-flight work belongs to the bank that was exported — and a clone runs *inside* one such operation, so carrying them would put the clone's own unfinished record in the copy.
### Import facts extracted outside Hindsight
An archive doesn't have to come from an export. If you run your own extraction pipeline, build the archive yourself and import it with `mode=merge`: your chunks and facts are stored as given — no LLM call, no re-chunking — while Hindsight still re-embeds the facts, resolves their entities against the bank, builds the semantic, temporal, entity and causal links, and — like a retain — fires `retain.completed` webhooks and auto-consolidation when the bank has them enabled.
The archive is a ZIP with a `manifest.json` and one JSON file per document under `documents/`:
```text
import.zip
├── manifest.json {"schema_version": 1, "source_bank_id": "external"}
└── documents/
└── session-2026-09-22.json
```
A document file:
```json
{
"id": "session-2026-09-22",
"original_text": "Full original session text...",
"tags": ["source:pi"],
"chunks": [{ "chunk_index": 0, "chunk_text": "Caller-defined source region..." }],
"facts": [
{
"text": "The user prefers lightweight local speech recognition models.",
"fact_type": "experience",
"chunk_index": 0,
"context": "Discussion of local speech recognition",
"mentioned_at": "2026-09-22T18:34:00Z",
"occurred_start": "2026-09-22T18:34:00Z",
"occurred_end": "2026-09-22T18:34:00Z",
"entities": ["Parakeet"],
"metadata": { "source_turn": "143" },
"tags": ["source:pi"],
"observation_scopes": "shared",
"causal_relations": []
}
]
}
```
| Fact field | Description |
|------------|-------------|
| `text` | Required. The memory, as it will be recalled. |
| `fact_type` | Required. `world` or `experience`. |
| `chunk_index` | Optional. The chunk in `chunks` this fact came from. |
| `context`, `metadata`, `tags`, `observation_scopes` | Optional. Same meaning as on a retain item, set per fact. |
| `mentioned_at`, `occurred_start`, `occurred_end` | Optional ISO 8601 dates. With none of them set, the fact is dated at import time. |
| `entities` | Optional entity names, resolved against the bank's existing entities. |
| `causal_relations` | Optional. `[{"relation_type": "caused_by", "target_fact_index": N}]`, where `N` is the position of another fact in the same document's `facts` list. |
Metadata and dates live on each fact; a document carries no metadata or timestamp of its own. Re-sending a document id follows `document_conflict` (`replace` to overwrite it).
### Python
```python
import io
import json
import zipfile
doc = {
"id": "session-2026-09-22",
"original_text": "Full original session text...",
"chunks": [{"chunk_index": 0, "chunk_text": "Caller-defined source region..."}],
"facts": [
{
"text": "The user prefers lightweight local speech recognition models.",
"fact_type": "experience",
"chunk_index": 0,
"mentioned_at": "2026-09-22T18:34:00Z",
"entities": ["Parakeet"],
}
],
}
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr("manifest.json", json.dumps({"schema_version": 1, "source_bank_id": "external"}))
z.writestr(f"documents/{doc['id']}.json", json.dumps(doc))
submission = await client.bank_transfer.import_bank_transfer(
"transfer-py-other",
("import.zip", buf.getvalue()),
mode="merge",
document_conflict="replace",
)
```
### Node.js
```javascript
import { crc32 } from 'node:zlib';
// Minimal uncompressed ZIP writer (Node has none built in); a library like jszip works too.
function zip(files) {
const locals = [], centrals = [];
let offset = 0;
for (const [name, text] of Object.entries(files)) {
const n = Buffer.from(name), d = Buffer.from(text), crc = crc32(d);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x21, 12);
local.writeUInt32LE(crc, 14); local.writeUInt32LE(d.length, 18); local.writeUInt32LE(d.length, 22);
local.writeUInt16LE(n.length, 26);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6);
central.writeUInt16LE(0x21, 14); central.writeUInt32LE(crc, 16); central.writeUInt32LE(d.length, 20);
central.writeUInt32LE(d.length, 24); central.writeUInt16LE(n.length, 28); central.writeUInt32LE(offset, 42);
locals.push(local, n, d);
centrals.push(central, n);
offset += 30 + n.length + d.length;
}
const dir = Buffer.concat(centrals), end = Buffer.alloc(22);
const count = Object.keys(files).length;
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(count, 8); end.writeUInt16LE(count, 10);
end.writeUInt32LE(dir.length, 12); end.writeUInt32LE(offset, 16);
return Buffer.concat([...locals, dir, end]);
}
const doc = {
id: 'session-2026-09-22',
original_text: 'Full original session text...',
chunks: [{ chunk_index: 0, chunk_text: 'Caller-defined source region...' }],
facts: [{
text: 'The user prefers lightweight local speech recognition models.',
fact_type: 'experience',
chunk_index: 0,
mentioned_at: '2026-09-22T18:34:00Z',
entities: ['Parakeet'],
}],
};
const archiveZip = zip({
'manifest.json': JSON.stringify({ schema_version: 1, source_bank_id: 'external' }),
[`documents/${doc.id}.json`]: JSON.stringify(doc),
});
const { data: external } = await sdk.importBankTransfer({
client: apiClient,
path: { bank_id: 'transfer-js-other' },
query: { mode: 'merge', document_conflict: 'replace' },
body: { file: new Blob([archiveZip]) },
});
```
### CLI
```bash
mkdir -p external/documents
echo '{"schema_version": 1, "source_bank_id": "external"}' > external/manifest.json
cat > external/documents/session-2026-09-22.json <<'JSON'
{
"id": "session-2026-09-22",
"original_text": "Full original session text...",
"chunks": [{"chunk_index": 0, "chunk_text": "Caller-defined source region..."}],
"facts": [{
"text": "The user prefers lightweight local speech recognition models.",
"fact_type": "experience",
"chunk_index": 0,
"mentioned_at": "2026-09-22T18:34:00Z",
"entities": ["Parakeet"]
}]
}
JSON
(cd external && zip -qr ../import.zip manifest.json documents)
curl --fail-with-body -H "Authorization: Bearer $API_KEY" -F "file=@import.zip" \
"$HINDSIGHT_URL/v1/default/banks/transfer-other-bank/transfer/import?mode=merge&document_conflict=replace"
```
### Go
```go
doc := map[string]any{
"id": "session-2026-09-22",
"original_text": "Full original session text...",
"chunks": []map[string]any{{"chunk_index": 0, "chunk_text": "Caller-defined source region..."}},
"facts": []map[string]any{{
"text": "The user prefers lightweight local speech recognition models.",
"fact_type": "experience",
"chunk_index": 0,
"mentioned_at": "2026-09-22T18:34:00Z",
"entities": []string{"Parakeet"},
}},
}
zipPath := filepath.Join(os.TempDir(), "import.zip")
out, _ := os.Create(zipPath)
zw := zip.NewWriter(out)
w, _ := zw.Create("manifest.json")
json.NewEncoder(w).Encode(map[string]any{"schema_version": 1, "source_bank_id": "external"})
w, _ = zw.Create("documents/session-2026-09-22.json")
json.NewEncoder(w).Encode(doc)
zw.Close()
out.Close()
file, _ = os.Open(zipPath)
external, _, err := client.BankTransferAPI.ImportBankTransfer(ctx, "transfer-go-other").
File(file).Mode("merge").DocumentConflict("replace").Execute()
```
### Clone a bank
`POST /v1/default/banks/{bank_id}/clone` — copy a bank into a new one in a single call, without handling an archive yourself. This is the export and import above run back to back on this instance: nothing is re-extracted and no LLM is called, so the clone's facts are exactly the source's, re-embedded with the same model.
### Python
```python
operation_id = await client.aclone_bank("transfer-py", "transfer-py-clone")
# The operation is recorded against the source bank
status = await client.operations.get_operation_status("transfer-py", operation_id)
```
### Node.js
```javascript
const cloneId = await client.cloneBank('transfer-js', 'transfer-js-clone');
// The operation is recorded against the source bank
const { data: cloneStatus } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'transfer-js', operation_id: cloneId },
});
```
### CLI
```bash
curl --fail-with-body -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/clone?target_bank_id=transfer-bank-clone"
# -> {"operation_id": "…", "status": "pending"}
```
### Go
```go
clone, _, err := client.BankTransferAPI.CloneBank(ctx, "transfer-go").
TargetBankId("transfer-go-clone").Execute()
// The operation is recorded against the source bank
status, _, err = client.OperationsAPI.GetOperationStatus(ctx, "transfer-go", clone.OperationId).Execute()
```
| Query param | Default | Description |
|-------------|---------|-------------|
| `target_bank_id` | — | Required. The bank to create; it must not already exist. |
| `include_data` | `true` | Copy the memories, everything backing them, and the mental models and knowledge pages synthesized from them. |
| `include_bank_config` | `true` | Copy the bank's config overrides, directives and webhooks. |
| `include_history` | `false` | Copy `audit_log` and `llm_requests`. |
The clone is independent from the moment it is made: later retains, consolidation and edits on either bank leave the other alone.
The operation is recorded against the **source** bank, because the target does not exist yet — poll the source's operations endpoint for status and the per-component counts.
> **⚠️ A clone inherits the source's webhooks**
>
Webhooks are part of a bank's configuration, so a clone made with the default flags will call the source's webhook endpoints. Pass `include_bank_config=false`, or delete them on the clone, when they point at a per-bank consumer.
### Document export & import (superseded)
The endpoints below still work exactly as documented and are unchanged; new integrations should use `/transfer/export` and `/transfer/import` above, which carry the same document archives plus the bank's own configuration.
Move documents — and the facts already extracted from them — between banks **without re-running the LLM**. Useful for testing a different embedding model, or copying data between banks/instances without paying for re-extraction. The archive carries documents, raw chunks, and extracted facts (entities by canonical name, causal links) — but **no embeddings or database ids**. On import, facts are re-embedded with the *target* bank's model and entities/links are recomputed against it, so imported documents are integrated with whatever already exists there.
### Export documents
`POST /v1/default/banks/{bank_id}/document-transfer/export` — runs as a **background operation** (a whole-bank export loads every unit and compresses a large archive, which on a big bank could exhaust memory and pin a connection). It returns `202` with an `operation_id`; poll the bank's operations endpoint, then download the archive from the `download_url` in `result_metadata`.
### Python
```python
# Submits the export (whole bank; pass document_ids=[...] to scope it),
# polls the operation until completed, downloads the archive.
archive = await client.aexport_documents("transfer-py")
with open("transfer-py-documents.zip", "wb") as f:
f.write(archive)
```
### Node.js
```javascript
// Submits the export (whole bank; pass { documentIds: [...] } to scope it),
// polls the operation until completed, downloads the archive.
const docArchive = await client.exportDocuments('transfer-js');
await writeFile('transfer-js-documents.zip', docArchive);
```
### CLI
```bash
# 1. Submit the export (whole bank; add ?document_id=… to scope it)
OPERATION_ID=$(curl -sf -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/document-transfer/export" | jq -r .operation_id)
# -> {"operation_id": "…", "status": "pending"}
# 2. Poll until completed
until [ "$(curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/operations/$OPERATION_ID" | jq -r .status)" = completed ]; do
sleep 1
done
DOWNLOAD_URL=$(curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-bank/operations/$OPERATION_ID" | jq -r .result_metadata.download_url)
# -> {"status":"completed","result_metadata":{
# "download_url":"/v1/default/files/download/banks/transfer-bank/exports/…/transfer.zip",
# "storage_key":"banks/transfer-bank/exports/…/transfer.zip","byte_size":12345,"filename":"transfer-bank-documents.zip"}}
# 3. Download the archive
curl -sf -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL$DOWNLOAD_URL" -o transfer-bank-documents.zip
```
### Go
```go
// 1. Submit the export (whole bank; add .DocumentId([]string{...}) to scope it)
export, _, err := client.DocumentTransferAPI.ExportDocuments(ctx, "transfer-go").Execute()
// 2. Poll until completed
var done *hindsight.OperationStatusResponse
for {
done, _, err = client.OperationsAPI.GetOperationStatus(ctx, "transfer-go", export.OperationId).Execute()
if err != nil || done.Status == "completed" || done.Status == "failed" {
break
}
time.Sleep(time.Second)
}
// 3. Download the archive (the client saves it to a temp file)
zipFile, _, err := client.DocumentTransferAPI.
DownloadFile(ctx, done.ResultMetadata["storage_key"].(string)).Execute()
```
| Query param | Description |
|-------------|-------------|
| `document_id` | Repeatable. Export only these documents; omit for the whole bank. |
| `include_observations` | Also export consolidated observations (default `false`). Only valid for a **whole-bank** export — combining it with `document_id` returns `400`. |
> **📝 The synchronous `GET …/document-transfer` was removed**
>
It loaded the entire bank into memory and held a database connection for the full request, which could take down the shared API on large banks. It now returns `410` pointing here. Use the async flow above. The download route (`GET /v1/default/files/download/{key}`) authorizes the caller against the bank the archive belongs to.
The archive lives as long as its export **operation record** — indefinitely by default, or until the operation is pruned when `HINDSIGHT_API_OPERATION_RETENTION_DAYS` is set (the archive is deleted in step with the row). Deleting the operation removes the archive immediately.
### Import documents
`POST /v1/default/banks/{bank_id}/document-transfer` — multipart upload (`file` = the ZIP). Runs as a **background operation** (re-embedding + entity resolution can take a while), so it returns `202` with an `operation_id`; poll the bank's operations endpoint for status and the result counts in `result_metadata`.
### Python
```python
with open("transfer-py-documents.zip", "rb") as f:
submission = await client.document_transfer.import_documents(
"transfer-py-other", ("transfer-py-documents.zip", f.read()), on_conflict="replace"
)
status = await client.operations.get_operation_status("transfer-py-other", submission.operation_id)
# status.result_metadata -> {"documents_imported": 3, "facts_imported": 42, "observations_imported": 5, ...}
```
### Node.js
```javascript
const { data: docImport } = await sdk.importDocuments({
client: apiClient,
path: { bank_id: 'transfer-js-other' },
query: { on_conflict: 'replace' },
body: { file: new Blob([await readFile('transfer-js-documents.zip')]) },
});
const { data: docImportStatus } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'transfer-js-other', operation_id: docImport.operation_id },
});
// docImportStatus.result_metadata -> { documents_imported: 3, facts_imported: 42, observations_imported: 5, ... }
```
### CLI
```bash
OPERATION_ID=$(curl -sf -H "Authorization: Bearer $API_KEY" -F "file=@transfer-bank-documents.zip" \
"$HINDSIGHT_URL/v1/default/banks/transfer-other-bank/document-transfer?on_conflict=replace" | jq -r .operation_id)
# -> {"operation_id": "…", "status": "pending"}
curl --fail-with-body -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/transfer-other-bank/operations/$OPERATION_ID"
# -> {"status":"completed","result_metadata":{"documents_imported":3,"facts_imported":42,"observations_imported":5,...}}
```
### Go
```go
file, _ = os.Open(zipFile.Name())
imported, _, err := client.DocumentTransferAPI.ImportDocuments(ctx, "transfer-go-other").
File(file).OnConflict("replace").Execute()
status, _, err = client.OperationsAPI.GetOperationStatus(ctx, "transfer-go-other", imported.OperationId).Execute()
// status.ResultMetadata -> {"documents_imported": 3, "facts_imported": 42, "observations_imported": 5, ...}
```
`on_conflict` controls what happens when a document id already exists in the target bank:
| Mode | Behavior |
|------|----------|
| `skip` (default) | Leave the existing document untouched. |
| `replace` | Delete the existing document's data and re-import. |
| `new-id` | Import a copy under a freshly generated id. |
### Observations
Consolidated observations are excluded by default — the target bank regenerates them from the imported facts during consolidation. Pass `include_observations=true` to carry them instead: they're restored with no LLM, their source references remapped to the imported facts (which are marked consolidated so the target won't re-consolidate them).
Because an observation can be derived from facts spanning several documents, `include_observations` is only supported on a **whole-bank export** (omit `document_id`); combining it with a document subset returns `400`.
> **⚠️ Imported observations are inserted as-is — no merge**
>
They are not merged or deduplicated against observations already in the target bank (consolidation merges related observations; import does not). Prefer importing observations into a fresh/empty bank, or omit `include_observations` and let the target consolidate the imported facts itself.
### Enabling / disabling
Both endpoints are gated by server-level flags (default `true`). A disabled endpoint returns `404`, and `/version` reports the state under `features.document_export_api` / `features.document_import_api` (the control plane hides the buttons accordingly).
| Variable | Gates |
|----------|-------|
| `HINDSIGHT_API_ENABLE_DOCUMENT_EXPORT_API` | `POST …/document-transfer/export` and `GET …/files/download/{key}` |
| `HINDSIGHT_API_ENABLE_DOCUMENT_IMPORT_API` | `POST …/document-transfer` |
## Splitting a bank in place
One bank has become two concerns — two teams, two tenants, two products — and you want
the memories to follow the split. This runbook moves a **subset of documents** from one
bank to another **on the same instance**, with no LLM re-extraction and no re-chunking:
facts are re-embedded with the target bank's model and entities are re-resolved against
it, but nothing is re-derived from text by a model.
To move a whole bank to an instance with a different embedding model, vector extension or
text-search backend, you want
[Migrating a bank to a new instance](#migrating-a-bank-to-a-new-instance) instead.
> **📝 The Admin CLI is not the tool for this**
>
`hindsight-admin export-bank` / `import-bank` work on a **whole bank**, and import is
not a merge — it fails if the target bank already exists. That is why the blue-green
runbook does not adapt to a split. Use the
[`/transfer/export` and `/transfer/import`](#bank-transfer-export--import) endpoints below.
### What a document subset carries
A subset export is narrower than a whole-bank archive, and the difference decides three
of the steps below.
| | Travels with a document subset |
|---|---|
| Documents, raw chunks | Yes |
| Extracted facts, entity names, causal links | Yes |
| Consolidated observations | **No** — the target re-consolidates from the imported facts |
| Mental models, knowledge pages | **No** — recreate them on the target |
| Bank config, directives, webhooks | **No** — passing `include_bank_config` with `document_id` is a `400` |
| `audit_log`, `llm_requests` | **No** — passing `include_history` with `document_id` is a `400` |
Observations are the reason, not an oversight: an observation can be consolidated from
facts spanning several documents, including documents outside your subset. Carrying it
would mean silently dropping every cross-document observation, so the export refuses the
combination instead. Each fact's consolidation lifecycle is dropped along with it, which
is what lets the target consolidate the subset from scratch.
### The runbook
Set `HINDSIGHT_URL` and `API_KEY` first. `SOURCE` is the bank you are splitting and
`TARGET` is the new one.
**1. Decide the cut and pin it to a list of document ids.**
In 1.0 a document set is addressed by id, tag or time, so the cut has to be expressible
one of those ways:
```bash
curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$SOURCE/documents?tags=project-x&tags_match=all_strict&limit=1000" \
| tee /tmp/listing.json | jq -r '.items[].id' > move.txt
wc -l move.txt
jq .total /tmp/listing.json
```
The response is paginated (`items`, `total`, `limit`, `offset`), so check `total` against
the line count and page with `offset=` if the cut is larger than one page. `tags_match`
takes `any_strict` (default), `any`, `all_strict` or `all`, and `start_date` / `end_date`
with `time_field=created_at|updated_at` narrow the set by a half-open time window.
Save the list to a file and work from it for the rest of the runbook. Every later step
reads the same `move.txt`, which is what makes the split auditable and repeatable after
an interruption.
> **⚠️ Do not retag documents to mark the subset**
>
It looks like the tidy way to label the cut, and it is the most expensive thing you can
do here. Changing a document's tags invalidates every observation derived from its
memories **and** resets `consolidated_at` on co-source memories from other documents, so
the bank re-consolidates a set whose size is the number of memories co-sourced with this
document's — on a densely co-sourced bank, many times the document's own memory count.
You would pay that once to apply the tag and again in step 7 when the documents are
deleted. A plain id list costs nothing.
**2. Create the target bank with its own configuration.**
```bash
hindsight bank create "$TARGET" --mission "Memory for project X"
```
Create it rather than cloning the source: a
[clone](#clone-a-bank) copies the whole bank including its webhooks, which would point
the new bank at the old bank's consumers.
**3. Export the subset from the source.**
`document_id` is repeatable. Leave `include_bank_config` and `include_history` off — with
`document_id` they are rejected.
```bash
ARGS=$(sed 's/^/document_id=/' move.txt | paste -sd'&' -)
OPERATION_ID=$(curl -s -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$SOURCE/transfer/export?$ARGS" | jq -r .operation_id)
```
The export is a background operation. Poll it, then download the archive from the
`download_url` in its `result_metadata`:
```bash
curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$SOURCE/operations/$OPERATION_ID" | jq .status
DOWNLOAD_URL=$(curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$SOURCE/operations/$OPERATION_ID" \
| jq -r .result_metadata.download_url)
curl -sf -H "Authorization: Bearer $API_KEY" "$HINDSIGHT_URL$DOWNLOAD_URL" -o split.zip
```
Read-only on the source: nothing has changed yet.
**4. Merge the archive into the target.**
```bash
curl --fail-with-body -H "Authorization: Bearer $API_KEY" -F "file=@split.zip" \
"$HINDSIGHT_URL/v1/default/banks/$TARGET/transfer/import?mode=merge&document_conflict=skip"
```
`mode=merge` folds the archive's documents into the bank in the URL. `document_conflict`
decides what happens to ids that already exist — `skip` (default), `replace`, or `new-id`.
In merge mode `target_bank_id` and the include flags are rejected with a `400`: a merge
takes the archive's documents and nothing else.
Poll the **target** bank's operations endpoint for status and the per-component counts in
`result_metadata`.
**5. Verify the target before you delete anything from the source.**
This is the rollback point, and the only one. The source is still whole; if anything looks
wrong, delete the target bank and start again.
```bash
curl -s -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$TARGET/documents?limit=1000" | jq '{total, ids: [.items[].id]}'
```
Check `total` against `wc -l move.txt`, confirm the ids are the ones you asked for, then
run two or three recall queries whose answers you already know.
**6. Rebuild what the subset did not carry.**
Observations first. If the target bank has `enable_auto_consolidation` on, the imported
facts are queued already and you only need to wait. Otherwise trigger it:
```bash
curl --fail-with-body -X POST -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$TARGET/consolidate"
```
Until this finishes the target has facts and no observation layer. Recall still answers —
which is exactly why this step gets skipped — but it answers from raw facts only, so the
bank looks shallower than the source for no apparent reason.
Then recreate the bank-level synthesis by hand: mental models, their directives, and the
knowledge-page tree do not travel with a subset.
**7. Purge the moved documents from the source.**
```bash
while read -r DOC; do
curl --fail-with-body -X DELETE -H "Authorization: Bearer $API_KEY" \
"$HINDSIGHT_URL/v1/default/banks/$SOURCE/documents/$DOC"
done < move.txt
```
Deleting a document removes its memory units and their links, deletes the observations
that referenced those memories, and resets `consolidated_at` on co-source memories from
other documents — so the source requeues the surviving sources itself rather than
stranding them. This is not undoable: step 5 was the rollback point.
**8. Let the source settle.**
The requeued co-sources re-consolidate automatically when the source bank has
auto-consolidation on, or on the next `POST /banks/$SOURCE/consolidate`. Afterwards,
re-check the source's mental models: their `based_on` evidence cited facts that have now
moved, so a refresh is worth running before anyone reads them again.
### Verifying the split was clean
| Check | How |
|---|---|
| Every intended document arrived | the target listing's `total` matches `wc -l move.txt` |
| Nothing unintended arrived | every `.items[].id` on the target appears in `move.txt` |
| The target has an observation layer | Observations exist for the imported facts, not just memories |
| The source no longer holds the subset | Each id in `move.txt` returns `404` on the source |
| The source's observations are coherent | No memories left with `consolidated_at` reset and no queue running |
| Mental models were recreated | The target's models exist and cite the target's facts |
### Common wrong turns
- **Retagging to mark the subset** (step 1) — pays the co-source re-consolidation cost
twice for a label an id list gives you free.
- **Passing `include_bank_config=true` with `document_id`** — a `400`, not a bank copy. A
subset is not a bank.
- **Expecting observations in the archive** — `include_observations` is whole-bank only,
by design. The target consolidates.
- **Deleting from the source before verifying the target** — step 5 is the rollback point
and step 7 is irreversible.
- **Treating the import's completion as the end** — an imported bank with no consolidation
run is facts without observations, and it looks fine.
- **Cloning the source as the target** — carries the source's webhooks, which then fire
for the new bank.
## Migrating a bank to a new instance
To move a bank to an instance configured with a different **embedding model**, **vector extension**, or **text-search backend** — which can't be changed in place on a populated bank — export the whole bank and import it into the new instance, where every embedding and index is re-derived from the stored text with **no LLM re-extraction**. This carries documents, facts, observations, bank config, mental models, directives, and webhooks (never embeddings).
Use the `hindsight-admin export-bank` / `import-bank` commands and follow the blue-green runbook in **[Admin CLI → Migrating a bank to a new instance](../admin-cli.md#migrating-a-bank-to-a-new-instance)**.
references/developer/api/mental-models.md
# Mental Models
User-curated summaries that provide high-quality, pre-computed answers for common queries.
See [Mental Models](../mental-models.md) for the concepts behind this API.
{/* Import raw source files */}
## What Are Mental Models?
Mental models are **saved reflect responses** that you curate for your memory bank. When you create a mental model, Hindsight runs a reflect operation with your source query and stores the result. During future reflect calls, these pre-computed summaries are checked first — providing faster, more consistent answers.
**Figure: Mental Models (API).** An animated diagram on the docs site; its narration, step by step:
- **create()**
1. You create a mental model: a name, the question it answers, and when it should refresh.
2. The API saves it right away. Its content does not exist yet.
3. The content is written in the background. The response is an operation id you can poll.
4. A refresh is a reflect run on the source query. It reads observations first…
5. …then raw facts, to check the details.
6. It writes the document and stores it with the memories it is based on.
- **auto refresh**
1. Later, your agent retains a new fact. Consolidation picks it up.
2. It writes an observation about Carol.
3. Before queueing a refresh, consolidation checks that the model’s scope holds a memory newer than the last one it read. If not, no refresh runs and no LLM is spent.
4. In delta mode it reads only the new memories, and answers with small edits instead of a rewrite.
5. Lines no edit touches are copied byte for byte. Only Carol’s line is added.
- **reflect()**
1. Now your agent asks a question.
2. Reflect searches mental models first, by meaning. Team overview matches, and it is up to date.
3. It covers the question, so the agent can answer from it. If it were stale or off topic, the agent would go on to observations and raw facts.
4. The answer comes back, citing the mental model it used.
### Why Use Mental Models?
| Benefit | Description |
|---------|-------------|
| **Consistency** | Same answer every time for common questions |
| **Speed** | Pre-computed responses are returned instantly |
| **Quality** | Manually curated summaries you've reviewed |
| **Control** | Define exactly how key topics should be answered |
### Hierarchical Retrieval
During reflect, the agent checks sources in priority order:
1. **Mental Models** — User-curated summaries (highest priority)
2. **Observations** — Consolidated knowledge
3. **Raw Facts** — Ground truth memories
Mental models are checked first because they represent your explicitly curated knowledge.
---
## Create a Mental Model
Creating a mental model runs a reflect operation in the background and saves the result:
### Python
```python
# Create a mental model (runs reflect in background)
result = client.create_mental_model(
bank_id=BANK_ID,
name="Team Communication Preferences",
source_query="How does the team prefer to communicate?",
tags=["team", "communication"]
)
# Returns an operation_id - check operations endpoint for completion
print(f"Operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Create a mental model (runs reflect in background)
const result = await client.createMentalModel(
BANK_ID,
'Team Communication Preferences',
'How does the team prefer to communicate?',
{ tags: ['team', 'communication'] },
);
// Returns an operation_id — check operations endpoint for completion
console.log(`Operation ID: ${result.operation_id}`);
```
### CLI
```bash
# Create a mental model (runs reflect in background)
hindsight mental-model create "$BANK_ID" \
"Team Communication Preferences" \
"How does the team prefer to communicate?"
```
### Go
```go
// Create a mental model (runs reflect in background)
result, _, _ := client.MentalModelsAPI.CreateMentalModel(ctx, mmBankID).
CreateMentalModelRequest(hindsight.CreateMentalModelRequest{
Name: "Team Communication Preferences",
SourceQuery: "How does the team prefer to communicate?",
Tags: []string{"team", "communication"},
}).Execute()
// Returns an operation_id — check operations endpoint for completion
fmt.Printf("Operation ID: %s\n", result.GetOperationId())
```
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Human-readable name for the mental model |
| `source_query` | string | Yes | The query to run to generate content |
| `id` | string | No | Custom ID for the mental model (alphanumeric lowercase with hyphens). Auto-generated if omitted. |
| `tags` | list | No | Tags that scope the model during reflect **and** filter source memories during refresh. Defaults to `all_strict` matching, so only memories carrying every listed tag are read. See [Tags and Visibility](#tags-and-visibility). |
| `max_tokens` | int | No | Maximum tokens for the mental model content |
| `trigger` | object | No | Trigger settings (see [Automatic Refresh](#automatic-refresh)) |
---
## Create with Custom ID
Assign a stable, human-readable ID to a mental model so you can retrieve or update it by name instead of relying on the auto-generated UUID:
### Python
```python
# Create a mental model with a specific custom ID
result_with_id = client.create_mental_model(
bank_id=BANK_ID,
name="Communication Policy",
source_query="What are the team's communication guidelines?",
id="communication-policy"
)
print(f"Created with custom ID: {result_with_id.operation_id}")
```
### Node.js
```javascript
// Create a mental model with a specific custom ID
const resultWithId = await client.createMentalModel(
BANK_ID,
'Communication Policy',
"What are the team's communication guidelines?",
{ id: 'communication-policy' },
);
console.log(`Created with custom ID: ${resultWithId.operation_id}`);
```
### CLI
```bash
# Create a mental model with a specific custom ID
hindsight mental-model create "$BANK_ID" \
"Communication Policy" \
"What are the team's communication guidelines?" \
--id communication-policy
```
### Go
```go
// Create a mental model with a specific custom ID
mmID := "communication-policy"
resultWithID, _, _ := client.MentalModelsAPI.CreateMentalModel(ctx, mmBankID).
CreateMentalModelRequest(hindsight.CreateMentalModelRequest{
Id: *hindsight.NewNullableString(&mmID),
Name: "Communication Policy",
SourceQuery: "What are the team's communication guidelines?",
}).Execute()
fmt.Printf("Created with custom ID: %s\n", resultWithID.GetOperationId())
```
> **💡 Tip**
>
Custom IDs must be lowercase alphanumeric and may contain hyphens (e.g. `team-policies`, `q4-status`). If a mental model with that ID already exists, the request is rejected.
---
## Automatic Refresh
Mental models can be configured to **automatically refresh** when observations are updated. This keeps them in sync with the latest knowledge without manual intervention.
### Trigger Settings
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `mode` | `"full"` \| `"delta"` | `"full"` | Refresh strategy. See [Refresh Mode](#refresh-mode) below. |
| `refresh_after_consolidation` | bool | false | Automatically refresh after observations consolidation |
| `refresh_cron` | string \| null | null | UTC 5-field cron expression for scheduled refreshes, such as `"0 3 * * *"` for daily at 03:00 UTC |
| `full_refresh_cron` | string \| null | null | UTC 5-field cron expression for a scheduled **full** refresh of a delta model, such as `"0 3 * * 0"` for Sundays at 03:00 UTC. Works alongside either trigger above. See [Scheduled Full Refresh](#scheduled-full-refresh). |
| `min_refresh_interval_seconds` | int \| null | null | Minimum seconds between two *automatic* refreshes of this model. See [Rate-limiting automatic refreshes](#rate-limiting-automatic-refreshes) below. `null` uses the bank/global default. |
| `tags_match` | string \| null | null | How the model's `tags` filter source memories during refresh: `any`, `all`, `any_strict`, `all_strict`, or `exact`. When `null`, a **tagged** model defaults to `all_strict` (a memory must carry every one of the model's tags). Set `"any"` to match memories carrying *any* of the tags — see [Tags and Visibility](#tags-and-visibility). |
| `tag_groups` | list \| null | null | Advanced boolean tag expressions that override flat `tags`/`tags_match` entirely. See the [Recall tags reference](./recall.md#tags). |
| `fact_types` | list \| null | null | Restrict which fact types the refresh reads: any of `world`, `experience`, `observation`. `null` means all three. Must not be an empty list. |
| `exclude_mental_models` | bool | false | Hide *all* other mental models from the refresh, so the model never synthesises from sibling models. |
| `exclude_mental_model_ids` | list \| null | null | Hide specific mental models by ID from the refresh. The model being refreshed is always excluded from itself. |
| `include_chunks` | bool \| null | null | Override whether the refresh's internal recall returns raw chunk text. `null` uses the bank/global `recall_include_chunks` default. |
| `recall_max_tokens` | int \| null | null | Override the token budget for facts retrieved during refresh. `null` uses the bank/global default. |
| `recall_chunks_max_tokens` | int \| null | null | Override the token budget for raw chunks retrieved during refresh. `null` uses the bank/global default. |
| `reflect_search_observations_max_tokens` | int \| null | null | Override the token budget for the refresh's `search_observations` calls. A smaller budget drops the lowest-ranked observations and shrinks the reflect context. `null` uses the shipped 5000. A refresh does not read the bank's `reflect_default_options`. |
| `reflect_search_observations_include_entities` | bool \| null | null | Override whether `search_observations` attaches resolved entity names, which can be more than half the tool payload. `null` means enabled. A refresh does not read the bank's `reflect_default_options`. |
| `budget` | string \| null | null | How many agent steps a refresh may spend, as a multiple of the server's `reflect_max_iterations`: `low` halves it, `mid` keeps it, `high` doubles it. `null` means `mid` — a refresh writes a whole document, so it does not inherit the `low` an ad-hoc reflect defaults to. |
| `response_schema` | object \| null | null | JSON Schema for structured output. When set, each refresh also stores a `structured_output` alongside the markdown content. See [Structured Output](#structured-output) below. |
| `keep_trace` | bool | false | Record how each refresh reached its result under `reflect_response.trace`. See [Troubleshoot a Refresh](#troubleshoot-a-refresh). |
When `refresh_after_consolidation` is enabled, the mental model will be re-generated every time the bank's observations are consolidated — ensuring it always reflects the latest synthesized knowledge.
When `refresh_cron` is set, Hindsight checks the schedule on the server's mental-model refresh tick and refreshes the model only if memories in its scope have changed since the last refresh. `refresh_cron` and `refresh_after_consolidation` are mutually exclusive, so a model refreshes either after consolidation or on a fixed UTC schedule, not both.
`last_refresh_failed_at` on the model (and on a page in the knowledge tree) carries when that happened, so a list view can show which models have stopped refreshing themselves without reading each one's history.
**A failed refresh pauses the automatic ones.** A failed refresh is retried by the worker (`HINDSIGHT_API_WORKER_MAX_RETRIES`, 3 by default) and then stops. None of `refresh_after_consolidation`, `refresh_cron` or `full_refresh_cron` queues that model again until a refresh succeeds, so a refresh that cannot work (a prompt too large for the model, an empty account, a delta that will not apply) costs a few attempts instead of an LLM bill every tick. The failure shows in the model's [history](#history). Fix the cause and refresh the model yourself: a successful refresh resumes the automatic ones. A refresh cut off by `HINDSIGHT_API_REFLECT_WALL_TIMEOUT`, which bounds a whole refresh the same way it bounds a reflect, counts as a failure too.
### Rate-limiting automatic refreshes
A refresh is a full reflect run: retrieval plus an agentic LLM loop. With
`refresh_after_consolidation` on a bank that ingests continuously, every small retain can
therefore pay for a rebuild of every model whose scope it touched — and a handful of models
on a chatty bank adds up fast.
`min_refresh_interval_seconds` puts a floor on how often that may happen:
```json
{
"trigger": {
"refresh_after_consolidation": true,
"min_refresh_interval_seconds": 1800
}
}
```
A trigger that fires inside the window is **not dropped**. Its refresh is queued and parked
until the window closes, and every further trigger in the meantime folds into that same
queued refresh — so a burst of twenty retains costs one refresh, and that refresh still
reads everything the twenty retains added. The only thing you trade away is immediacy: the
model can lag its memories by up to the interval.
The floor applies to every automatic trigger (`refresh_after_consolidation`,
`refresh_cron` and `full_refresh_cron`). It never applies to a refresh you ask for — the refresh endpoint, the MCP
tool, and the control plane's refresh button all run immediately, and additionally release a
parked refresh if one is waiting.
Set it per bank (or server-wide) with `mental_model_min_refresh_interval_seconds` /
`HINDSIGHT_API_MENTAL_MODEL_MIN_REFRESH_INTERVAL_SECONDS`; the per-model value wins when both
are set, so `min_refresh_interval_seconds: 0` exempts one model that does need to stay current
from a bank-wide floor. The default everywhere is `0` — no floor.
While a refresh is parked, its operation stays `pending` with `next_retry_at` set to the moment
the window closes, so `GET /operations` shows exactly what it is waiting for.
### Structured Output
A mental model's content is always markdown. Set `trigger.response_schema` to *also* attach a machine-readable view: a [JSON Schema](https://json-schema.org/) **object** with a non-empty `properties` map (nested objects and arrays are supported). Each refresh then stores a `structured_output` on the model's `reflect_response`, next to the markdown — you get both the prose and a typed object.
```json
{
"trigger": {
"response_schema": {
"type": "object",
"properties": {
"risk_level": { "type": "string" },
"open_questions": { "type": "array", "items": { "type": "string" } }
},
"required": ["risk_level"]
}
}
}
```
Key behaviours:
- **Extracted from the final document.** The structured output is parsed from the content that is actually stored — so in `delta` mode it reflects the whole merged document, not just the facts that changed in that refresh.
- **Fails loudly.** If a `response_schema` is configured but the structured extraction cannot be produced, the refresh **fails** rather than silently persisting content with no structured view. The model's previous content and `structured_output` are preserved, and the refresh can be retried.
- **Invalid schemas are rejected** at request time: the schema must be an object with at least one property, and each property's `type` must be one of `string`, `number`, `integer`, `boolean`, `array`, or `object`.
See [Reflect → `response_schema`](./reflect.md#response_schema) for how the same schema works on the `reflect` endpoint. The `structured_output` value appears on the model's `reflect_response` (see [Response Fields](#response-fields)).
### Staleness Gating
Both automatic triggers run the same check before spending an LLM call: **is there a memory in this model's resolved scope newer than its last refresh?** The model's `tags`/`tags_match`, `tag_groups`, and `fact_types` all apply to that check, so activity elsewhere in the bank does not trigger a rebuild, and a cron tick over an unchanged scope is skipped entirely. Memories still waiting to be consolidated count — they are already stored, so a model whose scope reaches them is considered stale.
**Untagged writes never make a tagged model stale.** Under `tags_match: "any"` or `"all"` a refresh also *reads* untagged memories, but staleness only counts writes that carry the model's tags. Otherwise, on a bank that mixes tagged and untagged writes, every such model would read stale after nearly every write (#4857). The same goes for `tag_groups` leaves using those modes; a group that selects untagged memories explicitly (an `exact` leaf with no tags, or a `not`) still counts them. A model with no tags has the whole bank as its scope, so any write marks it stale.
Every `is_stale` you can read is computed by that same rule: the flag on a single mental-model read, the one on the list, the one surfaced to the reflect agent, and the per-page flag on the [knowledge-base tree](./knowledge-pages.md). A model flagged stale is one a refresh would actually rewrite.
Listing does not cost one query per model — the whole page is answered together — so you do not have to approximate. If you are already holding `last_memory_write_at` from the bank stats endpoint, a model whose `last_memory_seen_at` is at or after it is provably up to date without asking at all: nothing in the bank changed, so nothing in that model's scope did.
**Deletions are invisible to it.** Staleness asks whether anything in scope has been *written* since the model last read the memories. Deleting an in-scope memory leaves no write behind, so it does not raise the flag, and a document that cites the deleted fact keeps reporting itself up to date until something in its scope is written or you refresh it yourself.
**Two timestamps answer two different questions.** `last_refreshed_at` is wall-clock: when a refresh last finished. It advances on every refresh that completes — including one that read the scope, found nothing new, and left the document alone — so a client driving refreshes itself can use it to tell "I already did this one" from "this one still needs a call". `last_memory_seen_at` is a position in the data: the newest in-scope memory the last refresh saw. It stands still while the model's scope is quiet, however many times you refresh, which is what makes it the right side of the staleness comparison. A model with a recent `last_refreshed_at` and an old `last_memory_seen_at` is not a contradiction — it means you refreshed a document that had nothing new to say.
### What a Refresh Reads
- **A point-in-time snapshot.** Each refresh is bounded by a database-time cutoff and only reads memories committed at or before it, so a write landing mid-refresh is never half-included. The watermark recorded afterwards is the newest in-scope memory the refresh actually saw — not `now()` — so a write that commits just after the snapshot stays "newer" and is picked up on the next round instead of being skipped.
- **Only what's new, in `delta` mode.** A delta refresh scopes its retrieval to memories created since the last refresh, so the LLM reads genuinely new information rather than re-reading the whole bank.
- **Cumulative grounding.** Even in delta mode, `reflect_response.based_on` accumulates: the facts from this refresh are merged with everything previous refreshes relied on, deduplicated by id. The document rests on all of them, not just the latest slice.
- **Nothing, if nothing is relevant.** A refresh that finds no topic-relevant memories leaves the content alone and only advances the watermark, so the same empty window stops re-triggering.
- **No LLM call at all, if there is nothing to read.** Before the agentic loop starts, a refresh checks whether anything is in reach under this model's own settings: an in-scope memory (`tags`/`tags_match`, `tag_groups` and `fact_types` all narrow it) inside the window it would read — the whole bank in `full` mode, everything since `last_memory_seen_at` in `delta` mode — or a sibling mental model this refresh could read (same tags, not in `exclude_mental_model_ids`), unless `exclude_mental_models` closed that door. Sibling models alone only count for a document that has nothing to lose — an empty one, or one no refresh has ever written with memories in scope: a document built from memories is never rewritten from sibling models once its own memory scope comes up empty. When there is nothing, the refresh completes immediately, leaves the document untouched, and spends nothing. This is the common case on a bank that was just created: its pages are refreshed the moment they exist, before anything has been retained.
- **A failed retrieval is not an empty one.** If a retrieval step raises — the database, the embedder or the reranker is unavailable — the refresh **fails** instead of writing what the model came up with without it. Content, `structured_output` and the watermark are all left untouched, so the retry reads the same window again. This is the distinction that matters: "we looked and there is nothing on this topic" is an answer and gets written; "we could not look" is a failure and gets preserved. Without it, a broken retriever silently replaced a document built over months with a generic "I don't have information about that".
### Refresh Mode
Two strategies are available for how a refresh produces the new content:
- **`full`** *(default)* — every refresh regenerates the entire content from scratch. Simple and predictable: the LLM synthesises a fresh document from the retrieved memories. Best when the document is short, when you want every refresh to potentially restructure the output, or when you're not yet sure what the final shape should be.
- **`delta`** — refresh emits a list of typed *operations* (add a section, append a bullet, replace a block, remove a stale paragraph) against the document's existing structure, then renders the result. Sections that aren't targeted by any operation are copied through **byte-identical** — no paraphrasing, no whitespace drift, no list-style normalisation. Best for long-lived "playbook"–style mental models where you want stability across refreshes and only the genuinely changed parts to move.
**Figure: Refresh modes: full vs delta.** An animated diagram on the docs site; its narration, step by step:
- **full**
1. Full is the default. Every refresh writes the whole document again.
2. It reads everything in the model’s scope, old and new.
3. The LLM writes a fresh document. Carol is in, but untouched sections come back reworded too, and small drifts add up over many refreshes.
- **delta**
1. Delta edits the document instead of rewriting it.
2. It only reads memories newer than the last one the previous refresh saw.
3. A second LLM call compares the new findings with the current sections and answers with edit operations, not a document.
4. The operation is applied. Every section it does not touch is copied through byte for byte, and the watermark moves to Sep.
- **delta falls back**
1. Delta needs something stable to edit. Here the source query was changed.
2. The topic moved, so the old structure may no longer fit. The refresh falls back to a full rewrite. The same happens when the model has no content yet.
3. It reads the whole scope, like full mode…
4. …and writes a new document for the new question. The next refresh can edit this one in delta mode again.
#### How delta mode works
Hindsight keeps an authoritative **structured** representation of the document — an ordered list of sections, each holding a list of blocks, where a block is one markdown fragment (a paragraph, a list, a table, a code fence) stored exactly as written. The markdown you read is a deterministic render of that structure. Because a block is never re-interpreted, formatting a refresh didn't touch cannot be rewritten by one — a table stays a table. A delta refresh never asks the LLM to rewrite the document; it asks for operations against that structure:
| Operation | Effect |
|---|---|
| `add_section` / `remove_section` | Add or drop a whole section |
| `rename_section` | Change a section's heading |
| `replace_section_blocks` | Replace a section's contents wholesale |
| `append_block` / `insert_block` | Add a block at the end of, or after a named block in, a section |
| `replace_block` / `remove_block` | Change or drop one block |
Anything no operation mentions is copied through untouched, so unchanged prose is preserved rather than regenerated and checked. This matters because "preserve the unchanged content" is only a soft constraint on an LLM — generating the next token from a gestalt of the input is what it intrinsically does, so instructed-to-preserve prose drifts over many refreshes.
Sections and blocks are addressed by id, never by position, so an operation cannot land on the wrong one by miscounting. Failure modes are conservative by design: an operation referencing a section or block that doesn't exist — or a block that lives in a different section than the one it names — is **dropped** rather than guessed at, and the rest of the operations still apply. When *every* operation points at something missing, the model is asked once more, shown the ids it got wrong and the sections the document actually has. The refresh records which ones were dropped and why, so you can see that part of that round's new information didn't make it into the document.
Delta mode falls back to a full regeneration automatically in two cases:
1. The mental model has no existing content yet (nothing to anchor edits on).
2. The `source_query` has changed since the last refresh (the topic has shifted; the existing structure may no longer apply).
**A delta refresh never replaces the document with a partial one.** Because delta retrieval only reads memories newer than the last refresh, an answer written from that window covers just the recent slice of the topic — it is material for editing the document, not a replacement for it. So when the edits can't be made at all — the provider call fails, the response can't be read, or every single operation is rejected — the existing content stays exactly as it is and the refresh **fails** instead of completing. Nothing is lost, the refresh's time window is not advanced, and a retry sees the same memories again. The same holds for an empty answer: a populated document is never overwritten with an empty one. A delta refresh is never turned into a full rewrite behind your back: if a model's delta refreshes keep failing, switch its `mode` to `full`.
| Use Case | Recommended Mode | Why |
|----------|-----------------|-----|
| Skill / playbook docs | `delta` | Sections live for many refreshes; only specific rules change |
| Onboarding summaries | `delta` | Adding new team members shouldn't restructure the doc |
| Real-time dashboards | `full` | Each refresh is a fresh snapshot |
| Short FAQ summaries | `full` | Whole-document regeneration is cheap and unambiguous |
### Python
```python
# Create a mental model with automatic refresh enabled
result = client.create_mental_model(
bank_id=BANK_ID,
name="Project Status",
source_query="What is the current project status?",
trigger={"refresh_cron": "0 3 * * *"}
)
# This mental model checks daily at 03:00 UTC and refreshes when scoped memories changed
print(f"Operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Create a mental model with automatic refresh enabled
const result2 = await client.createMentalModel(
BANK_ID,
'Project Status',
'What is the current project status?',
{ trigger: { refreshCron: '0 3 * * *' } },
);
// This mental model checks daily at 03:00 UTC and refreshes when scoped memories changed
console.log(`Operation ID: ${result2.operation_id}`);
```
### CLI
```bash
# Create a mental model and get its ID for subsequent operations
hindsight mental-model create "$BANK_ID" \
"Project Status" \
"What is the current project status?"
```
### Go
```go
// Create a mental model with automatic refresh enabled
refreshTrue := true
result2, _, _ := client.MentalModelsAPI.CreateMentalModel(ctx, mmBankID).
CreateMentalModelRequest(hindsight.CreateMentalModelRequest{
Name: "Project Status",
SourceQuery: "What is the current project status?",
Trigger: &hindsight.MentalModelTriggerInput{
RefreshAfterConsolidation: &refreshTrue,
},
}).Execute()
// This mental model will automatically refresh when observations are updated
fmt.Printf("Operation ID: %s\n", result2.GetOperationId())
```
### Scheduled Full Refresh
A `delta` model keeps unchanged sections byte-for-byte, which is the point — but over months
of small edits a document can drift from what a fresh write would say: wording shaped by
increments, sections that are hard to retire. `full_refresh_cron` adds a second, independent
schedule on which the model is refreshed in **full** mode, while every other refresh stays
`delta`:
```json
{
"trigger": {
"mode": "delta",
"refresh_after_consolidation": true,
"min_refresh_interval_seconds": 21600,
"full_refresh_cron": "0 3 * * 0"
}
}
```
This model is edited in place after each consolidation (at most every 6 hours), and rewritten
from scratch every Sunday at 03:00 UTC. Afterwards it carries on in `delta` mode — the
schedule never changes `mode`.
- **Independent of the other triggers.** It combines with `refresh_after_consolidation` or
with `refresh_cron`. If both crons are due on the same check, one refresh runs, in full mode.
- **Its own clock.** It comes due against `last_full_refreshed_at`, not `last_refreshed_at`,
so frequent delta refreshes never push it back.
- **Skipped when there is nothing to fix.** If no refresh ran since the last full one, the
tick is skipped. Unlike `refresh_cron` it is not gated on new memories: drift left by past
edits is still there on a quiet bank.
- **Same safety as any automatic refresh.** It honours `min_refresh_interval_seconds`, pauses
while the last refresh failed, and a full refresh that fails keeps the current content.
To run one full refresh now, send `{"mode": "full"}` to the [refresh endpoint](#refresh-a-mental-model).
### When to Use Automatic Refresh
| Use Case | Automatic Refresh | Why |
|----------|-------------------|-----|
| **Real-time dashboards** | ✅ Enabled | Status should always be current |
| **Policy summaries** | ❌ Disabled | Policies change infrequently, manual refresh preferred |
| **User preferences** | ✅ Enabled | Preferences evolve with new interactions |
| **FAQ answers** | ❌ Disabled | Answers are curated, should be reviewed before updating |
> **💡 Tip**
>
Enable automatic refresh for mental models that need to stay current. Disable it for curated content where you want to review changes before they go live.
---
## List Mental Models
### Python
```python
# List all mental models in a bank. The list returns metadata by default;
# detail="content" adds source_query/content/trigger.
mental_models = client.list_mental_models(bank_id=BANK_ID, detail="content")
for mental_model in mental_models.items:
print(f"- {mental_model.name}: {mental_model.source_query}")
```
### Node.js
```javascript
// List all mental models in a bank. The list returns metadata by default;
// detail: "content" adds source_query/content/trigger.
const mentalModels = await client.listMentalModels(BANK_ID, { detail: "content" });
for (const mm of mentalModels.items) {
console.log(`- ${mm.name}: ${mm.source_query}`);
}
```
### CLI
```bash
# List all mental models in a bank
hindsight mental-model list "$BANK_ID"
```
### Go
```go
// List all mental models in a bank. The list returns metadata by default;
// Detail("content") adds source_query/content/trigger.
mentalModels, _, _ := client.MentalModelsAPI.ListMentalModels(ctx, mmBankID).Detail("content").Execute()
for _, mm := range mentalModels.GetItems() {
fmt.Printf("- %s: %s\n", mm.GetName(), mm.GetSourceQuery())
}
```
A [knowledge page](./knowledge-pages.md) keeps its content in a mental model, so the list includes the models behind pages too. Each item carries `is_knowledge_page` to tell them apart, and `exclude_knowledge_pages=true` lists standalone mental models only (both the items and `total`).
---
## Get a Mental Model
### Python
```python
# Get a specific mental model
mental_model = client.get_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
print(f"Name: {mental_model.name}")
print(f"Content: {mental_model.content}")
print(f"Last refreshed: {mental_model.last_refreshed_at}")
```
### Node.js
```javascript
// Get a specific mental model
const mentalModel = await client.getMentalModel(BANK_ID, mentalModelId);
console.log(`Name: ${mentalModel.name}`);
console.log(`Content: ${mentalModel.content}`);
console.log(`Last refreshed: ${mentalModel.last_refreshed_at}`);
```
### CLI
```bash
# Get a specific mental model
hindsight mental-model get "$BANK_ID" "$MENTAL_MODEL_ID"
```
### Go
```go
// Get a specific mental model
mentalModel, _, _ := client.MentalModelsAPI.GetMentalModel(ctx, mmBankID, mentalModelID).Execute()
fmt.Printf("Name: %s\n", mentalModel.GetName())
fmt.Printf("Content: %s\n", mentalModel.GetContent())
fmt.Printf("Last refreshed: %s\n", mentalModel.GetLastRefreshedAt())
```
### Detail Levels
Both **List** and **Get** endpoints accept an optional `detail` query parameter that controls how much data is returned. This is useful for reducing response size, especially in agent boot flows or MCP clients where context budget is limited.
| Level | Fields Returned | Use Case |
|-------|----------------|----------|
| `metadata` | `id`, `bank_id`, `name`, `tags`, `is_stale`, `last_refreshed_at`, `last_memory_seen_at`, `created_at` | Inventory — "what models exist?" |
| `content` | All metadata fields + `source_query`, `content`, `max_tokens`, `trigger` | Agent boot — "what do the models say?" |
| `full` | All fields including `reflect_response` | Deep inspection — "what evidence backs this model?" |
The two endpoints default differently:
- **List** defaults to `metadata`. Listing is an index — returning every model's synthesized content by default let one request pull a whole bank's knowledge in bulk. Content is opt-in.
- **Get** defaults to `full`. You already named the one model you want.
### Python
```python
# List: metadata only, the default (smallest response)
client.list_mental_models(bank_id=BANK_ID)
# List with content but without provenance chains (opt-in)
client.list_mental_models(bank_id=BANK_ID, detail="content")
# Get one model — full detail is the default here
client.get_mental_model(bank_id=BANK_ID, mental_model_id=mental_model_id)
```
### Node.js
```javascript
// List: metadata only, the default (smallest response)
await client.listMentalModels(BANK_ID);
// List with content but without provenance chains (opt-in)
await client.listMentalModels(BANK_ID, { detail: 'content' });
// Get one model — full detail is the default here
await client.getMentalModel(BANK_ID, mentalModelId);
```
### CLI
```bash
# The CLI has no --detail flag; use the HTTP API
# List: metadata only, the default (smallest response)
curl "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/mental-models"
# List with content but without provenance chains (opt-in)
curl "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/mental-models?detail=content"
# Get one model — full detail is the default here
curl "$HINDSIGHT_URL/v1/default/banks/$BANK_ID/mental-models/$MENTAL_MODEL_ID"
```
### Go
```go
// List: metadata only, the default (smallest response)
client.MentalModelsAPI.ListMentalModels(ctx, mmBankID).Execute()
// List with content but without provenance chains (opt-in)
client.MentalModelsAPI.ListMentalModels(ctx, mmBankID).Detail("content").Execute()
// Get one model — full detail is the default here
client.MentalModelsAPI.GetMentalModel(ctx, mmBankID, mentalModelID).Execute()
```
The `detail` parameter is available on the `get_mental_model` MCP tool. The
`list_mental_models` MCP tool does not take it: it always returns metadata
(including `is_stale`), and an agent reads a specific model's content with
`get_mental_model`.
> **💡 Tip**
>
Use `detail=content` on the List endpoint for agent orientation flows that genuinely need every model's text. It includes everything the agent needs to understand the models without the heavyweight `reflect_response` provenance chains, which can exceed 200KB for banks with many models.
> **📝 Upgrading**
>
The List endpoint previously defaulted to `full`. A caller that omits `detail` and reads `content`, `source_query`, `max_tokens` or `trigger` off the listed items now gets `null` — pass `detail=content` explicitly.
### Response Fields
| Field | Type | Detail Level | Description |
|-------|------|-------------|-------------|
| `id` | string | metadata | Unique mental model ID |
| `bank_id` | string | metadata | Memory bank ID |
| `name` | string | metadata | Human-readable name |
| `tags` | list | metadata | Tags for filtering |
| `last_refreshed_at` | string | metadata | When a refresh last finished — wall-clock. Advances on every completed refresh, including one that found nothing new. Answers "have I already refreshed this?" |
| `last_memory_seen_at` | string | metadata | The newest in-scope memory the last refresh saw. Answers "is this behind the data?" — compare against `last_memory_write_at` from the stats endpoint |
| `last_full_refreshed_at` | string \| null | metadata | When a refresh last ran in full mode — asked for, scheduled, or a delta that fell back to full. `full_refresh_cron` comes due against this |
| `created_at` | string | metadata | When the mental model was created |
| `source_query` | string | content | The query used to generate content |
| `content` | string | content | The generated mental model text |
| `max_tokens` | int | content | Maximum tokens for the mental model content |
| `trigger` | object | content | Trigger settings (see [Automatic Refresh](#automatic-refresh)) |
| `reflect_response` | object | full | Full reflect response including `based_on` provenance facts |
---
## Refresh a Mental Model
Re-run the source query to update the mental model with current knowledge:
### Python
```python
# Refresh a mental model to update with current knowledge
result = client.refresh_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
print(f"Refresh operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Refresh a mental model to update with current knowledge
const refreshResult = await client.refreshMentalModel(BANK_ID, mentalModelId);
console.log(`Refresh operation ID: ${refreshResult.operation_id}`);
```
### CLI
```bash
# Refresh a mental model to update with current knowledge
hindsight mental-model refresh "$BANK_ID" "$MENTAL_MODEL_ID"
```
### Go
```go
// Refresh a mental model to update with current knowledge
refreshResult, _, _ := client.MentalModelsAPI.RefreshMentalModel(ctx, mmBankID, mentalModelID).Execute()
fmt.Printf("Refresh operation ID: %s\n", refreshResult.GetOperationId())
```
Refreshing is useful when:
- New memories have been retained that affect the topic
- Observations have been updated
- You want to ensure the mental model reflects current knowledge
**Full refresh on demand.** Send `{"mode": "full"}` as the body to rewrite the content from
scratch this once, whatever the model's `mode`. The model keeps its mode — a `delta` model
goes back to editing in place on its next refresh. Use it to clear the drift a `delta` model
builds up over many edits; to do it on a schedule, set
[`full_refresh_cron`](#scheduled-full-refresh).
### Python
```python
# Rewrite the content from scratch this once; the model keeps its mode
result = client.refresh_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id,
mode="full",
)
print(f"Full refresh operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Rewrite the content from scratch this once; the model keeps its mode
const fullResult = await client.refreshMentalModel(BANK_ID, mentalModelId, { mode: "full" });
console.log(`Full refresh operation ID: ${fullResult.operation_id}`);
```
### CLI
```bash
# Rewrite the content from scratch this once; the model keeps its mode
hindsight mental-model refresh "$BANK_ID" "$MENTAL_MODEL_ID" --full
```
### Go
```go
// Rewrite the content from scratch this once; the model keeps its mode
fullReq := hindsight.NewRefreshMentalModelRequest()
fullReq.SetMode("full")
fullResult, _, _ := client.MentalModelsAPI.RefreshMentalModel(ctx, mmBankID, mentalModelID).
RefreshMentalModelRequest(*fullReq).Execute()
fmt.Printf("Full refresh operation ID: %s\n", fullResult.GetOperationId())
```
**Refreshes coalesce.** A model has at most one refresh waiting at a time: if one is
already queued and has not started yet, this call returns *that* operation instead of
queueing an identical second one — the queued refresh reads the model as it stands when
it runs, so it already covers what you just asked for. Poll the returned `operation_id`
as usual. A refresh that is already *running* is not reused: it may have read the model
before your latest change, so a new operation is queued behind it. A full refresh that joins
a queued plain one turns that one into a full refresh.
**And they run one at a time.** A model's refreshes never overlap: the operation queued
behind a running one starts when that one finishes, so the model is written by one refresh
at a time instead of by whichever happened to finish last. This is per model — a bank
refreshes as many different models in parallel as it has workers for.
---
## Troubleshoot a Refresh
When a refresh produces a document you didn't expect — nothing changed, the wrong
things changed, or delta edits didn't apply — the stored content alone doesn't say
why. Two tools report the reasoning behind a refresh.
### Dry run: preview a refresh before it happens
`POST /mental-models/{id}/dry-run-refresh` runs the real pipeline and reports what a
refresh *would* do. Nothing is written: not the content, structured document,
watermark, nor `last_refreshed_at`. Because nothing is persisted, a delta dry run
reads exactly the window the next real refresh will, and repeating it reads that same
window again.
### Python
```python
# Preview what a refresh would do, without writing anything
preview = client.dry_run_refresh_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
print(f"Mode: {preview.effective_mode}, would persist: {preview.would_persist}")
print(preview.diff)
```
### Node.js
```javascript
// Preview what a refresh would do, without writing anything
const preview = await client.dryRunRefreshMentalModel(BANK_ID, mentalModelId);
console.log(`Mode: ${preview.effective_mode}, would persist: ${preview.would_persist}`);
console.log(preview.diff);
```
### CLI
```bash
# Preview what a refresh would do, without writing anything
hindsight mental-model dry-run-refresh "$BANK_ID" "$MENTAL_MODEL_ID"
```
### Go
```go
// Preview what a refresh would do, without writing anything
preview, _, err := client.MentalModelsAPI.DryRunRefreshMentalModel(ctx, mmBankID, mentalModelID).Execute()
if err != nil {
panic(err)
}
fmt.Printf("Mode: %s, would persist: %v\n", preview.GetEffectiveMode(), preview.GetWouldPersist())
fmt.Println(preview.GetDiff())
```
The response answers the questions the stored document can't:
| Field | What it tells you |
|-------|-------------------|
| `requested_mode` / `effective_mode` | Whether the refresh ran in the mode you configured |
| `mode_fallback_reason` | Why the delta edits weren't applied: `no_baseline_content`, `source_query_changed`, `structured_doc_unreadable`, `delta_ops_failed`, or `delta_ops_all_skipped` (every operation was rejected) |
| `scope` | The tags, match mode, and fact types that actually filtered memories — not the ones stored on the model |
| `window` | The `created_after`/`created_before` bounds read, and the watermark that would be persisted |
| `facts.retrieved` vs `facts.used` | How much retrieval returned versus how much the reflect agent judged relevant |
| `delta_operations` | The operations emitted, applied and skipped |
| `diff` | A unified diff from the stored content to the content it would write |
| `outcome` / `would_persist` | Whether a real refresh would write, keep the content, or fail |
| `warnings` | Conditions worth attention, in plain language |
The dry run takes no parameters, on purpose. It is the production refresh pipeline
with exactly two writes skipped — the content (and with it the structured document
and history entry) and the watermark that moves `last_refreshed_at`. Nothing about
how it runs can be configured, because a dry run you can configure stops predicting
the refresh it exists to predict. To preview a different configuration, change the
model with `PATCH` and run it again.
> **⚠️ Warning**
>
A dry run runs the same LLM calls a real refresh does, so it costs the same tokens
and takes the same time. It is validated and billed like a refresh.
### Keep traces: record every refresh as it happens
A dry run only explains a refresh you run yourself. Refreshes driven by
`refresh_after_consolidation`, `refresh_cron` or `full_refresh_cron` happen with nobody watching, and by
the time you notice a bad document, the run that produced it is gone.
Setting `trigger.keep_trace` records the same reasoning on every refresh of that
model — scheduled ones included — under `reflect_response.trace`:
### Python
```python
# Record how every refresh (scheduled ones too) reached its result
client.update_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id,
trigger={"mode": "delta", "keep_trace": True}
)
```
### Node.js
```javascript
// Record how every refresh (scheduled ones too) reached its result
await client.updateMentalModel(BANK_ID, mentalModelId, {
trigger: { mode: 'delta', keepTrace: true },
});
```
### CLI
```bash
# Record how every refresh (scheduled ones too) reached its result
hindsight mental-model update "$BANK_ID" "$MENTAL_MODEL_ID" \
--trigger-mode delta \
--trigger-keep-trace true
```
### Go
```go
// Record how every refresh (scheduled ones too) reached its result
mode := "delta"
keepTrace := true
_, _, err = client.MentalModelsAPI.UpdateMentalModel(ctx, mmBankID, mentalModelID).
UpdateMentalModelRequest(hindsight.UpdateMentalModelRequest{
Trigger: *hindsight.NewNullableMentalModelTriggerInput(&hindsight.MentalModelTriggerInput{
Mode: &mode,
KeepTrace: &keepTrace,
}),
}).Execute()
if err != nil {
panic(err)
}
```
Only the latest refresh's trace is kept, and it is recorded even when a refresh
fails — which is when it matters most, since a failed refresh leaves nothing else
behind to inspect.
#### What a trace contains
The trace is deliberately shaped like a [reflect](./reflect.md) trace: the calls the
agent made, plus the decision specific to a refresh. It is stored on the mental
model row and re-read on every fetch, so it holds nothing that can be derived from
somewhere else.
| Field | Contents |
|-------|----------|
| `effective_mode` | Whether the run ended up `full` or `delta` |
| `mode_fallback_reason` | Why delta was requested but not applied — `no_baseline_content`, `source_query_changed`, `structured_doc_unreadable`, `delta_ops_failed`, `delta_ops_all_skipped` |
| `outcome` | `content_written`, `content_preserved_no_new_facts`, `refresh_failed_empty_candidate`, or `refresh_failed_delta_not_applied` (the edits didn't apply, so the document was kept and the refresh failed). The operation record adds two the executor cannot produce, because they happen outside a run: `refresh_failed_structured_output` and `refresh_failed_error` (a retrieval tool raised, the agent produced no answer, or something unforeseen escaped the refresh) |
| `tool_calls[]` | Per call: `tool`, the agent's `reason`, the full `input`, `result_count`, `duration_ms`, and the `iteration` it belongs to |
| `llm_calls[]` | Per call: `scope` (`agent_1`, `agent_2`, …, `final`) and `duration_ms` |
| `delta_operations` | The operations emitted in delta mode, `applied` and `skipped` |
| `usage` | Token counts across the run's LLM calls |
| `recorded_at`, `duration_ms`, `warnings[]` | When it ran, how long it took, and anything worth a human's attention |
#### What a trace deliberately omits
- **Tool outputs.** Only `result_count` is kept. Recall payloads are large and the
trace is re-read on every model fetch, so storing them would grow the row without
bound. A count is enough to see *that* a tool came back empty; when you need the
raw prompts and responses, use [LLM request tracing](../monitoring.md), which stores
them separately and expires them on its own retention schedule.
- **The evidence.** The facts a refresh grounded the document on already live in
`reflect_response.based_on`, in the same shape reflect uses. The trace does not
duplicate them.
- **The resolved scope and time window.** These describe what the model's current
configuration reads rather than what a past run did, so they are returned by the
dry run instead of being written on every refresh.
This keeps a stored trace small: a few kilobytes for a typical run, dominated by the
tool inputs.
---
## Clear a Mental Model
Clear a mental model's content so the next refresh performs a **full re-synthesis** from scratch, regardless of the model's trigger mode.
This is useful for delta-mode models that have accumulated drift over many incremental refreshes. Over time, small inaccuracies can compound as each delta refresh only sees new facts since the last. Clearing and then refreshing produces a clean baseline from all facts.
### Python
```python
# Clear a mental model's content, then refresh for a full re-synthesis
client.clear_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
# Trigger a fresh full rebuild
result = client.refresh_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
print(f"Full refresh operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Clear a mental model's content, then refresh for a full re-synthesis
await client.clearMentalModel(BANK_ID, mentalModelId);
// Trigger a fresh full rebuild
const fullRefreshResult = await client.refreshMentalModel(BANK_ID, mentalModelId);
console.log(`Full refresh operation ID: ${fullRefreshResult.operation_id}`);
```
### CLI
```bash
# Clear a mental model's content, then refresh for a full re-synthesis
curl -s -X POST "${HINDSIGHT_URL}/v1/default/banks/${BANK_ID}/mental-models/${MENTAL_MODEL_ID}/clear"
# Trigger a fresh full rebuild
hindsight mental-model refresh "$BANK_ID" "$MENTAL_MODEL_ID"
```
### Go
```go
// Clear a mental model's content, then refresh for a full re-synthesis
client.MentalModelsAPI.ClearMentalModel(ctx, mmBankID, mentalModelID).Execute()
// Trigger a fresh full rebuild
fullRefreshResult, _, _ := client.MentalModelsAPI.RefreshMentalModel(ctx, mmBankID, mentalModelID).Execute()
fmt.Printf("Full refresh operation ID: %s\n", fullRefreshResult.GetOperationId())
```
The clear operation is synchronous and resets the content to an empty string. The model's configuration (name, source query, trigger settings) is preserved. Since the content is now empty, the next `/refresh` call will always perform a full regeneration — even if the model's trigger mode is set to `delta`.
> **💡 Tip**
>
To clear a delta model's drift, prefer a [full refresh](#refresh-a-mental-model) (`{"mode": "full"}`), or set
[`full_refresh_cron`](#scheduled-full-refresh) to do it on a schedule. Unlike clearing first,
a full refresh that fails leaves the current content in place.
---
## Update a Mental Model
Update the mental model's name:
### Python
```python
# Update a mental model's metadata
updated = client.update_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id,
name="Updated Team Communication Preferences",
trigger={"refresh_after_consolidation": True} # Enable auto-refresh
)
print(f"Updated name: {updated.name}")
```
### Node.js
```javascript
// Update a mental model's metadata
const updated = await client.updateMentalModel(BANK_ID, mentalModelId, {
name: 'Updated Team Communication Preferences',
trigger: { refreshAfterConsolidation: true },
});
console.log(`Updated name: ${updated.name}`);
```
### CLI
```bash
# Update a mental model's metadata
hindsight mental-model update "$BANK_ID" "$MENTAL_MODEL_ID" \
--name "Updated Team Communication Preferences"
```
### Go
```go
// Update a mental model's metadata
newName := "Updated Team Communication Preferences"
refreshAfter := true
updated, _, _ := client.MentalModelsAPI.UpdateMentalModel(ctx, mmBankID, mentalModelID).
UpdateMentalModelRequest(hindsight.UpdateMentalModelRequest{
Name: *hindsight.NewNullableString(&newName),
Trigger: *hindsight.NewNullableMentalModelTriggerInput(&hindsight.MentalModelTriggerInput{
RefreshAfterConsolidation: &refreshAfter,
}),
}).Execute()
fmt.Printf("Updated name: %s\n", updated.GetName())
```
---
## Delete a Mental Model
### Python
```python
# Delete a mental model
client.delete_mental_model(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
```
### Node.js
```javascript
// Delete a mental model
await client.deleteMentalModel(BANK_ID, mentalModelId);
```
### CLI
```bash
# Delete a mental model
hindsight mental-model delete "$BANK_ID" "$MENTAL_MODEL_ID" -y
```
### Go
```go
// Delete a mental model
client.MentalModelsAPI.DeleteMentalModel(ctx, mmBankID, mentalModelID).Execute()
```
---
## Tags and Visibility
Mental models support the same tag system as memories. When you assign tags to a mental model, those tags control both **which memories it reads** during refresh and **when it is surfaced** during reflect.
### How tags affect mental model refresh
> **⚠️ Warning**
>
Adding tags to a mental model narrows the pool of source memories its refresh can read from. If no memories carry those tags yet, refresh will return empty content (e.g. `"I cannot find any information…"`) even though direct `reflect` on the same query works. Backfill tags on the relevant memories first, or override the default via `trigger.tags_match` / `trigger.tag_groups`.
When a mental model is refreshed (manually or automatically), it runs an internal reflect call to regenerate its content. If the mental model has tags, that reflect call uses `all_strict` tag matching — meaning it will only read memories that carry **all** of the mental model's tags. Untagged memories are excluded.
```
Mental model tags: ["user:alice"]
During refresh, it reads:
✅ "Alice prefers async communication" — has "user:alice"
✅ "Team uses Slack for announcements" — has "user:alice" (plus other tags)
❌ "Company policy: no meetings on Fridays" — untagged, excluded
❌ "Bob dislikes long meetings" — no "user:alice" tag
```
This means a mental model tagged `["user:alice"]` will also pick up memories tagged `["user:alice", "team"]` — extra tags on a memory don't disqualify it. Only the mental model's own tags are required to be present.
#### Overriding the default with `tags_match`
The `all_strict` default is the safest choice for single-tag models, but it filters out everything for a **multi-tag** model whose memories only carry one tag each. If a model tagged `["projects", "mental-model"]` reads from memories tagged narrowly (`["project:status"]`, `["tooling"]`, …), no single memory carries *all* the model's tags and the refresh comes back empty.
To match memories that carry **any** of the model's tags — the same default `recall` and `reflect` use — set `trigger.tags_match` to `"any"` at creation:
### Python
```python
# Override how the model's tags filter source memories on refresh.
# A tagged model defaults to "all_strict" (a memory must carry EVERY tag);
# use "any" when your memories are tagged narrowly (one topic each), so the
# refresh reads any memory carrying at least one of the model's tags.
result = client.create_mental_model(
bank_id=BANK_ID,
name="Current Projects",
source_query="Which projects is the user currently working on?",
tags=["projects", "mental-model"],
trigger={"tags_match": "any"}
)
print(f"Operation ID: {result.operation_id}")
```
### Node.js
```javascript
// Override how the model's tags filter source memories on refresh.
// A tagged model defaults to 'all_strict' (a memory must carry EVERY tag);
// use 'any' when your memories are tagged narrowly (one topic each), so the
// refresh reads any memory carrying at least one of the model's tags.
const result3 = await client.createMentalModel(
BANK_ID,
'Current Projects',
'Which projects is the user currently working on?',
{
tags: ['projects', 'mental-model'],
trigger: { tagsMatch: 'any' },
},
);
console.log(`Operation ID: ${result3.operation_id}`);
```
### CLI
```bash
# Override how the model's tags filter source memories on refresh.
# A tagged model defaults to all_strict (a memory must carry EVERY tag);
# pass --tags-match any when your memories are tagged narrowly (one topic
# each), so the refresh reads any memory carrying at least one of the tags.
hindsight mental-model create "$BANK_ID" \
"Current Projects" \
"Which projects is the user currently working on?" \
--tags projects,mental-model \
--tags-match any
```
### Go
```go
// Override how the model's tags filter source memories on refresh.
// A tagged model defaults to "all_strict" (a memory must carry EVERY tag);
// use "any" when your memories are tagged narrowly (one topic each), so the
// refresh reads any memory carrying at least one of the model's tags.
result3, _, _ := client.MentalModelsAPI.CreateMentalModel(ctx, mmBankID).
CreateMentalModelRequest(hindsight.CreateMentalModelRequest{
Name: "Current Projects",
SourceQuery: "Which projects is the user currently working on?",
Tags: []string{"projects", "mental-model"},
Trigger: &hindsight.MentalModelTriggerInput{
TagsMatch: *hindsight.NewNullableString(hindsight.PtrString("any")),
},
}).Execute()
fmt.Printf("Operation ID: %s\n", result3.GetOperationId())
```
The MCP `create_mental_model` tool exposes the same option as a top-level `tags_match` argument. Available modes are `any`, `all`, `any_strict`, `all_strict`, and `exact` — see the [Recall tags reference](./recall.md#tags) for their exact semantics.
### How tags affect mental model lookup during reflect
When you call `reflect` with tags, those same tags are used to filter which mental models the agent can see. A mental model is visible only if its tags overlap with the tags on the reflect request.
For more details on tag matching modes (`any`, `any_strict`, `all`, `all_strict`) and worked examples, see the [Recall tags reference](./recall.md#tags).
### Listing mental model tags
`GET /v1/default/banks/{bank_id}/tags` accepts a `source` query parameter that selects which tag space to enumerate:
- `source=memories` *(default)* — tags attached to memory units.
- `source=mental_models` — tags attached to mental models in this bank.
Use the `mental_models` source to populate autocomplete or filter UIs over mental-model tags, distinct from the (typically larger) memory tag set.
---
## History
Every time a mental model's content changes (via refresh or manual update), the previous version is saved with a timestamp. You can retrieve the full change log with the history endpoint:
### Python
```python
# Get the change history of a mental model
history = client.get_mental_model_history(
bank_id=BANK_ID,
mental_model_id=mental_model_id
)
for entry in history:
print(f"Changed at: {entry['changed_at']}")
print(f"Previous content: {entry['previous_content']}")
```
### Node.js
```javascript
// Get the change history of a mental model
const history = await client.getMentalModelHistory(BANK_ID, mentalModelId);
for (const entry of history) {
console.log(`Changed at: ${entry.changed_at}`);
console.log(`Previous content: ${entry.previous_content}`);
}
```
### CLI
```bash
# Get the change history of a mental model
hindsight mental-model history "$BANK_ID" "$MENTAL_MODEL_ID"
```
### Go
```go
// Get the change history of a mental model
history, _, _ := client.MentalModelsAPI.GetMentalModelHistory(ctx, mmBankID, mentalModelID).Execute()
if entries, ok := history.([]interface{}); ok {
for _, entry := range entries {
if e, ok := entry.(map[string]interface{}); ok {
fmt.Printf("Changed at: %v\n", e["changed_at"])
fmt.Printf("Previous content: %v\n", e["previous_content"])
}
}
}
```
### Response
The endpoint returns a list of history entries, most recent first:
| Field | Type | Description |
|-------|------|-------------|
| `previous_content` | string \| null | The content before this change (`null` if not available, and always `null` on a failure record) |
| `changed_at` | string | ISO 8601 timestamp of when the change occurred |
| `kind` | `"refresh_failed"` \| absent | Present only on a **failure record** — a refresh that refused to write. Absent on version snapshots |
| `outcome` | string \| absent | On a failure record: the operation outcome, e.g. `refresh_failed_error` |
| `failure_reason` | string \| absent | On a failure record: why it refused — `retrieval_failed`, `no_answer`, `unexpected_error`, `empty_candidate`, `structured_doc_unreadable`, `delta_ops_failed`, `delta_ops_all_skipped`, `delta_not_applied`, `structured_output_failed` |
| `error_message` | string \| absent | On a failure record: the exception, as the operation reports it |
Each version entry captures the **content before the change** and when it happened. The current content is returned by the standard [Get a Mental Model](#get-a-mental-model) endpoint.
**Failures are recorded too.** A refresh that refuses to write produces no version, so before this it left no trace on the model at all — a document whose refreshes had been failing for a week was indistinguishable from one nobody had refreshed, and its stored `reflect_response` still described the last run that *succeeded*. Those runs now append a failure record carrying the reason and the exception. Read them by their `kind`: a client that only wants versions filters `kind` out, and one that wants to know whether the model is current checks whether the newest entry is a failure.
Retention is applied per kind, so a run of failures cannot evict the version history — each is capped at `HINDSIGHT_API_MENTAL_MODEL_HISTORY_MAX_ENTRIES` independently. A retried refresh records one entry per attempt.
> **📝 Note**
>
History tracking is enabled by default. Set `HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY=false` to disable it, and `HINDSIGHT_API_MENTAL_MODEL_HISTORY_MAX_ENTRIES` to change how many versions are kept per model (default `50`).
---
## Use Cases
| Use Case | Example |
|----------|---------|
| **FAQ Answers** | Pre-compute answers to common customer questions |
| **Onboarding Summaries** | "What should new team members know?" |
| **Status Reports** | "What's the current project status?" refreshed weekly |
| **Policy Summaries** | "What are our security policies?" |
---
## Next Steps
- [**Reflect**](./reflect.md) — How the agentic loop uses mental models
- [**Observations**](../observations.md) — How knowledge is consolidated
- [**Operations**](./operations.md) — Track async mental model creation
references/developer/api/operations.md
# Operations
Hindsight runs several maintenance and ingestion tasks asynchronously instead of blocking the API call that triggers them. These tasks share a single queue (`async_operations`) and a single worker pool, and the same REST endpoints — list, status, cancel, retry — work across every type.
This page explains each operation type, when it fires, and how to inspect or manage it.
{/* Import raw source files */}
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) and understand [how retain works](./retain.md).
## How operations work
When an API call needs background work, the request handler writes a row to the `async_operations` table with `status=pending` and returns immediately. A worker (running either in-process inside the API by default, or as a dedicated service — see [Services - Worker Service](../services.md#worker-service)) polls the table, claims pending rows, executes the corresponding handler, and marks the row `completed` or `failed`.
By default, every operation runs in-process: no external queue, no extra process to deploy. The same code paths support scaling out to dedicated worker processes when throughput demands it.
### Lifecycle
| Status | Meaning |
|--------|---------|
| `pending` | The row is queued. Either no worker has picked it up yet, or it is parked via `next_retry_at` in the future (e.g., backpressure, or an LLM provider's reported quota reset). `POST /…/retry` on a parked operation clears `next_retry_at` so it runs now. |
| `processing` | A worker has claimed the row and is actively running the handler. |
| `completed` | The handler returned successfully. |
| `failed` | The handler raised. `error_message` carries the reason; you can re-queue with `POST /…/retry`. |
| `cancelled` | The operation was cancelled via `DELETE /…/operations/{id}`. Works on `pending` and `processing` operations alike. |
The worker retries failed operations up to `HINDSIGHT_API_WORKER_MAX_RETRIES` times before settling on `failed`. Deterministic failures (e.g., invalid embedding dimensions, integrity violations, a provider content-policy refusal, or the LLM rejecting the configured key with HTTP 401/403) skip retries — they won't succeed by re-running. Fix the cause, then retry the operation.
Completed, failed, and cancelled operations are kept indefinitely by default. Set `HINDSIGHT_API_OPERATION_RETENTION_DAYS` to a positive number of days to bound that history: the background maintenance loop then prunes expired terminal rows in bounded batches, on its own schedule rather than as a side effect of task processing. PostgreSQL only — the maintenance loop does not run on Oracle, so operation history is unbounded there. The full row shares that TTL, so while an operation is retained its payload stays available — failed and cancelled operations can be retried, and completed ones inspected with `include_payload=true`. Pending and processing operations are never removed by retention cleanup.
## Operation types
Every operation has an `operation_type` in the database and a `task_type` in the payload. They're usually the same.
### `retain`
Submitted by `POST /v1/default/banks/{bank_id}/memories` with `async=true`, or by the multi-item `retain_batch` call. The handler runs the same pipeline as a synchronous retain: fact extraction (LLM), embedding generation, entity resolution, and link creation (temporal, semantic).
Use async retain when you're ingesting thousands of items and don't want the HTTP call to hold for minutes. The `operation_id` in the response lets you poll for completion.
#### Parent op: `retain_batch`
For large submissions, Hindsight automatically splits the input into sub-batches and creates a single `retain_batch` parent operation that tracks the children. The parent's status reflects the aggregate — `pending` until at least one child is running, `processing` while children execute, `completed` once every child has finished, `failed` if any child failed. Each child is itself a `retain` operation linked to the parent, so you can drill in for per-batch error messages.
When you list operations, the parent and its children all appear by default. Pass `exclude_parents=true` to hide the aggregate rows and show only individual `retain` jobs.
### `file_convert_retain`
Submitted by file upload endpoints. The handler runs MIME-specific conversion (PDF → text, DOCX → text, etc.) and then passes the extracted text into the retain pipeline. Failures here are **non-retryable** by default — a corrupted PDF or missing OCR won't improve on rerun, so the operation goes straight to `failed`.
Which parser runs (`markitdown`, `iris`, or `llama_parse`) is selected per deployment via `HINDSIGHT_API_FILE_PARSER`, and clients can override it per request — see [Configuration → File Processing](../configuration.md#file-processing).
### `consolidation`
Produces **observations** from new world/experience memories. See [Observations](../observations.md) for what they are and how they're synthesized.
Triggered automatically:
- After every retain that added world/experience facts (gated by per-bank `enable_auto_consolidation` and `enable_observations`).
- After deletes that invalidated existing observations (the source memory disappeared → derived observations are stale → re-run with the surviving co-source memories).
- Manually via `POST /v1/default/banks/{bank_id}/consolidate`. Pass `observation_scopes` to consolidate only memories matching specific tag combinations.
**Bank-deduped**: while one `consolidation` job is pending for a bank, repeat submits return the existing `operation_id` instead of stacking. Once the job starts processing, the next submit becomes the next pending slot.
### `refresh_mental_model`
A mental model has a `source_query` that defines which memories it summarizes. The handler re-runs that query, re-summarizes the result, and updates the model's content in place.
Triggered either manually via `POST /v1/default/banks/{bank_id}/mental-models/{id}/refresh`, or automatically by the auto-refresh schedule for mental models that have one configured.
### `graph_maintenance`
Reconciles derived state that goes stale after a delete. Every invocation drains two queues, both filled by the delete itself, so a run only ever looks at what that delete touched:
1. **Link top-up.** Drains the units whose outgoing temporal/semantic links lost a neighbour. For each, if the unit is under its cap (20 temporal, 50 semantic), Hindsight re-runs the same probes retain uses and inserts the missing links. Without this, the retain pipeline's top-K capping would leave surviving units permanently under-capped after every delete — degrading graph-expansion recall.
2. **Entity prune.** Drains the entities the delete may have stranded. Those with no remaining `unit_entities` reference are deleted (FK `ON DELETE CASCADE` removes their `entity_cooccurrences` rows with them); for the survivors, cooccurrence rows where both endpoints still exist but no current memory_unit references both are cleaned up — the cooccurrence was real when recorded, but every unit that witnessed it has since been deleted.
Bank-deduped at submit time, so concurrent triggers against the same bank coalesce into one drain.
Each run works in committed batches under a wall-clock budget. A backlog too large for one run — a bulk delete, say — is not an error: the run reports what it finished and the next one resumes where it stopped.
**Triggers:** any delete that removes memory_units — `DELETE /documents/{id}`, `DELETE /memories/{id}`, and re-retaining an existing `document_id` (the upsert path). A full bank wipe (`delete_bank`) is a no-op: there's nothing left in the bank to maintain.
### `webhook_delivery`
After certain operations complete (e.g., consolidation finishing on a bank with a registered webhook), Hindsight enqueues a `webhook_delivery` task. The handler POSTs the payload to the configured URL and retries on transient failures.
## Endpoints
All paths below are scoped by `bank_id`.
### List operations
```bash
GET /v1/default/banks/{bank_id}/operations
```
Query parameters:
| Param | Description |
|-------|-------------|
| `status` | Filter by `pending`, `processing`, `completed`, `failed`, `cancelled`. |
| `type` | Filter by `retain`, `file_convert_retain`, `consolidation`, `refresh_mental_model`, `graph_maintenance`, `webhook_delivery`. |
| `limit` | 1–100, default 20. |
| `offset` | Pagination offset. |
| `exclude_parents` | Exclude parent batch operations from results (large `retain_batch` calls create one parent + N children). |
### Python
```python
# List recent operations for a bank (default: 20 most recent).
result = await client.operations.list_operations("my-bank")
for op in result.operations:
print(op.id, op.task_type, op.status)
# Filter by status and type.
pending_recompute = await client.operations.list_operations(
"my-bank", status="pending", type="graph_maintenance"
)
# Hide retain_batch parent rows (show only individual child retain jobs).
flat = await client.operations.list_operations("my-bank", exclude_parents=True)
```
### Node.js
```javascript
// List recent operations for a bank (default: 20 most recent).
const { data: recent } = await sdk.listOperations({
client: apiClient,
path: { bank_id: 'my-bank' },
});
for (const op of recent.operations) {
console.log(op.id, op.task_type, op.status);
}
// Filter by status and type.
const { data: pendingRecompute } = await sdk.listOperations({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { status: 'pending', type: 'graph_maintenance' },
});
// Hide retain_batch parent rows (show only individual child retain jobs).
const { data: flat } = await sdk.listOperations({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { exclude_parents: true },
});
```
### CLI
```bash
hindsight operation list my-bank
```
### Go
```go
// List recent operations for a bank (default: 20 most recent).
recent, _, err := client.OperationsAPI.ListOperations(ctx, "my-bank").Execute()
if err != nil {
log.Fatalf("list operations: %v", err)
}
for _, op := range recent.Operations {
fmt.Println(op.Id, op.TaskType, op.Status)
}
// Filter by status and type.
_, _, _ = client.OperationsAPI.ListOperations(ctx, "my-bank").
Status("pending").
Type_("graph_maintenance").
Execute()
// Hide retain_batch parent rows (show only individual child retain jobs).
_, _, _ = client.OperationsAPI.ListOperations(ctx, "my-bank").
ExcludeParents(true).
Execute()
```
`items_count` is operation-specific — non-zero only for retain-shaped operations (it counts content items in the submission).
### Get operation status
### Python
```python
status = await client.operations.get_operation_status("my-bank", seeded_id)
print(status.status, status.error_message)
# Include the submission payload (can be large for retain batches).
detailed = await client.operations.get_operation_status(
"my-bank", seeded_id, include_payload=True
)
```
### Node.js
```javascript
const { data: status } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'my-bank', operation_id: '550e8400-e29b-41d4-a716-446655440000' },
});
console.log(status.status, status.error_message);
// Include the submission payload (can be large for retain batches).
const { data: detailed } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'my-bank', operation_id: '550e8400-e29b-41d4-a716-446655440000' },
query: { include_payload: true },
});
```
### CLI
```bash
hindsight operation get my-bank "$OPERATION_ID"
```
### Go
```go
status, _, err := client.OperationsAPI.GetOperationStatus(ctx, "my-bank", operationID).Execute()
if err != nil {
log.Fatalf("get status: %v", err)
}
fmt.Println(status.Status, status.ErrorMessage)
// Include the submission payload (can be large for retain batches).
_, _, _ = client.OperationsAPI.GetOperationStatus(ctx, "my-bank", operationID).
IncludePayload(true).
Execute()
```
Query parameters:
| Param | Description |
|-------|-------------|
| `include_payload` | Include the raw task payload (the submission params) in the response as `task_payload`. Default `false`; may be large. |
A few response fields are worth calling out:
| Field | Description |
|-------|-------------|
| `updated_at` | When the operation's row last changed — claim, progress heartbeat, or completion. |
| `progress` | Last-known progress snapshot for a running operation, or `null` if none was recorded (completed-instantly or pre-feature rows). |
| `task_payload` | The raw submission params; only populated when `include_payload=true`. |
`progress` is written at coarse phase/batch boundaries (consolidation, batch retain) and lets you tell a healthy long-running job from a frozen one: if `processed` keeps advancing across polls the job is alive; identical numbers with no movement in `at` mean it's stuck. Its shape:
| Field | Description |
|-------|-------------|
| `stage` | Coarse phase the operation last reported (e.g. `processing_batch`). |
| `at` | ISO-8601 timestamp when this snapshot was written. |
| `processed` | Units of work finished so far (sub-batches, memories), when known. |
| `total` | Total units of work for the operation, when known. |
| `detail` | Operation-specific counters (e.g. `observations_created`, `round`, `items_in_sub_batch`). |
### Cancel an operation
Cancels a `pending` or `processing` operation. Returns `409` if it has already reached a
terminal state (`completed`, `failed`, `cancelled`).
The row is marked `cancelled` straight away, but cancelling running work is **cooperative
and not immediate**. A worker executing the operation notices at its next checkpoint — the
boundary between sub-batches or documents for retain, between LLM batches for consolidation —
and stops there, so whatever it had already committed stays committed and the batch in
progress may still finish. Operation types without checkpoints run to the end; the row stays
`cancelled` either way, because no worker write may overwrite that status.
This is also how you clear an operation stranded in `processing` by a worker that was killed
before it could finish: nothing is running, so the cancel takes effect immediately. Use
`POST /…/operations/{id}/retry` to re-queue the work afterwards.
### Python
```python
# Cancel a pending operation before a worker claims it.
# Returns 409 if the operation is already processing/completed/failed.
try:
await client.operations.cancel_operation("my-bank", seeded_id)
except Exception:
# Already in a non-pending state — fine for this example.
pass
```
### Node.js
```javascript
// Cancel a pending operation before a worker claims it.
// Returns 409 if the operation is already processing/completed/failed.
await sdk.cancelOperation({
client: apiClient,
path: { bank_id: 'my-bank', operation_id: '550e8400-e29b-41d4-a716-446655440000' },
});
```
### CLI
```bash
hindsight operation cancel my-bank "$OPERATION_ID"
```
### Go
```go
// Cancel a pending operation before a worker claims it.
// Returns 409 if the operation is already processing/completed/failed.
_, _, _ = client.OperationsAPI.CancelOperation(ctx, "my-bank", operationID).Execute()
```
### Retry a failed operation
The row's status resets to `pending` and the worker picks it up again. Returns `409` if the operation isn't in `failed` or `cancelled` state.
### Python
```python
# Re-queue a failed (or cancelled) operation.
# Returns 409 if the operation isn't in failed/cancelled state.
try:
await client.operations.retry_operation("my-bank", seeded_id)
except Exception:
# Operation already in a terminal state we can't retry — fine here.
pass
```
### Node.js
```javascript
// Re-queue a failed (or cancelled) operation.
// Returns 409 if the operation isn't in failed/cancelled state.
await sdk.retryOperation({
client: apiClient,
path: { bank_id: 'my-bank', operation_id: '550e8400-e29b-41d4-a716-446655440000' },
});
```
### CLI
```bash
hindsight operation retry my-bank "$OPERATION_ID"
```
### Go
```go
// Re-queue a failed (or cancelled) operation.
// Returns 409 if the operation isn't in failed/cancelled state.
_, _, _ = client.OperationsAPI.RetryOperation(ctx, "my-bank", operationID).Execute()
```
## Async retain example
Submit a batch asynchronously and poll until the operation completes:
### Python
```python
# Submit a batch asynchronously — the call returns immediately with an
# operation_id you can poll.
submission = await client.aretain_batch(
bank_id="my-bank",
items=[
{"content": "Alice joined Google in 2023"},
{"content": "Bob prefers Python over JavaScript"},
],
retain_async=True,
)
op_id = submission.operation_id
while True:
s = await client.operations.get_operation_status("my-bank", op_id)
if s.status in ("completed", "failed", "cancelled"):
print(f"finished: {s.status}")
break
await asyncio.sleep(2)
```
### Node.js
```javascript
// Submit a large batch asynchronously — the call returns immediately with an
// operation_id you can poll.
const submission = await client.retainBatch('my-bank', [
{ content: 'Alice joined Google in 2023' },
{ content: 'Bob prefers Python over JavaScript' },
], { async: true });
const operationId = submission.operation_id;
while (true) {
const { data: s } = await sdk.getOperationStatus({
client: apiClient,
path: { bank_id: 'my-bank', operation_id: operationId },
});
if (['completed', 'failed', 'cancelled'].includes(s.status)) {
console.log(`finished: ${s.status}`);
break;
}
await new Promise((r) => setTimeout(r, 2000));
}
```
### CLI
```bash
# Submit an async retain and capture the operation_id from the JSON response.
OPERATION_ID=$(
hindsight memory retain my-bank "Alice joined Google in 2023" --async -o json \
| jq -r '.operation_id'
)
# Poll until the worker finishes — completed/failed/cancelled are all terminal.
while true; do
STATUS=$(hindsight operation get my-bank "$OPERATION_ID" -o json | jq -r '.status')
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "cancelled" ]; then
echo "finished: $STATUS"
break
fi
sleep 2
done
```
### Go
```go
// Submit a large batch asynchronously — the call returns immediately with
// an operation_id you can poll.
async := true
resp, _, err := client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Alice joined Google in 2023")},
{Content: hindsight.TextContent("Bob prefers Python over JavaScript")},
},
Async: &async,
}).Execute()
if err != nil {
log.Fatalf("retain: %v", err)
}
opID := resp.OperationId.Get()
for {
s, _, err := client.OperationsAPI.GetOperationStatus(ctx, "my-bank", *opID).Execute()
if err != nil {
log.Fatalf("poll: %v", err)
}
if s.Status == "completed" || s.Status == "failed" || s.Status == "cancelled" {
fmt.Println("finished:", s.Status)
break
}
time.Sleep(2 * time.Second)
}
```
## Worker tuning
Each worker has a single concurrency budget (`HINDSIGHT_API_WORKER_MAX_SLOTS`, default 10) shared across all operation types. Per-type slot reservations (`HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS`) carve out guaranteed capacity within that budget; remaining slots form a shared pool any type can use. See [Configuration → Worker Configuration](../configuration.md#distributed-workers) for the full table.
For most deployments the defaults are fine. Reserve slots for an operation type if you've seen it starved by a flood of another type (e.g., a long file_convert_retain blocking graph_maintenance on a deletion-heavy workload).
Slots are also rotated across banks. Each claim serves the next bank in turn — one operation — then fills the rest of the pool oldest-first from anywhere. So a bank ingesting in bulk cannot own the whole pool while another bank's single write waits behind its backlog, and it is not throttled either: when no one else is waiting it still takes every slot.
## Next Steps
- [**Documents**](./documents.md) — Track document sources
- [**Memory Banks**](./memory-banks.md) — Configure bank settings
references/developer/api/quickstart.md
# Quick Start
Get up and running with Hindsight in 60 seconds.
{/* Import raw source files */}
## Clients
<ClientsGrid />
## Start the API Server
### pip (API only)
```bash
pip install hindsight-api
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY
hindsight-api
```
API available at [http://localhost:8888](http://localhost:8888/docs)
### Docker (Full Experience)
```bash
export OPENAI_API_KEY=sk-xxx
docker run -it --pull always --name hindsight --restart unless-stopped --shm-size=1g -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- **API**: http://localhost:8888
- **Control Plane** (Web UI): http://localhost:9999
> **💡 Set a stable `HINDSIGHT_API_WORKER_ID` in production**
>
The worker uses the container hostname as its identity, which Docker sets to the container ID by default. That value changes on every restart, so any task that was being processed when the container went down stays parked under the old ID with no way for the new container to recognize it as its own.
Set `HINDSIGHT_API_WORKER_ID` to a stable value (e.g., `-e HINDSIGHT_API_WORKER_ID=hindsight-prod`) so the worker keeps the same identity across restarts. This is recommended even for single-container deployments. For diagnosis and recovery commands, see [Admin CLI - Recovering stuck operations](../admin-cli.md#recovering-stuck-or-zombie-operations).
> **💡 LLM Provider**
>
Hindsight requires an LLM with structured output support. Recommended: **Groq** with `gpt-oss-20b` for fast, cost-effective inference.
See [LLM Providers](../models.md#llm) for more details.
---
## Use the Client
### Python
```bash
pip install hindsight-client
```
```python
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
# Retain: Store information
client.retain(bank_id="my-bank", content="Alice works at Google as a software engineer")
# Recall: Search memories
client.recall(bank_id="my-bank", query="What does Alice do?")
# Reflect: Generate disposition-aware response
client.reflect(bank_id="my-bank", query="Tell me about Alice")
```
### Node.js
```bash
npm install @vectorize-io/hindsight-client
```
```javascript
import { HindsightClient } from '@vectorize-io/hindsight-client';
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
// Retain: Store information
await client.retain('my-bank', 'Alice works at Google as a software engineer');
// Recall: Search memories
await client.recall('my-bank', 'What does Alice do?');
// Reflect: Generate response
await client.reflect('my-bank', 'Tell me about Alice');
```
### CLI
```bash
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
```
```bash
# Retain: Store information
hindsight memory retain my-bank "Alice works at Google as a software engineer"
# Recall: Search memories
hindsight memory recall my-bank "What does Alice do?"
# Reflect: Generate response
hindsight memory reflect my-bank "Tell me about Alice"
```
### Go
```bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go
```
```go
cfg := hindsight.NewConfiguration()
cfg.Servers = hindsight.ServerConfigurations{
{URL: "http://localhost:8888"},
}
client := hindsight.NewAPIClient(cfg)
ctx := context.Background()
// Retain a memory
retainReq := hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Alice works at Google")},
},
}
client.MemoryAPI.RetainMemories(ctx, "my-bank").RetainRequest(retainReq).Execute()
// Recall memories
recallReq := hindsight.RecallRequest{
Query: "What does Alice do?",
}
resp, _, _ := client.MemoryAPI.RecallMemories(ctx, "my-bank").RecallRequest(recallReq).Execute()
for _, r := range resp.Results {
fmt.Println(r.Text)
}
// Reflect - generate response
reflectReq := hindsight.ReflectRequest{
Query: "Tell me about Alice",
}
answer, _, _ := client.MemoryAPI.Reflect(ctx, "my-bank").ReflectRequest(reflectReq).Execute()
fmt.Println(answer.GetText())
```
---
## What's Happening
| Operation | What it does |
|-----------|--------------|
| **Retain** | Content is processed, facts are extracted, entities are identified and linked in a knowledge graph |
| **Recall** | Four search strategies (semantic, keyword, graph, temporal) run in parallel to find relevant memories |
| **Reflect** | Retrieved memories are used to generate a disposition-aware response |
---
## Integrations
Browse all supported integrations in the Integrations Hub.
## Next Steps
- [**Retain**](./retain.md) — Advanced options for storing memories
- [**Recall**](./recall.md) — Search and retrieval strategies
- [**Reflect**](./reflect.md) — Disposition-aware reasoning
- [**Memory Banks**](./memory-banks.md) — Configure disposition and mission
- [**Server Deployment**](../installation.md) — Docker Compose, Helm, and production setup
references/developer/api/recall.md
# Recall Memories
Retrieve memories from a bank using multi-strategy recall.
When you **recall**, Hindsight runs four retrieval strategies in parallel — semantic similarity, keyword (BM25), graph traversal, and temporal — then fuses and reranks the results into a single ranked list. The response contains structured facts, not raw documents.
{/* Import raw source files */}
> **ℹ️ How Recall Works**
>
Learn about the four retrieval strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](../retrieval.md) guide.
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) to install the client and start the server.
## Basic Recall
### Python
```python
response = client.recall(bank_id="my-bank", query="What does Alice do?")
# response.results is a list of RecallResult objects, each with:
# - id: fact ID
# - text: the extracted fact
# - type: "world", "experience", or "observation"
# - context: context label set during retain
# - metadata: dict[str, str] set during retain
# - tags: list of tags
# - entities: list of entity name strings linked to this fact
# - occurred_start: ISO datetime of when the event started
# - occurred_end: ISO datetime of when the event ended
# - mentioned_at: ISO datetime of when the fact was retained
# - document_id: document this fact belongs to
# - chunk_id: chunk this fact was extracted from
# Example response.results:
# [
# RecallResult(id="a1b2...", text="Alice works at Google as a software engineer", type="world", context="career", ...),
# RecallResult(id="c3d4...", text="Alice got promoted to senior engineer", type="experience", occurred_start="2024-03-15T00:00:00Z", ...),
# ]
```
### Node.js
```javascript
const response = await client.recall('my-bank', 'What does Alice do?');
// response.results is an array of result objects, each with:
// - id: fact ID
// - text: the extracted fact
// - type: "world", "experience", or "observation"
// - context: context label set during retain
// - metadata: Record<string, string> set during retain
// - tags: string[] of tags
// - entities: string[] of entity names linked to this fact
// - occurredStart: ISO datetime of when the event started
// - occurredEnd: ISO datetime of when the event ended
// - mentionedAt: ISO datetime of when the fact was retained
// - documentId: document this fact belongs to
// - chunkId: chunk this fact was extracted from
// Example response.results:
// [
// { id: "a1b2...", text: "Alice works at Google as a software engineer", type: "world", context: "career", ... },
// { id: "c3d4...", text: "Alice got promoted to senior engineer", type: "experience", occurredStart: "2024-03-15T00:00:00Z", ... },
// ]
```
### CLI
```bash
hindsight memory recall my-bank "What does Alice do?"
```
### Go
```go
response, _, _ := client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What does Alice do?",
}).Execute()
// response.Results is a slice of RecallResult, each with:
// - Id: fact ID
// - Text: the extracted fact
// - Type: "world", "experience", or "observation"
// - Context: context label set during retain
// - Tags: []string of tags
// - Entities: []string of entity names linked to this fact
// - OccurredStart: ISO datetime of when the event started
// - OccurredEnd: ISO datetime of when the event ended
// - MentionedAt: ISO datetime of when the fact was retained
// - DocumentId: document this fact belongs to
for _, r := range response.GetResults() {
fmt.Println(r.GetText())
}
```
---
## Parameters
### query
The natural language question or statement to search for. This is the only required field. The query drives all four retrieval strategies simultaneously: it is embedded for semantic search, tokenized for BM25 keyword search, used to seed graph traversal, and parsed for temporal expressions. After retrieval, the raw query text is also passed to the cross-encoder reranker to re-score every candidate. Queries exceeding 500 tokens are rejected.
### types
Controls which categories of memory facts are searched. Accepted values are `world` (objective facts), `experience` (events and conversations), and `observation` (deduplicated, evidence-grounded beliefs consolidated from multiple memories). When omitted, all three types are searched.
Each type runs the full four-strategy retrieval pipeline independently, so narrowing `types` reduces both the result set and query cost.
### Python
```python
# Only world facts (objective information)
world_facts = client.recall(
bank_id="my-bank",
query="Where does Alice work?",
types=["world"]
)
```
```python
# Only experience (conversations and events)
experience = client.recall(
bank_id="my-bank",
query="What have I recommended?",
types=["experience"]
)
```
```python
# Only observations (consolidated knowledge)
observations = client.recall(
bank_id="my-bank",
query="What patterns have I learned?",
types=["observation"]
)
```
### Node.js
```javascript
await client.recall('my-bank', 'query', { types: ['world'] });
```
```javascript
await client.recall('my-bank', 'query', { types: ['experience'] });
```
```javascript
await client.recall('my-bank', 'query', { types: ['observation'] });
```
### CLI
```bash
hindsight memory recall my-bank "query" --fact-type world,observation
```
### Go
```go
// Only world facts (objective information)
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "Where does Alice work?",
Types: []string{"world"},
}).Execute()
```
```go
// Only experience (conversations and events)
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What have I recommended?",
Types: []string{"experience"},
}).Execute()
```
```go
// Only observations (consolidated knowledge)
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What patterns have I learned?",
Types: []string{"observation"},
}).Execute()
```
> **💡 About Observations**
>
Observations are deduplicated, evidence-grounded beliefs consolidated from multiple facts — preferences, recurring patterns, and durable learnings the memory bank has built up. Each observation references its supporting memories (with exact quotes), and is refined rather than overwritten when new evidence arrives. They are created and maintained automatically in the background after retain operations.
### prefer_observations
Because observations are consolidated from raw facts, recalling `observation` alongside `world` and `experience` can return the same information twice — once as the raw fact and once folded into an observation. With `prefer_observations` you get the best of both: you still recall every type, but whenever an observation in the results was built from a raw fact, that raw fact is dropped so the observation supersedes it. The freed slots are backfilled with the next-best results, so you don't lose coverage.
This lets you ask for everything without choosing between "raw facts only" (no consolidation) and "observations only" (which may lag behind the latest retains while consolidation catches up). Off unless the request or the bank turns it on. When a request leaves it out, the bank's `recall_prefer_observations` config decides (env `HINDSIGHT_API_RECALL_PREFER_OBSERVATIONS`, `false` unless configured), so a deployment can turn it on for clients that never pass it, such as MCP connectors. It has no effect unless both `observation` and at least one of `world`/`experience` are included in `types`.
### budget
Controls retrieval depth and breadth. Accepted values are `low`, `mid` (default), and `high`. Use `low` for fast simple lookups, `mid` for balanced everyday queries, and `high` when you need to find indirect connections or exhaustive coverage.
### Python
```python
# Quick lookup
results = client.recall(bank_id="my-bank", query="Alice's email", budget="low")
# Deep exploration
results = client.recall(bank_id="my-bank", query="How are Alice and Bob connected?", budget="high")
```
### Node.js
```javascript
// Quick lookup
const quickResults = await client.recall('my-bank', "Alice's email", { budget: 'low' });
// Deep exploration
const deepResults = await client.recall('my-bank', 'How are Alice and Bob connected?', { budget: 'high' });
```
### CLI
```bash
# Quick lookup
hindsight memory recall my-bank "Alice's email" --budget low
# Deep exploration
hindsight memory recall my-bank "How are Alice and Bob connected?" --budget high
```
### Go
```go
budgetLow := hindsight.LOW
// Quick lookup
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "Alice's email",
Budget: &budgetLow,
}).Execute()
// Deep exploration
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "How are Alice and Bob connected?",
Budget: &budgetHigh,
}).Execute()
```
### max_tokens
The maximum number of tokens the returned facts can collectively occupy. Defaults to `4096`. Only the `text` field of each fact is counted toward this budget — metadata, tags, entities, and other fields are not included. After reranking, facts are included in relevance order until this budget is exhausted — so you always get the most relevant memories that fit. A fact too long for the remaining budget is skipped rather than ending the selection, so shorter facts ranked behind it still come back. Hindsight is designed for agents, which think in tokens rather than result counts: set `max_tokens` to however much of your context window you want to allocate to memories.
> **📝 Note**
>
A query that matched something never comes back empty: if not even the top fact fits the budget, it is returned whole and over budget rather than clipped mid-sentence, because an empty result list would read as "this bank has no such memory" and a clipped fact would be a claim the memory never made. The one exception is `max_tokens=0`, which means "no facts" on purpose — it is how you ask for chunks alone.
### Python
```python
# Fill up to 4K tokens of context with relevant memories
results = client.recall(bank_id="my-bank", query="What do I know about Alice?", max_tokens=4096)
# Smaller budget for quick lookups
results = client.recall(bank_id="my-bank", query="Alice's email", max_tokens=500)
```
### Node.js
```javascript
// Fill up to 4K tokens of context with relevant memories
await client.recall('my-bank', 'What do I know about Alice?', { maxTokens: 4096 });
// Smaller budget for quick lookups
await client.recall('my-bank', "Alice's email", { maxTokens: 500 });
```
### CLI
```bash
# Fill up to 4K tokens of context with relevant memories
hindsight memory recall my-bank "What do I know about Alice?" --max-tokens 4096
# Smaller budget for quick lookups
hindsight memory recall my-bank "Alice's email" --max-tokens 500
```
### Go
```go
// Fill up to 4K tokens of context with relevant memories
mt4k := int32(4096)
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What do I know about Alice?",
MaxTokens: &mt4k,
}).Execute()
// Smaller budget for quick lookups
mt500 := int32(500)
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "Alice's email",
MaxTokens: &mt500,
}).Execute()
```
### query_timestamp
An ISO 8601 datetime representing when the query is being asked, from the user's perspective. When provided, it is used as the anchor for resolving relative temporal expressions in the query and for recency scoring — for example, if the query says "last month" and `query_timestamp` is `2023-05-30`, the temporal search window becomes approximately April 2023, and recency boosts are calculated as of May 30, 2023. Without it, the server's current time is used as the anchor. This field matters most for replaying historical conversations or building agents that need time-anchored recall.
### temporal_window
An explicit `{ "start": ..., "end": ... }` pair of ISO 8601 datetimes for the temporal part of the search. Supply it when you already know the period you mean — a date picker in your UI, or an agent that has already worked out what "last quarter" resolves to — and Hindsight uses those bounds directly instead of reading dates out of the query text.
```json
{ "query": "what did we decide about pricing", "temporal_window": { "start": "2023-04-01T00:00:00Z", "end": "2023-06-30T23:59:59Z" } }
```
**This ranks, it does not filter.** Hindsight searches several ways at once, and the window steers only the time-aware part of that search: memories dated inside it are surfaced and ranked higher, while everything else keeps being searched normally. Results dated outside the window are still returned, so this is not a way to restrict an answer to a period. Note also that the dates being compared are the *memory's own* dates — when the memory says something happened — not when it was stored.
Two smaller things worth knowing: bounds are inclusive and a naive datetime (one with no timezone) is read as UTC; and the window is ignored on banks that have time-aware search turned off. `temporal_window` replaces date extraction only — [`query_timestamp`](#query_timestamp) still anchors recency scoring, so it remains useful alongside it.
### include
An optional object controlling supplementary data returned alongside the main facts.
#### chunks
When enabled, the response includes the raw source text chunks from which each fact was extracted. Chunks are fetched before the `max_tokens` filter, so setting `max_tokens=0` returns no facts but can still return chunks. The `max_tokens` sub-option (default `8192`) controls the total chunk token budget independently of the main fact budget. This is useful when agents need surrounding context beyond the extracted fact text.
> **📝 Note**
>
When `include_chunks` is enabled, chunks are fetched based on the top-scored reranked results before token filtering. The last chunk is truncated (not dropped) to fit exactly within the budget, and each chunk carries a `truncated` flag indicating whether it was cut.
With a tag filter (`tags`, `tags_match`, `tag_groups`), a chunk is returned only when its **document's** tags pass the filter too. A fact can match through its own tags — for example a `kind:rule` fact projected from an [entity label](./memory-banks.md#entity-labels) — while the document it came from does not, and that document's other text must not come back with it. Such a fact is still returned; its chunk is not.
#### source_facts
When enabled and `types` includes `observation`, each observation result is accompanied by the original contributing facts it was synthesized from. Source facts are returned in a top-level `source_facts` dict keyed by fact ID, and each observation result carries a `source_fact_ids` list for cross-referencing. Facts are deduplicated across observations. The `max_tokens` sub-option (default `4096`) limits the total token budget for source facts.
> **📝 Note**
>
The budget is spent in result order, so when it runs out it is the lowest-ranked results that lose their source facts — the top results always keep theirs. `source_fact_ids` always lists every source, so an ID may have no entry in `source_facts`; the response sets `source_facts_truncated: true` when that is the budget's doing rather than a missing fact. Raise `max_tokens` (or set it to `-1`) if you need every source resolved.
### Python
```python
# Recall observations and include their source facts
response = client.recall(
bank_id="my-bank",
query="What patterns have I learned about Alice?",
types=["observation"],
include_source_facts=True,
max_source_facts_tokens=4096,
)
for obs in response.results:
print(f"Observation: {obs.text}")
if obs.source_fact_ids and response.source_facts:
print(" Derived from:")
for fact_id in obs.source_fact_ids:
fact = response.source_facts.get(fact_id)
if fact:
print(f" - [{fact.type}] {fact.text}")
```
### Node.js
```javascript
// Recall observations and include their source facts
const obsResponse = await client.recall('my-bank', 'What patterns have I learned about Alice?', {
types: ['observation'],
includeSourceFacts: true,
maxSourceFactsTokens: 4096,
});
for (const obs of obsResponse.results) {
console.log(`Observation: ${obs.text}`);
if (obs.source_fact_ids && obsResponse.source_facts) {
console.log(' Derived from:');
for (const factId of obs.source_fact_ids) {
const fact = obsResponse.source_facts[factId];
if (fact) console.log(` - [${fact.type}] ${fact.text}`);
}
}
}
```
### CLI
```bash
# Recall observations with source facts
hindsight memory recall my-bank "What patterns have I learned about Alice?" \
--fact-type observation
```
### Go
```go
// Recall observations and include their source facts
maxSFTokens := int32(4096)
sfOpts := hindsight.SourceFactsIncludeOptions{MaxTokens: &maxSFTokens}
obsResponse, _, _ := client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What patterns have I learned about Alice?",
Types: []string{"observation"},
Include: &hindsight.IncludeOptions{
SourceFacts: *hindsight.NewNullableSourceFactsIncludeOptions(&sfOpts),
},
}).Execute()
for _, obs := range obsResponse.GetResults() {
fmt.Printf("Observation: %s\n", obs.GetText())
for _, factID := range obs.GetSourceFactIds() {
if fact, ok := obsResponse.GetSourceFacts()[factID]; ok {
fmt.Printf(" - [%s] %s\n", fact.GetType(), fact.GetText())
}
}
}
```
#### entities
Enabled by default. When active, each returned fact includes the canonical names of entities associated with it. Set to `null` to skip the entity JOIN query and reduce response size. The `max_tokens` sub-option (default `500`) is a future-facing guard for entity data.
### tags
Filters recall to memories in the requested tag scope. `tags` defaults to `null` and
`tags_match` defaults to `any`.
The `tags_match` parameter controls the filtering logic:
| Mode | Untagged memories | Match condition |
|------|-------------------|-----------------|
| `any` (default) | Included | Memory has **at least one** of the specified tags |
| `any_strict` | Excluded | Memory has **at least one** of the specified tags |
| `all` | Included | Memory has **all** of the specified tags |
| `all_strict` | Excluded | Memory has **all** of the specified tags |
| `exact` | Excluded | Memory has **exactly** the specified tag set |
The defaults and empty-filter behavior are important:
| `tags` | `tags_match` | Eligible memories |
|--------|--------------|-------------------|
| Omitted, `null`, or `[]` | Omitted (`any`) | All tagged and untagged memories |
| Omitted, `null`, or `[]` | `any`, `all`, `any_strict`, or `all_strict` | All tagged and untagged memories; an empty tag list means no filter |
| Omitted, `null`, or `[]` | `exact` | Only untagged/global memories |
| Non-empty | `any` or `all` | Matching tagged memories plus untagged/global memories |
| Non-empty | `any_strict` or `all_strict` | Matching tagged memories only |
| Non-empty | `exact` | Memories whose complete tag set exactly equals `tags` |
> **📝 MCP empty-scope behavior**
>
For the MCP `recall` tool, `tags_match` is forwarded only when `tags` is present.
To select the untagged/global scope through MCP, pass both `tags: []` and
`tags_match: "exact"` rather than omitting `tags`.
#### Scenario setup
Consider a bank with these four memories:
| Memory | Tags |
|--------|------|
| "Alice prefers async communication" | `["user:alice"]` |
| "Bob dislikes long meetings" | `["user:bob"]` |
| "Team uses Slack for announcements" | `["user:alice", "team"]` |
| "Company policy: no meetings on Fridays" | *(untagged)* |
#### `any` — OR matching, includes untagged (default)
Returns memories that have **at least one** matching tag, plus untagged memories.
### Python
```python
response = client.recall(
bank_id="my-bank",
query="communication preferences",
tags=["user:alice"],
tags_match="any", # default
)
# Returns:
# [match] "Alice prefers async communication" — has "user:alice"
# [no match] "Bob dislikes long meetings" — no overlap with ["user:alice"]
# [match] "Team uses Slack for announcements" — has "user:alice"
# [match] "Company policy: no meetings on Fridays" — untagged, included by default
```
### Node.js
```javascript
await client.recall('my-bank', 'communication preferences', {
tags: ['user:alice'],
tagsMatch: 'any'
});
```
### CLI
```bash
hindsight memory recall my-bank "communication preferences" \
--tags "user:alice" --tags-match any
```
### Go
```go
// Filter recall to only memories tagged for a specific user
tagsMatch := "any"
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What feedback did the user give?",
Tags: []string{"user:alice"},
TagsMatch: &tagsMatch,
}).Execute()
```
Use this for **shared global knowledge + user-specific** patterns, where untagged memories represent information everyone should see.
#### `any_strict` — OR matching, excludes untagged
Same as `any` but untagged memories are excluded.
### Python
```python
response = client.recall(
bank_id="my-bank",
query="communication preferences",
tags=["user:alice"],
tags_match="any_strict",
)
# Returns:
# [match] "Alice prefers async communication" — has "user:alice"
# [no match] "Bob dislikes long meetings" — no overlap with ["user:alice"]
# [match] "Team uses Slack for announcements" — has "user:alice"
# [no match] "Company policy: no meetings on Fridays" — untagged, excluded
```
### Node.js
```javascript
await client.recall('my-bank', 'communication preferences', {
tags: ['user:alice'],
tagsMatch: 'any_strict'
});
```
### CLI
```bash
hindsight memory recall my-bank "communication preferences" \
--tags "user:alice" --tags-match any_strict
```
### Go
```go
// Strict mode: only return memories that have matching tags (exclude untagged)
tagsMatchStrict := "any_strict"
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What did the user say?",
Tags: []string{"user:alice"},
TagsMatch: &tagsMatchStrict,
}).Execute()
```
Use this when memories are **fully partitioned by tags** and untagged memories should never be visible.
#### `all` — AND matching, includes untagged
Returns memories that have **every** specified tag, plus untagged memories.
### Python
```python
response = client.recall(
bank_id="my-bank",
query="communication tools",
tags=["user:alice", "team"],
tags_match="all",
)
# Returns:
# [no match] "Alice prefers async communication" — missing "team"
# [no match] "Bob dislikes long meetings" — missing both tags
# [match] "Team uses Slack for announcements" — has both "user:alice" and "team"
# [match] "Company policy: no meetings on Fridays" — untagged, included by default
```
### Node.js
```javascript
await client.recall('my-bank', 'communication tools', {
tags: ['user:alice', 'team'],
tagsMatch: 'all'
});
```
### CLI
```bash
hindsight memory recall my-bank "communication tools" \
--tags "user:alice,team" --tags-match all
```
### Go
```go
// AND matching, includes untagged memories
tagsMatchAllMode := "all"
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "communication tools",
Tags: []string{"user:alice", "team"},
TagsMatch: &tagsMatchAllMode,
}).Execute()
```
Use this when memories must belong to a **specific intersection** of scopes (e.g., only memories relevant to both a user and a project), while still surfacing shared global knowledge.
#### `all_strict` — AND matching, excludes untagged
Returns memories that have **every** specified tag, and excludes untagged memories.
### Python
```python
response = client.recall(
bank_id="my-bank",
query="communication tools",
tags=["user:alice", "team"],
tags_match="all_strict",
)
# Returns:
# [no match] "Alice prefers async communication" — missing "team"
# [no match] "Bob dislikes long meetings" — missing both tags
# [match] "Team uses Slack for announcements" — has both "user:alice" and "team"
# [no match] "Company policy: no meetings on Fridays" — untagged, excluded
```
### Node.js
```javascript
await client.recall('my-bank', 'communication tools', {
tags: ['user:alice', 'team'],
tagsMatch: 'all_strict'
});
```
### CLI
```bash
hindsight memory recall my-bank "communication tools" \
--tags "user:alice,team" --tags-match all_strict
```
### Go
```go
// AND matching: require ALL specified tags to be present
tagsMatchAll := "all_strict"
client.MemoryAPI.RecallMemories(ctx, "my-bank").
RecallRequest(hindsight.RecallRequest{
Query: "What bugs were reported?",
Tags: []string{"user:alice", "bug-report"},
TagsMatch: &tagsMatchAll,
}).Execute()
```
Use this for strict scope enforcement where a memory must explicitly belong to **all** specified contexts.
> **💡 Extra tags are fine**
>
A memory with tags `["user:alice", "team", "project:x"]` will still match a filter of `["user:alice", "team"]` under `all_strict` — extra tags on the memory are not a problem. The filter only requires the memory to contain **at least** the specified tags.
#### `exact` — set equality, excludes untagged
Returns memories whose tag set is exactly equal to the specified tags, regardless of tag order. Unlike `all_strict`, memories with extra tags do not match.
Use this when filtering a precise observation scope returned by `GET /v1/default/banks/{bank_id}/observations/scopes`, where `["user:alice"]` should not also match observations scoped to `["user:alice", "project:x"]`.
> **💡 Filter to global (untagged) observations only**
>
The empty scope is a real scope — it's where `observation_scopes: "shared"` consolidation writes. Set `tags_match: "exact"` with **no tags** (omit `tags`, or pass `[]`) to recall **only** untagged/global memories and exclude every tagged one:
```json
{ "query": "...", "tags": [], "tags_match": "exact" }
```
With any other `tags_match` mode, absent or empty `tags` means "no tag filter" (all memories are eligible). Only under `exact` do absent/empty tags select "the global scope". This is the way to read back just the global observations after you've started using more specific scopes.
### tag_groups
`tag_groups` is a list of compound boolean tag filters. The groups in the list are AND-ed together at the top level. Each group is a recursive boolean expression: a **leaf** node `{tags, match}`, or a **compound** node `{and: [...]}`, `{or: [...]}`, or `{not: ...}`.
`tag_groups` defaults to `null`. The public REST and MCP request models treat
`tag_groups` and `tags` as mutually exclusive: if both are present, the request is
rejected. Use `tag_groups` by itself for compound filtering and normally leave the
top-level `tags_match` at its default, `any`. Each `tag_groups` leaf has its own
`match` value. The exception is top-level `tags_match: "exact"`: because exact
matching gives absent flat tags a meaning, it adds a global-only flat constraint
that is AND-ed with the compound expression.
#### Leaf node
```json
{ "tags": ["step:5", "step:8"], "match": "any_strict" }
```
`match` accepts the same values as `tags_match`: `any`, `all`, `any_strict`, `all_strict`, `exact`. Defaults to `any_strict`.
#### Fuzzy leaves
A leaf may set `resolve: "fuzzy"` (default `"exact"`) to match its tags against the bank's tags by trigram similarity instead of literally, so a filter on `typsecript` still reaches memories tagged `typescript`:
```json
{ "tags": ["typsecript"], "match": "any_strict", "resolve": "fuzzy" }
```
Each tag resolves to the bank tags scoring at least 0.45, and the leaf then matches those exactly — so `resolve` composes with every `match` mode. Similarity is length-sensitive: `kubernets` reaches `kubernetes`, but a short tag has too few trigrams to survive an edit (`kakfa` does not reach `kafka`). A tag that resolves to nothing matches nothing; the filter is never dropped. A 422 is returned if the bank has more than 5000 distinct tags, or if a `resolve: "fuzzy"` leaf with `match: "exact"` expands past 32 candidate scopes.
#### Compound nodes
```json
{ "and": [ <TagGroup>, <TagGroup>, ... ] }
{ "or": [ <TagGroup>, <TagGroup>, ... ] }
{ "not": <TagGroup> }
```
#### Examples
**Step filter AND user scope** — two top-level groups AND-ed:
```json
{
"tag_groups": [
{ "tags": ["step:5", "step:8", "step:12"], "match": "any_strict" },
{ "tags": ["user:ep_42"], "match": "all_strict" }
]
}
```
**Nested OR inside AND** — user must match, plus either step OR priority:
```json
{
"tag_groups": [
{ "tags": ["user:alice"], "match": "all_strict" },
{ "or": [
{ "tags": ["step:5"], "match": "any_strict" },
{ "tags": ["priority:high"], "match": "all_strict" }
]}
]
}
```
**Exclusion** — user must match, but archived memories are excluded:
```json
{
"tag_groups": [
{ "tags": ["user:alice"], "match": "all_strict" },
{ "not": { "tags": ["archived"], "match": "any_strict" } }
]
}
```
### trace
When set to `true`, the response includes a detailed debug trace covering the query embedding, entry points, per-strategy retrieval results, RRF fusion candidates, reranked results, temporal constraints detected, and per-phase timings. Has no effect on the retrieval logic itself. Useful for understanding why specific memories were or were not returned.
### min_scores
An optional object of per-stage score floors, each compared **inclusively** (`>=`). Any field you leave unset imposes no floor; omitting `min_scores` entirely (the default) applies no score filtering at all. The four fields operate at **two different levels of the pipeline**, and the level decides what a returned result is guaranteed to satisfy:
| field | level | effect | guaranteed by every result? |
|---|---|---|---|
| `semantic` | retrieval | minimum vector similarity, pushed into the **semantic arm's** SQL — prunes weak vector matches **before** fusion (overrides the global similarity minimum for this request) | no |
| `keyword` | retrieval | minimum keyword/full-text (BM25) score, pushed into the **keyword arm's** SQL — prunes weak keyword matches before fusion | no |
| `reranker` | post-query | minimum normalized cross-encoder score, applied to the ranked results | yes |
| `final` | post-query | minimum final ranking score, applied to the ranked results | yes |
```json
{ "query": "...", "min_scores": { "reranker": 0.5 } }
```
#### Retrieval floors constrain one arm, not the result
Recall runs [four retrieval arms](#results) — semantic, keyword, graph and temporal — and a memory reaches the response if **any** of them surfaced it. `semantic` and `keyword` prune inside the arm they name, so they change *which candidates are considered*, and with them the final ordering. They are **not predicates over each returned result**:
- a result surfaced only semantically reports `"keyword": null`, whatever `min_scores.keyword` you set;
- a result surfaced only by keyword reports `"semantic": null`, whatever `min_scores.semantic` you set;
- a result reached through the graph or temporal arm reports **neither**, and is unaffected by both floors.
Setting `semantic` and `keyword` together therefore does not restrict the response to results that clear both. That is deliberate: an intersection would discard exactly the strong single-arm matches hybrid retrieval exists to find — a paraphrase with no lexical overlap in common with the query, or an exact identifier like `amber-17` that the embedding scores poorly.
#### For abstention, use `reranker` or `final`
The post-query floors are applied to every scored result after fusion and reranking, so a returned result always clears them — and a query where nothing clears them returns no results. That is the floor to reach for when you want recall to abstain on a low-confidence or nonsense query. Note they gate a *combined* signal: `final` blends RRF rank, cross-encoder relevance, recency/temporal and strategy boosts, and `reranker` depends on the cross-encoder's calibration, so neither is a drop-in equivalent of a retrieval-stage cutoff.
Because freed slots are **not** backfilled, any floor can return fewer results than the budget allows.
**Use floors with care.** The reranker's scores are reliable for *ordering* but not as *absolute* values — a clearly-relevant memory can score `~0.001` on one query and `~1.0` on another, so a fixed cutoff risks silently dropping good results. Calibrate any threshold against the scores you actually observe (recall with no `min_scores` first and inspect the [`scores`](#scores) object). See the note under [`scores`](#scores) on why the scale is relative, not absolute, before relying on a fixed threshold.
**Not available with the TypeSafe reranker.** TypeSafe ranks the whole pool at once and returns each memory's rank position, not a relevance score: the top memory is `1.0` on every query, so a `reranker` floor would only keep a fixed share of the results. Recall rejects `min_scores.reranker` with HTTP 400 when TypeSafe is the configured reranker (and skips the floor if a failover chain falls back to it). Use `final` instead.
---
## Response
### results
The main list of recalled facts, ordered by relevance. Relevance is computed by running four retrieval strategies in parallel — semantic similarity, BM25 keyword, graph traversal, and temporal — fusing their rankings with Reciprocal Rank Fusion (RRF), then re-scoring the merged candidates with a cross-encoder reranker against the original query.
Each result carries a [`scores`](#scores) object (see below). Treat these as **relative** signals: they reflect the ranking within a single query, not an absolute, cross-query confidence — a `0.8` from one query is not comparable to a `0.8` from another. For most agents the right approach is to consume memories in order and let `max_tokens` determine how many fit, rather than filtering by score. The `scores` object (and the [`min_scores`](#min_scores) parameter) exist for callers that want to inspect the ranking or drop a low-confidence tail; calibrate any threshold against the scores you see on an unfiltered query.
Each item in `results` has the following fields:
#### id
The unique identifier of this fact. Use it to cross-reference with `source_facts` or for application-level deduplication.
#### text
The extracted fact text as stored in the memory bank.
#### type
The fact category: `world` for objective information, `experience` for events and conversations, or `observation` for consolidated knowledge synthesized over time.
#### context
The context label provided when the fact was retained (e.g., `"team meeting"`, `"slack"`). `null` if none was set.
#### metadata
The key-value string pairs attached when the fact was retained. `null` if none were set.
#### tags
The visibility-scoping tags attached to this fact.
#### entities
A list of canonical entity name strings linked to this fact. Only populated when `include.entities` is enabled (the default). `null` otherwise.
#### occurred_start / occurred_end
ISO 8601 datetimes representing when the described event started and ended. Extracted by the LLM from the content during retain. `null` if the content had no temporal information.
#### mentioned_at
ISO 8601 datetime of when this fact was retained into the bank.
#### document_id
The document ID this fact belongs to, as set during retain.
#### chunk_id
The ID of the source text chunk this fact was extracted from. Used to cross-reference with `chunks` in the response when `include.chunks` is enabled.
#### source_fact_ids
For `observation`-type results only: the IDs of the original facts this observation was synthesized from. Cross-references with `source_facts` in the response. `null` for other types or when `include.source_facts` is not enabled.
#### scores
An object of the per-stage scores for this result. `null` for `source_facts` entries, which are attached by provenance rather than ranked. Fields:
- **`final`** — the score this fact was ranked by (cross-encoder relevance × recency/temporal/evidence boosts). `results` is ordered by it descending. A relative signal, not a calibrated probability (see the note above).
- **`reranker`** — the cross-encoder's normalized relevance (`0`–`1`). `null` when the deployment uses a passthrough reranker (RRF/interleave modes), or a reranker that returns rank positions instead of scores (TypeSafe).
- **`semantic`** — the raw vector cosine similarity (`0`–`1`). `null` if this result was not surfaced by semantic search.
- **`keyword`** — the raw keyword/full-text (BM25) score (`≥ 0`, unbounded). `null` if this result was not surfaced by keyword search.
Each field is also a valid [`min_scores`](#min_scores) floor — but `semantic` and `keyword` gate their own retrieval arm rather than the returned result, so a `null` here is expected even when you set that floor. A non-null value always clears it. See [`min_scores`](#min_scores).
---
### source_facts
A dict keyed by fact ID containing full `RecallResult` objects for the source facts that contributed to observation results. Only present when `include.source_facts` is enabled. Facts are deduplicated — if two observations share a source fact, it appears once.
### source_facts_truncated
Whether the token budget cut the `source_facts` map short. When `true`, some IDs in `results[].source_fact_ids` have no entry in `source_facts` because the budget ran out — the references are not dangling. Only present when `include.source_facts` is enabled.
### chunks
A dict keyed by chunk ID containing the raw source text chunks associated with the returned facts. Only present when `include.chunks` is enabled. Each chunk has `id`, `text`, `chunk_index`, and `truncated` (whether the text was cut to fit the token budget).
### entities
A dict keyed by canonical entity name containing entity state objects. Only present when `include.entities` is enabled. Each entry has `entity_id`, `canonical_name`, and `observations`.
### trace
A debug object present only when `trace: true` was set in the request. Contains per-phase timings, retrieval breakdowns, and RRF fusion details.
references/developer/api/reflect.md
# Reflect
Generate a grounded, disposition-aware response using an agentic reasoning loop.
When you call **reflect**, Hindsight runs an agentic loop that autonomously searches the memory bank using multiple retrieval tools, applies the bank's disposition traits to shape the reasoning style, and produces a final answer grounded in what it found. Unlike recall — which returns raw facts — reflect returns a synthesized response written by the LLM.
{/* Import raw source files */}
> **ℹ️ How Reflect Works**
>
Learn about disposition-driven reasoning in the [Reflect Architecture](../reflect.md) guide.
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) to install the client and start the server.
## Basic Usage
### Python
```python
client.reflect(bank_id="my-bank", query="What should I know about Alice?")
```
### Node.js
```javascript
await client.reflect('my-bank', 'What should I know about Alice?');
```
### CLI
```bash
hindsight memory reflect my-bank "What do you know about Alice?"
```
### Go
```go
client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "What should I know about Alice?",
}).Execute()
```
---
## Parameters
### query
The question or prompt to reflect on. This is the only required field. If you have situational context that should influence the answer, include it directly in the query rather than as a separate field.
### budget
Controls how thoroughly the agent explores the memory bank before answering. Accepted values are `low` (default), `mid`, and `high`. At `low`, the agent does a shallow search optimized for speed. At `mid`, it checks multiple sources when the question warrants it. At `high`, it performs deep exploration across all knowledge levels and may use multiple query variations to find indirect connections. Use `high` for complex questions that require synthesizing information from many sources.
### Python
```python
response = client.reflect(
bank_id="my-bank",
query="We're considering a hybrid work policy. What do you think about remote work?",
budget="mid",
)
```
### Node.js
```javascript
const response = await client.reflect('my-bank', 'What do you think about remote work?', {
budget: 'mid',
context: "We're considering a hybrid work policy"
});
```
### CLI
```bash
hindsight memory reflect my-bank "Summarize my week" --budget high --max-tokens 8192
```
### Go
```go
budgetMid := hindsight.MID
client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "We're considering a hybrid work policy. What do you think about remote work?",
Budget: &budgetMid,
}).Execute()
```
### max_tokens
Limits the length of the final generated response. Defaults to `4096`. This does not affect how much the agent can retrieve during the agentic loop — only the final answer length.
### response_schema
An optional JSON Schema **object** with a non-empty `properties` map (nested objects and arrays are supported). When provided, the response includes a `structured_output` field **in addition to** the markdown `text`: the agent reasons to its answer, then a second pass extracts that answer into JSON matching your schema. `structured_output` is therefore a faithful projection of `text` — you get the readable answer *and* a typed object to program against, never one instead of the other. Invalid schemas (not an object, or no properties) are rejected before the call runs.
### Python
```python
from pydantic import BaseModel
# Define your response structure with Pydantic
class HiringRecommendation(BaseModel):
recommendation: str
confidence: str # "low", "medium", "high"
key_factors: list[str]
risks: list[str] = []
response = client.reflect(
bank_id="hiring-team",
query="Should we hire Alice for the ML team lead position?",
response_schema=HiringRecommendation.model_json_schema(),
)
# Parse structured output into Pydantic model
result = HiringRecommendation.model_validate(response.structured_output)
print(f"Recommendation: {result.recommendation}")
print(f"Confidence: {result.confidence}")
print(f"Key factors: {result.key_factors}")
```
### Node.js
```javascript
// Define JSON schema directly
const responseSchema = {
type: 'object',
properties: {
recommendation: { type: 'string' },
confidence: { type: 'string', enum: ['low', 'medium', 'high'] },
key_factors: { type: 'array', items: { type: 'string' } },
risks: { type: 'array', items: { type: 'string' } },
},
required: ['recommendation', 'confidence', 'key_factors'],
};
const structuredResponse = await client.reflect('my-bank', 'What do you know about Alice and her career?', {
responseSchema: responseSchema,
});
// Structured output (if returned)
if (structuredResponse.structuredOutput) {
console.log('Recommendation:', structuredResponse.structuredOutput.recommendation || 'N/A');
console.log('Key factors:', structuredResponse.structuredOutput.key_factors || []);
}
```
### CLI
```bash
# First, create a JSON schema file schema.json:
cat > schema.json << 'EOF'
{
"type": "object",
"properties": {
"recommendation": {"type": "string"},
"confidence": {"type": "string", "enum": ["low", "medium", "high"]},
"key_factors": {"type": "array", "items": {"type": "string"}}
},
"required": ["recommendation", "confidence", "key_factors"]
}
EOF
# Then use the --schema flag:
hindsight memory reflect hiring-team \
"Should we hire Alice for the ML team lead position?" \
--schema schema.json
# Cleanup the temporary schema file
rm -f schema.json
```
### Go
```go
// Define JSON schema for structured output
responseSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"recommendation": map[string]interface{}{"type": "string"},
"confidence": map[string]interface{}{"type": "string", "enum": []string{"low", "medium", "high"}},
"key_factors": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}},
"risks": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}},
},
"required": []string{"recommendation", "confidence", "key_factors"},
}
structuredResponse, _, _ := client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "Should we hire Alice for the ML team lead position?",
ResponseSchema: responseSchema,
}).Execute()
// Access structured output
if out := structuredResponse.GetStructuredOutput(); out != nil {
fmt.Println("Recommendation:", out["recommendation"])
fmt.Println("Key factors:", out["key_factors"])
}
```
### tags
Defines the visibility scope used throughout the reflect agent. It filters raw
facts, observations, and mental models that the agent can retrieve. The same
`tags` and `tags_match` values also select which tagged directives are injected
into the reflect prompt.
`tags` defaults to `null`, `tags_match` defaults to `any`, and `tag_groups`
defaults to `null`. For non-empty tags, raw facts, observations, and mental
models use the same matching modes as [recall tags](./recall.md#tags). Directives
have one additional rule: untagged directives are global and remain eligible
whenever a tag scope is supplied, including with a strict or exact match.
| Reflect configuration | Raw facts and observations | Mental models | Active directives |
|-----------------------|----------------------------|---------------|-------------------|
| Omit `tags`, `tags_match`, and `tag_groups` | All tagged and untagged data | All tagged and untagged models | Untagged/global directives only |
| `tags: []`, default `tags_match: "any"` | All tagged and untagged data | All tagged and untagged models | Untagged/global directives only |
| No tags, `tags_match: "exact"` | Untagged/global data only | Untagged/global models only | Untagged/global directives only |
| Non-empty `tags`, `any` or `all` | Matching tagged data plus untagged/global data | Matching models plus untagged/global models | Matching tagged directives plus untagged/global directives |
| Non-empty `tags`, `any_strict` or `all_strict` | Matching tagged data only | Matching tagged models only | Matching tagged directives plus untagged/global directives |
| Non-empty `tags`, `exact` | Data with exactly the requested tag set | Models with exactly the requested tag set | Exactly matching tagged directives plus untagged/global directives |
| Non-empty `tag_groups`, default top-level `tags_match` | Data matching the compound expression | Models matching the compound expression | Matching tagged directives plus untagged/global directives |
A `tag_groups` leaf may set `resolve: "fuzzy"`, as in
[recall](./recall.md#fuzzy-leaves); reflect resolves it once, before the agentic
loop starts, so every tool it runs filters on the same tags.
The first row is intentionally asymmetric: an unscoped reflect can search all
memories, but it does not load tagged directives. To create a directive that
applies to every reflect call, leave its `tags` empty. To create a scoped
directive, assign tags and pass a matching scope to `reflect`.
> **📝 `isolation_mode`**
>
`isolation_mode` is an internal `list_directives` option, not a public reflect
request parameter. Reflect always enables it. When neither `tags` nor
`tag_groups` is supplied, it limits directive loading to untagged directives.
There is currently no per-request switch to disable it.
> **📝 MCP omitted tags**
>
The MCP `reflect` tool forwards `tags_match` only when `tags` is present. To
request the empty exact scope through MCP, pass `tags: []` together with
`tags_match: "exact"`.
### Python
```python
# Filter reflection to only consider memories for a specific user
response = client.reflect(
bank_id="my-bank",
query="What does this user think about our product?",
tags=["user:alice"],
tags_match="any_strict" # Only use memories tagged for this user
)
```
### Node.js
```javascript
// Filter reflect to only use memories tagged for a specific user
await client.reflect('my-bank', 'What feedback did the user give?', {
tags: ['user:alice'],
tagsMatch: 'any_strict'
});
```
### CLI
```bash
hindsight memory reflect my-bank "What feedback did the user give?" \
--tags "user:alice" --tags-match any_strict
```
### Go
```go
// Filter reflection to only consider memories for a specific user
tagsMatch := "any_strict"
client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "What does this user think about our product?",
Tags: []string{"user:alice"},
TagsMatch: &tagsMatch,
}).Execute()
```
#### Common scope examples
Unscoped reflect searches all memories but applies only global directives:
```json
{
"query": "Summarize the current project status"
}
```
A project scope includes global data and directives alongside matching
`project:a` data and directives:
```json
{
"query": "Summarize the current project status",
"tags": ["project:a"]
}
```
A strict project scope excludes untagged memories, observations, and mental
models. Global directives still apply:
```json
{
"query": "Summarize the current project status",
"tags": ["project:a"],
"tags_match": "all_strict"
}
```
### tag_groups
Provides compound tag filtering with recursive `and`, `or`, and `not`
expressions. It affects the same reflect data sources and directive selection as
flat `tags`. `tag_groups` and `tags` are mutually exclusive in the public REST
request. Each leaf supplies its own matching mode and defaults to
`any_strict`; the top-level groups are AND-ed. Normally leave the top-level
`tags_match` at its default, `any`. Setting it to `exact` while using
`tag_groups` additionally constrains facts, observations, and mental models to
the global flat scope before applying the compound expression.
The MCP `reflect` tool currently exposes flat `tags` and `tags_match`, but not
`tag_groups`.
### include
Controls optional supplementary data returned alongside the main response.
#### include.facts
When enabled, the response includes a `based_on` object listing the memories, mental models, and directives the agent actually used to construct the answer. Only sources retrieved during the agent loop can appear here — citations are validated to prevent hallucinated references. Useful for transparency and verification.
### Python
```python
# include_facts=True enables the based_on field in the response
response = client.reflect(
bank_id="my-bank",
query="Tell me about Alice",
include_facts=True,
)
print("Response:", response.text)
print("\nBased on:")
for fact in (response.based_on.memories if response.based_on else []):
print(f" - [{fact.type}] {fact.text}")
```
### Node.js
```javascript
const sourcesResponse = await client.reflect('my-bank', 'Tell me about Alice', {
includeFacts: true
});
console.log('Response:', sourcesResponse.text);
console.log('\nBased on:');
for (const fact of (sourcesResponse.based_on?.memories || [])) {
console.log(` - [${fact.type}] ${fact.text}`);
}
```
### CLI
```bash
hindsight memory reflect my-bank "Tell me about Alice" --include-facts
```
### Go
```go
// include.facts enables the based_on field in the response
sourcesResponse, _, _ := client.MemoryAPI.Reflect(ctx, "my-bank").
ReflectRequest(hindsight.ReflectRequest{
Query: "Tell me about Alice",
Include: &hindsight.ReflectIncludeOptions{
Facts: map[string]interface{}{}, // empty map enables fact inclusion
},
}).Execute()
fmt.Println("Response:", sourcesResponse.GetText())
fmt.Println("\nBased on:")
if basedOn := sourcesResponse.GetBasedOn(); basedOn.Memories != nil {
for _, fact := range basedOn.GetMemories() {
fmt.Printf(" - [%s] %s\n", fact.GetType(), fact.GetText())
}
}
```
#### include.tool_calls
When enabled, the response includes a `trace` object with the full execution log of every tool call and LLM call made during the agentic loop, including inputs, outputs, and durations. Set `output: false` to include only tool inputs for a smaller payload. Useful for debugging why the agent reached a particular conclusion.
### reflect_search_observations_max_tokens
Token budget for the agent's `search_observations` tool when the model names no
budget of its own. Observation evidence is often the largest single contributor
to the reflect context on a big bank, so lowering this trades the lowest-ranked
observations for a smaller (cheaper, faster) LLM context. Defaults to `5000`.
### reflect_search_observations_include_entities
Whether `search_observations` attaches resolved entity names to each
observation. They are useful when the surface text uses an alias ("Bob" for
canonical "Robert Smith"), but they can be more than half the serialized tool
payload; setting `false` keeps the same observations and ranking with a much
smaller context. Defaults to `true`.
Both options can be defaulted for a whole bank with the
`reflect_default_options` config key (see
[Configuration](../configuration.md)); an explicit value on the request always
wins. A mental-model refresh is configured separately and does **not** read
`reflect_default_options` — set the trigger's fields of the same name, or the
bank's `knowledge_page_default_trigger`, instead (see
[Mental Models](./mental-models.md)).
---
## Response
### text
The synthesized answer as a well-formatted markdown string. This is the primary output of reflect. Still returned when `response_schema` is provided — `structured_output` is derived from it, not a replacement for it.
### structured_output
The LLM's response parsed according to the `response_schema` provided in the request. Only present when `response_schema` was set. `null` otherwise.
### structured_output_error
Why the structured view could not be produced. Present only when a `response_schema` was given and the extraction pass failed — a provider error, a timeout, or output that would not parse. The reflect itself still succeeds: you get `200` and the markdown `text`, and this field tells you the machine-readable half is missing because something broke, not because the answer held nothing matching your schema. A missing `structured_output` **without** this field is the latter, ordinary case (the endpoint omits null fields). Treat its presence as retryable, and as the signal to alert on if structured output stops working.
### based_on
The sources the agent used to construct the answer. Only present when `include.facts` was enabled. Contains three fields:
- `memories` — a list of memory facts (world, experience, observation) that were retrieved and cited. Each item has `id`, `text`, `type`, `context`, `occurred_start`, and `occurred_end`.
- `mental_models` — a list of mental models that were used. Each item has `id`, `text`, and `context`.
- `directives` — a list of directives that were enforced during reasoning. Each item has `id`, `name`, and `content`.
### usage
Token usage for all LLM calls made during the agentic loop: `input_tokens`, `output_tokens`, and `total_tokens`. Useful for cost tracking.
### trace
The full execution log of the agentic loop. Only present when `include.tool_calls` was enabled. Contains:
- `tool_calls` — each tool invocation with `tool` name (`lookup`, `recall`, `learn`, `expand`), `input`, `output` (if `output: true`), `duration_ms`, and `iteration` number.
- `llm_calls` — each LLM call with `scope` (e.g., `"agent_1"`, `"final"`) and `duration_ms`.
## When Reflect Fails
Reflect answers from evidence it gathered, so a run that could not gather it does not answer at all — it fails with a **500**, rather than returning a confident reply built on nothing:
- **A retrieval tool raised.** The database, the embedder or the reranker was unavailable. One failure in a batch fails the run: an answer written around a tool that never returned is indistinguishable from one over a bank that genuinely holds nothing on the topic, and callers store it as a real answer.
- **The model produced no answer.** The `done` call arrived empty, or the final synthesis returned nothing.
- **The model or transport cannot drive tool calls.** Reflect is driven entirely by structured tool calls; a transport that silently drops the tool definitions fails loudly so you can switch to a tool-calling-capable model.
- **The provider kept failing.** A non-context-overflow LLM error is retried once inside the loop and then given up on.
A **successful retrieval that returns nothing is not a failure**: the bank genuinely has nothing on the topic, and reflect says so in its answer.
Two cases are deliberately not failures. A run whose *context window* overflows synthesizes from the evidence it has — the prompt was too big for the model, which is a budgeting problem, not a broken dependency. And a tool call the model got *wrong* — a missing argument, a tool that does not exist — is returned to it as an error to fix, not raised.
references/developer/api/retain.md
# Ingest Data
Store documents, conversations, and raw content into Hindsight to automatically extract and create memories.
When you **retain** content, Hindsight doesn't just store the raw text—it intelligently analyzes the content to extract meaningful facts, identify entities, and build a connected knowledge graph. This process transforms unstructured information into structured, queryable memories.
{/* Import raw source files */}
> **ℹ️ How Retain Works**
>
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](../retain.md) guide.
> **💡 Prerequisites**
>
Make sure you've completed the [Quick Start](./quickstart.md) to install the client and start the server.
## Store a Document
A single retain call accepts one or more **items**. Each item is a piece of raw content — a conversation, a document, a note — that Hindsight will analyze and decompose into one or many memories. The content itself is never stored verbatim; what gets stored are the structured facts the LLM extracts from it.
### Python
```python
client.retain(
bank_id="my-bank",
content="Alice works at Google as a software engineer"
)
```
### Node.js
```javascript
await client.retain('my-bank', 'Alice works at Google as a software engineer');
```
### CLI
```bash
hindsight memory retain my-bank "Alice works at Google as a software engineer"
```
### Go
```go
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Alice works at Google as a software engineer")},
},
}).Execute()
```
### Retaining a Conversation
A full conversation should be retained as a single item. The LLM can parse any format — plain text, JSON, Markdown, or any structured representation — as long as it clearly conveys who said what and when. The example below uses a simple `Name (timestamp): text` format.
### Python
```python
# Retain an entire conversation as a single document.
# Format each message as "Name (timestamp): text" so the LLM can attribute
# facts to the right person and resolve temporal references across the thread.
conversation = "\n".join([
"Alice (2024-03-15T09:00:00Z): Hi Bob! Did you end up going to the doctor last week?",
"Bob (2024-03-15T09:01:00Z): Yes, finally. Turns out I have a mild peanut allergy.",
"Alice (2024-03-15T09:02:00Z): Oh no! Are you okay?",
"Bob (2024-03-15T09:03:00Z): Yeah, nothing serious. Just need to carry an antihistamine.",
"Alice (2024-03-15T09:04:00Z): Good to know. We'll avoid peanuts at the team lunch.",
])
client.retain(
bank_id="my-bank",
content=conversation,
context="team chat",
timestamp="2024-03-15T09:04:00Z",
document_id="chat-2024-03-15-alice-bob",
)
```
### Node.js
```javascript
// Retain an entire conversation as a single document.
// Format each message as "Name (timestamp): text" so the LLM can attribute
// facts to the right person and resolve temporal references across the thread.
const conversation = [
'Alice (2024-03-15T09:00:00Z): Hi Bob! Did you end up going to the doctor last week?',
'Bob (2024-03-15T09:01:00Z): Yes, finally. Turns out I have a mild peanut allergy.',
'Alice (2024-03-15T09:02:00Z): Oh no! Are you okay?',
'Bob (2024-03-15T09:03:00Z): Yeah, nothing serious. Just need to carry an antihistamine.',
'Alice (2024-03-15T09:04:00Z): Good to know. We\'ll avoid peanuts at the team lunch.',
].join('\n');
await client.retain('my-bank', conversation, {
context: 'team chat',
timestamp: '2024-03-15T09:04:00Z',
documentId: 'chat-2024-03-15-alice-bob',
});
```
### CLI
```bash
# Retain an entire conversation as a single document.
CONVERSATION="Alice (2024-03-15T09:00:00Z): Hi Bob! Did you end up going to the doctor last week?
Bob (2024-03-15T09:01:00Z): Yes, finally. Turns out I have a mild peanut allergy.
Alice (2024-03-15T09:02:00Z): Oh no! Are you okay?
Bob (2024-03-15T09:03:00Z): Yeah, nothing serious. Just need to carry an antihistamine.
Alice (2024-03-15T09:04:00Z): Good to know. We'll avoid peanuts at the team lunch."
hindsight memory retain my-bank "$CONVERSATION" \
--context "team chat" \
--doc-id "chat-2024-03-15-alice-bob"
```
### Go
```go
// Retain an entire conversation as a single document.
conversation := "Alice (2024-03-15T09:00:00Z): Hi Bob! Did you end up going to the doctor last week?\n" +
"Bob (2024-03-15T09:01:00Z): Yes, finally. Turns out I have a mild peanut allergy.\n" +
"Alice (2024-03-15T09:02:00Z): Oh no! Are you okay?\n" +
"Bob (2024-03-15T09:03:00Z): Yeah, nothing serious. Just need to carry an antihistamine.\n" +
"Alice (2024-03-15T09:04:00Z): Good to know. We'll avoid peanuts at the team lunch."
docID := "chat-2024-03-15-alice-bob"
context_ := "team chat"
ts := "2024-03-15T09:04:00Z"
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{
Content: hindsight.TextContent(conversation),
Context: *hindsight.NewNullableString(&context_),
DocumentId: *hindsight.NewNullableString(&docID),
Timestamp: *hindsight.NewNullableTimestamp(&hindsight.Timestamp{
String: &ts,
}),
},
},
}).Execute()
```
When the conversation grows — a new message arrives — just retain again with the full updated content and the same `document_id`. Hindsight will delete the previous version and reprocess from scratch, so memories always reflect the latest state of the conversation.
---
## Parameters
### content
The raw text to store. This is the only required field. Hindsight chunks the content, sends each chunk to the LLM for fact extraction, and stores the resulting structured facts — not the original text. A single `content` value can produce many memories depending on how much information it contains.
### timestamp
When the event described in the content actually occurred. Three forms are accepted:
| Value | Behaviour |
|-------|-----------|
| Omitted / `null` | Defaults to the current time at ingestion. |
| ISO 8601 string (e.g. `"2024-01-15T10:30:00Z"`) | Uses the provided datetime. |
| `"unset"` | Stores the content **without any timestamp**. Use this for timeless material such as reference documents, books, or fictional content where no real event time exists. |
The timestamp is injected into the LLM fact-extraction prompt so the model can resolve relative temporal references in the content — for example, if the content says "last Monday", the model uses the provided timestamp as the anchor to pin down the actual date. When `"unset"` is passed the prompt shows `Event Date: Unknown`, allowing the model to correctly return `N/A` for the `when` field of every extracted fact. Providing a real timestamp also enables temporal recall queries like "What happened last spring?" to work correctly.
### context
A short label describing the source or situation — for example `"team meeting"`, `"slack"`, or `"support ticket"`. It is injected directly into the LLM prompt, so it actively shapes how facts are extracted. The same sentence can mean something very different depending on context: "the project was terminated" in a `"performance review"` context versus a `"product roadmap"` context produces different memories.
Providing context consistently is one of the highest-leverage things you can do to improve memory quality.
### Python
```python
client.retain(
bank_id="my-bank",
content="Alice got promoted to senior engineer",
context="career update",
timestamp="2024-03-15T10:00:00Z"
)
```
### Node.js
```javascript
await client.retain('my-bank', 'Alice got promoted to senior engineer', {
context: 'career update',
timestamp: '2024-03-15T10:00:00Z'
});
```
### CLI
```bash
hindsight memory retain my-bank "Alice got promoted" \
--context "career update"
```
### Go
```go
ctxLabel := "career update"
ts2 := "2024-03-15T10:00:00Z"
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{
Content: hindsight.TextContent("Alice got promoted to senior engineer"),
Context: *hindsight.NewNullableString(&ctxLabel),
Timestamp: *hindsight.NewNullableTimestamp(&hindsight.Timestamp{
String: &ts2,
}),
},
},
}).Execute()
```
### metadata
Arbitrary key-value string pairs that provide context about this item. For example: `{"source": "slack", "channel": "engineering", "thread_id": "T123"}`. Metadata is included in the fact extraction prompt, so the LLM can use it as additional context when extracting facts — for instance, knowing the document title or source can improve accuracy. It is also stored on each memory unit and returned with every recalled memory, letting you do client-side filtering or static enrichment without extra lookups — for example, linking a memory back to its source URL, thread ID, or any application-specific identifier.
Values are stored and returned as strings. A key whose value is `null` is dropped rather than stored, so it will not come back on the recalled memory — send an empty string if you need the key to survive.
### document_id
A caller-supplied string that groups one or more items under a logical document. This field is the key to making retain **idempotent**.
When you provide a `document_id`, Hindsight upserts the document: if a document with that ID already exists in the bank, it and all its associated memories are deleted before the new content is processed and inserted. This means you can safely re-run retain on updated content — for example, a chat thread that grew since last time — without accumulating duplicate memories.
If you omit `document_id`, Hindsight assigns a random UUID, so re-ingesting the same content will create duplicate memories. How items without a `document_id` are grouped depends on the rest of the request:
| Request | Result |
|---------|--------|
| No item has a `document_id` | All items go into **one** new document. A request too large for one pass (over `HINDSIGHT_API_RETAIN_BATCH_TOKENS`) is split into parts, and each part becomes its own document. |
| Some items have a `document_id`, others don't | Each item without one becomes its **own** new document. It is never folded into another item's document, so replacing or deleting that document leaves it alone. |
| Items share the same `document_id` | Those items go into that one document, in request order. |
An item with an attachment always gets its own document when it has no `document_id`, so it counts as an item *with* one in the table above.
To keep every item separate, give each one its own `document_id`. To put several items in one document, give them the same `document_id`.
### update_mode
Controls how Hindsight handles an existing document when you retain with a `document_id` that already exists.
| Value | Behaviour |
|-------|-----------|
| `"replace"` *(default)* | Deletes the old document and all its memories, then processes the new content from scratch. This is the standard upsert described above. |
| `"append"` | Concatenates the new content onto the existing document text and reprocesses the combined document. Delta retain automatically skips unchanged chunks, so only the new portion triggers LLM extraction. |
Append mode requires a `document_id` — without one there is no existing document to append to.
**When to use append**: Use `"append"` for content that grows incrementally — for example, a log file, a journal, or a chat transcript where you receive new messages one at a time. Instead of re-sending the entire history on each update, send only the new content with `update_mode: "append"` and Hindsight will efficiently merge it with what it already has.
```json
{
"items": [
{
"content": "New entry to add to the existing document.",
"document_id": "my-growing-doc",
"update_mode": "append"
}
]
}
```
### entities
A list of entities you want to guarantee are recognized, merged with any entities the LLM extracts automatically. Each entry has a `text` field (the entity name) and an optional `type` (e.g., `"PERSON"`, `"ORG"`, `"CONCEPT"` — defaults to `"CONCEPT"` if omitted).
Use this when you know certain entities are important but the LLM might miss them or refer to them inconsistently across different parts of the content. Providing entities explicitly ensures they are always linked in the knowledge graph.
### resolve_entities
By default the names you pass in `entities` are **resolved** against the entities already in the bank: a name close to an existing one may be matched onto it rather than creating its own entity, based on name similarity plus how strongly it co-occurs with the other names in the same item. That is usually what you want — it is how `Dr. Waller` and `Dr Waller` end up as one entity instead of two.
Set `resolve_entities: false` when the names you are passing are authoritative and must be stored exactly as written. An existing entity is then reused only when its name matches case-insensitively, any other name creates a new entity, and your names are never merged with each other (`Alice` and `Alice Smith` stay two entities).
This applies **only to the entities you supply**. Auto-extracted entities are always resolved, because they are the extractor's guess at a name rather than yours — turning resolution off for them would fill the bank with near-duplicate entities.
The same flag exists on [editing a memory](./memories.md#resolving-entity-names), where it matters most: a correction you type by hand is exactly the case where a similar existing entity should not win.
### tags and document_tags
Tags control **visibility scoping** — which memories are visible during recall. A memory is only returned if its tags intersect with the tags filter provided in the recall request. This makes tags useful when a single memory bank serves multiple users or sessions and each should only see their own memories.
Use consistent naming patterns to keep tag filtering predictable. Common conventions: `user:<id>` for per-user scoping, `session:<id>` for session isolation, `room:<id>` for chat rooms, `topic:<name>` for category filtering. The bank also exposes a list-tags endpoint that returns all tags with their memory counts, useful for UI autocomplete or wildcard expansion.
See [Recall API](./recall.md#tags) for filtering by tags during retrieval.
### observation_scopes
Controls which [observations](../observations.md) this memory contributes to during consolidation. Each scope runs an independent pass, creating or updating observations tagged with only that scope's tags.
> **ℹ️ Scope isolation**
>
During consolidation, Hindsight uses `all_strict` matching to find existing observations to update — only observations whose tags exactly match the current scope are considered. This keeps scopes isolated: a memory consolidated under `["student:alice"]` will never bleed into an observation tagged `["student:alice", "teacher:bob"]`.
The examples below use a lesson transcript retained with `tags: ["student:alice", "teacher:bob", "session-id:s1"]`.
#### combined *(default)*
One consolidation pass using all tags together. The resulting observation is tagged with the full set.
- Observations created: `["student:alice", "teacher:bob", "session-id:s1"]`
- ✗ *"What does Alice struggle with across all her sessions?"* — no match, because no observation was ever built for `student:alice` alone
- ✗ *"How does Bob teach?"* — no match for `teacher:bob` alone
- ✓ *"What happened in session s1 with Alice and Bob?"* — exact match
**Use when** the memory is meaningful only as a whole and you never need to query any single tag in isolation.
#### shared
One consolidation pass over a single global, **untagged** scope. The memory's own tags are ignored for observation scoping (they stay on the source facts for recall filtering), so memories with *different* tags all consolidate into the **same** observation.
- Observations created: one untagged observation (`[]`)
- ✓ Untagged observations match every recall regardless of tag filter
- Deduplicates across volatile per-call tags: if each session is retained with a unique `session-id:…` tag, `combined` and `per_tag` create a fresh observation every session (the tag never repeats), whereas `shared` folds them all into one.
**Use when** your tags are per-call provenance (e.g. session ids) that you want for recall filtering and debugging but not as a consolidation boundary — keep the tag on `tags` and set `observation_scopes: "shared"`. Documents already retained without it can be moved in place with a [document PATCH](../observations.md#changing-scopes).
> **🚨 `shared` vs `[[]]` vs `[]`**
>
`shared` is equivalent to the explicit scope `[[]]` — a list containing one empty scope. Do **not** confuse it with `[]` (an empty list), which declares *zero* scopes and silently falls back to `combined`.
#### per_tag
One consolidation pass per individual tag. Each tag gets its own isolated observation that grows with every new memory sharing that tag.
- Observations created: `["student:alice"]` · `["teacher:bob"]` · `["session-id:s1"]`
- ✓ *"What does Alice struggle with across all her sessions?"*
- ✓ *"How does Bob teach?"*
- ✓ *"What happened in session s1?"*
- ✗ *"How does Alice perform specifically with Bob?"* — no observation for the `["student:alice", "teacher:bob"]` combination
- ✗ *"How does Bob teach in online sessions?"* — no observation for `["teacher:bob", "session-id:s1"]`
**Use when** content involves multiple tags that each represent an independent subject — the most common choice for multi-party content like conversations, lessons, or support sessions.
#### all_combinations
One pass per subset of tags — singles, pairs, triples, and so on. For 3 tags that is 7 passes.
- Observations created: all `"per_tag"` scopes above, plus `["student:alice", "teacher:bob"]` · `["student:alice", "session-id:s1"]` · `["teacher:bob", "session-id:s1"]` · `["student:alice", "teacher:bob", "session-id:s1"]`
- ✓ All questions from `"per_tag"` above
- ✓ *"How does Alice perform specifically with Bob?"* — matched by `["student:alice", "teacher:bob"]`
**Use when** you need observations at every granularity — per tag, per pair, per group.
#### custom
Pass an explicit list of tag sets. Each inner list is one scope.
```json
[["student:alice"], ["teacher:bob"], ["teacher:bob", "session-id:s1"]]
```
- Observations created: exactly those three scopes — nothing more
- ✓ *"What does Alice struggle with?"*
- ✓ *"How does Bob teach?"*
- ✓ *"How does Bob teach in session s1 specifically?"*
- ✗ *"What happened in session s1 regardless of teacher?"* — `["session-id:s1"]` alone was not included
**Use when** you know exactly which combinations are meaningful and want to avoid unnecessary passes.
#### Tag globs in custom scopes
A tag containing `*` is a glob, matched against the memory's own tags: `topic:*` stands for whatever `topic:` tags the memory carries. This lets you scope by a tag whose value you don't know when you send the request — typically one added by an [entity label](./memory-banks.md#entity-labels) with `tag: true` during extraction.
With a memory tagged `["topic:billing", "user:alice", "source:agent-a", "session:7"]`:
| `observation_scopes` | Observations created |
|---|---|
| `[["topic:*"]]` | `["topic:billing"]` |
| `[["topic:*", "user:*"]]` | `["topic:billing", "user:alice"]` |
| `[["topic:*"], ["user:*"]]` | `["topic:billing"]` · `["user:alice"]` |
| `[["topic:*", "team:x"]]` | `["team:x", "topic:billing"]` |
- A glob matching several tags puts all of them in the scope.
- A scope whose globs match nothing is skipped. If every scope is skipped, the memory falls back to `combined` — it never becomes untagged.
- Tags the scope leaves out (`source:`, `session:` above) stay on the memory for recall filtering; they just don't split observations. So memories about the same topic from different agents or sessions consolidate together — and in the same LLM call, which keeps consolidation cost proportional to the number of subjects rather than the number of memories.
> **🚨 Tags left out of the scope are not on the observation**
>
With `[["topic:*"]]`, Alice's and Bob's billing facts build one observation tagged only `topic:billing`. A recall filtered to `user:alice` won't match it, and anyone who can see `topic:billing` reads both. Add `user:*` to the scope to keep users apart.
#### Bank default
When an item sends no `observation_scopes`, the bank's [`default_observation_scopes`](./memory-banks.md#default_observation_scopes) applies (`combined` unless set). It accepts any value above, so `[["topic:*"]]` there groups every client's memories by topic without changing any client. The default is stored on each memory at retain, so changing it later doesn't re-scope memories already retained (reprocessing a document applies the current default) — move them with a [document PATCH](../observations.md#changing-scopes).
### Response
The synchronous retain response includes:
- `success` — whether the operation completed without errors
- `bank_id` — the memory bank that received the content
- `items_count` — number of items processed
- `async` — whether processing ran asynchronously
- `usage` — token usage for the LLM calls (`input_tokens`, `output_tokens`, `total_tokens`), only present for synchronous operations
---
## Batch Ingestion
Multiple items can be submitted in a single request. Batch ingestion is the recommended approach — it reduces network overhead and lets Hindsight optimize extraction across related content.
### Python
```python
client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Alice works at Google", "context": "career", "document_id": "conversation_001_msg_1"},
{"content": "Bob is a data scientist at Meta", "context": "career", "document_id": "conversation_001_msg_2"},
{"content": "Alice and Bob are friends", "context": "relationship", "document_id": "conversation_001_msg_3"}
]
)
```
### Node.js
```javascript
await client.retainBatch('my-bank', [
{ content: 'Alice works at Google', context: 'career', document_id: 'conversation_001_msg_1' },
{ content: 'Bob is a data scientist at Meta', context: 'career', document_id: 'conversation_001_msg_2' },
{ content: 'Alice and Bob are friends', context: 'relationship', document_id: 'conversation_001_msg_3' }
]);
```
### CLI
```bash
# Batch ingestion via individual retain calls (CLI processes items one at a time)
hindsight memory retain my-bank "Alice works at Google" \
--context "career" --doc-id "conversation_001_msg_1"
hindsight memory retain my-bank "Bob is a data scientist at Meta" \
--context "career" --doc-id "conversation_001_msg_2"
hindsight memory retain my-bank "Alice and Bob are friends" \
--context "relationship" --doc-id "conversation_001_msg_3"
```
### Go
```go
doc1 := "conversation_001_msg_1"
doc2 := "conversation_001_msg_2"
doc3 := "conversation_001_msg_3"
ctx1 := "career"
ctx2 := "relationship"
client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Alice works at Google"), Context: *hindsight.NewNullableString(&ctx1), DocumentId: *hindsight.NewNullableString(&doc1)},
{Content: hindsight.TextContent("Bob is a data scientist at Meta"), Context: *hindsight.NewNullableString(&ctx1), DocumentId: *hindsight.NewNullableString(&doc2)},
{Content: hindsight.TextContent("Alice and Bob are friends"), Context: *hindsight.NewNullableString(&ctx2), DocumentId: *hindsight.NewNullableString(&doc3)},
},
}).Execute()
```
---
## Files
Upload files directly — Hindsight converts them to text and extracts memories automatically. File processing always runs asynchronously and returns operation IDs for tracking.
**Supported formats:** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, PNG, GIF, etc. — OCR support depends on the configured parser), audio (MP3, WAV, FLAC, etc. — transcription), HTML, and plain text formats (TXT, MD, CSV, JSON, YAML, etc.)
### Python
```python
# Upload files and retain their contents as memories.
# Supports: PDF, DOCX, PPTX, XLSX, images (OCR), audio (transcription), and text formats.
# Pass file paths — the client reads and uploads them automatically.
result = client.retain_files(
bank_id="my-bank",
files=[EXAMPLES_DIR / "sample.pdf"],
context="quarterly report",
)
print(result.operation_ids) # Track processing via the operations endpoint
```
### Node.js
```javascript
// Upload files and retain their contents as memories.
// Supports: PDF, DOCX, PPTX, XLSX, images (OCR), audio (transcription), and text formats.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pdfBytes = readFileSync(join(__dirname, 'sample.pdf'));
const result = await client.retainFiles('my-bank', [
new File([pdfBytes], 'sample.pdf'),
], { context: 'quarterly report' });
console.log(result.operation_ids); // Track processing via the operations endpoint
```
### CLI
```bash
# Upload a single file (PDF, DOCX, PPTX, XLSX, images, audio, and more)
hindsight memory retain-files my-bank "$SAMPLE_FILE"
# Upload a directory of files
hindsight memory retain-files my-bank "$SCRIPT_DIR/"
# Queue files for background processing (returns immediately)
hindsight memory retain-files my-bank "$SCRIPT_DIR/" --async
```
### Go
```go
// Open a file and upload it — Hindsight converts it to text and extracts memories.
// Supports: PDF, DOCX, PPTX, XLSX, images (OCR), audio (transcription), and text formats.
f, err := os.Open("../../hindsight-docs/examples/api/sample.pdf")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
defer f.Close()
fileResp, _, _ := client.FilesAPI.FileRetain(ctx, "my-bank").
Files([]*os.File{f}).
Request(`{"files_metadata": [{"context": "quarterly report"}]}`).
Execute()
fmt.Println("Operation IDs:", fileResp.GetOperationIds()) // Track processing via the operations endpoint
```
The file retain endpoint always returns asynchronously. The response contains `operation_ids` — one per uploaded file — which you can poll via `GET /v1/default/banks/{bank_id}/operations` to track progress.
Upload up to 10 files per request (max 100 MB total). Each file becomes a separate document with optional per-file metadata:
### Python
```python
# Upload multiple files with per-file metadata (up to 10 files per batch).
# Each file gets its own context, document_id, and tags.
result = client.retain_files(
bank_id="my-bank",
files=[
EXAMPLES_DIR / "sample.pdf",
EXAMPLES_DIR / "sample.pdf", # Replace with a second file path
],
files_metadata=[
{"context": "quarterly report", "document_id": "q1-report", "tags": ["project:alpha"]},
{"context": "meeting notes", "document_id": "q1-notes", "tags": ["project:alpha"]},
],
)
print(result.operation_ids) # One operation ID per file
```
### Node.js
```javascript
// Upload multiple files with per-file metadata (up to 10 files per request)
const batchResult = await client.retainFiles('my-bank', [
new File([pdfBytes], 'report.pdf'),
new File([pdfBytes], 'notes.pdf'),
], {
filesMetadata: [
{ context: 'quarterly report', document_id: 'q1-report', tags: ['project:alpha'] },
{ context: 'meeting notes', document_id: 'q1-notes', tags: ['project:alpha'] },
]
});
console.log(batchResult.operation_ids); // One operation ID per file
```
### CLI
```bash
# Upload a single file (PDF, DOCX, PPTX, XLSX, images, audio, and more)
hindsight memory retain-files my-bank "$SAMPLE_FILE"
# Upload a directory of files
hindsight memory retain-files my-bank "$SCRIPT_DIR/"
# Queue files for background processing (returns immediately)
hindsight memory retain-files my-bank "$SCRIPT_DIR/" --async
```
### Go
```go
// Open a file and upload it — Hindsight converts it to text and extracts memories.
// Supports: PDF, DOCX, PPTX, XLSX, images (OCR), audio (transcription), and text formats.
f, err := os.Open("../../hindsight-docs/examples/api/sample.pdf")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
defer f.Close()
fileResp, _, _ := client.FilesAPI.FileRetain(ctx, "my-bank").
Files([]*os.File{f}).
Request(`{"files_metadata": [{"context": "quarterly report"}]}`).
Execute()
fmt.Println("Operation IDs:", fileResp.GetOperationIds()) // Track processing via the operations endpoint
```
> **ℹ️ File Storage**
>
Uploaded files are stored server-side (PostgreSQL by default, or S3/GCS/Azure for production). Configure storage via `HINDSIGHT_API_FILE_STORAGE_TYPE`. See [Configuration](../configuration.md#file-processing) for details.
---
## Async Ingestion
For large batches, use async ingestion to avoid blocking your application:
### Python
```python
# Start async ingestion (returns immediately)
result = client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Large batch item 1", "document_id": "large-doc-1"},
{"content": "Large batch item 2", "document_id": "large-doc-2"},
],
retain_async=True
)
# Check if it was processed asynchronously
print(result.var_async) # True
```
### Node.js
```javascript
// Start async ingestion (returns immediately)
await client.retainBatch('my-bank', [
{ content: 'Large batch item 1', document_id: 'large-doc-1' },
{ content: 'Large batch item 2', document_id: 'large-doc-2' },
], {
async: true
});
```
### CLI
```bash
hindsight memory retain my-bank "Meeting notes" --async
```
### Go
```go
// Start async ingestion (returns immediately)
asyncTrue := true
largeDoc1 := "large-doc-1"
largeDoc2 := "large-doc-2"
retainResp, _, _ := client.MemoryAPI.RetainMemories(ctx, "my-bank").
RetainRequest(hindsight.RetainRequest{
Items: []hindsight.MemoryItem{
{Content: hindsight.TextContent("Large batch item 1"), DocumentId: *hindsight.NewNullableString(&largeDoc1)},
{Content: hindsight.TextContent("Large batch item 2"), DocumentId: *hindsight.NewNullableString(&largeDoc2)},
},
Async: &asyncTrue,
}).Execute()
// Check if it was processed asynchronously
fmt.Println("Async:", retainResp.GetAsync())
```
When `async: true`, the call returns immediately with an `operation_id`. Processing runs in the background via the worker service. No `usage` metrics are returned for async operations.
### Safe Retries with `operation_id`
If an async retain response is lost or times out, you can't tell whether the operation was created. Retrying blindly risks enqueuing the same work twice — duplicate extraction, embeddings, and provider spend.
Supply your own `operation_id` (any UUID) to make retries safe:
```json
{
"items": [{ "content": "Alice works at Google", "document_id": "conv_123" }],
"async": true,
"operation_id": "3f2b8c1a-9d4e-4a7b-9c2f-1e6d5a4b3c2d"
}
```
- Re-submitting with the same `operation_id` returns the original operation and creates no new work, so a retry after a lost acknowledgement is a no-op.
- Reusing an `operation_id` that already belongs to a different operation returns HTTP `409`.
- Omitting `operation_id` keeps the default behavior — each request creates a new operation.
- Generate the id **before** you send the request and reuse it across retries. `operation_id` is ignored for synchronous retain, and requires all items in the request to resolve to a single strategy.
### Cut Costs 50% with Provider Batch APIs
When using async retain, enable the provider Batch API to reduce LLM fact-extraction costs by 50%. OpenAI, Groq, and Gemini all offer this discount in exchange for a processing window of up to 24 hours — a trade-off that's typically invisible when retain already runs in the background.
```bash
export HINDSIGHT_API_RETAIN_BATCH_ENABLED=true
```
Hindsight submits fact extraction calls as a batch job to the provider, polls for completion, and processes results automatically. No changes to your API calls are needed.
> **📝 Note**
>
Batch API cost savings require `async=true` in your retain request and a compatible provider (OpenAI, Groq, or Gemini).
references/developer/api/webhooks.md
# Webhooks
Hindsight can notify your application in real-time when memory events occur by sending HTTP POST requests to a URL you configure.
## Delivery and Retries
Webhooks are registered per memory bank and fire automatically when matching events occur. Each delivery attempt is tracked, and failed deliveries are retried with exponential backoff:
| Attempt | Delay after failure |
|---------|---------------------|
| 1 | 5 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | Permanent failure |
A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within the configured timeout (default 30 seconds). After 6 failed attempts, the delivery is marked as permanently failed and no further retries are made.
> **ℹ️ At-least-once delivery**
>
Webhook delivery tasks are queued in the same database transaction as the primary operation (e.g. the retain or consolidation write). This means if the server crashes after committing but before sending, the delivery task survives and will be retried. As a result, **your endpoint may receive the same event more than once** — deduplicate on `event_id` (also sent as the `X-Hindsight-Event-Id` header). Don't use `operation_id` for this: a batch retain fires one event per document under the same operation.
## Verifying Deliveries
When a webhook is registered with a secret, every delivery carries an HMAC-SHA256 signature of the exact request body. Verify it before trusting a payload — the URL alone is not proof the request came from Hindsight.
| Header | Value | Notes |
|--------|-------|-------|
| `X-Hindsight-Event` | The event type, e.g. `retain.completed` | Always sent |
| `X-Hindsight-Event-Id` | The event's `event_id` | Always sent. Same value on every retry and on every webhook the event goes to — use it to deduplicate |
| `X-Hindsight-Delivery-Id` | This event's delivery to this webhook | Always sent. Same value on every retry; matches the `id` listed by `GET /webhooks/{id}/deliveries` |
| `X-Hindsight-Signature` | `sha256=<hex>` over the raw body | Sent when a secret is configured |
| `X-Hub-Signature-256` | Identical to `X-Hindsight-Signature` | The conventional name for this construction, so GitHub-style receivers verify out of the box |
| `X-Hindsight-Signature-V2` | `t=<unix_seconds>,v1=<hex>` over `<t>.<raw body>` | Timestamped variant — use this if you want replay protection |
`X-Hindsight-Signature` and `X-Hub-Signature-256` always carry the same value: same secret, same algorithm, same bytes. Verify whichever one your framework already understands; there is no reason to check both.
> **⚠️ Prefer the timestamped signature**
>
`X-Hindsight-Signature` / `X-Hub-Signature-256` sign the body and nothing else, so they say *this payload came from Hindsight* but not *this payload is fresh*. A delivery captured off the wire stays verifiable forever. `X-Hindsight-Signature-V2` binds the payload to the time it was signed — check that `t` is within a tolerance you choose (five minutes is a common default) and reject anything older. The timestamp is inside the signed string, so it cannot be edited without breaking the MAC. It is re-signed on every retry attempt, so a delivery that is retried hours later still arrives with a fresh `t`.
```python
TOLERANCE_SECONDS = 300
def verify(secret: str, body: bytes, header: str) -> bool:
"""Verify an X-Hindsight-Signature-V2 header against the raw request body."""
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp, received = parts["t"], parts["v1"]
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False # too old (or too far in the future) — treat as a replay
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)
```
Two things to get right in any language:
- **Sign the raw bytes.** Re-serializing the parsed JSON changes whitespace and key order, and the signature will not match. Read the body before your framework decodes it.
- **Compare in constant time** (`hmac.compare_digest`, `crypto.timingSafeEqual`, …), never with `==`.
Custom headers set on a webhook's `http_config` cannot override `X-Hindsight-Event`, the id headers, or any signature header, so a receiver can trust those values whatever else is configured.
## Event Types
### `consolidation.completed`
Fired after Hindsight finishes consolidating new memories into observations for a bank.
**Payload:**
```json
{
"event_id": "3f6c2a9e-8d41-4b7a-9c0e-5e2f1d7a6b48",
"event": "consolidation.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:00Z",
"data": {
"observations_created": 3,
"observations_updated": 1,
"observations_deleted": null,
"error_message": null
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `observations_created` | `integer \| null` | Number of new observations created |
| `observations_updated` | `integer \| null` | Number of existing observations updated |
| `observations_deleted` | `integer \| null` | Number of observations deleted |
| `error_message` | `string \| null` | Set when `status` is `"failed"` |
**`status` values:** `"completed"` or `"failed"`
---
### `retain.completed`
Fired once per document after a retain operation completes (both synchronous and asynchronous). When retaining a batch of N documents, N separate events are fired.
**Payload:**
```json
{
"event_id": "3f6c2a9e-8d41-4b7a-9c0e-5e2f1d7a6b48",
"event": "retain.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:01Z",
"data": {
"document_id": "doc-abc123",
"tags": ["meeting", "q1-2026"],
"memory_unit_count": 12
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `document_id` | `string \| null` | The document ID if one was provided in the retain request |
| `tags` | `string[] \| null` | Document-level tags applied during retain |
| `memory_unit_count` | `number \| null` | Memory units the document owns after this retain. `null` when the request carried no `document_id`. |
**Notes:**
- For async retain (`async: true`), `operation_id` matches the `operation_id` returned by the retain API.
- For sync retain, `operation_id` is a generated identifier for tracing purposes.
- One event is fired per content item in the retain request.
- `memory_unit_count: 0` means fact extraction returned nothing for the document. The retain still succeeded and the text is stored, but `recall` and `reflect` search memories — so the document is not retrievable until it is [reprocessed](../retain.md#when-a-mission-excludes-everything-in-a-document). Watch this field to catch a retain mission that excludes more than intended.
---
### `memory_defense.triggered`
Fired when a bank's [Memory Defense](../memory-defense/index.md) policy acts on a retained item — once per item that is **redacted** or **blocked**. Items that pass cleanly do not fire an event. Requires a Memory Defense policy enabled on the bank and a webhook subscribed to this event type.
**Payload:**
```json
{
"event_id": "3f6c2a9e-8d41-4b7a-9c0e-5e2f1d7a6b48",
"event": "memory_defense.triggered",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "redact",
"timestamp": "2026-03-04T12:00:02Z",
"data": {
"action": "redact",
"detector": "sensitive_data",
"document_id": "doc-abc123",
"matched_types": ["github_token", "aws_access_key"],
"message": "Sensitive data pattern matched: github_token, aws_access_key"
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `action` | `string` | Action taken on the item: `"redact"` or `"block"` |
| `detector` | `string \| null` | The detector that matched (`"sensitive_data"`) |
| `document_id` | `string \| null` | The document ID if one was provided in the retain request |
| `matched_types` | `string[] \| null` | Labels of the redaction patterns that fired (e.g. `github_token`, `ssn_us`) |
| `message` | `string \| null` | Human-readable summary of what matched |
**`status` values:** mirrors `data.action` — `"redact"` or `"block"`.
**Notes:**
- A `redact` event means the secret was scrubbed and the redacted memory was still stored. A `block` event means the item was dropped; if every item in the retain request is blocked, the retain call returns `422`.
references/developer/configuration.md
# Configuration
Complete reference for configuring Hindsight services through environment variables.
Hindsight has two services, each with its own configuration prefix:
| Service | Prefix | Description |
|---------|--------|-------------|
| **API Service** | `HINDSIGHT_API_*` | Core memory engine |
| **Control Plane** | `HINDSIGHT_CP_*` | Web UI |
---
## API Service
The API service handles all memory operations (retain, recall, reflect).
### Database
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
| `HINDSIGHT_API_READ_DATABASE_URL` | Optional read-replica PostgreSQL URL. When set, recall queries (semantic, BM25, graph, temporal) are routed through a separate connection pool against this URL, offloading the primary. Typically points to a read-only endpoint (e.g., CNPG's `<cluster>-ro` service or Aurora reader endpoint). | Unset (uses primary) |
| `HINDSIGHT_API_MIGRATION_DATABASE_URL` | Direct PostgreSQL URL for running migrations, bypassing connection poolers (e.g. PgBouncer). When set, advisory locks and Alembic migrations use this URL instead of `DATABASE_URL`. | Falls back to `DATABASE_URL` |
| `HINDSIGHT_API_DATABASE_SCHEMA` | PostgreSQL schema name for tables | `public` |
| `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Run database migrations on API startup | `true` |
| `HINDSIGHT_API_MIGRATION_ISOLATION` | Run migrations in a subprocess instead of in the calling process: `true` (keeps Alembic's import graph and its psycopg2 sync engine out of a long-lived server process) or `false` | `false` |
| `HINDSIGHT_API_MIGRATION_CONCURRENCY` | Number of tenant schemas to migrate concurrently (PostgreSQL only). Each schema runs in its own process; within a schema migrations are always sequential. Each worker has a fixed startup cost (~1–2s to boot a fresh interpreter), so this only pays off with **many** schemas (roughly tens or more) or slow/high-latency migrations — for a handful of schemas it is slower than sequential. Each worker uses ~3 database connections, so keep `concurrency × 3` within your database's spare `max_connections` (and any PgBouncer pool limit). `1` = fully sequential. Measured at 20k schemas: the per-restart no-op resweep dropped from ~60min to ~11min (≈5×) at `concurrency=12`. | `1` |
| `HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES` | Comma-separated list of maintenance discovery routines this deployment installs itself (see [Owning a maintenance routine](#owning-a-maintenance-routine)). Migrations skip anything named here. | Empty (every routine installed) |
| `HINDSIGHT_API_DATABASE_BACKEND` | Database engine backend: `postgresql` or `oracle` (Oracle 23ai) | `postgresql` |
If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production.
To tune the embedded database, add PostgreSQL settings as a query string: `pg0://hindsight?max_connections=300&shared_buffers=256MB`. They apply when the embedded database starts, so restart it after changing them.
To run against Oracle Database 23ai instead, set `HINDSIGHT_API_DATABASE_BACKEND=oracle` and use an `oracle+oracledb://…` URL. See the [Oracle Database guide](./oracle.md) for full setup instructions.
The `DATABASE_SCHEMA` setting allows you to use a custom PostgreSQL schema instead of the default `public` schema. This is useful for:
- Multi-database setups where you want Hindsight tables in a dedicated schema
- Hosting platforms (e.g., Supabase) where `public` schema is reserved or shared
- Organizational preferences for schema naming conventions
```bash
# Example: Using a custom schema
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
export HINDSIGHT_API_DATABASE_SCHEMA=hindsight
```
Migrations will automatically create the schema if it doesn't exist and create all tables in the configured schema.
#### Owning a maintenance routine
The background maintenance loop finds work by calling four PostgreSQL routines —
`mental_models_with_cron`, `banks_needing_consolidation`, `schemas_with_expired_rows` and
`schemas_with_expired_operations`. The stock implementations scan every schema in the
database on each tick. On a large multi-tenant install that is a serial loop over
thousands of schemas, repeated once per tick in every process running the loop.
Because they are called by name, you can replace one with an implementation suited to
your installation — typically a registry table maintained by a row trigger, so discovery
costs one indexed read instead of a scan. List the ones you have replaced and migrations
will leave them alone:
```bash
export HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES=mental_models_with_cron,banks_needing_consolidation
```
Read from the environment of whatever process runs the migration, so it applies equally to
`hindsight-admin run-db-migration`, a migration Job, and migrate-on-startup.
Without this, your replacement survives only until the next migration that reinstalls the
routine: they are all `CREATE OR REPLACE` against the same name, so the migration wins and
nothing records that a custom implementation was discarded.
A few things to know:
- **Your replacement must match the stock signature.** The loop calls these with fixed
arguments and reads fixed columns back.
- **Nothing verifies that a replacement exists.** Naming a routine you have not installed
leaves whatever was there before — nothing, on a fresh database — and the maintenance
loop then fails on the missing function rather than quietly running the scan you were
trying to avoid.
- **Ownership is not stamped into the database.** Remove a name from the list and the next
migration reinstalls the stock routine, with no data fix-up.
- **Empty by default.** An installation that has not replaced anything is unaffected.
### Database Connection Pool
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DB_POOL_MIN_SIZE` | Minimum connections in the primary pool | `5` |
| `HINDSIGHT_API_DB_POOL_MAX_SIZE` | Maximum connections in the primary pool | `100` |
| `HINDSIGHT_API_READ_DB_POOL_MIN_SIZE` | Minimum connections in the read-replica pool (only used when `READ_DATABASE_URL` is set) | Falls back to `DB_POOL_MIN_SIZE` |
| `HINDSIGHT_API_READ_DB_POOL_MAX_SIZE` | Maximum connections in the read-replica pool (only used when `READ_DATABASE_URL` is set) | Falls back to `DB_POOL_MAX_SIZE` |
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds (asyncpg client-side) | `60` |
| `HINDSIGHT_API_DB_ACQUIRE_TIMEOUT` | Connection acquisition timeout in seconds. Bounds how long a caller waits for a free pool connection before failing (retried by the caller); `0` waits indefinitely. | `30` |
| `HINDSIGHT_API_DB_POOL_SLOW_ACQUIRE_THRESHOLD_SECONDS` | Log a `[DB POOL] Slow acquire` warning when getting a pool connection takes longer than this many seconds. Raise it when new connections are slow by nature (e.g. TLS to Aurora Serverless); `0` disables the warning. | `0.05` |
| `HINDSIGHT_API_DB_STATEMENT_TIMEOUT` | Postgres `statement_timeout` applied to every pool connection, in seconds. Server-side safety net for runaway queries. Does **not** apply to Alembic migrations (which run on a separate psycopg2 engine). Set to `0` to disable. | `600` |
| `HINDSIGHT_API_DB_MAX_PARALLEL_WORKERS_PER_GATHER` | Optional Postgres `max_parallel_workers_per_gather` applied to every pool connection of this process. Unset leaves the server default. Set to `0` on background-worker processes so bulk maintenance queries (consolidation, graph upkeep) run serially instead of fanning out across CPU cores shared with latency-sensitive traffic. | unset |
| `HINDSIGHT_API_DB_SESSION_SETUP_ON_ACQUIRE` | Whether the per-connection session settings above (`statement_timeout`, `max_parallel_workers_per_gather`, the trigram threshold, the vector-search tuning, and — on the `vchord` text-search backend — the search path) are re-applied every time a connection is taken from the pool, not only when it is first opened. Keep this on unless the same settings are already pinned on the database role or the database itself (`ALTER ROLE … SET`), because behind a transaction-mode connection pooler an acquire can be linked to a server connection that never received them (on a direct connection they now survive, so the re-apply is redundant there) — with the re-apply off and nothing pinned server-side, reused connections quietly run without them, and on `vchord` recall fails outright rather than merely degrading. When they *are* pinned server-side the re-apply changes nothing and only costs a round trip per acquire, which is worth reclaiming on busy deployments behind a transaction-mode connection pooler. `application_name` is always re-applied and is unaffected by this setting. | `true` |
| `HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD` | Postgres `pg_trgm.similarity_threshold` applied to every pool connection, governing how close a name must be for entity resolution's `%` trigram match to treat it as a candidate. Must be between `0` (exclusive) and `1`. Never applied below `HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY`, since a candidate under the merge floor can never be merged. Raise it above that floor to look at fewer, closer names. | `0.3` |
| `HINDSIGHT_API_ENTITY_INTRABATCH_MERGE_SIMILARITY` | Trigram similarity (pg_trgm-equivalent, computed in-memory) at/above which two brand-new names created by the **same** retain are merged into a single entity (in-batch dedup of surface-form variants — e.g. the same name with different emoji/case/suffix). Must be between `0` (exclusive) and `1`. This is a *merge* cutoff, deliberately stricter than the recall-only threshold above; raise it toward `1.0` to merge only near-identical forms. | `0.5` |
| `HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY` | Minimum trigram similarity a name must have with an **existing** entity before that entity can be reused for it, whatever the other resolution signals say. Sits between the recall threshold above (`0.15`, which only decides what is *considered*) and the same-batch fold-in cutoff below (`0.5`). Must be between `0` (exclusive) and `1`. Lower it for corpora of very short names, where trigram similarity is unavoidably low (`Jon`/`John` is `0.29`); raise it to merge only clear surface variants. | `0.3` |
For high-concurrency workloads, increase `DB_POOL_MAX_SIZE`. Each concurrent recall/think operation can use 2-4 connections.
To run migrations manually (e.g., before starting the API), use the admin CLI:
```bash
# Migrate the base schema plus all discovered tenant schemas
hindsight-admin run-db-migration
# Or migrate a specific schema only:
hindsight-admin run-db-migration --schema tenant_acme
```
### Vector Extension
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_VECTOR_EXTENSION` | Vector index algorithm: `pgvector`, `vchord`, `pgvectorscale`, or `scann` | `pgvector` |
| `HINDSIGHT_API_ANN_ITERATIVE_SCAN` | Let a vector index scan resume until the query's `LIMIT` is satisfied, instead of stopping when its first candidate list drains. With it off, a recall can never retrieve more rows than that list holds — on pgvector, `hnsw.ef_search` (200) — so a larger recall budget widens the SQL and retrieves nothing extra. Requires pgvector 0.8.0+; older servers reject the setting and it is dropped automatically after the first attempt. This is the operational kill switch: setting it to `false` and restarting restores the previous retrieval depth exactly, with no code change. | `true` |
| `HINDSIGHT_API_ANN_MAX_SCAN_TUPLES` | Ceiling on how many tuples a single resumed scan may visit (`hnsw.max_scan_tuples`). This is the knob that governs what iterative scans cost: the filters that thin a result — the similarity floor, tags, date ranges — are applied *after* the index scan, so a selective query resumes repeatedly, and this bounds both the CPU it can spend and the memory it can hold (pgvector otherwise caps the latter at `work_mem × hnsw.scan_mem_multiplier`, which at this default is never approached). Lower it to trade retrieval depth back for latency; the initial scan is not counted, so even `1` leaves the pre-existing depth intact. pgvector's own default is `20000`. Ignored when iterative scans are off. | `4000` |
Hindsight supports four PostgreSQL vector extensions:
#### **pgvector** (HNSW - default)
- In-memory index using Hierarchical Navigable Small World algorithm
- Works well for most embeddings and dataset sizes
- Fast for small-medium datasets (<10M vectors)
- Higher memory usage for large datasets
- Most widely deployed and supported
#### **pgvectorscale** (DiskANN - recommended for scale) ⭐
- Disk-based index using StreamingDiskANN algorithm
- **28x lower p95 latency** and **16x higher throughput** vs dedicated vector DBs
- **60-75% cost reduction** at scale (SSDs cheaper than RAM)
- Superior filtering performance with streaming retrieval model
- Optimized for large datasets (10M+ vectors)
- Supports both **pgvectorscale** (open source) and **pg_diskann** (Azure)
- **Installation:**
- Open source/self-hosted: `CREATE EXTENSION vector; CREATE EXTENSION vectorscale CASCADE;`
- Azure PostgreSQL: `CREATE EXTENSION vector; CREATE EXTENSION pg_diskann CASCADE;`
#### **vchord** (vchordrq)
- Alternative high-performance vector index
- Optimized for high-dimensional embeddings (3000+ dimensions)
- Includes integrated BM25 search capabilities
- Requires `vchord` extension
#### **scann** (AlloyDB ScaNN)
- Google's ScaNN index, available on **AlloyDB** and **AlloyDB Omni**
- Uses a single global vector index in `AUTO` mode (per-bank partial indexes are not used)
- **Installation:** `CREATE EXTENSION vector; CREATE EXTENSION alloydb_scann CASCADE;`
- **Index build is deferred** until a table reaches **10,000 populated embedding rows** — AlloyDB cannot build a ScaNN AUTO index on a near-empty table. Until that threshold is crossed, recall falls back to a sequential scan; the global index is built on the next API startup once enough rows exist.
- A ready-to-use Docker Compose stack is provided at [`docker/docker-compose/alloydb/docker-compose.yaml`](https://github.com/vectorize-io/hindsight/blob/main/docker/docker-compose/alloydb/docker-compose.yaml) for running Hindsight against AlloyDB Omni locally.
**When to use pgvectorscale (DiskANN):**
- Large datasets (10M+ vectors) ⭐
- Complex filtering requirements
- Cost-sensitive deployments
- Production workloads requiring high throughput
- When disk I/O is not a bottleneck
**When to use pgvector (HNSW):**
- Small-medium datasets (<10M vectors)
- Maximum query speed when all data fits in memory
- Simple nearest-neighbor queries without filters
- Standard PostgreSQL deployment preference
**When to use vchord:**
- High-dimensional embeddings (3000+ dimensions)
- Want integrated BM25 search
- Already using vchord for text search
**When to use scann:**
- Running on Google **AlloyDB** or **AlloyDB Omni**
- Want managed ScaNN with `AUTO` mode tuning
#### Limiting vector indexes on large deployments
On `pgvector`, `pgvectorscale` and `vchord`, a bank's memories are indexed per
`(bank, fact_type)`. By default every bank gets its own indexes when it is
created, which is the right thing for most deployments and needs no
configuration.
It stops being the right thing when you have thousands of banks. These indexes
all live on one shared table, and PostgreSQL inspects and locks **every** index
on a table whenever it plans a query against it — so an index created for one
bank is a cost paid by searches in every other bank. Past a few thousand banks,
planning slows sharply and the server eventually runs out of lock-table space,
failing reads *and* bank deletion alike.
Setting a minimum size fixes that: a bank only gets its own indexes once it is
large enough to benefit from them. Below the threshold PostgreSQL answers the
same search from the `(bank_id, fact_type)` B-tree plus a top-N sort, which for
a small bank is both faster and *exact* rather than approximate — so small banks
lose nothing. The number of indexes becomes proportional to the number of
**large** banks rather than to the number of banks, and bank count stops being a
limit.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_VECTOR_INDEX_MIN_ROWS` | Memories a bank needs, in one fact type, before that fact type gets its own vector index. `0` (the default) turns the threshold **off**: every bank is indexed from the moment it is created. `10000` is a good starting point for deployments with thousands of banks. A bank whose memories a custom store owns is outside this at any value — it has no rows here, so it is never given a per-bank index, and never has an existing one taken away (see `repair-bank` in the [admin CLI](./admin-cli.md)). | `0` |
| `HINDSIGHT_API_VECTOR_INDEX_MAINTENANCE_MIN_INTERVAL_SECONDS` | Shortest gap between two index-maintenance runs for one bank. Stops a bank whose size hovers at the threshold from building and dropping the same index repeatedly. Unused while the threshold is off. | `900` |
**With the threshold off (the default),** indexes are created inside the
transaction that creates the bank — instantly, because the bank is empty — and
dropped when the bank is deleted. Nothing inspects bank sizes, and no background
operation runs.
**With a threshold set,** bank creation, ingestion and import build no indexes at
all, so no request ever waits on index DDL. Instead, a write that could move a
bank across the threshold queues a background `vector_index_maintenance`
operation, which builds an index when a bank grows past the threshold and removes
it if the bank shrinks well below it (the gap between those two points prevents
churn at the boundary). You will see these operations in the bank's operations
list.
To reconcile without waiting for a write — after a restore, an upgrade, or an
extension switch — run:
```bash
hindsight-admin repair-bank --all
```
This works in both modes: with the threshold off it rebuilds any index a bank is
missing, and with one set it also drops what a bank no longer earns.
**Switching extensions:**
If you need to switch from one extension to another:
1. Set `HINDSIGHT_API_VECTOR_EXTENSION` to your desired extension (`pgvector`, `vchord`, `pgvectorscale`, or `scann`)
2. If your database has existing data, you'll get an error with migration instructions (note: switching **to** `scann` is allowed even with data — the existing index is dropped and rebuilt as ScaNN once the table has at least 10,000 embedding rows)
3. For empty databases, indexes will be automatically recreated on startup
**Learn more:**
- [HNSW vs. DiskANN comparison](https://www.tigerdata.com/learn/hnsw-vs-diskann)
- [pgvectorscale GitHub](https://github.com/timescale/pgvectorscale)
### Text Search Extension
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_TEXT_SEARCH_EXTENSION` | Text search backend: `native`, `vchord`, `pg_textsearch`, `pgroonga`, or `pg_search` | `native` |
| `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE` | PostgreSQL text search dictionary used by the `native` backend (e.g. `english`, `french`, `simple`, `zhparser`) | `english` |
| `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER` | ParadeDB `pg_search` tokenizer used when creating BM25 indexes. Empty uses ParadeDB's default tokenizer (`unicode_words`). | unset |
| `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_FUNCTION_SCHEMA` | Schema containing ParadeDB `pg_search` functions (`score`, `boolean`, `match`). Default is `paradedb`; set to `pgsearch` (or another valid identifier) on managed PostgreSQL distributions that install functions under a custom schema. | `paradedb` |
| `HINDSIGHT_API_BM25_MAX_QUERY_TERMS` | Cap on the number of terms in the native PostgreSQL BM25 `tsquery`. Long queries OR-join every normalized token, and because native ranking has no IDF and re-ranks every match, an uncapped query over common terms can scan and rank a large fraction of the bank and time out. When the query exceeds the cap, the most **selective** terms are kept — those with the lowest tenant-wide document frequency, read for free from `pg_stats` (autovacuum-maintained, no reindex) — so a blunt truncation doesn't discard the high-signal terms. `0` restores the historical uncapped behavior; the cap bounds only the `native` backend (other BM25 backends receive the raw query and rank via their own index-native operators). | `16` |
| `HINDSIGHT_API_BM25_SELECTIVE_TERMS` | When the cap above trims a query, whether to keep the most **selective** terms (lowest document frequency, from `pg_stats`) rather than the first N. `true` is strictly better for recall at no extra cost when stats exist; set `false` to opt out of the catalog read and cap by position. Ignored when the cap is `0`. | `true` |
| `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` | When set, forces every LLM-generated artifact (retain facts, consolidation observations, reflect responses) into this language. Free-form (e.g. `Spanish`, `Japanese`). | unset |
Hindsight supports five backends for BM25 keyword retrieval:
- **native** — PostgreSQL's built-in full-text search (`tsvector` + GIN). Language configurable.
- **vchord** — VectorChord BM25 (uses the `llmlingua2` multilingual tokenizer).
- **pg_textsearch** — Timescale's pg_textsearch extension. English-only.
- **pgroonga** — pgroonga full-text search. Multilingual / CJK out of the box.
- **pg_search** — ParadeDB pg_search. True BM25; the only backend that is Citus-compatible.
A bank can also use none of them: [`enable_text_search`](#recall-pipeline-stages)
switches the keyword arm off per bank, leaving pure vector search. Every setting in
this table is then unused by that bank.
To switch backends: set `HINDSIGHT_API_TEXT_SEARCH_EXTENSION`. With existing data, you'll get an error and migration instructions; with an empty database the columns/indexes are recreated automatically on startup.
`HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER` only applies when `HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pg_search`, and only when BM25 indexes are created. Changing it for an existing database requires rebuilding the `pg_search` indexes or recreating the database. Supported values are empty/unset, `unicode_words`, `simple`, `whitespace`, `literal`, `literal_normalized`, `chinese_compatible`, `icu`, `jieba`, `source_code`, `chinese_lindera`/`lindera(chinese)`, `japanese_lindera`/`lindera(japanese)`, `korean_lindera`/`lindera(korean)`, `ngram(min,max)`, and `edge_ngram(min,max)`.
For non-English banks (especially CJK) and the language/extraction-language tradeoffs, see the [Multilingual Support](./multilingual.md) page.
### LLM Provider
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-responses`, `openai-codex`, `chatgpt`, `claude-code`, `cursor`, `github-copilot`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `zai`, `opencode-go`, `meta`, `nous`, `xai-oauth`, `fireworks`, `ollama`, `ollama-cloud`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `litellmrouter`, `volcano`, `openrouter`, `requesty`, `none` | `openai` |
| `HINDSIGHT_API_LLM_API_KEY` | API key for providers that require one; unused by `github-copilot`. Optional for `anthropic` when [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) is configured (see the Anthropic example below) | - |
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` |
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default |
| `HINDSIGHT_API_LLM_MAX_CONCURRENT` | Max concurrent LLM requests | `32` |
| `HINDSIGHT_API_LLM_MAX_RETRIES` | Max retry attempts for LLM API calls | `3` |
| `HINDSIGHT_API_LLM_INITIAL_BACKOFF` | Initial retry backoff in seconds (exponential backoff) | `1.0` |
| `HINDSIGHT_API_LLM_MAX_BACKOFF` | Max retry backoff cap in seconds | `60.0` |
| `HINDSIGHT_API_LLM_TIMEOUT` | LLM request timeout in seconds. Honoured by every provider as a **total** deadline for one request, including the time spent reading a streamed response — a backend that keeps sending bytes is cut off at the deadline rather than read indefinitely. Raise it if your provider is legitimately slow; the per-operation variables below override it. | `120` |
| `HINDSIGHT_API_LLM_CONNECT_TIMEOUT` | Ceiling on the **connect** phase (TCP + TLS handshake) of an LLM request, in seconds. The read, write and pool phases keep the full `HINDSIGHT_API_LLM_TIMEOUT` budget; capping connect separately means an unreachable or wedged endpoint fails in seconds instead of consuming the whole request timeout. Effective value is the smaller of this and the request timeout. Set to `0` to disable the cap and use one value across all four phases. Applies to every provider whose HTTP client Hindsight constructs (all OpenAI-compatible backends, `openai-responses`, `chatgpt`, `anthropic`, `openai-codex`, `xai-oauth`, `fireworks`, `nous`, `llamacpp`); `litellm`/`litellmrouter` and `gemini`/`vertexai` own their transports and take a single total deadline instead, and `claude-code`/`cursor`/`github-copilot` drive a CLI rather than a socket. | `10` |
| `HINDSIGHT_API_LLM_HTTP_LOG_LEVEL` | Log level for the `httpx` and `httpcore` loggers. Set to `DEBUG` to trace each LLM request through its transport phases (`connect_tcp`, `send_request_headers`, `receive_response_headers`) — the way to tell a request that stalled before it was ever sent from one that was sent and never answered. Verbose; for diagnosis only. | `WARNING` |
| `HINDSIGHT_API_LLM_REASONING_EFFORT` | Reasoning effort for providers/models that support it (for example `none`, `low`, `medium`, `high`, `xhigh`). Set it and the value is sent as given, whatever your model is called — which is how you control thinking-token volume on a self-hosted reasoning model (vLLM, Ollama, llama.cpp, TGI), where `none` is often the only value that removes the thinking block. Leave it unset and no reasoning parameter is sent at all, so each model runs at its own default effort. Honoured by `openai` and every OpenAI-compatible provider, `openai-responses`, `chatgpt`, `openai-codex`, `xai`, `llamacpp`, and `litellm`/`litellmrouter` (which translate it per target provider). The native `gemini`/`vertexai`, `anthropic`, `claude-code` and `cursor` providers have no reasoning-effort control and log a warning at startup if you set one — reach those models through `litellm` to apply it. | Unset (model's own default) |
| `HINDSIGHT_API_LLM_TEMPERATURE` | Global override for the sampling temperature of internal LLM calls. Set a number in `[0.0, 2.0]`, or `none` (also `default`/`off`/empty) to **omit** the temperature parameter entirely — required for models that reject explicit temperatures, e.g. Azure `gpt-5.5`, which only accepts its default value. Per-operation variables below override this. | Per-operation defaults |
| `HINDSIGHT_API_LLM_TEMPERATURE_VERIFICATION` | Temperature for the startup connection check. Number in `[0.0, 2.0]` or `none` to omit. Overrides `HINDSIGHT_API_LLM_TEMPERATURE`. | `0.0` |
| `HINDSIGHT_API_LLM_TEMPERATURE_RETAIN` | Temperature for fact extraction during retain. Number in `[0.0, 2.0]` or `none` to omit. Overrides `HINDSIGHT_API_LLM_TEMPERATURE`. | `0.1` |
| `HINDSIGHT_API_LLM_TEMPERATURE_REFLECT` | Temperature for the reflect "thinking" step. Number in `[0.0, 2.0]` or `none` to omit. Overrides `HINDSIGHT_API_LLM_TEMPERATURE`. | `0.9` |
| `HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION` | Temperature for consolidation (dedup), and for the mental-model refresh's delta and retraction calls unless `HINDSIGHT_API_LLM_TEMPERATURE_MENTAL_MODEL_REFRESH` is set. Number in `[0.0, 2.0]` or `none` to omit. Overrides `HINDSIGHT_API_LLM_TEMPERATURE`. | `0.0` |
| `HINDSIGHT_API_LLM_TEMPERATURE_MENTAL_MODEL_REFRESH` | Temperature for the mental-model refresh's structured edit calls (delta and retraction). Number in `[0.0, 2.0]` or `none` to omit. Set it when the refresh runs its own model (`HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_*`) whose recommended temperature differs from consolidation's. | Falls back to `HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION` |
| `HINDSIGHT_API_LLM_VISION` | Whether the configured LLM can read images, overriding what the provider reports about itself. Leave unset and each provider answers for itself: Anthropic and Gemini report yes, OpenAI reports yes, `none` reports no, and every gateway-style backend (LiteLLM, Ollama, LM Studio, OpenRouter, an OpenAI-compatible proxy) reports *unknown*, because its catalogue mixes vision-capable and text-only models. A retain carrying [inline attachments](#inline-attachments-in-retain) is refused with `422` on both "no" and "unknown" — dropping an attachment silently would leave a document that looks retained with the information the caller cared about gone. Set `true` when you are running a vision model behind such a gateway; set `false` to refuse images against an endpoint that rejects them despite its model name. | Provider decides |
| `HINDSIGHT_API_VLM_PROVIDER` | Provider for the **vision slot** — the model used only for retain chunks that carry an [inline attachment](#inline-attachments-in-retain). Every text-only chunk keeps using the retain LLM, so a bank whose documents mostly *are* text does not pay a vision model's price for all of it. Unset, attachments go to the retain LLM. | Retain LLM |
| `HINDSIGHT_API_VLM_API_KEY` | API key for the vision slot. | Retain LLM's |
| `HINDSIGHT_API_VLM_MODEL` | Model for the vision slot. This is the model `HINDSIGHT_API_LLM_VISION` and the `422` vision check apply to, since it is the one that will actually be handed the attachment. | Retain LLM's |
| `HINDSIGHT_API_VLM_BASE_URL` | Base URL for the vision slot. Follows `HINDSIGHT_API_VLM_PROVIDER`'s own default when that is set and this is not — it never inherits the retain provider's host, which would send the request to the wrong endpoint with the wrong key. | Provider default |
| `HINDSIGHT_API_LLM_SEND_BANK_AS_USER` | Tag outbound LLM and embedding calls with `user=<bank_id>` so gateways (OpenRouter usage accounting, LiteLLM, Helicone) can attribute spend per bank. When enabled, the bank id is transmitted to the upstream provider as the end-user identifier. | `false` |
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
| `HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER` | Bedrock service tier: `flex` for 50% cost savings (best-effort inference), `priority` (guaranteed throughput), or `reserved` (provisioned capacity) | Unset (default tier) |
| `HINDSIGHT_API_LLM_GEMINI_SERVICE_TIER` | Gemini service tier: `flex` for 50% cost savings (best-effort inference) | Unset (default tier) |
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict of extra request-body params (e.g. `temperature`, `top_p`, `max_tokens`) merged into every LLM call. Applied across the OpenAI-compatible, Fireworks, Anthropic, Gemini/VertexAI and LiteLLM (incl. Bedrock/Router) providers. Each provider merges them in its own native parameter space, so use that provider's field names (e.g. `max_tokens` for OpenAI/Anthropic vs `max_output_tokens` for Gemini). The native Ollama structured-output path is a special case — see the note below. Also useful for custom model servers (e.g. vLLM `chat_template_kwargs`). | `null` |
| `HINDSIGHT_API_LLM_DEFAULT_HEADERS` | JSON dict passed as `default_headers` to provider SDK clients. Used by operators routing through proxies / request-tracing middleware (e.g. Cloudflare AI Gateway, Helicone, corporate proxies). Wired into the Anthropic provider, the OpenAI-compatible providers, Fireworks, Nous, the OpenAI Responses API, and the LiteLLM-backed providers (`litellm`, `litellmrouter`, `bedrock`); other providers can opt in. | `null` |
| `HINDSIGHT_API_LLM_CACHE_AFFINITY` | Pin a conversation to one backend prompt cache on the OpenAI-compatible providers, Fireworks and Nous. Server-side prompt caches are per backend server, so the same conversation has to reach the same one for a cache hit. `xai_conv_id` sends xAI's documented `x-grok-conv-id` header; `openai_prompt_cache_key` sends OpenAI's `prompt_cache_key` field; `auto` picks one from the configured base URL host (x.ai / grok.com → `xai_conv_id`; native OpenAI or an openai.com host → `openai_prompt_cache_key`; anything else, including Azure OpenAI, → `none`); `none` sends nothing. The id is the operation's trace id, so every LLM call of one retain/reflect/consolidation run shares it. `auto` is an allowlist rather than a best-effort probe, so an unknown OpenAI-compatible backend (vLLM, ollama, groq, openrouter, a custom proxy) receives byte-identical requests to before; set `none` to disable entirely. Anthropic and Gemini/Vertex do their own caching and ignore this. | `auto` |
| `HINDSIGHT_API_LLM_STRICT_SCHEMA` | Grammar-enforce structured output via `json_schema` `strict: true` instead of the soft "schema-in-prompt + `json_object`" path. Typed Pydantic response models are serialized directly into the OpenAI strict subset: every object rejects additional properties, every declared property is required, and nullable fields remain nullable. Use it with weaker self-hosted models that return prose preambles, markdown ` ```json ` fences, or invalid JSON — which otherwise fail to parse and wedge retain/consolidation. Applies to OpenAI-compatible backends (OpenAI, LM Studio, Ollama, Volcano, llama.cpp, vLLM), Codex, and LiteLLM; Gemini already enforces its native `response_schema` regardless, and providers without a strict mode ignore it. LM Studio, Ollama, and Volcano are the most common cases: their soft path skips even `json_object` mode (they don't honor it reliably), so a small model's raw output goes unconstrained — enabling this flag is the fix for a `JSONDecodeError` during retain on those backends. | `false` |
| `HINDSIGHT_API_LLM_STRICT_SCHEMA_RETAIN` | Override `HINDSIGHT_API_LLM_STRICT_SCHEMA` for retain (fact extraction) only. Applies to both the streaming and batch extraction paths. | Inherits global |
| `HINDSIGHT_API_LLM_STRICT_SCHEMA_REFLECT` | Override `HINDSIGHT_API_LLM_STRICT_SCHEMA` for reflect's structured-output extraction and a mental model's delta-refresh operations. | Inherits global |
| `HINDSIGHT_API_LLM_STRICT_SCHEMA_CONSOLIDATION` | Override `HINDSIGHT_API_LLM_STRICT_SCHEMA` for consolidation only (both the batch consolidation call and observation dedup). | Inherits global |
| `HINDSIGHT_API_LLM_SUPPORTS_MAX_ITEMS` | Whether the LLM backend accepts JSON Schema `maxItems` / `minItems` in structured-output schemas. Set to `false` for backends such as Bedrock Converse that reject these keywords; consolidation still enforces observation caps after parsing. | `true` |
| `HINDSIGHT_API_LLM_SUPPORTS_STRING_PATTERN` | Whether the LLM backend accepts JSON Schema `pattern` in structured-output schemas. When `true`, retain constrains `occurred_start` / `occurred_end` to an ISO timestamp, which stops a grammar-constrained model from reasoning inside the timestamp string — a failure that corrupts the date and can burn the entire completion budget on an unterminated response. Left `false` because support is narrow and rejection is a hard 400 at request time: Bedrock validates schemas against an allowlist that excludes this keyword, and OpenAI errors on unsupported keywords under `strict`. Backends that neither enforce nor reject it gain nothing. | `false` |
| `HINDSIGHT_API_LLM_STRUCTURED_OUTPUT_FORCED_TOOL` | Request structured output from the LiteLLM-backed providers (`litellm`, `litellmrouter`, `bedrock`) with a single forced tool call — the response schema becomes the tool's parameters — instead of `response_format`. Set to `true` for backends that reject `response_format` outright. This is region-dependent on Bedrock Claude: `ap-southeast-2` (`au.*` inference profiles) refuses the translated Converse `outputConfig` with `Extra inputs are not permitted`, while the same model in `us-east-1` (`us.*`) accepts it and needs nothing here. Verified against both. If the model answers without calling the tool, the reply is parsed as text as before. Other providers ignore it. | `false` |
| `HINDSIGHT_API_LLM_OPENAI_COMPATIBLE_JSON_MODE` | Whether the OpenAI-compatible backend honours `response_format={"type": "json_object"}` on the soft (non-strict) structured-output path. Leave unset and the provider decides: LM Studio, Ollama, Volcano and z.ai get the schema in the prompt only, llama.cpp follows `HINDSIGHT_API_LLAMACPP_NO_GRAMMAR`, and every other provider also sends `json_object`. z.ai is skipped because its `json_object` mode deletes the literal token `json` from the generated text, so `latest.json` comes back as `latest.` — valid JSON, silently corrupted. Set `false` for `provider=openai` pointed at a local server that can't constrain output: some such servers rewrite the prompt instead, and a thinking model can loop on the rewrite until the LLM timeout. This doesn't make a model write JSON. It only drops the request the server mishandles, so the model still has to follow the schema in the prompt. Set `true` to force `json_object` on a backend that would otherwise skip it. | Provider decides |
| `HINDSIGHT_API_LLM_CODEX_HOME` | Credentials directory for the `openai-codex` provider — the directory holding the `auth.json` it authenticates with. Overrides the process-wide `CODEX_HOME` for Hindsight's own LLM calls. Its reason to exist is that `CODEX_HOME` is process-wide: set this (and the per-member `HINDSIGHT_API_LLM_<n>_CODEX_HOME`) to run two independently authorized ChatGPT profiles in one process, so a [multi-LLM chain](#multi-llm-strategies-failover--round-robin) of two Codex members can fail over between accounts. | Unset (`CODEX_HOME`, else `~/.codex`) |
| `HINDSIGHT_API_LLM_CHATGPT_HOME` | Credentials directory for the `chatgpt` provider: holds `credentials.json` (written by `hindsight-admin chatgpt login`, or `login --from-file` on a headless server) and this host's `host_id`. Server-level only. See [ChatGPT Setup](./models.md#chatgpt-setup-sign-in-with-chatgpt). | `~/.hindsight/chatgpt` |
| `HINDSIGHT_API_LLM_OLLAMA_NUM_CTX` | Optional native Ollama `num_ctx` override. Leave unset to use the model/server default; set a positive integer only when you need a larger context window. Setting it also routes free-form calls (including the startup connection probe) through the native `/api/chat` API, since the OpenAI-compatible endpoint cannot express a context size — see the note below. | Unset |
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
| `HINDSIGHT_API_LLM_PROMPT_CACHE_ENABLED` | Reuse the fixed system prefix via the provider's explicit prompt cache, billed at the cached-input rate (Gemini/Vertex `CachedContent`). The cached prefix is shared across all banks and soft-fails to an uncached call. Set to `false` to disable. See [Models](./models.md#provider-capabilities). | `true` |
| `HINDSIGHT_API_REFLECT_PROMPT_CACHE_ENABLED` | For reflect specifically, roll a step-by-step context cache forward through the agent's tool loop so each turn reuses the whole prior conversation (system + tools + all prior tool results) at the cached-input rate instead of only the static prefix. Requires `HINDSIGHT_API_LLM_PROMPT_CACHE_ENABLED`. The per-reflect caches are ephemeral and deleted when the reflect ends. Set to `false` to run reflect uncached while leaving prompt caching on elsewhere — on Gemini that is currently the cheaper setting, because each cache's creation is billed at the full input rate plus storage and every rolling cache is read by exactly one call. | `true` |
| `HINDSIGHT_API_LLM_DEBUG_DUMP_4XX` | Diagnostic: when enabled, on any LLM `4xx` the provider logs `[LLM_4XX_DUMP]` with the request as actually assembled — the serialized request config (response schema + generation params, message bodies stripped) and length-capped per-message previews — so an otherwise-unreproducible rejected request can be inspected. Wired into all remote providers (Gemini/Vertex, OpenAI-compatible incl. Fireworks/Nous, Anthropic, LiteLLM incl. Router, Codex). Off by default; leave off in normal operation. | `false` |
When `HINDSIGHT_API_LLM_PROVIDER=ollama`, Hindsight no longer sends the previous native API default `num_ctx=16384` unless you set it explicitly. To keep the old request behavior, set `HINDSIGHT_API_LLM_OLLAMA_NUM_CTX=16384`; otherwise Ollama uses the model Modelfile or server default.
**When Hindsight uses Ollama's native API.** Structured-output calls always use the native `/api/chat` API, for schema enforcement. Free-form calls join them as soon as `HINDSIGHT_API_LLM_OLLAMA_NUM_CTX` is set, because Ollama's OpenAI-compatible handler parses a fixed set of fields and drops the rest — there is no way to express `num_ctx` on `/v1/chat/completions`, nested under `options` or otherwise. That matters on a shared Ollama host: Ollama keys a loaded model instance by context size, so a call at the server default reloads the model and re-tunes it for every other consumer.
**`HINDSIGHT_API_LLM_EXTRA_BODY` on the native Ollama path.** The native request body has *two tiers* — this differs from the OpenAI-compatible endpoint, where the SDK flattens everything to top-level. On the native path `extra_body` is split accordingly:
- **Top-level native fields** (`think`, `keep_alive`, ...) pass through directly. For example, gpt-oss models require a thinking level for structured extraction, so set `HINDSIGHT_API_LLM_EXTRA_BODY='{"think": "low"}'` (thinking is disabled by default).
- **Generation parameters** (`seed`, `top_p`, `top_k`, `num_ctx`, `temperature`, ...) live under Ollama's `options` object, so nest them: `HINDSIGHT_API_LLM_EXTRA_BODY='{"options": {"seed": 42, "top_p": 0.9}}'`. On the OpenAI-compatible endpoints these same params are top-level instead.
**Provider Examples**
```bash
# Groq (recommended for fast inference)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
# For free tier users: override to on_demand if you get service_tier errors
# export HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=on_demand
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Optional: Use Flex Processing for 50% cost savings (with variable latency)
# export HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER=flex
# OpenAI Responses API (/v1/responses)
export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-5.6
# Uses the OpenAI Responses API instead of chat/completions. Unlike
# chat/completions, it supports reasoning together with function tools, so
# reflect's tool-calling search loop can run with a real reasoning effort.
# Recommended for reasoning models (gpt-5.x, o-series) that reject
# `reasoning_effort` alongside tools on chat/completions (e.g. gpt-5.6-terra).
# export HINDSIGHT_API_LLM_REASONING_EFFORT=high
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
# Optional: Use Gemini Flex for 50% cost savings (best-effort inference)
# export HINDSIGHT_API_LLM_GEMINI_SERVICE_TIER=flex
# Anthropic
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Or, with no static key: Workload Identity Federation. Leave HINDSIGHT_API_LLM_API_KEY
# unset and the Anthropic SDK trades the platform identity token for short-lived
# access tokens, refreshing them on its own.
# export ANTHROPIC_FEDERATION_RULE_ID=fdrl_xxxxxxxxxxxx
# export ANTHROPIC_ORGANIZATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_xxxxxxxxxxxx
# export ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic/token # e.g. a projected k8s service-account token
# Vertex AI (Google Cloud - uses native genai SDK)
export HINDSIGHT_API_LLM_PROVIDER=vertexai
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-gcp-project-id
export HINDSIGHT_API_LLM_VERTEXAI_REGION=us-central1
# Optional: use ADC (gcloud auth application-default login) or provide service account key:
# export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/service-account-key.json
# Ollama (local, no API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3
# Ollama Cloud (hosted Ollama endpoint, requires API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama-cloud
export HINDSIGHT_API_LLM_API_KEY=your-ollama-cloud-api-key
export HINDSIGHT_API_LLM_MODEL=gemma3:12b
# LM Studio (local, no API key)
export HINDSIGHT_API_LLM_PROVIDER=lmstudio
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
export HINDSIGHT_API_LLM_MODEL=your-local-model
# llama.cpp (built-in local inference, no external server needed)
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
# No API key, base URL, or external server required.
# Auto-downloads Gemma 4 E2B (~3.5 GB GGUF) on first run.
# See "Built-in llama.cpp" section below for all configuration options.
# OpenAI-compatible endpoint (Chat Completions API, /v1/chat/completions)
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_MODEL=your-model-name
# OpenAI-compatible endpoint (Responses API, /v1/responses)
# Same as above but targets /v1/responses instead of /v1/chat/completions.
export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_MODEL=your-model-name
# OpenAI Codex (ChatGPT Plus/Pro subscription - uses OAuth, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_MODEL=gpt-5.6-luna
# No API key needed - uses OAuth tokens from ~/.codex/auth.json
# For long-running services, set CODEX_HOME to a dedicated auth directory so
# Hindsight doesn't share (and lose) its refresh token with another Codex process.
# See Models docs → "Isolating Codex auth for long-running services".
# export CODEX_HOME=/var/lib/hindsight/codex
# Per-provider override of CODEX_HOME. Set it (with the indexed
# HINDSIGHT_API_LLM_<n>_CODEX_HOME below) to run two independently authorized
# ChatGPT profiles in one process.
# export HINDSIGHT_API_LLM_CODEX_HOME=/var/lib/hindsight/codex-a
# Claude Code (Claude Pro/Max subscription - uses OAuth)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929
# No API key needed - uses `claude auth login` credentials.
# Set HINDSIGHT_API_LLM_API_KEY to a token from `claude setup-token` to authenticate with a key instead.
# Cursor (Cursor subscription - drives the cursor-agent CLI in headless mode)
export HINDSIGHT_API_LLM_PROVIDER=cursor
export HINDSIGHT_API_LLM_MODEL=auto # or any id from `cursor-agent --list-models`
# No API key needed - uses `cursor-agent login` credentials.
# Set HINDSIGHT_API_LLM_API_KEY (or CURSOR_API_KEY) to authenticate with a key instead.
# An agent CLI turn takes 15-30s, well past the 30s reflect default, so raise the
# timeouts or reflect burns its first iteration on a timeout retry.
export HINDSIGHT_API_LLM_TIMEOUT=300
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=180
# The CLI exposes no response-format or tool-definition flags, so Hindsight emulates
# structured output and tool calling through the prompt and parses the JSON back. That
# is less reliable than a native schema: prefer a stronger named model over `auto` for
# reflect, which is the operation that leans hardest on tool calls.
# Volcano Engine (ByteDance - OpenAI-compatible)
export HINDSIGHT_API_LLM_PROVIDER=volcano
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
export HINDSIGHT_API_LLM_MODEL=doubao-pro-32k
# OpenRouter (OpenAI-compatible, access 100+ models)
export HINDSIGHT_API_LLM_PROVIDER=openrouter
export HINDSIGHT_API_LLM_API_KEY=your-openrouter-api-key
export HINDSIGHT_API_LLM_MODEL=qwen/qwen3.5-9b
# Structured-output calls (fact extraction, consolidation) send
# provider.require_parameters=true, so OpenRouter only routes them to upstreams
# that support response_format. Any "provider" keys you set in
# HINDSIGHT_API_LLM_EXTRA_BODY (e.g. "only") are kept and take precedence.
# That also means every parameter sent must be supported, temperature included.
# Models that reject temperature (e.g. anthropic/claude-haiku-5.5) then fail with
# "No endpoints found that can handle the requested parameters". Fix: omit it.
# export HINDSIGHT_API_LLM_TEMPERATURE=none
# Requesty (OpenAI-compatible gateway)
export HINDSIGHT_API_LLM_PROVIDER=requesty
export HINDSIGHT_API_LLM_API_KEY=your-requesty-api-key
export HINDSIGHT_API_LLM_MODEL=openai/gpt-4o-mini
# DeepSeek (OpenAI-compatible, https://api.deepseek.com)
export HINDSIGHT_API_LLM_PROVIDER=deepseek
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
# Notes:
# - `deepseek-v4-flash` defaults to thinking mode at the API level (treated as
# `deepseek-reasoner`). Hindsight handles this transparently; the reflect
# agent will not crash with "deepseek-reasoner does not support this
# tool_choice".
# - Use `deepseek-v4-pro` for the higher-quality reasoning route.
# - Use `deepseek-chat` for the non-thinking alias (faster, cheaper).
# z.ai (Zhipu GLM series, OpenAI-compatible, https://z.ai)
export HINDSIGHT_API_LLM_PROVIDER=zai
export HINDSIGHT_API_LLM_API_KEY=your-zai-api-key
export HINDSIGHT_API_LLM_MODEL=glm-4.5-flash # or glm-4.5-air for the paid tier
# Default base_url: https://api.z.ai/api/coding/paas/v4 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# opencode-go (OpenAI-compatible)
export HINDSIGHT_API_LLM_PROVIDER=opencode-go
export HINDSIGHT_API_LLM_API_KEY=your-opencode-go-api-key
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
# Default base_url: https://opencode.ai/zen/go/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# Meta Model API (Muse Spark, OpenAI-compatible, https://ai.developer.meta.com)
export HINDSIGHT_API_LLM_PROVIDER=meta
export HINDSIGHT_API_LLM_API_KEY=your-meta-model-api-key
export HINDSIGHT_API_LLM_MODEL=muse-spark-1.3
# Default base_url: https://api.meta.ai/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# Muse Spark always reasons: leave HINDSIGHT_API_LLM_REASONING_EFFORT unset or set it to
# minimal/low/medium/high/xhigh. "none" is rejected with HTTP 400. Reasoning tokens are
# billed against the output budget, so keep the per-operation max-token limits generous.
# Muse Spark reasons before every reply, so calls are slow: reflect's 30s default
# deadline is not enough for its final synthesis and the call fails after retries.
# Raise it (and the global deadline) when using this provider:
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=300
export HINDSIGHT_API_LLM_TIMEOUT=300
# Nous Portal (OpenAI-compatible; no API key — uses your `hermes portal` login)
export HINDSIGHT_API_LLM_PROVIDER=nous
export HINDSIGHT_API_LLM_MODEL=deepseek/deepseek-v4-flash
# No API key needed — reads a rotating JWT from ~/.hermes/auth.json (run `hermes portal` first).
# Default base_url: https://inference-api.nousresearch.com/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# See the "Nous Portal Setup" section in the Models guide for the login flow.
# SuperGrok subscription (device-code OAuth; no API key). This is the subscription
# lane, NOT xAI API support — for API-key access to api.x.ai use
# HINDSIGHT_API_LLM_PROVIDER=openai with HINDSIGHT_API_LLM_BASE_URL=https://api.x.ai/v1.
export HINDSIGHT_API_LLM_PROVIDER=xai-oauth
export HINDSIGHT_API_LLM_MODEL=grok-4.5
# No API key needed — reads an OAuth grant from ~/.hindsight/xai_oauth.json.
# Log in once: python -m hindsight_api.engine.providers.xai_oauth_auth login
# Default base_url: https://api.x.ai/v1
# See the "SuperGrok Subscription Setup" section in the Models guide for the login flow.
# AWS Bedrock (native support - no API key needed, uses AWS credentials)
export HINDSIGHT_API_LLM_PROVIDER=bedrock
export HINDSIGHT_API_LLM_MODEL=us.amazon.nova-2-lite-v1:0
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_REGION_NAME=us-east-1
# Optional: Use Flex tier for 50% cost savings (with variable latency)
# export HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER=flex
# LiteLLM (100+ providers via LiteLLM SDK)
# Azure OpenAI via LiteLLM
export HINDSIGHT_API_LLM_PROVIDER=litellm
export HINDSIGHT_API_LLM_API_KEY=your-azure-api-key
export HINDSIGHT_API_LLM_MODEL=azure/gpt-4o
# Together AI via LiteLLM
export HINDSIGHT_API_LLM_PROVIDER=litellm
export HINDSIGHT_API_LLM_API_KEY=your-together-api-key
export HINDSIGHT_API_LLM_MODEL=together_ai/meta-llama/Llama-3-70b-chat-hf
# No LLM (chunk storage + semantic search only, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=none
# Retain automatically uses chunks mode (no fact extraction)
# Recall works normally (semantic search, BM25, graph retrieval)
# Reflect returns HTTP 400 (requires an LLM)
# Consolidation/observations are disabled
```
> **💡 OpenAI Codex, Claude Code & Vertex AI Setup**
>
For detailed setup instructions for **ChatGPT plan** ([Sign in with ChatGPT](./models.md#chatgpt-setup-sign-in-with-chatgpt)), **OpenAI Codex** (ChatGPT Plus/Pro), **Claude Code** (Claude Pro/Max), and **Vertex AI** (Google Cloud), see the [Models documentation](./models.md#openai-codex-setup-chatgpt-pluspro).
### SuperGrok OAuth (`xai-oauth`)
`HINDSIGHT_API_LLM_PROVIDER=xai-oauth` authenticates with a SuperGrok subscription via
device-code OAuth instead of an API key. Log in once with
`python -m hindsight_api.engine.providers.xai_oauth_auth login`; the grant is stored on disk
and refreshed automatically. Every variable below is optional — the defaults match the
vendor's own client, so a normal deployment sets none of them.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_XAI_OAUTH_TOKEN_PATH` | Where the OAuth grant is stored. Both the login and the refresh follow it, so this relocates the store rather than overriding only the read. | `~/.hindsight/xai_oauth.json` |
| `HINDSIGHT_API_XAI_OAUTH_BASE_URL` | Deployment-wide endpoint override. Takes precedence over `HINDSIGHT_API_LLM_BASE_URL`, which deployments often set for an unrelated proxy. | `https://api.x.ai/v1` |
| `HINDSIGHT_API_XAI_OAUTH_CLIENT_ID` | OAuth client id used at login and refresh. | xAI's published Grok CLI client |
| `HINDSIGHT_API_XAI_OAUTH_SCOPE` | Scope string requested at login. | vendor default |
| `HINDSIGHT_API_XAI_OAUTH_REFRESH_SKEW_SECONDS` | How long before expiry a token counts as due for refresh. Widen it if this deployment calls the provider rarely (a cron or gateway shape). | `60` |
| `HINDSIGHT_API_XAI_OAUTH_REFRESH_TIMEOUT_SECONDS` | Per-request timeout for discovery, device-code and refresh calls. | `20` |
| `HINDSIGHT_API_XAI_OAUTH_DEBUG_HEADERS` | Log an allowlist of response headers (`via`, `x-request-id`, `cf-ray`, `server`, `date`) on a non-2xx reply. Diagnostic only; never logs credentials, cookies or bodies. | `false` |
This is the subscription lane, not xAI API-key support — for `api.x.ai` with an API key use
`HINDSIGHT_API_LLM_PROVIDER=openai` with `HINDSIGHT_API_LLM_BASE_URL=https://api.x.ai/v1`.
### LLM Router (LiteLLM Router)
`HINDSIGHT_API_LLM_PROVIDER=litellmrouter` runs the default LLM through [LiteLLM's `Router`](https://docs.litellm.ai/docs/routing). The config JSON is forwarded verbatim — for fallback chains, load-balancing, rate limits, routing strategies, and the rest of the supported keys, see the [LiteLLM Router docs](https://docs.litellm.ai/docs/routing). Hindsight always issues completions against `model_name: "default"`, so include at least one entry with that name.
| Variable | Description |
|----------|-------------|
| `HINDSIGHT_API_LLM_LITELLMROUTER_CONFIG` | JSON object passed to `litellm.Router(**config)`. Required when provider is `litellmrouter`. |
| `HINDSIGHT_API_{RETAIN,REFLECT,CONSOLIDATION}_LLM_LITELLMROUTER_CONFIG` | Per-operation overrides. Fall back to the default config when unset. |
```bash
export HINDSIGHT_API_LLM_PROVIDER=litellmrouter
export HINDSIGHT_API_LLM_LITELLMROUTER_CONFIG='{
"model_list": [
{"model_name": "default", "litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-..."}},
{"model_name": "fallback", "litellm_params": {"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-..."}}
],
"fallbacks": [{"default": ["fallback"]}]
}'
```
The config is a credential field — never returned by the bank-config API. Hindsight already retries calls; set `"num_retries": 0` in the Router config to avoid double-retries. Batch APIs aren't supported in router mode.
### Multi-LLM Strategies (failover / round-robin)
Configure additional LLMs **by index** alongside the primary, then choose a strategy for routing across them. This is a provider-agnostic alternative to the LiteLLM Router: the indexed LLMs can be any mix of providers, each fully configured.
The unindexed `HINDSIGHT_API_LLM_*` config is the **primary** (member 1). Extra members are numbered from 1:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLM_<n>_PROVIDER` | Provider for extra member `n` (`n` = 1, 2, ...). Presence of this var defines the member; indices must be contiguous from 1. | - |
| `HINDSIGHT_API_LLM_<n>_API_KEY` | API key for member `n` (required unless the provider needs none). | - |
| `HINDSIGHT_API_LLM_<n>_MODEL` | Model for member `n`. | Provider default |
| `HINDSIGHT_API_LLM_<n>_BASE_URL` | Base URL for member `n`. | Provider default |
| `HINDSIGHT_API_LLM_<n>_REASONING_EFFORT` | Reasoning effort for member `n`. | `HINDSIGHT_API_LLM_REASONING_EFFORT` |
| `HINDSIGHT_API_LLM_<n>_EXTRA_BODY` / `_DEFAULT_HEADERS` | Per-member JSON overrides. | - |
| `HINDSIGHT_API_LLM_<n>_BEDROCK_SERVICE_TIER` / `_GEMINI_SERVICE_TIER` | Per-member service tier. | - |
| `HINDSIGHT_API_LLM_<n>_VERTEXAI_PROJECT_ID` / `_VERTEXAI_REGION` / `_VERTEXAI_SERVICE_ACCOUNT_KEY` | Per-member Vertex AI project, region, and service-account key path (for a `vertexai` member). Each falls back to the global `HINDSIGHT_API_LLM_VERTEXAI_*` when unset. | Global / `us-central1` / ADC |
| `HINDSIGHT_API_LLM_<n>_CODEX_HOME` | Per-member Codex credentials directory — the directory holding the `auth.json` this member authenticates with (for an `openai-codex` member). Set it so two Codex members run as two independently authorized ChatGPT profiles; without it every member resolves the same store. Falls back to the global `HINDSIGHT_API_LLM_CODEX_HOME`, then `CODEX_HOME`, then `~/.codex`. | Global / `CODEX_HOME` / `~/.codex` |
| `HINDSIGHT_API_LLM_<n>_LITELLMROUTER_CONFIG` | Per-member LiteLLM Router config JSON (for a `litellmrouter` member). Falls back to the global `HINDSIGHT_API_LLM_LITELLMROUTER_CONFIG` when unset. | - |
| `HINDSIGHT_API_LLM_<n>_TIMEOUT` | Per-member request timeout, in seconds. Falls back to the operation's timeout when unset. | Operation timeout |
| `HINDSIGHT_API_LLM_<n>_MAX_RETRIES` | Per-member retry budget. Falls back to the operation's `MAX_RETRIES` when unset; `0` is a valid setting and means fail over immediately. | Operation `MAX_RETRIES` |
| `HINDSIGHT_API_LLM_STRATEGY` | JSON routing strategy across the chain. Unset = single primary LLM (no change). | - |
The strategy JSON supports three modes:
- `{"mode": "failover"}` — try members in order (primary first); on a member's failure (after its own retries) advance to the next.
- `{"mode": "round-robin"}` — rotate the starting member per request to spread load, then fall through the rest on failure. Add `"weights": [3, 1, ...]` (positive ints, one per member, primary first) for an **unbalanced** rotation.
- `{"mode": "metadata", "routes": [...]}` — **retain only**: pick the member from each retained item's own `metadata`. See [Metadata routing](#metadata-routing) below.
**Cooldown.** In `failover` and `round-robin`, a member that fails (after its own retries) is moved to the back of the order for `cooldown_seconds` (default `60`), so later requests stop waiting on its errors and timeouts — useful when a member has run out of a daily or weekly quota. If it fails again once the cooldown is over, the cooldown doubles, up to one hour; its next success clears it. A cooling member is only deprioritized, never dropped: if every other member fails too, it is still tried. Set `"cooldown_seconds": 0` to try every member in order on every request. The cooldown is held in memory per process, so a restart clears it.
```bash
# Primary OpenAI, failover to Groq then Anthropic
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-...
export HINDSIGHT_API_LLM_1_PROVIDER=groq
export HINDSIGHT_API_LLM_1_API_KEY=gsk-...
export HINDSIGHT_API_LLM_2_PROVIDER=anthropic
export HINDSIGHT_API_LLM_2_API_KEY=sk-ant-...
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover"}'
# Weighted round-robin: serve the primary 3x as often as member 1
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "round-robin", "weights": [3, 1]}'
# Failover, re-probing a failed member after 5 minutes (then 10, 20, ... up to 1h)
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover", "cooldown_seconds": 300}'
```
#### Metadata routing
`{"mode": "metadata"}` sends each retained item to the chain member its own metadata selects, so one deployment can extract different documents with different models:
```bash
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-...
export HINDSIGHT_API_LLM_1_PROVIDER=ollama
export HINDSIGHT_API_LLM_1_MODEL=qwen3:8b
export HINDSIGHT_API_LLM_STRATEGY='{
"mode": "metadata",
"routes": [{"key": "classification", "value": "sensitive", "member": 1}]
}'
```
Retaining `{"content": "...", "metadata": {"classification": "sensitive"}}` extracts facts on the local `qwen3:8b`; everything else uses the primary.
- **Routes are matched per item, in declared order — the first match wins.** An item that matches no route uses member `0` (the primary).
- **Values are compared as strings**, because retain metadata is free-form JSON and route values come from an env var: `"member": 1` matches metadata `1` and `"1"`. A list value matches if any entry does, so `{"labels": ["pii", "eu"]}` matches a route on `labels` = `pii`.
- **Nothing is stored.** The selection is made when the item's extraction prompt is built and is not persisted, so changing the routes changes only future retains. Reprocessing a document replays its original metadata and therefore re-routes the same way.
- **Each retain item is one extraction prompt**, so a batch mixing differently routed items is fine — every item goes to its own member.
**What this does not do.** Metadata routing chooses *which model extracts a document*. It is not a data boundary: the facts extracted from a routed document are stored in the same bank as everything else, and recall, reflect, consolidation, mental-model refresh and dry-run extraction all continue to use the primary LLM. If you need a document's content kept away from a provider entirely, use a separate bank with a per-bank LLM configuration instead.
Two further limits:
- **`update_mode: "append"` routes on the metadata supplied with the append call**, not the stored document's. An append re-extracts the stored body together with the new text, so resupply the same metadata to keep it on the same member.
- **Batch retain is not supported** with this mode. `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` submits every item of an operation as a single job to a single member, which cannot honour per-item routes, so the combination is rejected at startup.
**Per-operation chains.** Each operation can define its own members + strategy with the `RETAIN` / `REFLECT` / `CONSOLIDATION` / `MENTAL_MODEL_REFRESH` prefix (e.g. `HINDSIGHT_API_RETAIN_LLM_1_PROVIDER`, `HINDSIGHT_API_RETAIN_LLM_STRATEGY`). A per-operation slot with no indexed members (or no strategy) inherits the global chain — except `MENTAL_MODEL_REFRESH`, which inherits the reflect chain (and, when reflect declares none, the global one).
The indexed members are credential fields — never returned by the bank-config API and server-level only (not per-bank configurable). **Batch retain** runs on the first batch-capable member in declared order, which need not be the primary — so a chain whose primary has no batch API can still use `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` as long as one member supports it. That member serves the whole batch (submit, polling and retrieval all target the account that holds it), so batch does not fail over the way the interactive retain/reflect/consolidation calls do. An in-flight batch is bound to the account that submitted it, so if the worker restarts mid-batch it resumes on that same account even when the chain has since been reordered or extended. Removing that member — or rotating its API key — while a batch is still running makes the operation fail with an explicit error instead of polling a different account.
**Retries inside a chain.** A failover chain is itself a retry: when a member
fails, the next one is tried. Retrying a non-terminal member first only delays
that handoff, and when it is failing because it is saturated the immediate
retry is likely to fail the same way. The last member is the opposite case —
it has nowhere to fail over to, so its retry budget is the only thing between a
transient error and a failed request, and it is where honouring a `Retry-After`
pays. A single operation-wide `MAX_RETRIES` cannot express both, so set
`HINDSIGHT_API_LLM_MAX_RETRIES=0` to fail over promptly and give the final
member its own budget with `HINDSIGHT_API_LLM_<n>_MAX_RETRIES`.
### Built-in llama.cpp
The `llamacpp` provider runs a llama.cpp server as a managed subprocess — no external LLM server needed. On first run it auto-downloads a default GGUF model (~3.5 GB) into `~/.hindsight/models`. Requires the `local-llm` extra: `pip install 'hindsight-api-slim[local-llm]'`.
> **⚠️ Not available in the Docker image**
>
The published `ghcr.io/vectorize-io/hindsight` image deliberately leaves llama.cpp out, so `HINDSIGHT_API_LLM_PROVIDER=llamacpp` cannot run there — it fails immediately with a message telling you so.
For local inference in Docker, run llama.cpp as its own container and point Hindsight at its OpenAI-compatible API with `HINDSIGHT_API_LLM_PROVIDER=openai` and `HINDSIGHT_API_LLM_BASE_URL`. A ready-to-run setup is in [`docker/docker-compose/local-llm/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/local-llm).
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLAMACPP_MODEL_PATH` | Path to a GGUF model file. If not set, auto-downloads `gemma-4-E2B-it-Q4_K_M` from HuggingFace. | Auto-download |
| `HINDSIGHT_API_LLAMACPP_GPU_LAYERS` | Number of layers to offload to GPU. `-1` = all layers (recommended). `0` = CPU only. | `-1` |
| `HINDSIGHT_API_LLAMACPP_CONTEXT_SIZE` | Context window size in tokens. | `8192` |
| `HINDSIGHT_API_LLAMACPP_CHAT_FORMAT` | Chat template format. `null` = auto-detect from GGUF metadata (recommended). | Auto-detect |
| `HINDSIGHT_API_LLAMACPP_NO_GRAMMAR` | Disable JSON grammar enforcement. Faster inference but less reliable JSON output. | `false` |
| `HINDSIGHT_API_LLAMACPP_EXTRA_ARGS` | Space-separated extra CLI args passed to the llama.cpp server (e.g. `--n_threads 8 --type_k 1`). | - |
```bash
# Minimal setup (auto-downloads model, uses GPU)
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
# Custom model with tuning
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2
export HINDSIGHT_API_LLAMACPP_MODEL_PATH=~/.hindsight/models/my-model.gguf
export HINDSIGHT_API_LLAMACPP_CONTEXT_SIZE=16384
export HINDSIGHT_API_LLAMACPP_NO_GRAMMAR=true # faster, less reliable JSON
export HINDSIGHT_API_LLAMACPP_EXTRA_ARGS="--n_threads 8"
```
> **📝 Note**
>
The llama.cpp server is shared across all LLM operations (retain, reflect, consolidation). Set `HINDSIGHT_API_LLM_MAX_CONCURRENT=2` to allow retain and consolidation to run concurrently without blocking each other.
Auto-downloaded models land in `~/.hindsight/models`. Keep that directory on persistent storage — if it is discarded between restarts, every restart re-downloads the full model.
### Per-Operation LLM Configuration
Different memory operations have different requirements. **Retain** (fact extraction) benefits from models with strong structured output capabilities, while **Reflect** (reasoning/response generation) can use lighter, faster models. Configure separate LLM models for each operation to optimize for cost and performance.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RETAIN_LLM_PROVIDER` | LLM provider for retain operations | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
| `HINDSIGHT_API_RETAIN_LLM_API_KEY` | API key for retain LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
| `HINDSIGHT_API_RETAIN_LLM_MODEL` | Model for retain operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
| `HINDSIGHT_API_RETAIN_LLM_BASE_URL` | Base URL for retain LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
| `HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT` | Extra cap on concurrent retain LLM requests, composed with the global cap. Unset → only the global cap applies. | Unset |
| `HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES` | Max retries for retain | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
| `HINDSIGHT_API_RETAIN_LLM_INITIAL_BACKOFF` | Initial backoff for retain retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
| `HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF` | Max backoff cap for retain retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
| `HINDSIGHT_API_RETAIN_LLM_TIMEOUT` | Timeout for retain requests (seconds) | Falls back to `HINDSIGHT_API_LLM_TIMEOUT` |
| `HINDSIGHT_API_RETAIN_LLM_REASONING_EFFORT` | Reasoning effort for retain operations | Falls back to `HINDSIGHT_API_LLM_REASONING_EFFORT` |
| `HINDSIGHT_API_RETAIN_LLM_EXTRA_BODY` | Extra request-body params (JSON dict) for retain operations | Falls back to `HINDSIGHT_API_LLM_EXTRA_BODY` |
| `HINDSIGHT_API_RETAIN_LLM_CACHE_AFFINITY` | Prompt-cache affinity mode for retain operations | Falls back to `HINDSIGHT_API_LLM_CACHE_AFFINITY` |
| `HINDSIGHT_API_REFLECT_LLM_PROVIDER` | LLM provider for reflect operations | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
| `HINDSIGHT_API_REFLECT_LLM_API_KEY` | API key for reflect LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
| `HINDSIGHT_API_REFLECT_LLM_MODEL` | Model for reflect operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
| `HINDSIGHT_API_REFLECT_LLM_BASE_URL` | Base URL for reflect LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
| `HINDSIGHT_API_REFLECT_LLM_MAX_CONCURRENT` | Extra cap on concurrent reflect LLM requests, composed with the global cap. Unset → only the global cap applies. | Unset |
| `HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES` | Max retries for reflect | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
| `HINDSIGHT_API_REFLECT_LLM_INITIAL_BACKOFF` | Initial backoff for reflect retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
| `HINDSIGHT_API_REFLECT_LLM_MAX_BACKOFF` | Max backoff cap for reflect retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
| `HINDSIGHT_API_REFLECT_LLM_TIMEOUT` | Timeout for reflect requests (seconds). Reflect answers a caller who is holding an HTTP request open and makes several sequential LLM calls, so its default is shorter than the global one — one stalled call must not outlive the caller. | `30`, or `HINDSIGHT_API_LLM_TIMEOUT` when that is set explicitly |
| `HINDSIGHT_API_REFLECT_LLM_REASONING_EFFORT` | Reasoning effort for reflect operations | Falls back to `HINDSIGHT_API_LLM_REASONING_EFFORT` |
| `HINDSIGHT_API_REFLECT_LLM_EXTRA_BODY` | Extra request-body params (JSON dict) for reflect operations | Falls back to `HINDSIGHT_API_LLM_EXTRA_BODY` |
| `HINDSIGHT_API_REFLECT_LLM_CACHE_AFFINITY` | Prompt-cache affinity mode for reflect operations | Falls back to `HINDSIGHT_API_LLM_CACHE_AFFINITY` |
| `HINDSIGHT_API_REFLECT_MAX_COMPLETION_TOKENS` | Transport-level output cap (`max_completion_tokens`) for reflect's final synthesis call. Unset means uncapped: the model runs to a natural stop and the reflect/mental-model `max_tokens` governs *visible* length via a prompt directive plus a post-hoc rewrite, not by truncating the provider call. On thinking models the raw provider budget is consumed by reasoning tokens, so a hard cap here would cut pages off mid-word. Set an integer only to enforce a hard cost ceiling on the synthesis call. | Unset (uncapped) |
| `HINDSIGHT_API_CONSOLIDATION_LLM_PROVIDER` | LLM provider for observation consolidation | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_API_KEY` | API key for consolidation LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_MODEL` | Model for consolidation operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_BASE_URL` | Base URL for consolidation LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT` | Extra cap on concurrent consolidation LLM requests, composed with the global cap. Unset → only the global cap applies. | Unset |
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_RETRIES` | Max retries for consolidation | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_INITIAL_BACKOFF` | Initial backoff for consolidation retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_BACKOFF` | Max backoff cap for consolidation retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT` | Timeout for consolidation requests (seconds) | Falls back to `HINDSIGHT_API_LLM_TIMEOUT` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_REASONING_EFFORT` | Reasoning effort for consolidation operations | Falls back to `HINDSIGHT_API_LLM_REASONING_EFFORT` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_EXTRA_BODY` | Extra request-body params (JSON dict) for consolidation operations | Falls back to `HINDSIGHT_API_LLM_EXTRA_BODY` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_CACHE_AFFINITY` | Prompt-cache affinity mode for consolidation operations | Falls back to `HINDSIGHT_API_LLM_CACHE_AFFINITY` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_PROVIDER` | LLM provider for the automatic mental-model refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_PROVIDER` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_API_KEY` | API key for the refresh LLM | Falls back to `HINDSIGHT_API_REFLECT_LLM_API_KEY` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MODEL` | Model for the automatic refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_MODEL` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_BASE_URL` | Base URL for the refresh LLM | Falls back to `HINDSIGHT_API_REFLECT_LLM_BASE_URL` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MAX_CONCURRENT` | Extra cap on concurrent refresh LLM requests, composed with the global cap. Unset → only the global cap applies. | Unset |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MAX_RETRIES` | Max retries for the refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_INITIAL_BACKOFF` | Initial backoff for refresh retries (seconds) | Falls back to `HINDSIGHT_API_REFLECT_LLM_INITIAL_BACKOFF` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MAX_BACKOFF` | Max backoff cap for refresh retries (seconds) | Falls back to `HINDSIGHT_API_REFLECT_LLM_MAX_BACKOFF` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_TIMEOUT` | Timeout for refresh requests (seconds). Nobody is waiting on a background refresh, so this is usually set much higher than the reflect timeout. | Falls back to `HINDSIGHT_API_LLM_TIMEOUT`, never to `HINDSIGHT_API_REFLECT_LLM_TIMEOUT` (that one is sized for a caller waiting on the answer) |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_REASONING_EFFORT` | Reasoning effort for the automatic refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_REASONING_EFFORT` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_EXTRA_BODY` | Extra request-body params (JSON dict) for the refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_EXTRA_BODY` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_CACHE_AFFINITY` | Prompt-cache affinity mode for the refresh | Falls back to `HINDSIGHT_API_REFLECT_LLM_CACHE_AFFINITY` |
**Automatic mental-model refresh.** The background refresh that runs after consolidation
drives the same agent as interactive reflect, so by default it uses the reflect LLM and
nothing above needs setting. The `MENTAL_MODEL_REFRESH_LLM_*` group exists because the two
want opposite tradeoffs on a single-GPU self-hosted box: interactive reflect favours a
reasoning model (a human is waiting on the quality), while the background refresh favours
a fast no-think model that cannot blow the wall timeout, cannot exceed the completion-token
budget on a reasoning chain, and does not halve interactive decode speed by running
alongside it. Every field falls back to its `REFLECT_LLM_*` counterpart, which in turn falls
back to the global `LLM_*` — set none of them and behaviour is exactly as before.
One refresh setting sits outside the group: the structured edit calls (delta and
retraction) take their temperature from `HINDSIGHT_API_LLM_TEMPERATURE_MENTAL_MODEL_REFRESH`,
which falls back to `HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION`.
> **💡 When to Use Per-Operation Config**
>
- **Retain**: Use models with strong structured output (e.g., GPT-4o, Claude) for accurate fact extraction
- **Reflect**: Use faster/cheaper models (e.g., GPT-4o-mini, Groq) for reasoning and response generation
- **Mental model refresh**: On shared/local hardware, point it at a no-think or cheaper hosted model so the background job cannot destabilise interactive reflect
- **Recall**: Does not use LLM (pure retrieval), so no configuration needed
**Example: Separate Models for Retain and Reflect**
```bash
# Default LLM (used as fallback)
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Use GPT-4o for retain (strong structured output)
export HINDSIGHT_API_RETAIN_LLM_MODEL=gpt-4o
# Use faster/cheaper model for reflect
export HINDSIGHT_API_REFLECT_LLM_PROVIDER=groq
export HINDSIGHT_API_REFLECT_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_REFLECT_LLM_MODEL=llama-3.3-70b-versatile
```
**Example: Tuning Retry Behavior for Rate-Limited APIs**
```bash
# For Anthropic with tight rate limits (10k output tokens/minute)
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Reduce concurrent requests for retain to avoid rate limits
export HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=3
# Fail faster with fewer retries
export HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES=3
# Or increase backoff times to wait out rate limit windows
export HINDSIGHT_API_RETAIN_LLM_INITIAL_BACKOFF=2.0 # Start at 2s instead of 1s
export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1min
```
> **📝 Per-operation concurrency composes with the global cap**
>
`HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT`, `HINDSIGHT_API_REFLECT_LLM_MAX_CONCURRENT`,
`HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT`, and
`HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MAX_CONCURRENT` add an extra cap that applies *on top of*
`HINDSIGHT_API_LLM_MAX_CONCURRENT`. A retain call counts against both the retain cap and the
global cap; a reflect call without a per-op cap is bounded only by the global cap.
To reserve headroom for live chat/reflect on a rate-limited provider, cap retain and
consolidation below the global value — e.g. global=4, retain=1, consolidation=1 leaves
two slots that retain/consolidation cannot consume. The mental-model refresh cap covers
the background refresh, its dry run and its delta operations, and is separate from the
reflect cap — capping it is how you stop the background job from starving interactive
reflect on shared hardware.
Unlike the per-operation timeout and retry/backoff knobs, the `*_LLM_MAX_CONCURRENT`
caps are process-global semaphores read from the environment once at startup. They are
server-level only (not overridable per tenant/bank) and a change requires a restart.
### Embeddings
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `onnx`, `tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `cohere`, `google`, `zeroentropy`, `nvidia`, `litellm`, or `litellm-sdk` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS` | Applies to **every** provider: truncate each text to this many tokens (counted with `HINDSIGHT_API_TOKENIZER_ENCODING`, approximate) before embedding, so oversized content is truncated instead of failing the embed call permanently. The budget covers the whole payload, including any client-side prefix an asymmetric model needs. The default matches the input limit of essentially every remote embedding model (OpenAI `text-embedding-3-*`, Bedrock Titan V2, Cohere v3, a stock llama.cpp context) — set it to your model's real limit if it differs, or to `0` to send text uncapped. **The cap is counted in Hindsight's tokens, not your provider's.** If your model uses a different tokenizer (for example `BAAI/bge-m3`, `nomic-embed-text`, or most open-source models behind TEI or an OpenAI-compatible endpoint), the same text can count up to ~2× more tokens on the provider's side, especially code, logs, or minified text. Text cut to exactly 8192 then still exceeds the model's limit, and the embed call fails with a 400. For these models, convert the limit: divide the model's real limit by how many of its tokens one Hindsight token becomes on your content, e.g. `4096` for an 8192-token model when embedding code. (Deprecated alias: `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MAX_INPUT_TOKENS`.) | `8192` |
| `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` | Text prepended to every search before it is embedded. Set it when your endpoint serves an asymmetric model that expects a search instruction — e.g. `task: search result \| query: ` for `google/embeddinggemma-300m`, or `query: ` for E5. Applies to the providers that only accept plain text (`tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `litellm`, `litellm-sdk`); see the note below for the ones that don't need it. Trailing spaces are kept as written. | - (no prefix) |
| `HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX` | Text prepended to every stored memory/document before it is embedded — e.g. `title: none \| text: ` for `google/embeddinggemma-300m`, or `passage: ` for E5. Same providers as above. Trailing spaces are kept as written. | - (no prefix) |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider. Models that ship their own search-text and stored-text instructions (e.g. the Qwen3-Embedding family) have them applied automatically — see the note below. | `BAAI/bge-small-en-v1.5` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU` | Force CPU mode for local embeddings even when CUDA/XPU is available | `false` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID` | Hugging Face model repo for the ONNX provider. Used for auto-download and as the tokenizer fallback. | `intfloat/multilingual-e5-small` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH` | Local path to the ONNX graph. When unset, Hindsight downloads `HINDSIGHT_API_EMBEDDINGS_ONNX_FILE` from `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID`. | - |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH` | Hugging Face tokenizer repo or local tokenizer directory. Set this when using `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH`. | Falls back to `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_FILE` | ONNX file path inside the Hugging Face repo. Hindsight also downloads the conventional external-data sidecar with `_data` suffix when present. | `onnx/model.onnx` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS` | Expected embedding dimensions. Startup fails if the loaded model returns a different size. | Auto-detected |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_MAX_TOKENS` | Max tokenizer length for ONNX embeddings. | `512` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_POOLING` | Pooling strategy for token embeddings: `mean` or `cls`. Ignored when the ONNX graph returns a pre-pooled 2-D embedding output. | `mean` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_NORMALIZE` | L2-normalize ONNX vectors before storage. | `true` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX` | Prefix applied to query/search text before ONNX embedding. Keep `query: ` for E5 models; set to empty for non-E5 models such as MiniLM or BGE. | `query: ` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX` | Prefix applied to stored memory/document text before ONNX embedding. Keep `passage: ` for E5 models; set to empty for non-E5 models such as MiniLM or BGE. | `passage: ` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_OUTPUT_NAME` | Optional ONNX output name to request when an exported graph exposes a pooled embedding output. | - |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_BATCH_SIZE` | Texts per ONNX forward pass. The provider runs in-process, so this is what bounds the activation tensor (and therefore peak memory) when a caller embeds a large list — an import, for example. | `32` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_CPU_MEM_ARENA` | Enable ONNX Runtime's CPU memory arena. The arena caches freed blocks and never returns them, so RSS holds its high-water mark for the life of the process. | `false` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_DEVICE` | ONNX Runtime execution device. `cuda` is opt-in and requires a compatible `onnxruntime-gpu` installation in a custom image; an unavailable CUDA provider fails startup. | `cpu` |
| `HINDSIGHT_API_EMBEDDINGS_ONNX_CUDA_DEVICE_ID` | NVIDIA device ID passed to `CUDAExecutionProvider` when the ONNX device is `cuda`. | `0` |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_EMBEDDINGS_TEI_BATCH_SIZE` | Max texts per TEI `/embed` request, and the unit the client fans out over (see `HINDSIGHT_API_EMBEDDINGS_MAX_CONCURRENT_REQUESTS`). TEI's own `--max-client-batch-size` (32 by default) is a hard validation error rather than a soft cap, so raising this above the server's value fails the request instead of being clamped | `32` |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL` | Custom base URL for OpenAI-compatible API (e.g., Azure OpenAI) | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_BATCH_SIZE` | Max inputs per `embeddings.create` call for `openai`/`openrouter` providers — lower this when the upstream endpoint enforces stricter limits (e.g. DashScope caps at 10) | `100` |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS` | Optional requested output dimensions for OpenAI `text-embedding-3` models (e.g., `384` to match an existing pgvector schema) | - |
| `HINDSIGHT_API_EMBEDDINGS_MAX_CONCURRENT_REQUESTS` | Embedding requests a remote provider keeps in flight for one `encode()` call. This is what buys throughput from an embedding service: the same TEI server sustains ~903 texts/s at one in-flight request and ~2,080 at eight. Applies to every remote provider (`tei`, `openai`, `cohere`, `zeroentropy`, `litellm`, `litellm-sdk`, `google`, ...); the in-process `local`/`onnx` backends are unaffected. Lower it when the embedding service or your provider quota cannot take the parallelism | `8` |
| `HINDSIGHT_API_EMBEDDINGS_MAX_RETRIES` | Retries after the first attempt when a remote embedding call fails transiently (5xx, timeout, connection error). `0` disables retrying. Applies to the `litellm`, `litellm-sdk`, `google` (Gemini API / Vertex AI), `cohere` and `zeroentropy` providers — none of which retry on their own — including Gemini `429 RESOURCE_EXHAUSTED` quota responses; 4xx auth/validation errors are never retried. The `openai`, `openai-codex`, `openrouter` and `requesty` providers pass it to the OpenAI SDK as its retry count; an OpenAI `429 insufficient_quota` (out of credits) then parks the operation for 15 minutes instead of failing it. (`tei` uses its own built-in retry instead.) | `4` |
| `HINDSIGHT_API_EMBEDDINGS_INITIAL_BACKOFF` | Initial backoff in seconds between embedding retries (doubles per attempt, with jitter) | `0.5` |
| `HINDSIGHT_API_EMBEDDINGS_MAX_BACKOFF` | Cap on the backoff between embedding retries, in seconds | `4.0` |
| `HINDSIGHT_API_EMBEDDINGS_RETRY_BUDGET` | Wall-clock ceiling, in seconds, on the time one `encode()` call may spend retrying (failed attempts plus backoff). Keeps a degraded provider from stalling a synchronous recall. | `15.0` |
| `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_API_KEY` | OpenRouter API key for embeddings (falls back to `HINDSIGHT_API_OPENROUTER_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_REQUESTY_API_KEY` | Requesty API key for embeddings (falls back to `HINDSIGHT_API_REQUESTY_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_REQUESTY_MODEL` | Requesty embedding model | `openai/text-embedding-3-small` |
| `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL` | OpenRouter embedding model | `perplexity/pplx-embed-v1-0.6b` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY` | ZeroEntropy API key for embeddings | - |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL` | ZeroEntropy embedding model | `zembed-1` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_BASE_URL` | Custom base URL for ZeroEntropy-compatible API | `https://api.zeroentropy.dev` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_DIMENSIONS` | Output dimensions for `zembed-1`. Supported values: `2560`, `1280`, `640`, `320`, `160`, `80`, `40` | `1280` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_ENCODING_FORMAT` | Response encoding: `float` or `base64`. Hindsight decodes either format to float vectors before storage. | `float` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_BATCH_SIZE` | Max inputs per ZeroEntropy embed request | `100` |
| `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_LATENCY` | Optional latency mode: `fast` or `slow`. Leave unset to use ZeroEntropy's default routing. | - |
| `HINDSIGHT_API_EMBEDDINGS_NVIDIA_API_KEY` | NVIDIA NIM API key (`nvapi-...`) for embeddings | - |
| `HINDSIGHT_API_EMBEDDINGS_NVIDIA_MODEL` | NVIDIA embedding model | `nvidia/llama-nemotron-embed-vl-1b-v2` |
| `HINDSIGHT_API_EMBEDDINGS_NVIDIA_BASE_URL` | Base URL; requests go to `{base}/embeddings` (point it at a self-hosted NIM to use one) | `https://integrate.api.nvidia.com/v1` |
| `HINDSIGHT_API_EMBEDDINGS_NVIDIA_DIMENSIONS` | Output dimensions sent with each request. Must be a size the model accepts (`nvidia/nemotron-3-embed-1b` only does `2048`, which needs a vector extension other than `pgvector`) | `1024` |
| `HINDSIGHT_API_EMBEDDINGS_NVIDIA_BATCH_SIZE` | Max inputs per NVIDIA embed request | `100` |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY` | Cohere API key for embeddings (falls back to `HINDSIGHT_API_COHERE_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_OUTPUT_DIMENSIONS` | Output embedding dimensions for Cohere (e.g., `256`, `512`, `1024`). When set, overrides the model's default dimension. | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE` | LiteLLM proxy base URL for embeddings (falls back to `HINDSIGHT_API_LITELLM_API_BASE`) | `http://localhost:4000` |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY` | LiteLLM proxy API key for embeddings (optional, depends on proxy config; falls back to `HINDSIGHT_API_LITELLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL` | LiteLLM embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `text-embedding-3-small` |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_DIMENSIONS` | Vector width the configured LiteLLM model returns. When set, the startup dimension probe is skipped, so the API boots even while the proxy is still starting. Declares the width rather than requesting it (the value is not sent to the proxy); a wrong value fails the first embedding call with an explicit error. | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY` | LiteLLM SDK API key for direct embedding provider access (optional — omit for providers that use ambient credentials, e.g. AWS Bedrock with IAM) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL` | LiteLLM SDK embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `cohere/embed-english-v3.0` |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL_ID` | **Bedrock only.** The target LiteLLM actually invokes, when it differs from `..._MODEL` — typically an [application inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) ARN, which you need when a Service Control Policy denies `bedrock:InvokeModel` on the bare model id. LiteLLM picks the Bedrock request/response shape from `..._MODEL`, so leave that a recognizable id (e.g. `bedrock/amazon.titan-embed-text-v2:0`) and put the ARN here. | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_BASE` | Custom base URL for LiteLLM SDK embeddings (optional) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS` | Optional output embedding dimensions (provider-dependent, e.g., `768` for Gemini embedding models) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_ENCODING_FORMAT` | Encoding format for embedding responses. Set to empty string to omit the parameter (needed for Voyage AI, Gemini). | `float` |
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_API_KEY` | Gemini API key for embeddings (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL` | Gemini embedding model. The `gemini-embedding-2` family (e.g. `gemini-embedding-2-preview`) is supported on both the Gemini API and Vertex AI — Hindsight sends each text as its own input so these multimodal models return one vector per fact instead of fusing the batch into a single embedding. | `gemini-embedding-001` |
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_BATCH_SIZE` | Max texts per `embed_content` request, and the unit the client fans out over (see `HINDSIGHT_API_EMBEDDINGS_MAX_CONCURRENT_REQUESTS`) | `100` |
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY` | Output embedding dimensions (Gemini supports configurable dimensionality) | `768` |
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_FORCE_IPV4` | Force the Gemini embeddings client to use an IPv4-only HTTP transport. Useful in environments where IPv6 egress is broken (e.g. some Docker/VPC setups) and AAAA DNS records cause long hangs. | `false` |
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_PROJECT_ID` | Vertex AI project ID for embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID`) | - |
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_REGION` | Vertex AI region for embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_REGION`) | - |
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_SERVICE_ACCOUNT_KEY` | Service account key for Vertex AI embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY`) | - |
Embedding provider selection, credentials, base URLs, model choices, dimensions, encoding format, batch sizes, and latency modes are static server-level settings. They are not hierarchical per-bank overrides. The ONNX settings above are also static, matching the existing `embeddings_local_*` settings.
> **📝 Models that treat searches and stored text differently**
>
Some embedding models are trained to see a short instruction in front of a search, and nothing (or a different instruction) in front of the text being stored. With the `local` provider, Hindsight applies whichever instructions the model itself ships with — you don't configure anything. The default `BAAI/bge-small-en-v1.5` ships none, so nothing changes for existing deployments; `Qwen/Qwen3-Embedding-*` ships one for searches only, which is what makes those models retrieve accurately.
The rare case to watch for is a model that also instructs the **stored** side. Switching to one of those changes how new memories are indexed, so anything already stored was indexed differently and will compare poorly against it. Re-index the bank (export and re-import it) after adopting such a model. Search-side-only instructions, which covers every model listed above, need no re-indexing.
Most providers can't discover those instructions the way `local` does. A remote endpoint that takes plain text and returns a vector — `tei`, `litellm`, `litellm-sdk`, and anything behind an OpenAI-compatible `/embeddings` URL (`llama-server`, `infinity-emb`, vLLM, ...) — only ever receives the text, so you supply the instructions yourself with `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` and `HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX`. Leaving them unset sends the text unchanged, which is what the symmetric hosted models expect.
Two providers need nothing and ignore both settings: `local` (applies the model's own prompts) and `zeroentropy` (tells the API which side it is embedding). The `onnx` provider has its own pair, `HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX` / `..._PASSAGE_PREFIX`, which default to the E5 instructions.
#### Local ONNX embeddings
The ONNX provider runs embedding models in-process with ONNX Runtime. Install the optional deps when building your own API environment:
```bash
pip install 'hindsight-api-slim[local-onnx]'
# or, in this repository:
uv sync --project hindsight-api-slim --extra local-onnx
```
CUDA execution is opt-in. Set `HINDSIGHT_API_EMBEDDINGS_ONNX_DEVICE=cuda` only in an environment that has a compatible `onnxruntime-gpu` wheel and CUDA/cuDNN runtime. The official images intentionally keep the CPU runtime and do not grow when this feature is unused. For a Docker deployment, use the ready-to-build recipe at [`docker/docker-compose/cuda-onnx/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/cuda-onnx), which installs the GPU wheel into a private image based on the slim image:
```bash
docker compose -f docker/docker-compose/cuda-onnx/docker-compose.yaml up --build
```
The recipe accepts `BASE_IMAGE`, `ONNXRUNTIME_GPU_VERSION`, and `HINDSIGHT_API_EMBEDDINGS_ONNX_CUDA_DEVICE_ID` for release, runtime, and device selection. Hindsight fails startup when CUDA is requested but the provider is unavailable or the initialized session does not activate it; it does not silently fall back to CPU in that mode. The recipe installs CUDA/cuDNN libraries in the private image; the host needs a compatible NVIDIA driver and GPU passthrough. Individual operators may still run on CPU through normal graph partitioning. See the [recipe README](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/cuda-onnx) for dependency isolation, unreleased-checkout builds, and real GPU tests.
You can either let Hindsight download the model from Hugging Face at startup by setting `HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID`, or pre-download the ONNX graph and tokenizer files under the Hindsight repository root.
```bash
cd /path/to/hindsight
mkdir -p models
MODEL_ID=intfloat/multilingual-e5-small
MODEL_DIR=models/intfloat__multilingual-e5-small
uv run --project hindsight-api-slim --extra local-onnx python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=os.environ["MODEL_ID"],
local_dir=os.environ["MODEL_DIR"],
allow_patterns=[
"onnx/model.onnx",
"onnx/model.onnx_data",
"*.json",
"*.txt",
"*.model",
],
)
PY
```
Then start Hindsight with paths relative to the repository root:
```bash
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH=./models/intfloat__multilingual-e5-small/onnx/model.onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH=./models/intfloat__multilingual-e5-small
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX="query: "
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX="passage: "
```
For Docker deployments, mount the same model directory and use container paths:
```yaml
services:
hindsight:
volumes:
- ./models:/app/models:ro
environment:
HINDSIGHT_API_EMBEDDINGS_PROVIDER: onnx
HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH: /app/models/intfloat__multilingual-e5-small/onnx/model.onnx
HINDSIGHT_API_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH: /app/models/intfloat__multilingual-e5-small
HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS: "384"
HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX: "query: "
HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX: "passage: "
```
Model-specific examples:
```bash
# sentence-transformers/all-MiniLM-L6-v2: 384 dimensions, no E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""
# intfloat/multilingual-e5-small: 384 dimensions, keep E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=intfloat/multilingual-e5-small
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX="query: "
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX="passage: "
# sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2: 384 dimensions, no E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""
# BAAI/bge-m3: 1024 dimensions, no E5 prefixes; keep onnx/model.onnx_data next to model.onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=BAAI/bge-m3
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=1024
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""
```
> **⚠️ Warning**
>
Do not mix embeddings from different models in the same vector index. Switching from `local` to `onnx`, or changing ONNX models, requires re-embedding existing memories/documents even when the vector dimensions happen to match. For example, `BAAI/bge-small-en-v1.5` and `intfloat/multilingual-e5-small` both produce 384-dimensional vectors, but their embedding spaces are not semantically comparable.
> **⚠️ Warning**
>
The default ONNX query/document prefixes (`query: ` and `passage: `) are for E5 models. Clear both prefix variables for non-E5 models such as MiniLM or BGE, otherwise Hindsight will prepend E5-style text to models that were not trained with that format.
#### Common Pitfall: Provider-Specific Embedding Env Var Names
Embedding environment variables include a provider segment in the key name:
`HINDSIGHT_API_EMBEDDINGS_{PROVIDER}_{PARAMETER}`
For example, when `HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai`:
| Wrong | Correct |
|---|---|
| `HINDSIGHT_API_EMBEDDINGS_BASE_URL` | `HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL` |
| `HINDSIGHT_API_EMBEDDINGS_MODEL` | `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` |
| `HINDSIGHT_API_EMBEDDINGS_API_KEY` | `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` |
This differs from LLM variables, which follow `HINDSIGHT_API_LLM_{PARAMETER}` without a provider segment.
> **⚠️ Warning**
>
If embedding keys are misnamed, Hindsight may fall back to default OpenAI embedding settings (for example, `text-embedding-3-small`) and fail with auth errors against the wrong endpoint.
#### DeepSeek and Embeddings
DeepSeek is supported as an **LLM** provider, but it does **not** expose an embeddings endpoint. If your LLM is DeepSeek, use a different embedding provider (for example `local`, `openai`, `cohere`, or `google`).
```bash
# Local (default) - uses SentenceTransformers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# Local with custom model requiring trust_remote_code
# WARNING: Only enable trust_remote_code for models you trust (security risk)
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=your-custom-model
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE=true
# OpenAI - cloud-based embeddings
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=*** # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions by default
# export HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS=384 # optional reduced output size
# OpenAI Codex OAuth - uses existing ChatGPT/Codex login, no API key needed
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai-codex
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions by default
# export HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS=384 # optional reduced output size
# Azure OpenAI - embeddings via Azure endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
export HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
# TEI - HuggingFace Text Embeddings Inference (recommended for production)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
# OpenRouter - access 100+ embedding models
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openrouter
export HINDSIGHT_API_EMBEDDINGS_OPENROUTER_API_KEY=your-openrouter-api-key # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL=perplexity/pplx-embed-v1-0.6b
# ZeroEntropy - zembed-1
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=zeroentropy
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL=zembed-1
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_DIMENSIONS=1280
# export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_ENCODING_FORMAT=base64 # optional
# export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_LATENCY=fast # optional
# NVIDIA NIM - asymmetric retrieval embeddings (sends input_type=query/passage)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=nvidia
export HINDSIGHT_API_EMBEDDINGS_NVIDIA_API_KEY=nvapi-your-key
# export HINDSIGHT_API_EMBEDDINGS_NVIDIA_MODEL=nvidia/llama-nemotron-embed-vl-1b-v2 # default
# export HINDSIGHT_API_EMBEDDINGS_NVIDIA_DIMENSIONS=1024 # default
# Cohere - cloud-based embeddings
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
# Optional: override output dimensions (for Matryoshka-capable models)
# export HINDSIGHT_API_EMBEDDINGS_COHERE_OUTPUT_DIMENSIONS=512
# Azure-hosted Cohere - embeddings via custom endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
export HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
# LiteLLM proxy - unified gateway for multiple providers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or cohere/embed-english-v3.0
# export HINDSIGHT_API_EMBEDDINGS_LITELLM_DIMENSIONS=1536 # optional: declare the width, skip the startup probe
# Google - Gemini API (API key auth)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_API_KEY=xxxxxxxxxxxx # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001 # 768 dimensions (default)
# export HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY=768 # configurable: 256, 512, 768, 1024, etc.
# Google - Vertex AI auth (auto-detected when project ID is set)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001
export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_PROJECT_ID=your-gcp-project-id # falls back to HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID
# export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_REGION=us-central1 # falls back to HINDSIGHT_API_LLM_VERTEXAI_REGION
# export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json # falls back to LLM config, or uses ADC
# LiteLLM SDK - direct API access without proxy server (recommended)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY=your-provider-api-key
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=cohere/embed-english-v3.0
# Optional: request a specific output dimension when the provider supports it
# export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS=768
# Optional (any provider): the input-token limit inputs are truncated to.
# Defaults to 8192; set it to your model's real limit, or 0 to send text uncapped
# export HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS=8192
# Supported LiteLLM SDK embedding providers:
# - cohere/embed-english-v3.0 (1024 dimensions)
# - openai/text-embedding-3-small (1536 dimensions)
# - together_ai/togethercomputer/m2-bert-80M-8k-retrieval
# - huggingface/sentence-transformers/all-MiniLM-L6-v2
# - voyage/voyage-2
```
#### Embedding Dimensions
Hindsight automatically detects the embedding dimension from the model at startup and adjusts the database schema accordingly. The default model (`BAAI/bge-small-en-v1.5`) produces 384-dimensional vectors, while OpenAI models produce 1536 or 3072 dimensions.
For `litellm-sdk`, if you set `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS`, startup uses that output size when the underlying provider supports LiteLLM's `dimensions` parameter (otherwise behavior is unchanged). The same dimension-change rules below apply.
For `zeroentropy`, zembed-1 supports `2560`, `1280`, `640`, `320`, `160`, `80`, and `40` dimensions. ZeroEntropy's API default is `2560`; Hindsight defaults to `1280` so the provider works with the default pgvector HNSW index. Use `2560` with a vector extension that supports higher-dimensional indexes, such as DiskANN/pgvectorscale or ScaNN.
> **⚠️ Dimension Changes**
>
Once memories are stored, you cannot change the embedding dimension without losing data. If you need to switch to a model with different dimensions:
1. **Empty database**: The schema is adjusted automatically on startup
2. **Existing data**: Either delete all memories first, or use a model with matching dimensions
Supported OpenAI embedding dimensions:
- `text-embedding-3-small`: 1536 dimensions
- `text-embedding-3-large`: 3072 dimensions
- `text-embedding-ada-002`: 1536 dimensions (legacy)
Google's `gemini-embedding-001` produces 3072 dimensions natively but supports configurable output dimensionality. Set `HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY` to control the output size (default: 768).
ZeroEntropy's `zembed-1` supports Matryoshka dimensions: `2560`, `1280`, `640`, `320`, `160`, `80`, and `40`. Hindsight defaults to `1280` for this provider.
### Reranker
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, `cohere`, `openrouter`, `zeroentropy`, `siliconflow`, `nvidia`, `typesafe`, `alibaba`, `google`, `flashrank`, `onnx`, `litellm`, `litellm-sdk`, `jina-mlx`, or `rrf` | `local` |
| `HINDSIGHT_API_RERANKER_MAX_TOKENS_PER_CANDIDATE` | Applies to **every** provider. If set, truncate each rerank candidate to this many tokens (tiktoken, approximate) before reranking. Set it below the model's context window (e.g. `900` for a 1024-token model), or lower to bound request size when documents can be very long (keeps a CPU-only rerank server from running out of memory). Each fallback member has its own (`HINDSIGHT_API_RERANKER_<n>_MAX_TOKENS_PER_CANDIDATE`). Off by default. (Deprecated alias: `HINDSIGHT_API_RERANKER_LITELLM_MAX_TOKENS_PER_DOC`.) | - |
| `HINDSIGHT_API_RERANKER_MAX_RETRIES` | Retries after the first attempt when a remote rerank call fails transiently (5xx, timeout, connection error, `429` quota). `0` disables retrying. Applies to every remote provider except `tei`, which has its own retry loop; the in-process providers (`local`, `flashrank`, `onnx`, `jina-mlx`, `rrf`) are unaffected. 4xx auth/validation errors are never retried. | `3` |
| `HINDSIGHT_API_RERANKER_INITIAL_BACKOFF` | Initial backoff in seconds between rerank retries (doubles per attempt, with jitter) | `0.5` |
| `HINDSIGHT_API_RERANKER_MAX_BACKOFF` | Cap on the backoff between rerank retries, in seconds | `4.0` |
| `HINDSIGHT_API_RERANKER_RETRY_BUDGET` | Wall-clock ceiling, in seconds, on the time one rerank may spend retrying (failed attempts plus backoff). Tighter than the embedding budget because rerank runs after retrieval has already spent time on the same request. With a fallback chain configured, each member spends its own budget before the chain advances. | `10.0` |
| `HINDSIGHT_API_RERANKER_SEND_BANK_AS_HEADER` | Add `X-Hindsight-Bank-Id: <bank_id>` to remote reranker requests. Enable only for trusted endpoints because this transmits the current bank ID. Covers TEI, Cohere-compatible HTTP, LiteLLM proxy, and LiteLLM SDK transports. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
| `HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU` | Force CPU mode for local reranker even when CUDA/XPU is available | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FP16` | Half-precision (FP16) inference for the local reranker. Faster on CUDA; quality-identical. Disabled by default because some CPUs lack native FP16 support. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING` | Sort pairs by token length before batching to reduce padding waste. 36–54% faster across models; quality-identical by construction. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE` | Batch size for local reranker `predict()`. Optimal value varies by hardware and model (smaller batches can outperform larger ones). | `32` |
| `HINDSIGHT_API_RERANKER_LOCAL_TIMEOUT` | Wall-clock ceiling for scoring one recall's candidates. On expiry the unscored candidates keep their pre-rerank (RRF) order behind the scored ones, so recall still returns. Set far above any healthy rerank — it is a safety valve for an over-sized local model on CPU, not a latency target. `0` disables. | `300` |
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE` | Batch size for TEI reranking | `128` |
| `HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT` | Max concurrent TEI reranking requests | `8` |
| `HINDSIGHT_API_RERANKER_TEI_HTTP_TIMEOUT` | HTTP request timeout for TEI reranker (seconds). Increase when using a slower CPU-based reranker under load. | `30.0` |
| `HINDSIGHT_API_RERANKER_OPENROUTER_API_KEY` | OpenRouter API key for reranking (falls back to `HINDSIGHT_API_OPENROUTER_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_RERANKER_OPENROUTER_MODEL` | OpenRouter rerank model | `cohere/rerank-v3.5` |
| `HINDSIGHT_API_RERANKER_OPENROUTER_TIMEOUT` | HTTP request timeout for OpenRouter reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_OPENROUTER_BASE_URL` | Rerank endpoint URL (point at a Cohere-compatible gateway/proxy for metering) | `https://openrouter.ai/api/v1/rerank` |
| `HINDSIGHT_API_RERANKER_COHERE_API_KEY` | Cohere API key for reranking (falls back to `HINDSIGHT_API_COHERE_API_KEY`) | - |
| `HINDSIGHT_API_RERANKER_COHERE_MODEL` | Cohere rerank model | `rerank-english-v3.0` |
| `HINDSIGHT_API_RERANKER_COHERE_BASE_URL` | Custom base URL for any Cohere-compatible `/rerank` endpoint (Azure AI Foundry, Jina, Voyage, self-hosted BGE, etc.). When set, the `cohere` provider bypasses the Cohere SDK and calls the endpoint directly via HTTP. | - |
| `HINDSIGHT_API_RERANKER_COHERE_TIMEOUT` | Request timeout for the Cohere reranker (seconds). Applies to both the native Cohere SDK and the Cohere-compatible HTTP path enabled by `HINDSIGHT_API_RERANKER_COHERE_BASE_URL`. | `60.0` |
| `HINDSIGHT_API_RERANKER_LITELLM_API_BASE` | LiteLLM proxy base URL for reranking (falls back to `HINDSIGHT_API_LITELLM_API_BASE`) | `http://localhost:4000` |
| `HINDSIGHT_API_RERANKER_LITELLM_API_KEY` | LiteLLM proxy API key for reranking (optional, depends on proxy config; falls back to `HINDSIGHT_API_LITELLM_API_KEY`) | - |
| `HINDSIGHT_API_RERANKER_LITELLM_MODEL` | LiteLLM **proxy** rerank model (use provider prefix, e.g., `cohere/rerank-english-v3.0`) | `cohere/rerank-english-v3.0` |
| `HINDSIGHT_API_RERANKER_LITELLM_TIMEOUT` | HTTP request timeout for the LiteLLM proxy reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_LITELLM_SDK_API_KEY` | LiteLLM **SDK** API key for direct reranking (no proxy needed) | - |
| `HINDSIGHT_API_RERANKER_LITELLM_SDK_MODEL` | LiteLLM SDK rerank model (e.g., `deepinfra/Qwen3-reranker-8B`) | `cohere/rerank-english-v3.0` |
| `HINDSIGHT_API_RERANKER_LITELLM_SDK_API_BASE` | Custom API base URL for LiteLLM SDK (optional) | - |
| `HINDSIGHT_API_RERANKER_LITELLM_SDK_TIMEOUT` | Request timeout for the LiteLLM SDK reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_ZEROENTROPY_API_KEY` | ZeroEntropy API key for reranking | - |
| `HINDSIGHT_API_RERANKER_ZEROENTROPY_MODEL` | ZeroEntropy rerank model (`zerank-2`, `zerank-2-small`) | `zerank-2` |
| `HINDSIGHT_API_RERANKER_ZEROENTROPY_BASE_URL` | Custom base URL for ZeroEntropy-compatible API (e.g., mock server, proxy, or self-hosted deployment) | `https://api.zeroentropy.dev` |
| `HINDSIGHT_API_RERANKER_ZEROENTROPY_TIMEOUT` | HTTP request timeout for ZeroEntropy reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_SILICONFLOW_API_KEY` | SiliconFlow API key for reranking | - |
| `HINDSIGHT_API_RERANKER_SILICONFLOW_MODEL` | SiliconFlow rerank model (e.g., `BAAI/bge-reranker-v2-m3`) | `BAAI/bge-reranker-v2-m3` |
| `HINDSIGHT_API_RERANKER_SILICONFLOW_BASE_URL` | Base URL for the SiliconFlow `/rerank` endpoint | `https://api.siliconflow.cn/v1` |
| `HINDSIGHT_API_RERANKER_SILICONFLOW_TIMEOUT` | HTTP request timeout for SiliconFlow reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_NVIDIA_API_KEY` | NVIDIA NIM API key (`nvapi-...`) for reranking | - |
| `HINDSIGHT_API_RERANKER_NVIDIA_MODEL` | NVIDIA rerank model, with its namespace (it is part of the request path) | `nvidia/llama-nemotron-rerank-vl-1b-v2` |
| `HINDSIGHT_API_RERANKER_NVIDIA_BASE_URL` | Base URL; requests go to `{base}/retrieval/{model}/reranking` | `https://ai.api.nvidia.com/v1` |
| `HINDSIGHT_API_RERANKER_NVIDIA_TIMEOUT` | HTTP request timeout for NVIDIA reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_NVIDIA_BATCH_SIZE` | Max passages per rerank request (the hosted API caps it at 512) | `512` |
| `HINDSIGHT_API_RERANKER_NVIDIA_MAX_CONCURRENT` | Max in-flight NVIDIA rerank requests | `8` |
| `HINDSIGHT_API_RERANKER_TYPESAFE_API_KEY` | TypeSafe API key for reranking | - |
| `HINDSIGHT_API_RERANKER_TYPESAFE_MODEL` | TypeSafe model used to judge relevance | `jev-latest` |
| `HINDSIGHT_API_RERANKER_TYPESAFE_BASE_URL` | Base URL for the TypeSafe API | `https://api.typesafe.ai` |
| `HINDSIGHT_API_RERANKER_TYPESAFE_TIMEOUT` | HTTP request timeout for the TypeSafe reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_TYPESAFE_MAX_CONCURRENT` | Maximum in-flight TypeSafe requests. | `24` |
| `HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES` | Ask a second question that cuts the ranked list where relevance ends, returning only the relevant candidates. Shrinks what recall returns — see the note below. | `false` |
| `HINDSIGHT_API_RERANKER_ALIBABA_API_KEY` | Alibaba Cloud DashScope API key for reranking | - |
| `HINDSIGHT_API_RERANKER_ALIBABA_MODEL` | DashScope rerank model | `qwen3-rerank` |
| `HINDSIGHT_API_RERANKER_ALIBABA_TIMEOUT` | HTTP request timeout for the Alibaba Cloud DashScope reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_GOOGLE_PROJECT_ID` | Google Cloud project ID for Discovery Engine reranking (falls back to `HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID`) | - |
| `HINDSIGHT_API_RERANKER_GOOGLE_MODEL` | Google Discovery Engine ranking model | `semantic-ranker-default-004` |
| `HINDSIGHT_API_RERANKER_GOOGLE_SERVICE_ACCOUNT_KEY` | Path to service account JSON key (falls back to `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY`). If unset, uses ADC. | - |
| `HINDSIGHT_API_RERANKER_GOOGLE_TIMEOUT` | HTTP request timeout for Google Discovery Engine reranker (seconds). | `60.0` |
| `HINDSIGHT_API_RERANKER_FLASHRANK_MODEL` | FlashRank model for fast CPU-based reranking | `ms-marco-MiniLM-L-12-v2` |
| `HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR` | Cache directory for FlashRank models | System default |
| `HINDSIGHT_API_RERANKER_FLASHRANK_CPU_MEM_ARENA` | Enable ONNX Runtime CPU memory arena for FlashRank. When `true`, ONNX pre-allocates a memory arena that never shrinks, causing RSS to grow monotonically. `false` trades slightly slower per-call allocation for bounded RSS. | `false` |
| `HINDSIGHT_API_RERANKER_FLASHRANK_BATCH_SIZE` | Passages scored per FlashRank forward pass. Each pass allocates attention tensors sized `batch × heads × seq²`, and FlashRank pads a batch to its longest passage, so raising this raises peak memory sharply on long candidates. Lower it if the reranker is the memory ceiling on a large bank. | `32` |
| `HINDSIGHT_API_RERANKER_ONNX_MODEL_ID` | Hugging Face repo for the `onnx` provider. Hindsight downloads `HINDSIGHT_API_RERANKER_ONNX_FILE` from it, and uses it as the tokenizer fallback. | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_ONNX_MODEL_PATH` | Local path to an ONNX cross-encoder graph. Use this for models whose repo ships no ONNX file (export them yourself, see below). | - |
| `HINDSIGHT_API_RERANKER_ONNX_TOKENIZER_NAME_OR_PATH` | Hugging Face tokenizer repo or local tokenizer directory. Set this with `HINDSIGHT_API_RERANKER_ONNX_MODEL_PATH`. | Falls back to `HINDSIGHT_API_RERANKER_ONNX_MODEL_ID` |
| `HINDSIGHT_API_RERANKER_ONNX_FILE` | ONNX file path inside the Hugging Face repo (also downloads its `_data` sidecar when present). | `onnx/model.onnx` |
| `HINDSIGHT_API_RERANKER_ONNX_MAX_TOKENS` | Max tokens per (query, document) pair; longer pairs are truncated. | `512` |
| `HINDSIGHT_API_RERANKER_ONNX_BATCH_SIZE` | Pairs scored per forward pass. Bounds peak memory the same way as the FlashRank setting above. | `32` |
| `HINDSIGHT_API_RERANKER_ONNX_THREADS` | CPU threads one forward pass may use. `0` uses the native thread cap (`OMP_NUM_THREADS`, by default `min(16, available CPUs)`). Set `2`–`4` on a laptop to keep recall from pinning the whole CPU. | `0` |
| `HINDSIGHT_API_RERANKER_ONNX_MAX_CONCURRENT` | Forward passes allowed at once. Concurrent recalls queue behind each other, so CPU use never exceeds `THREADS` cores. | `1` |
| `HINDSIGHT_API_RERANKER_ONNX_CPU_MEM_ARENA` | Enable ONNX Runtime's CPU memory arena. It never returns freed memory, so RSS holds its high-water mark. | `false` |
| `HINDSIGHT_API_RERANKER_ONNX_TIMEOUT` | Wall-clock ceiling for scoring one recall's candidates, same as `HINDSIGHT_API_RERANKER_LOCAL_TIMEOUT`: on expiry the unscored candidates keep their pre-rerank order behind the scored ones. `0` disables. | `300` |
| `HINDSIGHT_API_RERANKER_JINA_MLX_MODEL_PATH` | Local path to downloaded `jina-reranker-v3-mlx` model (auto-downloads from HuggingFace if unset) | - |
> **📝 Apple Silicon: the local providers run on CPU**
>
PyTorch's MPS (Metal) backend is never used for the local embeddings or reranker
models. It caches a kernel and allocator pool per input tensor shape and never releases
them, so a variable-length workload grows without bound (idle instances reached ~20 GB),
and concurrent inference aborts the process with a Metal validation assertion. The
`HINDSIGHT_API_EMBEDDINGS_LOCAL_ALLOW_MPS` and `HINDSIGHT_API_RERANKER_LOCAL_ALLOW_MPS`
flags that used to opt in have been removed; setting them now logs a warning and does
nothing. For GPU reranking on Apple Silicon use `HINDSIGHT_API_RERANKER_PROVIDER=jina-mlx`.
CUDA and Intel XPU are unaffected and still auto-select.
#### Reranker failover chain
Reranking is a refinement stage, but by default a reranker that is unreachable takes
recall down with it. Configure extra rerankers **by index** and Hindsight tries them
in order: if a member fails (timeout, connection error, HTTP error, or an unusable
response), the next one serves the request.
The unindexed `HINDSIGHT_API_RERANKER_*` config is the **primary** (member 0). Extra
members are numbered from 1, and every setting of member `n` carries the same index —
`HINDSIGHT_API_RERANKER_<n>_<SETTING>` for any `<SETTING>` in the table above:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RERANKER_<n>_PROVIDER` | Provider for fallback member `n` (`n` = 1, 2, ...). Presence of this var defines the member; indices must be contiguous from 1. Unset = no fallback (default). | - |
| `HINDSIGHT_API_RERANKER_<n>_<SETTING>` | Any other reranker setting, for member `n` — e.g. `HINDSIGHT_API_RERANKER_1_TEI_URL`, `HINDSIGHT_API_RERANKER_1_COHERE_API_KEY`, `HINDSIGHT_API_RERANKER_1_TEI_HTTP_TIMEOUT`. | Same built-in default as the primary |
An indexed member is read **in isolation**: it inherits nothing from the primary and
nothing from the shared provider keys (`HINDSIGHT_API_COHERE_API_KEY` and friends), so
spell out every setting it needs with its own index. Unset settings fall back to the
same built-in defaults the primary uses. Chain-level settings
(`HINDSIGHT_API_RERANKER_MAX_CANDIDATES`, `HINDSIGHT_API_RERANKER_SEND_BANK_AS_HEADER`)
stay global and are not indexed.
```bash
# Primary: a self-hosted reranker that is not always up.
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://workstation:8081
# Member 1: a hosted reranker to fall back on.
export HINDSIGHT_API_RERANKER_1_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_1_COHERE_API_KEY=...
# Member 2: last resort — no reranking, keep the fusion order rather than fail.
export HINDSIGHT_API_RERANKER_2_PROVIDER=rrf
```
Ending the chain with `rrf` makes recall **fail open**: results come back in the order
the retrieval stages produced, exactly as if no reranker were configured, instead of
the request failing. Without an `rrf` member (or with every member down) the error still
surfaces, which is the default behaviour.
Notes:
- Members are tried in order on every request; there is no circuit breaker, so a member
that is down costs its own timeout on each request before the next one is tried. Keep
the timeouts of an unreliable primary short (`HINDSIGHT_API_RERANKER_TEI_HTTP_TIMEOUT`
and the per-provider `*_TIMEOUT` settings).
- A member that fails to initialize at startup is logged, not fatal — it is retried on
the next request that reaches it.
- Scores are not comparable across providers (some return calibrated `[0, 1]` relevance,
others logits), so a request served by a fallback member can score differently from one
served by the primary. Failovers are logged at `WARNING`.
- The indexed members are credential fields — never returned by the bank-config API, and
server-level only (not per-bank configurable).
> **💡 Sizing a TEI reranker**
>
TEI reserves one slot per text in a rerank request and rejects the request outright
once its pool is full, so start it with at least
`HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT × HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE`
slots — 1024 for the defaults above, versus TEI's own default of 512:
```bash
text-embeddings-router --max-concurrent-requests 2048 ...
```
Below that threshold, heavy recall traffic makes the reranker reject work it has the
capacity to do. Rejections are retried with backoff, so an undersized pool shows up as
slower recall rather than errors.
```bash
# Local (default) - uses SentenceTransformers CrossEncoder
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Local with custom model requiring trust_remote_code (e.g., jina-reranker-v2)
# WARNING: Only enable trust_remote_code for models you trust (security risk)
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=jinaai/jina-reranker-v2-base-multilingual
export HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE=true
# TEI - for high-performance inference
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
# OpenRouter - access reranking models via OpenRouter
export HINDSIGHT_API_RERANKER_PROVIDER=openrouter
export HINDSIGHT_API_RERANKER_OPENROUTER_API_KEY=your-openrouter-api-key # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_RERANKER_OPENROUTER_MODEL=cohere/rerank-v3.5
# Cohere - cloud-based reranking
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# Any Cohere-compatible /rerank endpoint (Azure AI Foundry, Jina, Voyage, self-hosted BGE, etc.)
#
# Setting HINDSIGHT_API_RERANKER_COHERE_BASE_URL switches the `cohere` provider
# off the Cohere SDK and onto a plain HTTP client that speaks the standard
# Cohere rerank wire format:
# Request: POST {base_url} (or {base_url}/rerank, depending on host)
# Authorization: Bearer <api_key>
# {"model": "...", "query": "...", "documents": [...], "return_documents": false}
# Response: {"results": [{"index": 0, "relevance_score": 0.9}, ...]}
#
# Any service implementing this contract works here. For Azure AI Foundry the
# base_url is the full invoke URL; for SiliconFlow you can also use the
# dedicated `siliconflow` provider below.
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0 # whatever model the endpoint serves
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-cohere-compatible-endpoint.com
# ZeroEntropy - cloud-based reranking (state-of-the-art accuracy)
export HINDSIGHT_API_RERANKER_PROVIDER=zeroentropy
export HINDSIGHT_API_RERANKER_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_ZEROENTROPY_MODEL=zerank-2 # or zerank-2-small
# export HINDSIGHT_API_RERANKER_ZEROENTROPY_BASE_URL=https://your-custom-endpoint.com # optional
# SiliconFlow - cloud reranking via SiliconFlow's Cohere-compatible /rerank endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=siliconflow
export HINDSIGHT_API_RERANKER_SILICONFLOW_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_SILICONFLOW_MODEL=BAAI/bge-reranker-v2-m3
# export HINDSIGHT_API_RERANKER_SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1 # default
# NVIDIA NIM - native reranking API (POST {base}/retrieval/{model}/reranking)
export HINDSIGHT_API_RERANKER_PROVIDER=nvidia
export HINDSIGHT_API_RERANKER_NVIDIA_API_KEY=nvapi-your-key
# export HINDSIGHT_API_RERANKER_NVIDIA_MODEL=nvidia/llama-nemotron-rerank-vl-1b-v2 # default
# TypeSafe - relevance judged by a typed-decision model (Jev), not a /rerank endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=typesafe
export HINDSIGHT_API_RERANKER_TYPESAFE_API_KEY=your-api-key
# export HINDSIGHT_API_RERANKER_TYPESAFE_MODEL=jev-latest # default
# export HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true # also prune irrelevant candidates
# Alibaba Cloud DashScope - qwen3-rerank via Cohere-compatible /reranks endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=alibaba
export HINDSIGHT_API_RERANKER_ALIBABA_API_KEY=your-dashscope-api-key # or set DASHSCOPE_API_KEY
export HINDSIGHT_API_RERANKER_ALIBABA_MODEL=qwen3-rerank # default, can omit
# LiteLLM proxy - unified gateway for multiple reranking providers (requires running LiteLLM proxy server)
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_RERANKER_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_RERANKER_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0 # or voyage/rerank-2, together_ai/...
# LiteLLM SDK - direct API access without proxy (recommended for simplicity)
export HINDSIGHT_API_RERANKER_PROVIDER=litellm-sdk
export HINDSIGHT_API_RERANKER_LITELLM_SDK_API_KEY=your-deepinfra-api-key
export HINDSIGHT_API_RERANKER_LITELLM_SDK_MODEL=deepinfra/Qwen3-reranker-8B # or cohere/rerank-english-v3.0, etc.
# Google Discovery Engine - cloud-based semantic reranking
export HINDSIGHT_API_RERANKER_PROVIDER=google
export HINDSIGHT_API_RERANKER_GOOGLE_PROJECT_ID=your-gcp-project-id
export HINDSIGHT_API_RERANKER_GOOGLE_SERVICE_ACCOUNT_KEY=/path/to/service-account.json # optional, uses ADC if unset
export HINDSIGHT_API_RERANKER_GOOGLE_MODEL=semantic-ranker-default-004 # or semantic-ranker-fast-004
# Jina MLX - Apple Silicon native reranking (no GPU/cloud required)
# Model (~1.2 GB) is downloaded automatically from HuggingFace Hub on first use.
export HINDSIGHT_API_RERANKER_PROVIDER=jina-mlx
```
#### LiteLLM Proxy vs SDK
- **`litellm`**: Requires running a separate LiteLLM proxy server. Good for centralized configuration, rate limiting, and caching.
- **`litellm-sdk`**: Direct API access without proxy. Simpler setup, lower latency, fewer infrastructure components.
Both support the same providers:
- **Cohere** (`cohere/rerank-english-v3.0`, `cohere/rerank-multilingual-v3.0`)
- **DeepInfra** (`deepinfra/Qwen3-reranker-8B`, `deepinfra/bge-reranker-v2-m3`)
- **Together AI** (`together_ai/Salesforce/Llama-Rank-V1`)
- **HuggingFace** (`huggingface/BAAI/bge-reranker-v2-m3`)
- **Voyage AI** (`voyage/rerank-2`)
- **Jina AI** (`jina_ai/jina-reranker-v2`)
- **AWS Bedrock** (`bedrock/...`)
#### TypeSafe
TypeSafe is not a `/rerank` endpoint. It evaluates typed *questions* against a *state*,
and this provider asks two of them.
**Rank — one question for the whole pool.** A Choice returns a probability for every
option, summing to 1, so Hindsight makes the candidates the options and reads the
ranking straight off the answer: one call, however many candidates. Judged together the
model only has to say which candidate beats which, rather than pin each one to an
absolute scale in isolation — on a 200-question LoCoMo set that scored recall@1 0.94
against 0.87 for one call per candidate, at a thirtieth of the calls.
A Choice accepts at most 255 options, so a larger pool is ranked in rounds and the
winners are then ranked against each other. Probabilities are normalised within a
single call, so rounds cannot simply be concatenated.
**Cut — `HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true`.** A second question, a
Score over the ranked shortlist, asks how far down the list relevance extends; whatever
falls past that point is left out. Recall then returns the relevant candidates in order
and nothing else. No threshold is tuned — the model picks the depth.
A Score is used rather than adding a "none of these" option to the Choice because Score
levels are *ordered*, which is what a cut point needs. As a Choice option, "none of
these" is just another rival for the probability mass, and it wins outright on hard
queries: 35 of 200 questions came back completely empty, against none with the Score.
There is deliberately no "nothing is relevant" level, so at least one candidate always
survives. Recall runs on a pool retrieval already judged plausible, and one weak memory
the caller can dismiss beats silence.
The flag is off by default because it meaningfully shrinks what recall returns. Turn it
on when the consumer is an LLM prompt (reflect, for instance) and every irrelevant
memory is wasted context.
**Scores are positions, not confidences.** A Choice probability is a share of one pool:
0.7 means "the best of these", not "relevant", and two pools are not comparable. The
provider therefore hands back rank positions, and exactly `0.0` for anything past the
cut.
#### Local ONNX reranker
The `onnx` provider runs any cross-encoder exported to ONNX in-process with ONNX Runtime, so a model outside FlashRank's catalog needs neither PyTorch nor a TEI server. It needs the `local-onnx` extra (`pip install 'hindsight-api-slim[local-onnx]'`). Scores are the model's raw logits, the same as the `local` provider, so switching between the two keeps the ranking. Models with a single output score and binary `[irrelevant, relevant]` heads both work.
With no settings it downloads `onnx/model.onnx` from the default `cross-encoder/ms-marco-MiniLM-L-6-v2` repo. For a model whose repo ships no ONNX file, export it once and point Hindsight at the files:
```bash
uvx --from "optimum-onnx[onnxruntime]" optimum-cli export onnx \
--model nlpai-lab/KURE-Reranker-nano --task text-classification ./models/kure-reranker-nano
export HINDSIGHT_API_RERANKER_PROVIDER=onnx
export HINDSIGHT_API_RERANKER_ONNX_MODEL_PATH=./models/kure-reranker-nano/model.onnx
export HINDSIGHT_API_RERANKER_ONNX_TOKENIZER_NAME_OR_PATH=./models/kure-reranker-nano
```
One forward pass runs at a time and uses up to `HINDSIGHT_API_RERANKER_ONNX_THREADS` cores (the native thread cap by default), so concurrent recalls queue instead of oversubscribing the CPU. On a laptop, set the thread count to `2`–`4`, and lower `HINDSIGHT_API_RERANKER_MAX_CANDIDATES` if recall latency matters more than the last bit of ranking quality: rerank cost grows linearly with the candidate count.
> **📝 On Apple Silicon, `local` is usually faster**
>
PyTorch uses Apple's Accelerate matrix units, which ONNX Runtime's CPU backend does not. On an M3 Max the `local` provider reranked 2–4x faster than `onnx` with the same model and the same scores. The `onnx` provider pays off on Linux and x86 servers, and wherever you want to run without PyTorch.
#### Jina MLX (Apple Silicon)
The `jina-mlx` provider uses [`jinaai/jina-reranker-v3-mlx`](https://huggingface.co/jinaai/jina-reranker-v3-mlx), optimized for Apple Silicon. The model (~1.2 GB) is downloaded from HuggingFace Hub automatically on first startup and cached locally.
`mlx` is Apple's Metal framework, so the `local-ml` extra only installs it on Apple Silicon — its Linux build is CPU-only and slower than the `local` provider. Selecting `jina-mlx` elsewhere fails at startup unless you install `mlx`/`mlx-lm` yourself.
> **📝 License**
>
`jina-reranker-v3-mlx` is licensed under CC BY-NC 4.0. Contact Jina AI for commercial usage.
### Decision model
A decision model answers typed questions (pick one of these, how likely is this) in well
under a second. Operations that support one hand it the in-between judgments an LLM call
would otherwise make, and keep the LLM for writing. Set it up once here: every operation
that supports a decision model shares it, unless that operation overrides it or turns it off.
This is separate from the `typesafe` [reranker](#reranker), which keeps its own
`HINDSIGHT_API_RERANKER_TYPESAFE_*` settings.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DECISION_MODEL_PROVIDER` | `none`, `typesafe`, `cloudflare` or `openai` (see below). | `none` |
| `HINDSIGHT_API_DECISION_MODEL_API_KEY` | API key (Cloudflare: an API token with Workers AI access). Required when the provider is not `none`. | - |
| `HINDSIGHT_API_DECISION_MODEL_MODEL` | Model name. | provider default |
| `HINDSIGHT_API_DECISION_MODEL_BASE_URL` | API base URL. Required for `cloudflare`. | provider default |
| `HINDSIGHT_API_DECISION_MODEL_TIMEOUT` | Request timeout in seconds. | `60` |
| `HINDSIGHT_API_DECISION_MODEL_MAX_CONCURRENT` | Most requests in flight at once, for each operation using the decision model. | `24` |
| `HINDSIGHT_API_DECISION_MODEL_MAX_REQUEST_TOKENS` | Most tokens one request may carry, counted on the whole request body. Larger asks are split across requests. Set it to the served model's context window less a margin. | `26000` |
Every request is retried on its own when it is rate-limited (429) or fails transiently.
| Provider | API | Default model | Default base URL |
|----------|-----|---------------|------------------|
| `typesafe` | [TypeSafe](https://typesafe.ai) System One (`/v1/systemone`) | `jev-latest` | `https://api.typesafe.ai` |
| `cloudflare` | [Clef](https://blog.cloudflare.com/clef-decision-models) on Workers AI (System One body, `/@cf/cloudflare/<model>`) | `clef` (or `clef-flash`) | none: set `https://api.cloudflare.com/client/v4/accounts/<account_id>/ai/run`, or an AI Gateway `https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway>/workers-ai` |
| `openai` | [OpenAI Decisions API](https://developers.openai.com/api/docs/guides/decisions) (`/v1/decisions`, public beta) | `gpt-6-luna` | `https://api.openai.com` |
**Per-operation overrides.** `HINDSIGHT_API_{RECALL,REFLECT,CONSOLIDATION}_DECISION_MODEL_{PROVIDER,API_KEY,MODEL,BASE_URL}`
replace the global value for that operation only. Key, model and URL fall back to the global
value only while the operation uses the global provider; an operation that switches to another
provider takes that provider's defaults and needs its own key. Set an operation's provider to
`none` to turn the decision model off for it, or give one operation a decision model without a
global one.
```bash
export HINDSIGHT_API_DECISION_MODEL_PROVIDER=typesafe
export HINDSIGHT_API_DECISION_MODEL_API_KEY=your-typesafe-key
export HINDSIGHT_API_REFLECT_DECISION_MODEL_MODEL=jev-preview # reflect only
export HINDSIGHT_API_CONSOLIDATION_DECISION_MODEL_PROVIDER=openai # consolidation on OpenAI
export HINDSIGHT_API_CONSOLIDATION_DECISION_MODEL_API_KEY=your-openai-key
```
**Recall.** With a decision model, recall uses it in place of the [reranker](#reranker).
It asks of each candidate whether showing it would help answer the question, telling it
the date the question is asked (the recall's question date, else today), and orders the
pool by that. That order is final: recall skips the recency, temporal and proof-count
boosts it applies to a cross-encoder, and publishes no reranker score, so
`min_scores.reranker` is rejected. To keep the cross-encoder for recall
while other operations use the decision model, set
`HINDSIGHT_API_RECALL_DECISION_MODEL_PROVIDER=none`.
**Consolidation.** With a decision model, consolidation asks it, for each new fact, how the
fact relates to the observations recall found for it: does it restate one, add something new
that touches none, change one, or is it chatter. Restatements become evidence for their
observation, new facts become observations as stated, and chatter is skipped, all without an
LLM call; only the rest reach the consolidation LLM. On the consolidation eval this cut LLM
calls by about 40% at the same pass rate. Turn it off per bank with
[`HINDSIGHT_API_CONSOLIDATION_USE_DECISION_MODEL`](#observations), or for every bank with
`HINDSIGHT_API_CONSOLIDATION_DECISION_MODEL_PROVIDER=none`.
**Knowledge pages.** With a decision model, a delta refresh of a knowledge page skips reflect.
It reads the memories added since the page's last refresh and asks the decision model, for each,
whether it could change the page. Only those reach the LLM that edits the page, together with the
bank's directives; none of them means no LLM call at all. A memory it drops is not offered again.
Above 200 new memories, or when the decision model fails, the refresh runs reflect as before. On
the refresh cost eval this took a 20-memory refresh from 3 LLM calls to 1 and cut its input tokens
by about 70%. Only the `typesafe` provider is supported, as for reflect. Turn it off per bank with
[`HINDSIGHT_API_MENTAL_MODEL_DELTA_USE_DECISION_MODEL`](#reflect).
### Authentication
By default, Hindsight runs without authentication. For production deployments, enable API key authentication using the built-in tenant extension:
```bash
# Enable the built-in API key authentication
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
export HINDSIGHT_API_TENANT_API_KEY=your-secret-api-key
```
When enabled, all requests must include the API key in the `Authorization` header:
```bash
curl -H "Authorization: Bearer your-secret-api-key" \
http://localhost:8888/v1/default/banks
```
Requests without a valid API key receive a `401 Unauthorized` response.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_TENANT_EXTENSION` | Dotted path to the loaded tenant extension. Set to `hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension` to require an API key on every request. | *(none; auth disabled)* |
| `HINDSIGHT_API_TENANT_API_KEY` | Shared API key checked by the built-in API-key extension. Sent by clients as `Authorization: Bearer <key>`. | *(none)* |
| `HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS` | Comma-separated request headers copied into `RequestContext.extra_headers` for extensions to read, matched case-insensitively. Empty means extensions see only the `Authorization` header. | *(none)* |
If you are enabling Memory Defense, see `docs/developer/memory-defense/` for the policy schema, detector catalog, and audit trail.
#### Forwarding extra headers to extensions
A custom `TenantExtension` normally only sees the `Authorization` header, as `RequestContext.api_key`. That is not enough when the bearer token identifies the *proxy* rather than the caller — for example behind an authenticating gateway that presents one shared service identity to Hindsight, and carries the per-caller identity in a second header.
List those headers to have them forwarded:
```bash
export HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion,x-request-origin
```
They arrive in `RequestContext.extra_headers`, keyed by lower-cased name, on both the HTTP and MCP transports, and thread through to `OperationValidatorExtension` hooks with the rest of the request context. Only listed headers that are actually present on the request appear; the setting is unset by default, so extensions receive no header data unless you opt in.
A listed header that arrives **more than once** is dropped (with a warning) rather than resolved to one of its values, so a duplicate can never silently override the value your proxy injected. Headers forwarded this way are only as trustworthy as the proxy in front of Hindsight: list a header only if that proxy sets it and strips any client-supplied copy. The setting is server-level and cannot be overridden per tenant or bank.
> **💡 Custom Authentication**
>
For advanced authentication (JWT, OAuth, multi-tenant schemas), implement a custom `TenantExtension`. See the [Extensions documentation](./extensions.md) for details.
### Server
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_HOST` | Bind address | `0.0.0.0` |
| `HINDSIGHT_API_PORT` | Server port | `8888` |
| `HINDSIGHT_API_BASE_PATH` | Base path for API when behind reverse proxy (e.g., `/hindsight`) | `""` (root) |
| `HINDSIGHT_API_WORKERS` | Number of uvicorn worker processes | `1` |
| `HINDSIGHT_API_ACCESS_LOG` | Enable uvicorn access log (`true`, `1`, `yes`, `on` to enable) | `false` |
| `HINDSIGHT_API_DAEMON_LOG` | Where `--daemon` redirects the server's stdout and stderr. Set this per profile when several daemons run on one machine, so their output does not interleave into a single file. | `~/.hindsight/daemon.log` |
| `HINDSIGHT_API_FREE_THREADING` | What to do when running on a free-threaded (`python3.14t`) interpreter: `strict` refuses to start if the GIL is still enabled, `warn` logs and continues, `off` disables the check. Unset lets the interpreter decide — `strict` on a free-threaded build, `off` on a normal one. | interpreter-dependent |
| `HINDSIGHT_API_LOG_LEVEL` | Log level: `debug`, `info`, `warning`, `error` | `info` |
| `HINDSIGHT_API_LOG_FORMAT` | Log format: `text` or `json` (structured logging for cloud platforms) | `text` |
| `HINDSIGHT_API_LOG_JSON_FIELDS` | Comma-separated allowlist of JSON log fields to emit (e.g. `severity,message,tenant`). Available: `severity`, `message`, `timestamp`, `logger`, `tenant`, `exception`. Empty = all fields. | `""` (all) |
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server at `/mcp/{bank_id}/` | `true` |
| `HINDSIGHT_API_GZIP_MIN_SIZE` | Minimum response size (bytes) to gzip. Compressing a recall response costs ~5% of its CPU, so a CPU-bound (rather than bandwidth-bound) deployment can raise this past its typical response size. Negative disables compression entirely. | `1024` |
| `HINDSIGHT_API_LOOP_LAG_REPORT_SECONDS` | Diagnostic: log event-loop lag percentiles (`[loop-lag]`) every N seconds (minimum 1), to tell a slow await from an oversubscribed loop. `0` disables the probe. | `0` |
| `HINDSIGHT_API_LOOP_LAG_METRIC` | Record every event-loop lag sample in the `hindsight_event_loop_lag_seconds` histogram (sampled every 50 ms), independent of the log reports above. A loop that is busy but never blocked shows up here and nowhere else: the recall phase timers stay fast while requests wait for the loop. | `false` |
| `HINDSIGHT_API_METRICS_WORKER_LABEL` | With `--workers N` every worker is its own process with its own metrics, but they share one port, so a scrape of `/metrics` reaches one worker at random: counters jump between processes (a rate over them reads every switch as a reset) and a saturated worker is invisible. When on, each worker publishes a snapshot of its metrics every 5 s and `/metrics` returns every live worker's series, each labelled `api_worker="<slot>"` (slot `0..N-1`). One port and one scrape target; the extra label is the only visible change. | `false` |
| `HINDSIGHT_API_TOKENIZER_ENCODING` | Vocabulary used for every token count and chunk boundary (recall budgets, chunk sizes, prompt fitting, embedding truncation). `o200k_base` matches current OpenAI models and counts non-Latin text far closer to what they actually charge; `cl100k_base` reproduces the counts Hindsight produced before this default changed. Server-level: token budgets are only comparable between banks if they are all counted the same way. Other bundled vocabulary: `o200k_harmony`. | `o200k_base` |
| `HINDSIGHT_API_MODEL_INIT_TIMEOUT` | Wall-clock cap (seconds) on startup model/connection initialization. If embeddings, the cross-encoder, or LLM verification block (e.g. an offline model download or an unreachable provider), the server fails fast with a clear error instead of hanging forever. Increase if a legitimate first-time model download needs more time. | `300` |
| `HINDSIGHT_API_STARTUP_WAIT_SECONDS` | **Docker image only.** How long the container waits for the API to answer `/health` before it stops and restarts. Raising `HINDSIGHT_API_MODEL_INIT_TIMEOUT` above the default raises this wait too, so a slow first-time model download is not cut short; set this to override the wait on its own. | `300`, or `HINDSIGHT_API_MODEL_INIT_TIMEOUT` + 30s when that is longer |
### Egress proxy
Outbound calls (LLM providers and gateways, remote embeddings and rerankers, document
parsers, the Supabase tenant extension) follow the standard proxy environment variables —
`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`. There is no Hindsight-specific proxy setting: set
these on the process (pod env, systemd unit, shell) before it starts.
Three things to know:
- **The proxy URL itself must be `http://`.** On the API's own HTTP calls (embeddings,
rerankers, parsers, gateways reached directly) an `https://` proxy URL is ignored with a
warning, and `ALL_PROXY` / SOCKS is dropped. Provider SDKs that bring their own HTTP client
(OpenAI, Anthropic) do read `ALL_PROXY`, so don't rely on it either way — set `HTTP_PROXY`
and `HTTPS_PROXY`.
- **Put local endpoints in `NO_PROXY`.** Nothing is exempted automatically — not even
`localhost` or `127.0.0.1`. A deployment that sets `HTTP_PROXY` and also runs a local
Ollama, LM Studio, llama.cpp or TEI must list those hosts in `NO_PROXY`, or their calls
are sent to the proxy. This includes the llama.cpp server Hindsight spawns itself: the
readiness probe is always direct, but the completions that follow go through the OpenAI
SDK and will honour the proxy.
- **Credentials in `~/.netrc` are applied too**, to the destination host, not just the proxy.
A netrc entry matching a provider host does not override the `Authorization` header
Hindsight sends — the call fails outright with `Cannot combine AUTHORIZATION header with
AUTH argument`. Remove the entry, or point `NETRC` at a file without it.
Webhook delivery is the one caller that ignores all of this on purpose: its SSRF guard
validates the address it resolved, and a proxy would contact one that was never checked.
### Retrieval
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_GRAPH_RETRIEVER` | Graph retrieval algorithm | `link_expansion` |
| `HINDSIGHT_API_LINK_EXPANSION_PER_ENTITY_LIMIT` | Max target units expanded per entity in `link_expansion` graph retrieval (LATERAL fanout cap per entity; bounds high-fanout entities). | `200` |
| `HINDSIGHT_API_LINK_EXPANSION_TIMEOUT` | Timeout (seconds) for the graph expansion queries in `link_expansion` retrieval. On timeout, world/experience expansion falls back to semantic+causal links; observation expansion returns no graph results. | `10` |
| `HINDSIGHT_API_RECALL_MAX_CONCURRENT` | Max concurrent recall operations per worker (backpressure) | `32` |
| `HINDSIGHT_API_RECALL_CONNECTION_BUDGET` | Max concurrent DB connections per recall operation | `4` |
| `HINDSIGHT_API_ADMISSION_RECALL_MAX_IN_FLIGHT` | Concurrent recalls admitted per worker before requests queue. `0` derives it from the CPU budget this process has (cgroup quota) divided by `HINDSIGHT_API_WORKERS`; a negative value disables the lane. A latency target, not a capacity limit: throughput is unchanged either way, but too low throttles I/O-bound work and too high rebuilds the queue. | `0` (derived) |
| `HINDSIGHT_API_ADMISSION_RECALL_MAX_WAIT_MS` | How long a recall may queue for a slot before being refused with 503 + `Retry-After`. A queued request whose client disconnects releases its place immediately. Absorbs bursts; lower it if load is persistently over capacity, where a long queue just delays the same refusals. | `30000` |
| `HINDSIGHT_API_ADMISSION_REFLECT_MAX_IN_FLIGHT` | As above, for reflect. | `0` (derived) |
| `HINDSIGHT_API_ADMISSION_REFLECT_MAX_WAIT_MS` | As above, for reflect. | `5000` |
| `HINDSIGHT_API_ADMISSION_RETAIN_MAX_IN_FLIGHT` | As above, for retain. Only bites on the synchronous path; an async retain returns as soon as the operation is queued. | `0` (derived) |
| `HINDSIGHT_API_ADMISSION_RETAIN_MAX_WAIT_MS` | As above, for retain. | `2000` |
| `HINDSIGHT_API_RECALL_MAX_QUERY_TOKENS` | Maximum token length of a recall query. Longer queries are cut to the limit instead of failing. `0` disables the limit. | `500` |
| `HINDSIGHT_API_QUERY_ANALYZER_LANGUAGES` | Restrict the locales `dateparser` considers when extracting temporal constraints from a recall query, as a comma-separated list of language codes (e.g. `en` or `en,zh`). Empty keeps full auto-detection across all supported locales. Restricting is significantly faster (auto-detection dominates recall's CPU cost) and avoids locale misdetection on a known-language corpus, but explicit dates written in an unlisted locale will then misparse rather than yield no constraint — only set this when you know which languages your queries use. Does not affect Chinese, which is handled before `dateparser` runs. | _(empty)_ |
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES` | Max candidates to rerank per recall (RRF pre-filters the rest) | `300` |
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES_LOW` | Override the reranker candidate cap for `budget=low` recalls (the cross-encoder is the dominant cost of a large recall, so a lower cap trades some depth for latency). `0` falls back to `HINDSIGHT_API_RERANKER_MAX_CANDIDATES`. | `0` |
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES_MID` | Override the reranker candidate cap for `budget=mid` recalls. `0` falls back to `HINDSIGHT_API_RERANKER_MAX_CANDIDATES`. | `0` |
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES_HIGH` | Override the reranker candidate cap for `budget=high` recalls. `0` falls back to `HINDSIGHT_API_RERANKER_MAX_CANDIDATES`. | `0` |
| `HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY` | Minimum cosine similarity a candidate must reach to be returned by the semantic retrieval strategy. Must be between `0` and `1`. | `0.3` |
| `HINDSIGHT_API_GRAPH_SEED_MIN_SIMILARITY` | Minimum cosine similarity for a memory to seed graph retrieval. This is independent from the main semantic retrieval threshold. Must be between `0` and `1`. | `0.3` |
| `HINDSIGHT_API_TEMPORAL_SEMANTIC_MIN_SIMILARITY` | Minimum cosine similarity for temporal retrieval entry points and spread neighbors. This is independent from the main semantic retrieval threshold. Must be between `0` and `1`. | `0.1` |
| `HINDSIGHT_API_SEMANTIC_LINK_MIN_SIMILARITY` | Minimum cosine similarity for creating semantic links during normal retain, streaming retain, and graph-maintenance relinking. This directly controls semantic graph density. Must be between `0` and `1`. | `0.7` |
| `HINDSIGHT_API_BM25_MIN_SCORE` | Minimum BM25 score a row must exceed to enter fusion. Gates out zero-score, non-matching rows on backends (notably `vchord`) whose operator ranks every document instead of pre-filtering to query-term matches. `0` keeps only genuine term matches; raise it to require stronger matches. | `0` |
| `HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE` | Cap on candidates each retrieval source (semantic, BM25, graph, temporal) contributes to RRF, applied before the global reranker cap. Prevents one over-expanding backend from filling the reranker budget on its own. `0` disables the cap. | `0` |
| `HINDSIGHT_API_RECALL_STRATEGY_BOOSTS` | Prioritise one or more retrieval sources over the others on recall, as a comma-separated `strategy:level` list (e.g. `graph:high` to strongly favour graph hits, or `graph:high,bm25:low`). Strategies: `semantic`, `bm25`, `graph`, `temporal`. Levels: `low` (gentle — mainly protects the source's candidates from being dropped before reranking), `medium` (moderate preference), `high` (strong — the source takes the large majority of the reranker's candidate budget). The pre-cap boost works in rank space and only runs when the merged pool is larger than `HINDSIGHT_API_RERANKER_MAX_CANDIDATES`; it does not rewrite RRF scores. Comparing one boosted arm with one other arm, a boosted candidate at rank `r` outranks the other arm's candidate at rank `s` when `r < divisor * s` (divisors: `low` 2, `medium` 4, `high` 8). That is a single-arm comparison, not a guarantee about the fused list, and it does not depend on pool size. After a cross-encoder rerank, each favoured candidate gets an additive nudge that is the level's full amount at rank 1 and shrinks with its rank in that source (rank-1 amounts: `low` 0.05, `medium` 0.2, `high` 0.5). The decay reuses the same divisor as an initial scale, so `high` halves by rank 9. The `temporal` strategy is the exception: its rank is mostly how close a memory's date is to the middle of the query's date window, which says nothing about relevance among the memories inside the window, so every candidate the temporal source surfaced (including memories linked to those) gets the full amount whatever its rank. For the other sources the decay reduces how far a deep hit moves. For every source, the nudge does not cap the final rank change, it does not guarantee a strong direct match stays ahead, and nudges from two arms add. On a passthrough reranker — recall mode `rrf`, a provider named `rrf`, or a failover chain that has degraded to its `rrf` member — that post-rerank nudge is skipped. If the pool is within the cap, the setting then has no effect; if the pool exceeds the cap, it only changes which candidates enter, and the final order follows raw RRF plus the recency, temporal and proof multipliers. Only the strategies you list are boosted — any you omit keep their normal weight (no implicit boost). A strategy written without a level (`graph` or `graph:`) defaults to `medium`. Empty disables the feature. | _(empty)_ |
| `HINDSIGHT_API_RECENCY_DECAY_FUNCTION` | Shape of the recency boost applied during reranking — how a memory's age is turned into a small freshness adjustment to its final rank. `linear` (default) decays in a straight line from full freshness (today) to a floor reached at `HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS`. `exponential` decays by half-life: a memory is treated as neutral (no boost or penalty) at `HINDSIGHT_API_RECENCY_DECAY_HALFLIFE_DAYS`, younger memories are boosted and older ones penalised, with a smooth fade rather than a hard cutoff. `none` disables recency entirely (age never affects ranking). | `linear` |
| `HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS` | For the `linear` decay function: the number of days over which a memory fades from full freshness to the minimum. Only used when `HINDSIGHT_API_RECENCY_DECAY_FUNCTION=linear`. | `365` |
| `HINDSIGHT_API_RECENCY_DECAY_HALFLIFE_DAYS` | For the `exponential` decay function: the age (in days) at which a memory is considered neutral — younger memories get a recency boost, older ones a penalty. Smaller values favour very recent memories more aggressively. Only used when `HINDSIGHT_API_RECENCY_DECAY_FUNCTION=exponential`. | `90` |
| `HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY` | Track history of content changes to each mental model (previous content + timestamp), stored one row per change in the `mental_model_history` table. Set to `false` to disable entirely — no history rows are written, reducing storage if audit trails are not needed. **This is how you turn the feature off** (not a zero cap). | `true` |
| `HINDSIGHT_API_MENTAL_MODEL_MIN_REFRESH_INTERVAL_SECONDS` | Minimum seconds between two *automatic* refreshes of the same mental model — the after-consolidation trigger and the cron schedule. A trigger that fires sooner is not dropped: its refresh is queued and parked until the window closes, and every further trigger in the meantime folds into that one queued refresh, so a burst of small retains costs one refresh instead of one per retain. Raise it when a bank ingests continuously and its models do not need to be current to the minute — the parked refresh still sees everything that accumulated while it waited. Explicit refreshes (API, MCP, control plane) ignore the floor and run immediately, and additionally release a parked refresh they fold into. `0` = no floor, every trigger refreshes at once. Hierarchical — overridable per bank via the [config API](#hierarchical-configuration), and per model via `trigger.min_refresh_interval_seconds` (which wins, including an explicit `0` to exempt one hot model from a bank-wide floor). | `0` |
| `HINDSIGHT_API_KNOWLEDGE_PAGE_DEFAULT_TRIGGER` | Default refresh settings for newly created knowledge pages, as a JSON object merged over the built-in default (`{"mode": "delta", "fact_types": ["observation"], "exclude_mental_models": true, "refresh_after_consolidation": true}`). Set only the fields you want to change — e.g. `{"refresh_cron": "0 * * * *"}` makes new pages refresh hourly instead of after every consolidation. A trigger sent when creating the page still wins; existing pages are not changed. Configurable per bank. | - |
| `HINDSIGHT_API_MENTAL_MODEL_HISTORY_MAX_ENTRIES` | Max history rows kept per mental model. On each refresh the previous version is inserted into the `mental_model_history` table and the oldest rows beyond this cap are deleted, so per-model history can't grow without bound. `0` or a negative value **removes the cap** (history then grows with every refresh — unbounded); to turn history off entirely set `HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY=false` instead. | `50` |
The five embedding-dependent gates—main semantic retrieval, graph seeds, temporal retrieval, semantic-link
construction, and observation deduplication—serve different precision/recall tradeoffs and are intentionally
configured independently. Their defaults preserve the behavior calibrated for `BAAI/bge-small-en-v1.5`.
Changing embedding models can shift cosine-similarity distributions even when both models return normalized
vectors, so recalibrate all five values against their respective tasks before production use.
Changes to `HINDSIGHT_API_SEMANTIC_LINK_MIN_SIMILARITY` are not retroactive. The new value applies when new
semantic links are created during retain, streaming retain, or graph maintenance; maintenance does not remove
existing links below a raised threshold, and nodes that already have enough semantic links are not recomputed
when the threshold is lowered. To make an existing semantic graph fully conform to a new threshold, rebuild the
graph or re-ingest the source data into a new memory bank.
#### Graph Retrieval Algorithm
- **`link_expansion`** (default): Fast graph expansion from semantic seeds via entity co-occurrence, semantic kNN, and causal links. Target latency under 100ms.
#### Recall budget mapping
The recall request takes a `budget` parameter (`low` / `mid` / `high`, default `mid`) that maps to an integer `thinking_budget` used by every retrieval method (semantic, BM25, graph, temporal). These knobs control that mapping. They are hierarchical — overridable per bank via the [config API](#hierarchical-configuration).
Two functions are available:
- **`fixed`** (default — preserves legacy behavior): `thinking_budget = recall_budget_fixed_<level>` (independent of `max_tokens`).
- **`adaptive`**: `thinking_budget = round(max_tokens * recall_budget_adaptive_<level>)`, clamped to `[recall_budget_min, recall_budget_max]`. Useful when callers vary `max_tokens` and you want retrieval breadth to scale with the requested output size.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RECALL_BUDGET_FUNCTION` | Mapping function: `fixed` or `adaptive`. | `fixed` |
| `HINDSIGHT_API_RECALL_BUDGET_FIXED_LOW` | Items per retrieval method per fact type when `budget=low` and function is `fixed`. | `100` |
| `HINDSIGHT_API_RECALL_BUDGET_FIXED_MID` | Items per retrieval method per fact type when `budget=mid` and function is `fixed`. | `300` |
| `HINDSIGHT_API_RECALL_BUDGET_FIXED_HIGH` | Items per retrieval method per fact type when `budget=high` and function is `fixed`. | `1000` |
| `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_LOW` | Ratio of request `max_tokens` used when `budget=low` and function is `adaptive`. | `0.025` |
| `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_MID` | Ratio of request `max_tokens` used when `budget=mid` and function is `adaptive`. | `0.075` |
| `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_HIGH` | Ratio of request `max_tokens` used when `budget=high` and function is `adaptive`. | `0.25` |
| `HINDSIGHT_API_RECALL_BUDGET_MIN` | Floor for the adaptive function (after clamping). | `20` |
| `HINDSIGHT_API_RECALL_BUDGET_MAX` | Ceiling for the adaptive function (after clamping). | `2000` |
#### Recall pipeline stages
Recall runs four retrieval arms (semantic, BM25, graph, temporal) and then reranks the
fused candidates with a cross-encoder. Each stage costs latency, and a bank whose content
has no relational or temporal structure pays for arms it cannot use — for example a bank
ingested with `retain_extraction_mode: chunks` and used as plain retrieval.
These switch the individual stages off. All are hierarchical — overridable per bank via the
[config API](#hierarchical-configuration) — so one bank can run lean without changing how the
rest of the deployment recalls. Semantic always runs; it is the baseline retrieval.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_TEXT_SEARCH` | Run the keyword (BM25) retrieval arm. `false` leaves **pure vector search** — the arm is left out of the query entirely rather than filtered to nothing, so its SQL, its query tokenization and its `pg_stats` term-selection lookup are all skipped. Also drops the keyword arm from knowledge-page search. | `true` |
| `HINDSIGHT_API_ENABLE_TEMPORAL_RETRIEVAL` | Run the temporal retrieval arm. `false` also skips the date-aware query analysis that feeds it — without a detected constraint there is nothing to filter on. | `true` |
| `HINDSIGHT_API_ENABLE_GRAPH_RETRIEVAL` | Run the entity/link graph traversal arm. `false` skips those queries and returns no graph results. | `true` |
| `HINDSIGHT_API_ENABLE_RERANKING` | Rerank fused candidates with the cross-encoder. `false` returns the RRF-fused ordering directly — faster, but less precise. | `true` |
Turning all four off reduces recall to a single vector query, which is the
lowest-latency configuration there is.
Disabling text search leaves the write path alone: `search_vector` and its index are
still maintained, so a bank can be switched back without a reindex. The
[text-search backend](#text-search-extension) it would have used is simply unread.
##### Default `prefer_observations`
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RECALL_PREFER_OBSERVATIONS` | Value of recall's [`prefer_observations`](./api/recall.md#prefer_observations) when a REST or MCP request leaves it out: drop raw facts that a returned observation was consolidated from, so results carry no duplicates. A value in the request still wins. Hierarchical — overridable per bank. Useful when callers you don't control (MCP connectors, generic agents) never pass the flag. | `false` |
##### Pairing with the retain side: plain-retrieval ("RAG") banks
These recall toggles only remove work from the *read* path. If a bank is being used as
plain retrieval, the ingestion path should be configured to match — otherwise it still
pays for LLM work whose output recall no longer uses:
- **`retain_extraction_mode: chunks`** skips LLM fact extraction entirely and stores each
chunk as-is. This returns before any LLM queue or lock is acquired, so it removes the
LLM call from retain rather than just shortening it — normally the dominant cost of
ingestion.
- **`enable_observations: false`** skips consolidation, the other background LLM workload.
Configured together, the bank behaves like a conventional vector store: chunks in, hybrid
search out, no LLM on either path. Set both sides in one call:
```bash
curl -X PUT "$HINDSIGHT_API_URL/v1/default/banks/plain-retrieval-bank" \
-H "Authorization: Bearer $HINDSIGHT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"retain_extraction_mode": "chunks",
"enable_observations": false,
"enable_temporal_retrieval": false,
"enable_graph_retrieval": false,
"enable_reranking": false
}'
```
The trade-off is the point of the product: no extracted facts, entities, or links means no
graph or temporal structure to retrieve, and no mental models to reflect over. Use it for
banks that are genuinely plain retrieval — or to benchmark Hindsight against a baseline
vector store on equal terms — not as a general latency fix.
### Retain
Controls the retain (memory ingestion) pipeline.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` | Max completion tokens for fact extraction LLM calls | `64000` |
| `HINDSIGHT_API_RETAIN_CHUNK_SIZE` | Max characters per chunk for fact extraction. Larger chunks extract fewer LLM calls but may lose context. | `3000` |
| `HINDSIGHT_API_RETAIN_STRUCTURED_CHUNK_SIZE` | Max characters for a single JSONL line or conversation turn to keep whole. Unset uses `HINDSIGHT_API_RETAIN_CHUNK_SIZE`. Must be a positive integer when set. | - |
| `HINDSIGHT_API_RETAIN_ATTACHMENT_MAX_SIZE_MB` | Max decoded size of a single attachment sent as inline retain content. Above every mainstream provider's own per-file ceiling, so the provider's limit binds first for legitimate content while an abusive upload is refused at the ingress. | `20` |
| `HINDSIGHT_API_RETAIN_ATTACHMENT_MAX_COUNT` | Max inline attachments in one retain item. Split larger documents across several items. | `50` |
| `HINDSIGHT_API_RETAIN_MAX_ATTACHMENTS_PER_CHUNK` | Max attachments in one extraction chunk. `HINDSIGHT_API_RETAIN_CHUNK_SIZE` budgets **text only** — a placeholder costs the ~22 characters it occupies and nothing more — so this is what bounds attachments, matching a provider's per-request limit. Lower it for a model with a smaller context. Configurable per bank. | `8` |
| `HINDSIGHT_API_RETAIN_EXTRACTION_MODE` | Fact extraction mode: `concise`, `verbose`, `verbatim`, `chunks`, or `custom` | `concise` |
| `HINDSIGHT_API_RETAIN_MISSION` | What this bank should pay attention to during extraction. Steers the LLM without replacing the extraction rules — works alongside any extraction mode. | - |
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Full prompt override for fact extraction (only used when mode is `custom`). Replaces built-in extraction rules entirely. | - |
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
| `HINDSIGHT_API_RETAIN_OPTIONAL_FACT_DIMENSIONS` | Let a fact leave `when`/`where`/`who`/`why` empty instead of writing `"N/A"`. The four keys stay required in the schema; their values become `string \| null` and the prompt stops naming a placeholder. Turn it on for a small self-hosted model under `HINDSIGHT_API_LLM_STRICT_SCHEMA`, where "every property is required" means a fact with no date of its own must still emit *some* string — and the nearest plausible one is a date the text stated about something else. Off by default because it is not neutral on a capable model: with `why` droppable, "the user asked me to refactor X" tends to come back as its own `world` fact instead of riding along as the agent fact's rationale. Server-level, not per-bank. | `false` |
| `HINDSIGHT_API_RETAIN_BATCH_ENABLED` | Use LLM Batch API for fact extraction (50% cost savings, only with async operations) | `false` |
| `HINDSIGHT_API_RETAIN_MAX_CONCURRENT` | Max concurrent retain DB phases (HNSW reads + writes). Limits I/O contention during high-concurrency ingestion. | `4` |
| `HINDSIGHT_API_RETAIN_SUBBATCH_CONCURRENCY` | Sub-batches of one document processed at a time. Most of a sub-batch is a store round-trip, so overlapping a few hides that wait. `1` keeps the splitter one slice ahead of the work, which bounds how much of a large document is resident. | `1` |
| `HINDSIGHT_API_RETAIN_WALL_TIMEOUT` | Wall-clock ceiling in seconds for one retain task in the worker. A retain that blocks indefinitely (lock contention, an unreachable LLM endpoint) is cancelled and marked `failed` instead of holding its worker slot until the process restarts, so it can be retried. Set well above your slowest healthy retain; `0` disables. | `3600` |
| `HINDSIGHT_API_RETAIN_BATCH_TOKENS` | Max characters per sub-batch for async retain auto-splitting | `10000` |
| `HINDSIGHT_API_RETAIN_CHUNK_BATCH_SIZE` | Max chunks per streaming batch when retain ingests long documents. Each chunk produces roughly 17 facts, so the default 100 chunks ≈ 1700 facts per batch. Lower to cap memory/LLM pressure on large documents; raise for smaller chunks. Configurable per bank. | `100` |
| `HINDSIGHT_API_RETAIN_MEMORY_BUDGET_MB` | Megabytes of extracted-but-unwritten state one retain operation may hold. `HINDSIGHT_API_RETAIN_CHUNK_BATCH_SIZE` bounds how many chunks are in flight, but not what they weigh — a chunk carries however many facts the extractor found in it — so this is the ceiling to size a worker against: peak per retain is roughly this figure whatever the document. Budget for `WORKER_MAX_SLOTS` concurrent retains. Over budget, extraction waits for the write path to catch up rather than growing. `0` disables it and restores the chunk-count-only bound. | `128` |
| `HINDSIGHT_API_RETAIN_ENTITY_LOOKUP` | How retain finds *candidate* existing entities for an extracted name: `trigram` probes the pg_trgm index for similar names, `full` loads every entity in the bank and keeps the ones whose name is an exact or substring match. Both then run the same scoring pass (see [How entity resolution decides](#how-entity-resolution-decides)) — so this is not only a performance choice, it changes which names can merge at all. | `trigram` |
| `HINDSIGHT_API_RETAIN_ENTITY_RESOLUTION_BATCH_SIZE` | Max unique entity names per fuzzy candidate lookup query (`trigram` on PG, `oracle_fuzzy` on Oracle). Bounds query size so very wide retain batches don't time out a single `unnest(...)` join on banks with many entities. | `100` |
| `HINDSIGHT_API_RETAIN_ENTITY_RESOLUTION_MAX_CANDIDATES` | Max candidates scored per entity mention. The fuzzy lookup keeps only this many best matches per name (ranked by trigram / Jaro-Winkler similarity) before the scoring pass. On banks holding thousands of near-identical names an uncapped candidate set turns one retain into minutes of CPU, which stalls the worker's health checks; matches ranked below the first ~100 never win anyway. Raise only if entities that should merge are being duplicated. | `200` |
| `HINDSIGHT_API_RETAIN_DEFAULT_STRATEGY` | Default retain strategy name. When set, all retain calls without an explicit `strategy` parameter use this strategy. | - |
| `HINDSIGHT_API_RETAIN_BATCH_POLL_INTERVAL_SECONDS` | How often a retain waits between status checks against the **LLM provider's** Batch API while a submitted batch job runs. This is not a worker or ingestion knob — it does not control how often Hindsight picks up pending retains (see [`HINDSIGHT_API_WORKER_POLL_INTERVAL_MS`](#distributed-workers) for that) and only applies when the provider's batch mode is in use. Setting it to `0` does not disable anything: it removes the wait entirely and polls the provider's status endpoint in a tight loop. | `60` |
| `HINDSIGHT_API_STORE_DOCUMENT_TEXT` | Persist the raw source text alongside extracted memories. Set to `false` to skip storing it (`documents.original_text` NULL, `chunks.chunk_text` empty). Hierarchical — overridable per bank via the [config API](#hierarchical-configuration), so a data-minimizing bank can keep only derived facts while others retain the raw source. | `true` |
| `HINDSIGHT_API_FAIL_ON_EXTRACTION_ERRORS` | When `true`, a retain operation that accumulated any fact-extraction errors is marked `failed` (with an error message including the count) instead of `completed`, so silently-dropped facts surface as a hard failure. Default preserves existing behavior. Static, server-level. | `false` |
> **Batch-capable providers.** `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` only works with a retain LLM provider that implements a batch API: `openai`, `groq`, `gemini`, and `fireworks`. Batch always requires async retain (`async=true`); a sync retain with batch enabled errors. Other providers fail fast at startup.
>
> **Gemini** uses the [Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api) (flat 50% input + output discount, 24h SLA — typically minutes). It needs no extra settings beyond `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` and an API-key `gemini` provider; Vertex AI (`vertexai`) is not batch-capable.
#### Inline attachments in retain
A retain item's `content` can be a plain string, as it always could, or an ordered
list of text, image and file blocks so an attachment sits where it actually appears:
```json
{
"content": [
{"type": "text", "text": "To reset the VPN, click the button shown:"},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}},
{"type": "text", "text": "...then reconnect."},
{"type": "file", "source": {"type": "base64", "media_type": "application/pdf", "data": "..."},
"filename": "escalation-policy.pdf"}
]
}
```
The point is *position*. Extraction runs the interleaved text and attachments
through a vision model, so the model reads a screenshot beside the sentence that
introduces it, rather than being handed a caption you produced beforehand. That is
the same bargain the rest of Hindsight offers for text: hand it the raw content
and trust the extractor.
`image` and `file` are separate types because the providers separate them —
Anthropic has distinct image and document blocks, OpenAI has `image_url` and file
parts — so carrying your own distinction through means the conversion never has to
guess from the media type alone.
This is distinct from [`POST /files/retain`](#file-processing), which converts a
whole file to markdown as its **own** document — still the right tool for scanned
PDFs and office documents when you want them parsed rather than looked at, but it
separates the content from the prose around it.
**Accepted types.** Any well-formed `type/subtype` is accepted. There is no
allowlist: whether the model can read a format is the model's answer to give, and
a provider that rejects one fails the retain with its own error, which is more
informative than a guess made at the ingress. Bytes are served back under the
Content-Type the caller declared.
> **⚠️ Warning**
>
Because the declared type is served verbatim, a bank writer who retains active
content (an SVG or an HTML file) can have it execute in the dataplane's origin
when it is fetched. Treat write access to a bank as equivalent to being able to
host content on that origin.
What happens to the bytes:
- They are hashed (sha256) and stored **content-addressed** under the document
that carries them, so the same bytes always have the same id, an attachment
repeated within one document is stored once, and re-retaining an unchanged
document is a no-op. Two *different* documents carrying the same attachment
hold a copy each — dedup across documents is deliberately given up, so that
deleting a document never has to ask whether another one still needs the bytes.
- Storage goes through the same backend as uploaded files — `native`
(PostgreSQL), `s3`, `gcs`, `azure`. See [File storage](#file-storage).
- The document's stored text keeps a placeholder (`⟦hs-att:...⟧`) where the
attachment sat, so chunking, idempotency, `update_mode=append` and
re-extraction behave exactly as they do for text.
- Which attachments a document carries is derived from that text on every write,
and each `attachments` row names its owning document. So deleting a document —
or re-retaining it without the attachment — reclaims exactly its own, the same
way on every backend, including a bank whose documents live in a memories store.
- Every read surface returns the attachments alongside the text —
`chunks[].attachments` and each memory's `attachments` on recall, plus
get-document, get-chunk, get-memory and list-memories — each with a
bank-authorized `url` serving the original bytes.
Extracted **facts** never carry the placeholder: a fact reads `[image:
image/png]` where the attachment was, and the machine-readable handle travels
beside it in `attachments`. A content hash is not knowledge.
A **memory's** `attachments` are the ones that fact was actually drawn from, not
every attachment in its chunk. They are stored on the memory itself
(`memory_units.attachment_ids`), like its tags, so they travel with the memory
rather than living in a separate edge table. Extraction runs one call per chunk, and a chunk
holding a screenshot also holds the prose around it, so the chunk's attachments
would otherwise be shown against every fact the call produced — the architecture
diagram offered as the evidence for the paragraph about paging policy. The
extractor is asked which attachments each fact came from, and a fact stated in
the surrounding text has no `attachments` at all. That emptiness is the feature:
an attachment shown beside a memory means the model looked at it to produce that
memory.
A **chunk's** `attachments` stay exactly what they were — everything the chunk
references — because that is a question about the chunk, not about a fact.
Two things will refuse the retain outright, both with `422`, rather than dropping
attachments silently:
- The retain LLM is not vision-capable, or Hindsight cannot tell that it is. See
[`HINDSIGHT_API_LLM_VISION`](#llm-provider).
- `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true`. The batch path builds provider
request bodies directly and never sees the interleaved content.
#### Fireworks batch inference
Fireworks AI's batch API is **not** OpenAI `/v1/batches`-compatible — it is a proprietary, account-scoped dataset/job workflow on a separate control-plane host (`https://api.fireworks.ai`), distinct from the OpenAI-compatible inference host (`https://api.fireworks.ai/inference/v1`). Hindsight adapts it transparently, so enabling batch is the same as any other provider plus one required setting: your Fireworks **account id**. (This is separate from the existing LiteLLM `fireworks_ai/...` online path, which is unaffected.)
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FIREWORKS_ACCOUNT_ID` | Fireworks account id. **Required** for `fireworks` batch retain — the control-plane endpoints are `/v1/accounts/{account_id}/...`. Static, server-level. | - |
| `HINDSIGHT_API_FIREWORKS_BATCH_BASE_URL` | Fireworks batch control-plane host. | `https://api.fireworks.ai` |
| `HINDSIGHT_API_FIREWORKS_BATCH_MAX_WAIT_SECONDS` | Max time to wait for a batch job before surfacing a failure. Guards against the Fireworks gotcha where a non-batch-eligible model leaves the job `PENDING` forever. | `86400` (24h) |
```bash
# Fireworks batch retain (50% cost savings, async only)
export HINDSIGHT_API_RETAIN_LLM_PROVIDER=fireworks
export HINDSIGHT_API_RETAIN_LLM_API_KEY=fw_xxxxxxxxxxxx
export HINDSIGHT_API_RETAIN_LLM_MODEL=accounts/fireworks/models/llama-v3p1-8b-instruct
export HINDSIGHT_API_FIREWORKS_ACCOUNT_ID=your-account-id
export HINDSIGHT_API_RETAIN_BATCH_ENABLED=true
```
> **Entity labels** (`entity_labels`) and **free-form entity extraction** (`entities_allow_free_form`) are configured per bank via the [bank config API](api/memory-banks.md#retain-configuration), not as global environment variables — each bank can have its own controlled vocabulary. See [Entity Labels](retain.md#entity-labels) for details.
#### How entity resolution decides
Resolving an extracted name against the entities already in a bank happens in **two
stages, and each stage uses a different measure of similarity**. That distinction matters
before you tune any of the thresholds above: the number that is easiest to compute by
hand is not the number that decides the match.
**Stage 1 — which existing entities are considered.** With `trigram` (the default),
Postgres returns entities whose lowercased canonical name is trigram-similar to the
extracted name, gated by
[`HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD`](#database-connection-pool) — **`0.3`**,
and never below `HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY`, because stage 2 rejects
anything under that floor anyway. At most
`HINDSIGHT_API_RETAIN_ENTITY_RESOLUTION_MAX_CANDIDATES` survive, ranked by that
similarity. With `full`, candidates are instead the entities whose name is an exact or
substring match. Label entities never enter this stage; they resolve by exact match only.
**Stage 2 — whether one of them is reused.** Each candidate is scored, and the best one
is reused if it clears **`0.6`**. Otherwise a new entity is created.
| Signal | Weight | What it measures |
|--------|--------|------------------|
| Name similarity | up to **0.5** | A character-sequence ratio between the two lowercased names (Python's `difflib.SequenceMatcher`) — **not** the trigram similarity from stage 1 |
| Co-occurring entities | up to **0.3** | How many of the *other* entities extracted from the same fact this candidate already co-occurs with, each weighted by how selective it is |
| Temporal proximity | up to **0.2** | How close the fact's date is to the candidate's `last_seen`, decaying linearly to zero at 7 days |
Before any of that is counted, the candidate must reach
`HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY` (**`0.3`**) in trigram similarity. That floor
exists because the two stages measure different things and can disagree, most sharply on
short names: the sequence ratio rewards any shared run of characters, so `Tigran` and `Iran`
score `0.80` on it — higher than `Alice`/`Alice Chen` (`0.67`), a merge the resolver exists
to make — while trigram ranks them correctly at `0.20` and `0.55`. Without the floor, the
remaining signals are worth `0.5` of the `0.6` needed, so a name that merely resembled an
existing entity could be merged onto it by the bank's history alone.
Two further checks run before the score. A candidate must **agree word by word**: every word of
the shorter name has to find a counterpart in the longer one (an equal word, an abbreviation of
one, or a near-miss spelling). Whole-name similarity otherwise lets a long shared word drown out
a completely different short one — `John Smith` and `Jane Smith` are `0.47` by trigram and `0.80`
by sequence ratio, so two people sharing a surname would merge. Single-word names are exempt,
since with one word the floor above already is the word-level check. And in the other direction,
a candidate whose trigram set is *identical* to the name's is reused outright, whatever the other
signals say: pg_trgm builds trigrams per word, so identical sets mean the two forms differ only in
case, punctuation or decoration (`Wren 🎵` and `Wren`, `GPT-4` and `GPT 4`).
The co-occurrence weighting matters for the same reason. An entity such as `user` co-occurs
with nearly every fact in a mature bank, so sharing it says almost nothing; each shared
entity is discounted by how many distinct entities it co-occurs with, and a partner seen
alongside a hundred others is worth a tenth of one seen alongside a single other.
So there are three thresholds, and they answer different questions:
| Threshold | Default | Question |
|---|---|---|
| `ENTITY_TRGM_SIMILARITY_THRESHOLD` | `0.3` | Which existing entities are even looked at? (never below the merge floor) |
| `ENTITY_MERGE_MIN_SIMILARITY` | `0.3` | Which of them may be merged onto? |
| `ENTITY_INTRABATCH_MERGE_SIMILARITY` | `0.5` | Which brand-new names in one retain are folded together? |
**If unrelated entities are merging,** raise `HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY`.
Genuine surface variants of one name usually sit between `0.4` and `0.7`, so there is room
above the default. For names that must never be fuzzy-matched at all, model them as
[entity labels](retain.md#entity-labels), or pass them yourself with
[`resolve_entities: false`](api/retain.md#resolve_entities).
**If variants that should merge are staying separate,** lower
`HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY` — short names are the usual reason, since
trigram similarity on them is unavoidably low. The probe in stage 1 follows the floor down
unless `HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD` is set higher, so lower that too if
you raised it. If lowering the floor changes nothing, check that `HINDSIGHT_API_RETAIN_ENTITY_RESOLUTION_MAX_CANDIDATES` is not truncating the
right candidate away on a bank with many similar names.
#### Skip storing raw document text
By default Hindsight keeps a verbatim copy of everything you retain so you can later read the source, re-process a document, or export it. For deployments that only want to keep the extracted memories (facts, entities, mental models) and not the source text, set:
```bash
export HINDSIGHT_API_STORE_DOCUMENT_TEXT=false
```
When disabled, the full retain pipeline still runs — chunking, fact extraction, embedding, and entity linking are unchanged, so **memory quality and recall are not affected** (recall reads from the extracted memories, never from the raw text). The difference is purely what gets persisted:
- `documents.original_text` is stored as `NULL` instead of the raw payload.
- The raw chunk text is dropped (stored as empty), while the chunk's content hash is still kept so incremental re-retain of the same document continues to deduplicate correctly.
**`update_mode="append"` is rejected when text storage is disabled.** Append rebuilds a document by reading back its previously stored text and adding to it. With nothing stored, appending would silently drop the prior content, so an append retain returns an error instead. Use `update_mode="replace"` (the default).
**Features that degrade when text storage is disabled** (because they read the source text back):
- **Document export** carries no source text; re-importing such a bank cannot re-run extraction from the original payload.
- **Reading a document's source** (the get-document, list-chunks, and get-chunk endpoints, including their MCP equivalents) returns empty content.
- **Recall with `include_chunks=true`** returns empty `chunk_text` — the facts themselves are unaffected, but the surrounding source-chunk context is no longer available.
- **Reflect** no longer offers the `expand` tool (which fetches a memory's source chunk/document), and its `recall` step stops attaching source chunks, since there is no stored text to return. Reflection over the extracted memories is otherwise unaffected.
- **Reprocessing a document** from its stored text is a no-op (there is nothing to reprocess).
This is a static, server-level setting and cannot be overridden per bank.
#### Customizing retain: when to use what
There are five levels of customization for the retain pipeline. Start with the simplest that covers your needs:
| Goal | Use |
|------|-----|
| Steer what topics to focus on or deprioritize | `HINDSIGHT_API_RETAIN_MISSION` |
| Extract more detail per fact | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose` |
| Store chunks as-is, LLM extracts metadata | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatim` |
| Store chunks as-is, zero LLM cost | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunks` |
| Completely replace the extraction rules | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` + `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` |
**`HINDSIGHT_API_RETAIN_MISSION` — steer extraction without replacing it (recommended starting point)**
Tell the bank what to pay attention to during extraction, in plain language. The mission is injected into the extraction prompt alongside the built-in rules — it narrows focus without replacing the underlying logic. Works with any extraction mode (`concise`, `verbose`, `verbatim`, `custom`). Ignored in `chunks` mode.
```bash
export HINDSIGHT_API_RETAIN_MISSION="Focus on technical decisions, architecture choices, and team member expertise. Deprioritize social or personal information."
```
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose` — more detail per fact**
Use when you need richer facts with full context, relationships, and verbosity. Slower and uses more tokens than `concise`.
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatim` — store chunks as-is**
Each chunk is stored as a single memory unit with its original text preserved exactly — no summarization or rewriting. The LLM still runs to extract entities, temporal information, and location so the chunk is fully indexed and retrievable. Useful for RAG-style indexing, document ingestion pipelines, or benchmarks where you want the original text in memory rather than LLM-generated summaries.
```bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatim
```
**`retain_strategies` / `retain_default_strategy` — per-call extraction strategy**
Named strategies let you ingest different content types into the same bank using different extraction settings. A strategy is a set of hierarchical field overrides applied on top of the resolved bank config.
Any field in the hierarchical config can be overridden per strategy, including `retain_extraction_mode`, `retain_chunk_size`, `retain_structured_chunk_size`, `entity_labels`, `entities_allow_free_form`, `retain_mission`, etc.
Configure strategies via the bank config API:
```json
{
"retain_default_strategy": "conversations",
"retain_strategies": {
"conversations": {
"retain_extraction_mode": "concise",
"retain_chunk_size": 3000,
"retain_structured_chunk_size": 12000
},
"documents": {
"retain_extraction_mode": "chunks",
"retain_chunk_size": 800,
"entity_labels": null,
"entities_allow_free_form": false
}
}
}
```
Then specify the strategy at retain time:
```python
# Uses default strategy ("conversations")
client.retain_batch(bank_id, items=[{"content": "Alice joined the team today"}])
# Explicitly use document strategy
client.retain_batch(bank_id, items=[{"content": "...document text...", "strategy": "documents"}])
```
If no `strategy` is specified in a retain call, `retain_default_strategy` is used. If neither is set, the bank/global config applies directly.
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunks` — zero LLM cost**
Each chunk is stored as-is with no LLM call whatsoever. No entity extraction, no temporal indexing — only embeddings are generated for semantic search. User-provided entities passed via `RetainContent.entities` are the sole source of entity data. Use when ingestion speed and cost matter more than structured metadata.
```bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunks
```
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` + `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` — full control**
Replaces the built-in selectivity rules entirely. The structural parts of the prompt (output format, temporal handling, coreference resolution) remain intact — only the extraction guidelines are replaced.
Use this when `retain_mission` isn't sufficient and you need strict inclusion/exclusion logic.
```bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
export HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="ONLY extract facts that are:
✅ Technical decisions and their rationale
✅ Architecture patterns and design choices
✅ Performance metrics and benchmarks
DO NOT extract:
❌ Greetings or social conversation
❌ Process chatter (\"let me check\", \"one moment\")
❌ Anything that would not be useful in 6 months"
```
### File Processing
Configuration for the file upload and conversion pipeline (used by `POST /v1/default/banks/{bank_id}/files/retain`).
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_FILE_UPLOAD_API` | Enable the file upload API endpoint | `true` |
| `HINDSIGHT_API_ENABLE_DOCUMENT_EXPORT_API` | Enable the [document export](./api/memory-banks.md#document-export--import-superseded) endpoint (`GET /document-transfer`) | `true` |
| `HINDSIGHT_API_ENABLE_DOCUMENT_IMPORT_API` | Enable the [document import](./api/memory-banks.md#document-export--import-superseded) endpoint (`POST /document-transfer`) | `true` |
| `HINDSIGHT_API_FILE_PARSER` | Server-side default parser or fallback chain (comma-separated, e.g. `iris,markitdown`) | `markitdown` |
| `HINDSIGHT_API_FILE_PARSER_ALLOWLIST` | Comma-separated list of parsers clients are allowed to request. If not set, all registered parsers are allowed. | — |
| `HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE` | Max files per upload request | `10` |
| `HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB` | Max total upload size per request (MB) | `100` |
| `HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN` | Delete stored files after memory extraction completes | `true` |
#### Parser selection
Clients can override the server default by passing `parser` in the request body of `POST /v1/default/banks/{bank_id}/files/retain`. Both the server default and the per-request field accept a single parser name or an ordered **fallback chain** — each parser is tried in sequence until one succeeds.
```bash
# Server default: try iris first, fall back to markitdown if iris fails
export HINDSIGHT_API_FILE_PARSER=iris,markitdown
# Restrict what clients may request (optional — defaults to all registered parsers)
export HINDSIGHT_API_FILE_PARSER_ALLOWLIST=markitdown,iris
```
```json
// Per-request override (in the JSON body of the file retain endpoint)
{
"parser": "iris",
"files_metadata": [
{ "document_id": "report" },
{ "document_id": "fallback_doc", "parser": ["iris", "markitdown"] }
]
}
```
Clients that request a parser not in the allowlist receive HTTP 400.
#### Parser: markitdown (default)
Local file-to-markdown conversion using [Microsoft's markitdown](https://github.com/microsoft/markitdown). No external service is required by default.
**Supported formats:** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, PNG — requires optional OCR for text extraction), audio (MP3, WAV — transcription), HTML, TXT, MD, CSV.
For image workloads, MarkItDown can optionally use an OpenAI-compatible OCR/vision endpoint. This is disabled by default. Without it, image uploads fail with an actionable configuration error instead of low-level parser output. When enabled, configure the MarkItDown OCR API key, base URL, and model explicitly; they do not inherit from `HINDSIGHT_API_LLM_*` because MarkItDown uses the OpenAI SDK directly. The selected endpoint must implement OpenAI Chat Completions and the selected model must support image input.
This OCR path uses MarkItDown's image converter hook. It applies to image inputs such as JPG and PNG (and image handling inside converters that consume MarkItDown's `llm_client`), but it does not rasterize scanned PDF pages into images. Scanned PDFs with no text layer may still extract poorly through the default PDF converter. For scanned PDFs or complex document layouts, use an OCR-capable document parser such as `iris` or `llama_parse`, or configure a parser fallback chain like `llama_parse,markitdown`.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_ENABLED` | Enable MarkItDown image OCR through an OpenAI-compatible OCR/vision endpoint | `false` |
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_API_KEY` | API key for MarkItDown OCR; required when OCR is enabled | — |
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_BASE_URL` | OpenAI-compatible Chat Completions base URL for MarkItDown OCR; required when OCR is enabled | — |
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_MODEL` | OCR/vision model with image-input support; required when OCR is enabled | — |
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_PROMPT` | OCR prompt passed to MarkItDown's image converter | Built-in OCR prompt |
| `HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_DEFAULT_HEADERS` | Optional JSON dict passed as `default_headers` to the OCR OpenAI SDK client (proxies / request-tracing middleware) | `null` |
```bash
# Configure a dedicated OpenAI-compatible OCR/vision endpoint for MarkItDown OCR
export HINDSIGHT_API_FILE_PARSER=markitdown
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_ENABLED=true
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_API_KEY=your-vision-api-key
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_BASE_URL=https://vision.example/v1
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_MODEL=ocr-or-vision-model
# Optional custom headers for proxies / request tracing
# export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_DEFAULT_HEADERS='{"X-Component-Id":"hindsight-ocr"}'
```
#### Parser: iris
Cloud-based extraction via [Vectorize Iris](https://docs.vectorize.io/build-deploy/extract-information/understanding-iris/). Higher quality extraction for complex documents, powered by a remote AI service.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN` | Vectorize API token | — |
| `HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID` | Vectorize organization ID | — |
**Supported formats:** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, JPEG, PNG, GIF, BMP, TIFF, WEBP), HTML, TXT, MD, CSV.
```bash
# Use iris as the only parser
export HINDSIGHT_API_FILE_PARSER=iris
export HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN=your-vectorize-token
export HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID=your-org-id
# Or: try iris first, fall back to markitdown if iris fails or rejects the file type
export HINDSIGHT_API_FILE_PARSER=iris,markitdown
```
#### Parser: llama_parse
Cloud-based extraction via [LlamaParse](https://docs.cloud.llamaindex.ai/llamaparse) (LlamaIndex). Strong extraction for complex layouts — tables, charts, multi-column PDFs.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY` | LlamaCloud API key (typically starts with `llx-`) | — |
**Supported formats:** PDF, DOCX, PPTX, XLSX, HTML, EPUB, RTF, TXT, and many more — see the [LlamaParse docs](https://docs.cloud.llamaindex.ai/llamaparse/features/supported_document_types) for the full list.
```bash
# Use llama_parse as the only parser
export HINDSIGHT_API_FILE_PARSER=llama_parse
export HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY=llx-your-api-key
# Or: try llama_parse first, fall back to markitdown
export HINDSIGHT_API_FILE_PARSER=llama_parse,markitdown
```
```bash
# Increase batch limits for large file imports
export HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE=20
export HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB=500
# Keep files after processing (useful for debugging or re-processing)
export HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN=false
```
### File Storage
Files uploaded via the file retain API are stored in an object storage backend before conversion. Choose the backend that fits your infrastructure.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_STORAGE_TYPE` | Storage backend: `native`, `s3`, `gcs`, or `azure` | `native` |
| `HINDSIGHT_API_FILE_STORAGE_EXTENSION` | `module.path:ClassName` naming your own `FileStorage` implementation, used instead of the built-in backends. Every other `HINDSIGHT_API_FILE_STORAGE_*` variable is passed to it as a lowercased config dict. Implement `delete_prefix` too, or deleting a bank leaves that bank's stored files in your backend. | unset |
#### Native (PostgreSQL)
Files are stored as `BYTEA` in the `file_storage` table. No additional infrastructure required. Suitable for development and small deployments.
```bash
# Native storage is the default — no additional configuration needed
export HINDSIGHT_API_FILE_STORAGE_TYPE=native
```
#### S3 / S3-Compatible
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_STORAGE_S3_BUCKET` | S3 bucket name | - |
| `HINDSIGHT_API_FILE_STORAGE_S3_REGION` | AWS region | - |
| `HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT` | Custom endpoint URL (for S3-compatible stores like MinIO, Cloudflare R2, Tigris) | AWS default |
| `HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID` | AWS access key ID | - |
| `HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY` | AWS secret access key | - |
For S3-compatible providers that don't expose AWS-style regions (MinIO, Cloudflare R2, Tigris), set `HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto`. The value is required for SigV4 request signing but is ignored by the service.
```bash
# AWS S3
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-files
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=us-east-1
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# S3-compatible (MinIO, Cloudflare R2, etc.)
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-bucket
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto
export HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT=https://your-minio.example.com
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=minioadmin
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=minioadmin
# Tigris (S3-compatible, single global endpoint)
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-bucket
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto
export HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT=https://t3.storage.dev
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=tid_your_access_key
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=tsec_your_secret_key
```
#### Google Cloud Storage
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_STORAGE_GCS_BUCKET` | GCS bucket name | - |
| `HINDSIGHT_API_FILE_STORAGE_GCS_SERVICE_ACCOUNT_KEY` | Path to service account JSON key file | ADC if not set |
```bash
export HINDSIGHT_API_FILE_STORAGE_TYPE=gcs
export HINDSIGHT_API_FILE_STORAGE_GCS_BUCKET=my-hindsight-files
# Optional: use service account key file (otherwise falls back to ADC)
export HINDSIGHT_API_FILE_STORAGE_GCS_SERVICE_ACCOUNT_KEY=/path/to/key.json
```
#### Azure Blob Storage
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_STORAGE_AZURE_CONTAINER` | Azure container name | - |
| `HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_NAME` | Azure storage account name | - |
| `HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_KEY` | Azure storage account key | - |
```bash
export HINDSIGHT_API_FILE_STORAGE_TYPE=azure
export HINDSIGHT_API_FILE_STORAGE_AZURE_CONTAINER=hindsight-files
export HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_NAME=mystorageaccount
export HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_KEY=base64encodedkey==
```
#### Storage Backend Comparison
| Backend | Best For | Notes |
|---------|----------|-------|
| `native` | Development, small deployments | No extra infrastructure, stored in PostgreSQL |
| `s3` | Production, AWS deployments | Works with any S3-compatible store |
| `gcs` | Production, GCP deployments | Supports ADC for keyless auth |
| `azure` | Production, Azure deployments | Uses account key auth |
> **💡 Production Recommendation**
>
For production deployments, use `s3`, `gcs`, or `azure` to avoid storing large binary files in your PostgreSQL database. Set `HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN=true` (the default) to delete files after memory extraction, which minimizes storage costs.
### Observations (Experimental) {#observations}
Observations are deduplicated, evidence-grounded knowledge consolidated from multiple facts. Each observation tracks its supporting memories and a proof count, and is refined — not overwritten — when new evidence arrives.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
| `HINDSIGHT_API_ENABLE_AUTO_CONSOLIDATION` | Automatically trigger consolidation after retain, delete, and update operations. When `false`, consolidation only runs when explicitly triggered via the [consolidate endpoint](api/operations.md#consolidation). Configurable per bank. | `true` |
| `HINDSIGHT_API_CONSOLIDATION_USE_DECISION_MODEL` | When a decision model is set up for consolidation (see [Decision model](#decision-model)), it settles the easy facts before the consolidation LLM call: a fact that restates an observation is attached as its evidence, a fact that adds a new entity or attribute is recorded as its own observation, and chatter is skipped. Every fact it does not settle (one that changes what an observation says, one whose answers disagree, one whose request failed) goes to the LLM as before. Under a bank's own observations mission, a new fact is recorded as stated only when the mission wants it and its wording breaks none of the mission's rules (no names, trends only, …); chatter is left to the LLM, which applies the mission. Set `false` to send every fact to the LLM. Hierarchical (per bank). | `true` |
| `HINDSIGHT_API_CONSOLIDATION_WALL_TIMEOUT` | Ceiling in seconds on how long one consolidation task in the worker may run **without making progress**. Every batch that commits restarts the clock, so a large backlog is never cut short — only a genuinely stalled job is. A stalled consolidation is cancelled and marked `failed` (which the API will retry, unlike `processing`, and which lets the reconcile sweep re-schedule the bank) instead of holding its reserved worker slot until the process restarts. `0` disables. | `7200` |
| `HINDSIGHT_API_CONSOLIDATION_RECONCILE_INTERVAL_SECONDS` | Interval for the background sweep that re-schedules consolidation for banks with unconsolidated facts but no consolidation in progress — recovering facts left unscheduled when a consolidation operation failed terminally (e.g. the LLM provider was unavailable). Only applies to banks with auto-consolidation enabled. `0` disables the sweep — but see the note below: that stops work being *scheduled*, not work already queued from *running*. | `300` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_TICK_SECONDS` | How often the background loop checks for cron-scheduled mental models that are due for a refresh. This is only the *check* cadence; the actual schedule is the per-model `trigger.refresh_cron` expression set on the mental model. A due model is refreshed only when it is stale (new memories in its scope since the last refresh). It also sets a **floor on cron granularity** — at the default, a `* * * * *` schedule fires every 5 minutes, not every minute; lower it if you need finer schedules, at the cost of a more frequent cross-tenant scan (see [Background Maintenance](#background-maintenance)). `0` disables the sweep — but see the note below: that stops work being *scheduled*, not work already queued from *running*. | `300` |
| `HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY` | Track history of changes to each observation (previous text/tags/dates + timestamp), stored one row per change in the `observation_history` table. Set to `false` to disable entirely — no history rows are written. **This is how you turn the feature off** (not a zero cap). | `true` |
| `HINDSIGHT_API_OBSERVATION_HISTORY_MAX_ENTRIES` | Max history rows kept per observation. On each update the previous version is inserted into the `observation_history` table and the oldest rows beyond this cap are deleted, so an often-reinforced observation's history can't grow without bound. `0` or a negative value **removes the cap** (unbounded); to turn history off entirely set `HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY=false` instead. | `50` |
| `HINDSIGHT_API_CONSOLIDATION_MAX_ATTEMPTS` | Outer retry attempts for the consolidation LLM batch call, used only for connection and rate-limit style failures (with backoff between attempts). Malformed or schema-invalid output is not re-sent — the batch fails immediately and is split into smaller batches instead. A provider quota exhaustion is not retried here at all: the whole job is rescheduled for when quota reopens. Each retried attempt uses the inner retry budget (`HINDSIGHT_API_CONSOLIDATION_LLM_MAX_RETRIES`), so worst-case API calls per batch = `MAX_ATTEMPTS × (LLM_MAX_RETRIES + 1)`. | `3` |
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
| `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND` | Maximum memories processed per consolidation round. When the limit is reached, the job yields its worker slot and re-queues itself so other banks get fair scheduling. Mental model refreshes only run on the final round. `0` = unlimited. Configurable per bank. | `100` |
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
| `HINDSIGHT_API_CONSOLIDATION_MAX_COMPLETION_TOKENS` | Max completion tokens requested for each consolidation LLM batch call. Unset by default, so each provider keeps its implicit output budget. Set this when a provider applies a low hidden cap (e.g. Bedrock imported models) that truncates consolidation output. | `unset` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE` | Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Configurable per bank. | `8` |
| `HINDSIGHT_API_CONSOLIDATION_DEDUP_THRESHOLD` | Cosine similarity at/above which a newly-created or freshly-updated observation is reconciled against an existing near-identical one via a focused 1-by-1 LLM "merge or keep" call (the model reads both texts, so a number/negation/entity difference is respected). Catches near-duplicate observations that weaker consolidation models emit even when shown the twin, as well as duplicates that arise when an update rewrites an observation into a near-twin of another. Set to `1.0` to disable. Postgres only — consolidation skips reconciliation on Oracle regardless of this value. | `0.97` |
| `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM` | Maximum number of tag groups consolidated concurrently within one consolidation op. Each group acquires per-scope locks before processing, so groups whose write scopes overlap (e.g. under `per_tag` / `all_combinations` / explicit-list `observation_scopes`) automatically serialise on the overlapping scopes — actual concurrency may be lower than this cap when scopes contend. Set to `1` for fully sequential behaviour. Higher values raise peak LLM QPS and connection-pool usage during consolidation proportionally — tune down if your LLM provider rate-limits tightly or your DB pool is small. Configurable per bank. | `4` |
| `HINDSIGHT_API_CONSOLIDATION_RECALL_BUDGET` | Budget level for the recall pass inside consolidation (`low`, `mid`, `high`). Lower budgets fetch fewer candidate rows, reducing peak memory usage on large banks. | `low` |
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `4096` |
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION` | Per-observation token cap for source facts in the consolidation prompt. Each observation independently gets at most this many tokens of source facts. `-1` = unlimited. Configurable per bank. | `256` |
| `HINDSIGHT_API_OBSERVATIONS_MISSION` | What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave unset to use the server default. | - |
| `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE` | Maximum number of observations allowed per tag scope. When the limit is reached, consolidation will only update or delete existing observations — no new ones are created. Applies per tag scope (e.g., per-tag when using `per_tag` observation scopes). Observations with no tags are not subject to this limit. `-1` = unlimited. Configurable per bank. | `-1` |
| `HINDSIGHT_API_OBSERVATION_SCOPE_LIMITS` | **Deprecated** — use `CONSOLIDATION_STRATEGIES`, which carries the mission too; this is still honoured, but consulted only afterwards. Per-scope overrides of `MAX_OBSERVATIONS_PER_SCOPE`, as a JSON array of `{"scope": [tag-globs], "limit": int}` rules. Each `scope` is a list of [fnmatch](https://docs.python.org/3/library/fnmatch.html) globs; a consolidation scope matches under *exact cover* — every tag must be matched by a glob and every glob must match a tag, so `["shared"]` matches the scope `{shared}` but not `{run_1, shared}`. The first matching rule wins; scopes that match no rule fall back to `MAX_OBSERVATIONS_PER_SCOPE`. Example: `[{"scope": ["shared"], "limit": -1}, {"scope": ["run_*", "shared"], "limit": 50}]` keeps the `{shared}` scope unlimited while capping each `{run_*, shared}` scope at 50. Configurable per bank. | - |
| `HINDSIGHT_API_DEFAULT_OBSERVATION_SCOPES` | `observation_scopes` applied at retain to items that send none: `combined`, `per_tag`, `all_combinations`, `shared`, or a JSON list of tag lists, where a tag containing `*` is a glob over the memory's own tags (e.g. `[["topic:*"]]` groups memories by their topic tags only). Stored on each memory, so changing it doesn't re-scope memories already retained until their document is reprocessed. See [observation_scopes](./api/retain.md#observation_scopes). Configurable per bank. | `combined` |
| `HINDSIGHT_API_CONSOLIDATION_STRATEGIES` | Per-scope consolidation settings, as a JSON array of `{"scopes": [{"tags": [tag-globs], "tags_match": "all" \| "exact"}, ...], "observations_mission": str, "max_observations_per_scope": int, "consolidation_source_facts_max_tokens": int, "consolidation_source_facts_max_tokens_per_observation": int}` strategies. Each strategy lists the scopes it claims — a scope is a list of [fnmatch](https://docs.python.org/3/library/fnmatch.html) globs, and a consolidation pass is claimed when *any* of the strategy's patterns matches its tags. Each pattern's `tags_match` sets how: `"all"` (default) — the scope has every tag in the pattern, other tags allowed; `"exact"` — exactly those tags and no others. Every setting is optional. **Exactly one strategy applies to a scope: the first in the list that claims it.** Whatever that strategy leaves unset — and every scope no strategy claims — uses the bank-wide `OBSERVATIONS_MISSION`, `MAX_OBSERVATIONS_PER_SCOPE`, `CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` and `CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION`. This supersedes `OBSERVATION_SCOPE_LIMITS` and is what lets a single bank be federated across user / team / company tag scopes: each scope is consolidated by its own LLM call, so each can follow a different brief. Example: `[{"scopes": [{"tags": ["company:*"]}], "observations_mission": "Record only generalized, industry-level trends. Never name a specific company, deal size, or funding stage."}]` keeps per-user scopes detailed while the company-wide scope stays generic. Configurable per bank. | - |
> **📝 Disabling a sweep stops scheduling, not execution**
>
Background work runs in two independent stages, and the interval knobs above only
govern the first:
1. **Scheduling** — the maintenance loop periodically sweeps for work that is due
(consolidation to reconcile, mental models to refresh) and enqueues an
operation for it. `HINDSIGHT_API_CONSOLIDATION_RECONCILE_INTERVAL_SECONDS=0`
and `HINDSIGHT_API_MENTAL_MODEL_REFRESH_TICK_SECONDS=0` switch these sweeps
off, so no *new* work is enqueued.
2. **Execution** — the worker poller claims and runs whatever is already pending,
on its own cadence
([`HINDSIGHT_API_WORKER_POLL_INTERVAL_MS`](#distributed-workers), default
500ms). It is not affected by the interval knobs at all, and it picks up
operations that were queued before the sweeps were disabled — including ones
recovered from a previous run after a restart.
So zeroing the intervals quiesces a *quiet* deployment, but on one with a backlog
the worker keeps consolidating and refreshing regardless. To stop the process
executing background work entirely, disable the worker itself:
```bash
HINDSIGHT_API_WORKER_ENABLED=false
```
This matters when isolating a workload for benchmarking or debugging: with only
the intervals zeroed, a queued consolidation or retain can still run alongside
whatever you are measuring and be attributed to it.
#### Customizing observations: when to use what
| Goal | Use |
|------|-----|
| Default behavior: durable specific facts, no ephemeral state | Leave unset |
| Change what observations *are* for this bank (different shape, different purpose) | `HINDSIGHT_API_OBSERVATIONS_MISSION` |
**`HINDSIGHT_API_OBSERVATIONS_MISSION` — redefine what this bank synthesises**
By default, observations are durable, specific beliefs consolidated from memories — the kind of knowledge that stays true over time (preferences, skills, relationships, recurring patterns). Each one is grounded in the source memories that support it. Ephemeral state is filtered out. Contradictions are tracked with temporal markers rather than overwriting the prior belief.
Set `HINDSIGHT_API_OBSERVATIONS_MISSION` to replace this definition entirely. Write a plain-language description of what observations should be for your use case. The LLM will use this instead of the default rules when deciding what to create or update. Leave it unset to keep the server default.
> **💡 When to use observations_mission**
>
Use it when the default durable-knowledge behavior doesn't match your use case. Common scenarios:
- You want **broader event summaries** rather than isolated facts
- You want observations **grouped by time period** (weekly, monthly)
- You want a **different granularity** (one observation per project rather than per fact)
- You have a **domain-specific** notion of what's worth remembering
**Example: Weekly event summaries**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are broad summaries of project events grouped by week. Each observation should capture what happened, what was decided, and what was blocked — not individual facts. Merge related events into cohesive weekly narratives."
```
**Example: Person-centric knowledge**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are durable facts about specific named people: their preferences, skills, relationships, and behavioral patterns. Only create observations for facts that are stable over time and tied to a named individual."
```
**Example: Support ticket patterns**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are recurring patterns in customer support interactions: common failure modes, frequently requested features, and pain points that appear across multiple tickets."
```
### Reflect
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_REFLECT_MAX_ITERATIONS` | Max tool call iterations before forcing a response | `10` |
| `HINDSIGHT_API_REFLECT_USE_DECISION_MODEL` | When a decision model is set up for reflect (see [Decision model](#decision-model)), reflect is decision-orchestrated: one LLM call rewrites the request into a search query (or finds there is nothing to search, and reflect returns an empty answer at once), the layers run in parallel with that query, and the decision model prunes the evidence and judges whether it already answers the request. If it does, the answer is the next LLM call. If not, the LLM writes the next query and another parallel round runs, judged the same way. Fewer LLM calls than agent turns, so it costs less. If the decision model fails, reflect falls back to agent turns. Only the `typesafe` provider is supported here for now: with another provider reflect keeps agent turns and logs a warning. Set `false` to keep agent turns (one LLM turn per retrieval layer, each writing its own query). Hierarchical (per bank). | `true` |
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_USE_DECISION_MODEL` | The same, for the reflect that refreshes a knowledge page. Hierarchical (per bank). | `true` |
| `HINDSIGHT_API_MENTAL_MODEL_DELTA_USE_DECISION_MODEL` | When a decision model is set up for reflect (see [Decision model](#decision-model)), a delta knowledge-page refresh skips reflect: it asks the decision model which of the memories added since its last run could change the page, and edits the page from those alone. None of them means no LLM call. Set `false` to run reflect for delta refreshes too. Hierarchical (per bank). | `true` |
| `HINDSIGHT_API_REFLECT_MAX_CONTEXT_TOKENS` | Max accumulated context tokens in the reflect loop before forcing final synthesis. Prevents `context_length_exceeded` errors on large banks. Lower this if your LLM has a context window smaller than 128K. | `100000` |
| `HINDSIGHT_API_REFLECT_WALL_TIMEOUT` | Wall-clock timeout in seconds for the entire reflect operation. If exceeded, the request returns HTTP 504. Also bounds a whole mental-model refresh in the worker: one that runs past it is cancelled and marked failed instead of holding its worker slot. | `300` |
| `HINDSIGHT_API_REFLECT_MISSION` | Global reflect mission (identity and reasoning framing). Overridden per bank via config API. | - |
| `HINDSIGHT_API_REFLECT_SOURCE_FACTS_MAX_TOKENS` | Token budget for source facts in `search_observations` during reflect. `-1` disables source facts (default), `0` enables with no limit, `>0` enables with a token budget. Hierarchical — can be overridden per bank via config API. | `-1` |
| `HINDSIGHT_API_REFLECT_DEFAULT_OPTIONS` | Default reflect options as a JSON object, applied whenever a reflect request leaves the option unset. Mental-model refreshes do not read it — they take these settings from their own trigger, defaulted per bank by `knowledge_page_default_trigger`. `reflect_search_observations_max_tokens` sets the budget for the `search_observations` tool (a smaller budget drops the lowest-ranked observations and shrinks the reflect context); `reflect_search_observations_include_entities` turns off the resolved entity names attached to each observation, which can be more than half the tool payload. E.g. `{"reflect_search_observations_max_tokens": 3000, "reflect_search_observations_include_entities": false}`. Hierarchical — can be overridden per bank via config API. | - |
#### Internal recall (used by reflect and mental model refresh)
These knobs control the recall tool the reflect agent runs (including when refreshing a mental model). They set what a `recall` tool call gets when the model does not ask for a specific budget; an explicit ask from the model wins, within the bounds reflect applies to any model-supplied token argument. They are hierarchical — overridable per bank via the config API, and individually overridable per mental model via the `trigger.include_chunks`, `trigger.recall_max_tokens`, and `trigger.recall_chunks_max_tokens` fields.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RECALL_INCLUDE_CHUNKS` | Whether the internal recall returns raw chunk text alongside facts. Set `false` to skip chunks and save prompt budget. | `true` |
| `HINDSIGHT_API_RECALL_MAX_TOKENS` | Token budget for facts returned by the internal recall. | `2048` |
| `HINDSIGHT_API_RECALL_CHUNKS_MAX_TOKENS` | Token budget for raw chunks returned by the internal recall. | `1000` |
#### Disposition
Disposition traits control how the bank reasons during reflect operations. Each trait is on a scale of 1–5. These are hierarchical — they can be overridden per bank via the [config API](#hierarchical-configuration).
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DISPOSITION_SKEPTICISM` | How skeptical vs trusting (1=trusting, 5=skeptical) | `3` |
| `HINDSIGHT_API_DISPOSITION_LITERALISM` | How literally to interpret information (1=flexible, 5=literal) | `3` |
| `HINDSIGHT_API_DISPOSITION_EMPATHY` | How much to consider emotional context (1=detached, 5=empathetic) | `3` |
### MCP Server
Configuration for MCP server endpoints.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server at `/mcp/{bank_id}/` | `true` |
| `HINDSIGHT_API_MCP_ENABLED_TOOLS` | Comma-separated allowlist of MCP tools to expose globally (empty = all tools) | - |
| `HINDSIGHT_API_MCP_STATELESS` | Use stateless HTTP transport (POST-only). When `false`, enables stateful mode with GET/SSE support for server-initiated messages | `false` |
| `HINDSIGHT_API_MCP_AUTH_TOKEN` | Bearer token for MCP authentication (optional) | - |
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | Memory bank ID for local MCP | `mcp` |
| `HINDSIGHT_API_MCP_INSTRUCTIONS` | Additional instructions appended to retain/recall tool descriptions | - |
**Tool Access Control:**
`HINDSIGHT_API_MCP_ENABLED_TOOLS` restricts which MCP tools are registered at the server level. This is useful for read-only deployments or limiting surface area:
```bash
# Expose only recall (read-only deployment)
export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall
# Expose recall and reflect only
export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall,reflect
```
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`, `get_knowledge_base_tree`, `search_knowledge_base`, `get_knowledge_page`, `create_knowledge_folder`, `create_knowledge_page`, `update_knowledge_node`, `delete_knowledge_node`.
This can also be overridden per bank via the [config API](#hierarchical-configuration):
```bash
# Restrict a specific bank to read-only MCP access
curl -X PATCH http://localhost:8888/v1/default/banks/config-demo-bank/config \
-H "Content-Type: application/json" \
-d '{"updates": {"mcp_enabled_tools": ["recall"]}}'
```
When a bank-level `mcp_enabled_tools` is set, tools not in the list return a clear error when invoked (they still appear in the tools list for MCP protocol compatibility).
**MCP Authentication:**
By default, the MCP endpoint is open. For production deployments, set `HINDSIGHT_API_MCP_AUTH_TOKEN` to require Bearer token authentication:
```bash
export HINDSIGHT_API_MCP_AUTH_TOKEN=your-secret-token
```
Clients must then include the token in the `Authorization` header. See [MCP Server documentation](./mcp-server.md#authentication) for details.
**Local MCP instructions:**
```bash
# Example: instruct MCP to also store assistant actions
export HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls and decisions made."
```
### Distributed Workers
Configuration for background task processing. By default, the API processes tasks internally. For high-throughput deployments, run dedicated workers. See [Services - Worker Service](./services.md#worker-service) for details.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_WORKER_ENABLED` | Enable the internal worker in the API process. Set to `false` to stop this process claiming and running queued background operations (consolidation, retain, mental model refresh) — this, not the maintenance interval knobs, is what quiesces execution. | `true` |
| `HINDSIGHT_API_WORKER_ID` | Unique worker identifier | hostname |
| `HINDSIGHT_API_WORKER_POLL_INTERVAL_MS` | Database polling interval in milliseconds | `500` |
| `HINDSIGHT_API_WORKER_MAX_RETRIES` | Max retries before marking task failed | `3` |
| `HINDSIGHT_API_WORKER_TASK_RETRY_BACKOFF_SECONDS` | Seconds between retries on transient task failure | `60` |
| `HINDSIGHT_API_BACKPRESSURE_DEFER_SECONDS` | How long a task the store shed for backpressure is held before it is retried. Long enough that the backlog has a real chance to drain — retrying into a still-full store just sheds again — and short enough that a cleared backlog is not left waiting. Deferrals do not count against `HINDSIGHT_API_WORKER_MAX_RETRIES`. | `120` |
| `HINDSIGHT_API_WORKER_HTTP_PORT` | HTTP port for worker metrics/health (worker CLI only) | `8889` |
| `HINDSIGHT_API_WORKER_MAX_SLOTS` | Maximum concurrent tasks per worker (total across all operation types) | `10` |
| `HINDSIGHT_API_OPERATION_RETENTION_DAYS` | Static server-wide retention window for completed, failed, and cancelled operation rows, including their task payload and result metadata. `0` (the default) keeps them indefinitely; set a positive number of days to enable automatic pruning. | `0` |
| `HINDSIGHT_API_OPERATION_CLEANUP_BATCH_SIZE` | Maximum expired terminal operation rows deleted per tenant schema during each cleanup cycle. Together with the cleanup interval this sets the drain rate for a backlog. Must be a positive integer. | `1000` |
| `HINDSIGHT_API_OPERATION_CLEANUP_INTERVAL_SECONDS` | How often the cleanup cycle runs. Each cycle costs one cross-tenant discovery round-trip, so the interval is the main lever on that cost for deployments with many tenants (see [Background Maintenance](#background-maintenance)). `0` disables the job. | `900` |
| `HINDSIGHT_API_WORKER_CONSOLIDATION_RESERVED_SLOTS` | Reserved (minimum) slots for consolidation within `WORKER_MAX_SLOTS` — a floor that guarantees capacity, **not** a per-type cap (bank-serialization preserved). See the note below. | `2` |
| `HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY` | Per-bank priority for consolidation scheduling (see note below) | _(unset)_ |
| `HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS` | Reserved (minimum) slots for retain within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_FILE_CONVERT_RETAIN_RESERVED_SLOTS` | Reserved (minimum) slots for file_convert_retain within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_REFRESH_MENTAL_MODEL_RESERVED_SLOTS` | Reserved (minimum) slots for refresh_mental_model within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_GRAPH_MAINTENANCE_RESERVED_SLOTS` | Reserved (minimum) slots for graph_maintenance within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_IMPORT_DOCUMENTS_RESERVED_SLOTS` | Reserved (minimum) slots for import_documents within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_EXPORT_BANK_RESERVED_SLOTS` | Reserved (minimum) slots for export_bank within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_IMPORT_BANK_RESERVED_SLOTS` | Reserved (minimum) slots for import_bank within `WORKER_MAX_SLOTS`. | `0` |
| `HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS` | **Deprecated** alias for `..._RESERVED_SLOTS`, where `<TYPE>` is one of `CONSOLIDATION`, `RETAIN`, `FILE_CONVERT_RETAIN`, `REFRESH_MENTAL_MODEL`, `GRAPH_MAINTENANCE`, `IMPORT_DOCUMENTS`. The name is misleading — it always set the reservation floor, never a ceiling. Still honored but logs a warning; will be removed in a future release. Setting both it and `..._RESERVED_SLOTS` is an error. | _(unset)_ |
Terminal operations use one coherent retention window for the entire row. Hindsight does not scrub the task payload when an operation finishes: failed and cancelled operations need it for retry, while `include_payload=true` on completed operations is an explicit debugging surface. Keeping payload, result metadata, progress, and status together also avoids partial operation histories. Once a terminal row's `updated_at` is older than the configured window, the background maintenance loop prunes it in bounded per-schema batches, alongside the other scheduled sweeps. PostgreSQL only — the maintenance loop does not run on Oracle. Pending and processing rows are never pruned by this cleanup.
> **📝 Worker slot reservations are a floor, not a cap**
>
`..._RESERVED_SLOTS` reserves a guaranteed *minimum* number of slots (a floor) for an operation type within `WORKER_MAX_SLOTS`, so a saturated pool can't starve it. It is **not** a per-type maximum. The sum of all reservations must not exceed `WORKER_MAX_SLOTS` (startup raises `ValueError` otherwise). Remaining capacity (`WORKER_MAX_SLOTS - sum of reservations`) forms a **shared pool** usable by any type on a first-come basis, and a type whose reservation is full **overflows into the shared pool** — so a type's real ceiling is `WORKER_MAX_SLOTS`, regardless of its reservation.
Example: `MAX_SLOTS=10`, `CONSOLIDATION_RESERVED_SLOTS=2`, `RETAIN_RESERVED_SLOTS=3`, `REFRESH_MENTAL_MODEL_RESERVED_SLOTS=2` → shared pool = `10 - (2+3+2) = 3`.
With the defaults (`MAX_SLOTS=10`, `CONSOLIDATION_RESERVED_SLOTS=2`, all other reservations `0`), 2 slots are always reserved for consolidation and the remaining 8 form the shared pool; consolidation may still overflow the shared pool up to 10. Set `CONSOLIDATION_RESERVED_SLOTS=0` to release its reserved capacity into the shared pool. Consolidation's bank-serialization constraint (no two consolidation tasks for the same bank concurrently) is preserved regardless of which pool claims the slot.
The legacy `..._MAX_SLOTS` env vars are a deprecated alias for `..._RESERVED_SLOTS` — despite the name they set the reservation floor, never a ceiling. They still work but log a warning; migrate to `..._RESERVED_SLOTS`.
> **📝 Consolidation bank priority**
>
`HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY` controls which banks' consolidation tasks are claimed first when a slot becomes available. Format: comma-separated `bank-pattern:priority` pairs where higher numbers mean higher priority. Patterns support `*` as a wildcard; a bare `*` is the catch-all default for unlisted banks (defaults to `1` if omitted).
Example:
```
HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY="shadow-*:10,staging-*:5,*:1"
```
This ensures `shadow-*` banks are always consolidated before others, even if their tasks were submitted later. Useful for deployments with asymmetric bank sizes where a large bank might be starved by many small banks cycling through limited slots. Bank-serialization (max one concurrent consolidation per bank) is preserved regardless of priority. When unset, consolidation tasks are claimed in `created_at` order (default behavior).
### Performance Optimization
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_SKIP_LLM_VERIFICATION` | Skip LLM connection check on startup | `false` |
#### Bank alias cache
A bank alias is an extra id a bank answers to, so callers can be moved onto a new id without downtime. Resolving it happens on every bank-scoped request and is cached per `(schema, alias)` — misses included, since most requests name a real bank. The TTL is therefore how long a newly added alias stays invisible, and a removed one keeps routing, on API replicas other than the one that changed it.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_BANK_ALIAS_CACHE_TTL_SECONDS` | Time-to-live (seconds) for a resolved alias. `0` disables the cache and resolves against the database on every request. | `10` |
| `HINDSIGHT_API_BANK_ALIAS_CACHE_MAX_ENTRIES` | Maximum number of cached `(schema, alias)` entries before LRU eviction. | `2048` |
#### Bank stats cache
`get_bank_stats` aggregates over `memory_links` (joining `memory_units`), which can be a multi-second scan on banks with millions of rows. Because the result is intentionally approximate — it backs a UI widget and the freshness hint inside `reflect` — it is cached per `(schema, bank)` for a few tens of seconds, which also coalesces concurrent misses onto a single in-flight query. Tune it for high-concurrency or large-bank deployments:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_BANK_STATS_CACHE_TTL_SECONDS` | Time-to-live (seconds) for the `get_bank_stats` result cache. `0` disables caching, so every call runs the query. | `60` |
| `HINDSIGHT_API_BANK_STATS_CACHE_MAX_ENTRIES` | Maximum number of cached `(schema, bank)` entries before LRU eviction. Bounds memory in deployments with many banks. | `1024` |
#### Native thread pools
When local embeddings or reranking run in-process, the underlying BLAS/ML libraries (OpenBLAS, OpenMP, MKL) each spawn a worker pool sized to the host CPU count. Because Hindsight already parallelizes across requests via its own thread-pool executors, those native pools oversubscribe the CPU — on a many-core host the process can accumulate well over 100 native threads. This inflates memory and, under contention, can degrade throughput. Hindsight therefore bounds each native pool to **16 threads** (or the number of *available* CPUs, whichever is smaller) by default, capping runaway growth on large hosts while leaving within-call parallelism intact.
"Available" CPUs is the budget actually granted to the process — the smallest of the CPU-affinity set, the cgroup CPU quota (`--cpus` / cpuset), and the host core count. This matters in containers: the BLAS libraries otherwise size their pools to the *host's* cores even when the container is limited to a few, so a `--cpus=4` container on a 64-core host would spawn far more BLAS threads than it can run. The quota is read from the process's own cgroup and its parents, so a systemd unit's `CPUQuota=` counts too, not only a container's limit. The startup log prints the resulting count and the quota found (`Native threads: ...`).
ONNX Runtime (the FlashRank and `onnx` rerankers, ONNX embeddings) does not read `OMP_NUM_THREADS`, so Hindsight passes the same count to every ONNX session directly, with one inter-op thread and busy-waiting turned off.
To tune, set any of these to the desired thread count; the value you set is always honored (the default applies only when the variable is unset):
| Variable | Description | Default |
|----------|-------------|---------|
| `OMP_NUM_THREADS` | OpenMP worker threads (torch, some BLAS builds); also the thread count given to each ONNX Runtime session | `min(16, available CPUs)` |
| `OPENBLAS_NUM_THREADS` | OpenBLAS worker threads (numpy's default BLAS) | `min(16, available CPUs)` |
| `MKL_NUM_THREADS` | Intel MKL worker threads (numpy/torch when MKL-backed) | `min(16, available CPUs)` |
| `NUMEXPR_NUM_THREADS` | numexpr expression-engine threads | `min(16, available CPUs)` |
For a server handling many concurrent requests, lower values (down to `1`) favor request-level parallelism and minimize thread count; for low-concurrency deployments running large local-model batches, the default leaves room for within-call parallelism. These must be set in the process environment before startup (the libraries read them once, at load time), so they cannot be overridden per-tenant or per-bank.
### Webhooks
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_WEBHOOK_URL` | Global webhook URL for event delivery | - (disabled) |
| `HINDSIGHT_API_WEBHOOK_SECRET` | HMAC signing secret for webhook payloads | - (unsigned) |
| `HINDSIGHT_API_WEBHOOK_EVENT_TYPES` | Comma-separated list of event types to deliver via webhook | `consolidation.completed` |
| `HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS` | Comma-separated hosts or IP/CIDR ranges permitted as webhook destinations in addition to public addresses. Private, loopback, and link-local ranges (including the cloud metadata address) are blocked unless listed here. | - (public only) |
| `HINDSIGHT_API_WEBHOOK_EXPOSE_RESPONSE_BODY` | Return the raw upstream response body in the delivery-history API. Off by default to avoid exposing internal response contents; the delivery status code is always returned. | `false` |
> **⚠️ Webhook destinations are SSRF-guarded**
>
Because webhook URLs are caller-supplied, the delivery worker refuses to send to private, loopback, or link-local addresses (e.g. `169.254.169.254`, `127.0.0.1`, `10.0.0.0/8`) by default, and it does not return upstream response bodies to callers. Use `HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS` to allow specific internal destinations (such as `127.0.0.1` for local testing), and `HINDSIGHT_API_WEBHOOK_EXPOSE_RESPONSE_BODY=true` only if you trust those destinations and need the response body for debugging.
### Audit Logging
Audit logging captures mutating operations (retain, recall, reflect, bank config updates, [Memory Defense](memory-defense/index.md) redact/block actions, etc.) into an `audit_log` table, queryable via the `/audit-logs` endpoint.
**Audit logging is disabled by default.** With `HINDSIGHT_API_AUDIT_LOG_ENABLED=false`, the `audit_log` table stays empty and `/audit-logs` returns `{"total": 0, "items": []}` regardless of activity. Set the flag to `true` and restart the API to start capturing events.
**Auditing can be turned on or off for individual banks.** `audit_log_enabled` is hierarchical — overridable per bank via the [config API](#hierarchical-configuration) or the bank's **Configuration → Audit Logging** toggle in the control plane. The override works in both directions: a bank can opt *in* while the server default is `false`, or opt *out* while the default is `true`. Banks with no override follow the server default. Unlike the env var, a per-bank change takes effect immediately — no restart.
The actions allowlist and retention window stay server-level: retention is a global sweep across all tenants with no bank in scope.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_AUDIT_LOG_ENABLED` | Whether audit events are written. Hierarchical — can be overridden per bank via the config API. | `false` |
| `HINDSIGHT_API_AUDIT_LOG_ACTIONS` | Comma-separated allowlist of action types to audit (empty = all eligible actions) | `""` |
| `HINDSIGHT_API_AUDIT_LOG_RETENTION_DAYS` | Number of days to retain audit log entries. `-1` = keep forever. | `-1` |
### LLM Request Tracing
LLM request tracing records every LLM call Hindsight makes — for retain, reflect, and consolidation — into an `llm_requests` table, queryable per bank via the `/llm-requests` endpoint. Each row captures the input messages, the model output, token usage (input / output / cached / total, taken from the provider response), finish reason, provider/model, timing, and caller metadata. **Failed calls are recorded too** (`status = "error"` with the error message), so the table is useful for debugging what the LLM is doing and why a call failed. Capture is wired into the OpenTelemetry GenAI recording path (the same `record_llm_call` hook used for OTLP span export), so it stays consistent with the provider-reported request details.
**LLM request tracing is enabled by default**, with traced rows retained for 1 day. To disable it entirely set `HINDSIGHT_API_LLM_TRACE_ENABLED=false` and restart the API — the `llm_requests` table then stays empty and `/llm-requests` returns `{"total": 0, "items": []}` regardless of activity.
> **Note:** Traced rows contain the full prompt and model output, which may include sensitive memory content and can be large. Use `HINDSIGHT_API_LLM_TRACE_MAX_CHARS` to bound how much of each payload is stored, tighten `HINDSIGHT_API_LLM_TRACE_RETENTION_DAYS`, or set `HINDSIGHT_API_LLM_TRACE_ENABLED=false` to turn tracing off in sensitive environments.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLM_TRACE_ENABLED` | Master switch for LLM request tracing. Must be `true` for any calls to be recorded. | `true` |
| `HINDSIGHT_API_LLM_TRACE_SCOPES` | Comma-separated allowlist of call scopes to trace (e.g. `retain_extract_facts,reflect`; empty = all scopes) | `""` |
| `HINDSIGHT_API_LLM_TRACE_RETENTION_DAYS` | Number of days to retain trace rows. `-1` = keep forever. | `1` |
| `HINDSIGHT_API_LLM_TRACE_MAX_CHARS` | Truncate stored input/output beyond this many characters (keeps the row, stores a truncated preview). | `50000` |
### Background Maintenance {#background-maintenance}
A single periodic loop in every API/worker process drives Hindsight's recurring
housekeeping: the `audit_log` / `llm_requests` retention sweeps, terminal
operation cleanup, the consolidation reconcile, and the cron-scheduled mental
model refresh.
The loop runs in **every** process with no leader election, which is safe by
construction — the sweeps are idempotent deletes, and the two jobs that enqueue
work dedupe against in-flight operations inside the inserting transaction. What
it is not is free at scale: each job starts with a cross-tenant discovery call
that probes every schema holding the relevant table, and every process pays that
probe. The cost scales with tenant count while the work it finds does not, so on
a large multi-tenant deployment **the cadences are the knob that matters**.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RETENTION_SWEEP_INTERVAL_SECONDS` | How often expired `audit_log` and `llm_requests` rows are deleted across all tenant schemas. Retention windows are measured in days, so this only affects how promptly expired rows disappear. `0` disables the sweeps. | `3600` |
| `HINDSIGHT_API_OPERATION_CLEANUP_INTERVAL_SECONDS` | How often expired terminal operation rows are pruned. See [Distributed Workers](#distributed-workers) for the retention window and batch size. `0` disables the job. | `900` |
| `HINDSIGHT_API_MAINTENANCE_START_JITTER_SECONDS` | Upper bound on a random delay applied before a process runs its **first** maintenance tick. Every job is due on that first tick, so without an offset a fleet started together — a deploy or rolling restart — runs every sweep in every process at the same instant. `0` disables the jitter for a deterministic start. | `60` |
> **💡 Tuning for many tenants**
>
At a few hundred tenants the defaults are irrelevant. At tens of thousands, the
per-tick discovery scan dominates, and the jobs whose retention is counted in
*days* have no reason to probe every schema every minute. Raise
`HINDSIGHT_API_OPERATION_CLEANUP_INTERVAL_SECONDS` and
`HINDSIGHT_API_MENTAL_MODEL_REFRESH_TICK_SECONDS` first — they are the two most
frequent sweeps — and keep the jitter enabled so restarts don't align the fleet.
### Programmatic Configuration
You can also configure the API programmatically using `MemoryEngine.from_env()`:
```python
from hindsight_api import MemoryEngine
memory = MemoryEngine.from_env()
await memory.initialize()
```
---
## Observability & Tracing
Hindsight provides OpenTelemetry-based observability for LLM calls, conforming to GenAI semantic conventions.
### OpenTelemetry Tracing
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_OTEL_TRACES_ENABLED` | Enable distributed tracing for LLM calls | `false` |
| `HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL (e.g., Grafana LGTM, Langfuse, etc.) | - |
| `HINDSIGHT_API_OTEL_EXPORTER_OTLP_HEADERS` | Headers for OTLP exporter (format: "key1=value1,key2=value2") | - |
| `HINDSIGHT_API_OTEL_SERVICE_NAME` | Service name for traces. Applies to the API and to standalone workers, which default to `hindsight-worker` when it is unset. | `hindsight-api` |
| `HINDSIGHT_API_OTEL_DEPLOYMENT_ENVIRONMENT` | Deployment environment name (e.g., development, staging, production) | `development` |
| `HINDSIGHT_API_METRICS_INCLUDE_BANK_ID` | Include `bank_id` in OTel metric attributes. Enable only for deployments with few banks — high cardinality causes unbounded memory growth. | `false` |
| `HINDSIGHT_API_METRICS_INCLUDE_TENANT` | Include a per-tenant (`tenant`, the schema) label on OTel metrics. Enable only for deployments with few tenants — one series set per tenant, multiplied through every histogram bucket, causes unbounded memory growth. | `false` |
| `HINDSIGHT_API_RECALL_DIAGNOSTIC_PHASES` | Record the diagnostic recall phases (`diagnostic="true"` on `hindsight.recall.phase.duration`) — subsets of other phases, useful only while diagnosing. Disable to cut instrument overhead on a busy recall path. | `true` |
| `HINDSIGHT_API_RECALL_PHASE_SAMPLE_EVERY` | Record 1 in N `hindsight.recall.phase.duration` observations. Each observation is sampled independently, so percentiles stay unbiased and only the histogram counts scale by 1/N. Raise it (e.g. `10`) to cut metrics CPU at high recall rates. | `1` |
| `HINDSIGHT_API_METRICS_BACKLOG_ENABLED` | Expose async-operation queue depth and consolidation-backlog gauges (`hindsight_async_operations`, `hindsight_consolidation_backlog`, `hindsight_consolidation_failed`). Runs periodic per-schema `COUNT` queries on a background task. | `false` |
| `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` | Comma-separated URL patterns excluded from request tracing | `health,metrics` |
**Features:**
- Continues the caller's trace when a `traceparent` header is present
- Full prompts and completions recorded as events
- Token usage tracking (input/output)
- Model and provider information
- Error tracking with finish reasons
- Conforms to OpenTelemetry GenAI semantic conventions v1.37+
**OTLP-Compatible Backends:**
The tracing implementation uses standard OTLP HTTP protocol, so it works with any OTLP-compatible backend:
- **Grafana LGTM** (Recommended for local dev): All-in-one stack with Tempo traces, Loki logs, Mimir metrics, and Grafana UI
- **Langfuse**: LLM-focused observability and analytics
- **OpenLIT**: Built-in LLM dashboards, cost tracking
- **DataDog, New Relic, Honeycomb**: Commercial platforms
**Example Configuration:**
```bash
# Enable tracing
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
# Configure endpoint (example: OpenLIT Cloud)
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.openlit.io
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer olit-xxx"
# Optional: Custom service name and environment
export HINDSIGHT_API_OTEL_SERVICE_NAME=hindsight-production
export HINDSIGHT_API_OTEL_DEPLOYMENT_ENVIRONMENT=production
```
**Local Development:**
For local development, we recommend the Grafana LGTM stack which provides traces, metrics, and logs in a single container:
```bash
./scripts/dev/start-grafana.sh
```
See `scripts/dev/grafana/README.md` for detailed setup instructions.
Other options: See `scripts/dev/openlit/README.md` for OpenLIT or `scripts/dev/jaeger/README.md` for standalone Jaeger.
### Runtime-Stall Diagnostics
The API and worker run the `/health` handler and all task work on a single asyncio
event loop, and `/health` acquires a database connection. So a liveness probe can
fail for two very different reasons: the **event loop is blocked** by synchronous
work (a restart helps), or the **connection pool is exhausted** and `/health` can't
get a connection even though the loop is idle (a restart usually doesn't help). These
diagnostics tell the two apart from the logs and metrics alone, instead of leaving
you with an opaque restart.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LOOP_WATCHDOG_ENABLED` | Run a background thread that detects event-loop stalls and logs the blocking stack. Also emits `hindsight_event_loop_stalls` / `hindsight_event_loop_stall_duration`. | `true` |
| `HINDSIGHT_API_LOOP_WATCHDOG_STALL_THRESHOLD_MS` | Log a stall once the loop is unresponsive for at least this long. | `1000` |
| `HINDSIGHT_API_LOOP_WATCHDOG_POLL_INTERVAL_MS` | How often the watchdog thread pings the loop. | `250` |
| `HINDSIGHT_API_DB_ACQUIRE_WARN_THRESHOLD_MS` | Log a warning (with pool stats) when acquiring a pooled connection waits at least this long. | `1000` |
The DB-pool acquire path also exposes `hindsight_db_pool_waiting` (callers currently
queued for a connection) and the `hindsight_db_pool_acquire_wait` histogram. A slow
or failing `/health` response additionally carries `db_acquire_ms`, `db_pool_waiting`,
`db_pool_in_use`, and `db_pool_max` for triage.
### CPU Profiling
The loop watchdog above answers "is the loop blocked?". This answers the next question --
*what is burning the CPU?* -- for a process you may not be able to attach a debugger to.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_PROFILE` | JSON object; unset disables profiling entirely. `{"every": 60, "top": 20}` | unset |
`every` is the seconds between reports, `top` the rows in each. Set it and the API emits a
CPU report to its normal log stream on that interval:
```
HINDSIGHT_API_PROFILE='{"every": 60, "top": 20}'
```
```
[profile] thread hindsight-loop- 1.41 cores
[profile] thread asyncio_ 0.37 cores
[profile] 1598 functions active in the last 60s
[profile] tottime=0.923 cumtime=1.833 ncalls=15842 <method 'encode' of 'builtins.CoreBPE' objects>
[profile] tottime=0.691 cumtime=7.039 ncalls=1511590 <built-in method builtins.isinstance>
```
Each row is one log record tagged `[profile]`, so `grep '\[profile\]'` over the logs
reconstructs the table.
**It reports to the logs on purpose.** The case this exists for is a process dying without
explanation. A file inside the container dies with the container unless a volume was mounted
in advance, and an HTTP endpoint needs a live process and a route to it -- which is exactly
what a crashing process does not offer. Container runtimes keep the previous container's
stdout (`kubectl logs --previous`), so the last report before a crash is still readable
afterwards. Each report is flushed as it is written, because a fatal signal takes buffered
output with it.
#### Reading the report
Every report begins with per-thread CPU read from `/proc`, and that table is the arbiter:
profiler overhead cannot distort it, so when the two disagree, `/proc` is right. It also
shows whether work is spread across event loops or concentrated on one.
Three things will mislead you otherwise:
- **`tottime` is a function's own CPU; `cumtime` includes everything it called.** A
coroutine high in `cumtime` may simply be awaiting. Ranking by `cumtime` once put an ASGI
middleware at 77% of loop CPU when removing it changed throughput by 13%.
- **cProfile roughly halves throughput and over-weights functions called very often.** Read
the ranking, not the milliseconds, and confirm anything load-bearing by changing one thing
and re-measuring.
- **Numbers are per-window.** Each report is the delta since the previous one, not a running
total since boot.
#### Limits
Profiling is process-wide and single-instance: since Python 3.12 the profiler is a global
monitoring tool, so nothing else in the process may profile at the same time (a second
attempt logs `tool 2 is already in use` and leaves profiling off). `py-spy` remains the
better tool when you can attach to the process; this exists for the cases where you cannot.
### Metrics
Hindsight exposes Prometheus metrics at the `/metrics` endpoint, including:
- LLM call duration and token usage
- Operation duration (retain/recall/reflect)
- HTTP request metrics
- Database connection pool metrics
Metrics are always enabled and available at `http://localhost:8888/metrics`.
---
## Control Plane
The Control Plane is the web UI for managing memory banks.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_CP_DATAPLANE_API_URL` | URL of the API service | `http://localhost:8888` |
| `HINDSIGHT_CP_DATAPLANE_API_KEY` | Bearer token the Control Plane sends as `Authorization: Bearer <key>` on every request to the API service. Required when the API service is auth-protected; omit for a public API. | *(none — no `Authorization` header sent)* |
| `HINDSIGHT_CP_ACCESS_KEY` | Access key to protect the Control Plane UI. When set, users must enter this key to log in. | *(none — auth disabled)* |
| `HINDSIGHT_CP_MAX_UPLOAD_SIZE` | Maximum size of a single file-upload request the Control Plane accepts before truncating it. Accepts a size string (`100mb`, `1gb`) or a number of bytes. Raise this to upload files larger than the default, and keep it in line with the API's `HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB`. | `100mb` |
| `NEXT_PUBLIC_BASE_PATH` | Base path for Control Plane UI when behind reverse proxy (e.g., `/hindsight`) | `""` (root) |
```bash
# Point Control Plane to a remote API service
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
# Authenticate to an auth-protected API service
export HINDSIGHT_CP_DATAPLANE_API_KEY=my-dataplane-bearer-token
# Protect the Control Plane with an access key
export HINDSIGHT_CP_ACCESS_KEY=my-secret-key
```
### Hierarchical Configuration
Hindsight supports per-bank configuration overrides through a hierarchical system: **Global (env vars) → Tenant → Bank**.
#### Type-Safe Config Access
To prevent accidentally using global defaults when bank-specific overrides exist, Hindsight enforces type-safe config access:
**In Application Code:**
```python
from hindsight_api.config import get_config
# ✅ Access static (infrastructure) fields
config = get_config()
host = config.host # OK - static field
port = config.port # OK - static field
# ❌ Attempting to access bank-configurable fields raises an error
chunk_size = config.retain_chunk_size # ConfigFieldAccessError!
```
**Error Message:**
```
ConfigFieldAccessError: Field 'retain_chunk_size' is bank-configurable and cannot
be accessed from global config. Use ConfigResolver.resolve_full_config(bank_id, context)
to get bank-specific config.
```
**For Bank-Specific Config:**
```python
# Internal code that needs bank-specific settings
from hindsight_api.config_resolver import ConfigResolver
# Resolve full config for a specific bank
config = await config_resolver.resolve_full_config(bank_id, request_context)
chunk_size = config.retain_chunk_size # ✅ Uses bank-specific value
```
This design prevents bugs where global defaults are used instead of bank overrides, making it impossible to make this mistake at compile/development time.
#### Security Model
Configuration fields are categorized for security:
1. **Configurable Fields** - Safe behavioral settings that can be customized per-bank:
- Retention: `retain_chunk_size`, `retain_structured_chunk_size`, `retain_extraction_mode`, `retain_mission`, `retain_custom_instructions`
- Observations: `enable_observations`, `enable_auto_consolidation`, `observations_mission`, `max_observations_per_scope`
- MCP access control: `mcp_enabled_tools`
2. **Credential Fields** - NEVER exposed or configurable via API:
- API keys: `*_api_key` (all LLM API keys)
- Infrastructure: `*_base_url` (all base URLs)
3. **Static Fields** - Server-level only, cannot be overridden:
- Infrastructure: `database_url`, `port`, `host`, `worker_count`
- Provider/Model selection: `llm_provider`, `llm_model` (requires presets - not yet implemented)
- Performance tuning: `llm_max_concurrent`, `llm_timeout`, retrieval settings, optimization flags
#### Concurrent Config Writes
Config writes to the same bank are serialized. Validation and persistence run as
one unit: the write locks the bank row, re-checks the update against the
overrides actually committed at that moment, and only then merges it. Two
requests can therefore never combine into a configuration that neither of them
validated — for example one raising `retain_chunk_size` while the other removes
the retain strategy that made the larger size legal.
There is no conflict status to handle and no retry to implement. A request that
loses the race is rejected with the same `400` validation error it would have
received had the two updates arrived one after the other; a request whose fields
are still valid against the newer state succeeds. Fields that no constraint spans
are merged independently, so unrelated concurrent updates all survive.
#### Enabling the API
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_BANK_CONFIG_API` | Allow clients to change per-bank configuration (`PATCH`/`DELETE .../config`). Reading a bank's config is always allowed. | `true` |
| `HINDSIGHT_API_ENABLE_BANK_LLM_HEALTH` | Enable the per-bank LLM connectivity probe (`POST /v1/default/banks/{bank_id}/health/llm`). It makes a real provider call, so it is **off by default** — enable it to expose the endpoint. Returns status only — never the provider/model/endpoint. | `false` |
| `HINDSIGHT_API_ENABLE_DRY_RUN_EXTRACT` | Enable the dry-run extraction preview endpoint (`POST /v1/default/banks/{bank_id}/memories/dry-run-extract`). Runs extraction only — makes a real LLM call but stores nothing. Set to `false` to remove the endpoint (returns `404`). | `true` |
| `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE` | Bank template manifest (JSON) applied automatically to every newly-created bank. See below. | _(unset)_ |
##### `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE`
Server-level default bank template. When set, the manifest is applied once
to every bank the server creates — triggered the first time a bank is
touched (via `PUT /v1/default/banks/{bank_id}`, `/import`, `/retain`, etc.).
The value is a JSON-encoded `BankTemplateManifest` with the same shape
accepted by `POST /v1/default/banks/{bank_id}/import` (see the `bank`,
`mental_models`, and `directives` sections in the Bank Templates API).
Precedence: fields set by the template become per-bank overrides, so they
take precedence over the equivalent `HINDSIGHT_API_*` env-var defaults
(e.g. `HINDSIGHT_API_RETAIN_EXTRACTION_MODE`). Users can still override
individual fields later via `PATCH /v1/default/banks/{bank_id}/config`;
the template is **not** re-applied on subsequent accesses, so explicit
overrides are never clobbered.
A malformed manifest (bad JSON, unknown version, schema errors) is logged
and ignored — bank creation still succeeds with plain defaults, so a
broken server-level setting cannot wedge all callers.
Example (compact, single-line JSON as required by env vars):
```bash
export HINDSIGHT_API_DEFAULT_BANK_TEMPLATE='{"version":"1","bank":{"reflect_mission":"Help support agents remember customer interactions.","retain_extraction_mode":"verbose","disposition_empathy":5},"directives":[{"name":"Be concise","content":"Always respond concisely.","priority":10}]}'
```
#### API Endpoints
- `GET /v1/default/banks/{bank_id}/config` - View resolved config (filtered by permissions)
- `PATCH /v1/default/banks/{bank_id}/config` - Update bank overrides (only allowed fields)
- `DELETE /v1/default/banks/{bank_id}/config` - Reset to defaults
#### Permission System
Tenant extensions can control which fields banks are allowed to modify via `get_allowed_config_fields()`:
```python
class CustomTenantExtension(TenantExtension):
async def get_allowed_config_fields(self, context, bank_id):
# Option 1: Allow all configurable fields
return None
# Option 2: Allow specific fields only
return {"retain_chunk_size", "retain_custom_instructions"}
# Option 3: Read-only (no modifications)
return set()
```
#### Examples
```bash
# Update retention settings for a bank
curl -X PATCH http://localhost:8888/v1/default/banks/config-demo-bank/config \
-H "Content-Type: application/json" \
-d '{
"updates": {
"retain_chunk_size": 4000,
"retain_extraction_mode": "custom",
"retain_custom_instructions": "Focus on technical details and implementation specifics"
}
}'
# Note: retain_extraction_mode must be "custom" to use retain_custom_instructions
# View resolved config (respects permissions)
curl http://localhost:8888/v1/default/banks/config-demo-bank/config
# Reset to defaults
curl -X DELETE http://localhost:8888/v1/default/banks/config-demo-bank/config
```
**Security Notes:**
- Credentials (API keys, base URLs) are never returned in responses
- Only configurable fields can be modified
- Responses are filtered by tenant permissions
- Attempting to set credentials returns 400 error
### Reverse Proxy / Subpath Deployment
To deploy Hindsight under a subpath (e.g., `example.com/hindsight/`):
1. Set both environment variables to the same path:
```bash
HINDSIGHT_API_BASE_PATH=/hindsight
NEXT_PUBLIC_BASE_PATH=/hindsight
```
2. Configure your reverse proxy to:
- Forward `/hindsight/*` requests to Hindsight
- Preserve the full path in forwarded requests
- Set appropriate proxy headers (X-Forwarded-Proto, X-Forwarded-For)
**Example: Nginx Configuration**
```nginx
location /hindsight/ {
proxy_pass http://localhost:8888/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
**Example: Traefik Configuration**
```yaml
http:
routers:
hindsight:
rule: "PathPrefix(`/hindsight`)"
service: hindsight
middlewares:
- hindsight-stripprefix
middlewares:
hindsight-stripprefix:
stripPrefix:
prefixes:
- "/hindsight"
services:
hindsight:
loadBalancer:
servers:
- url: "http://localhost:8888"
```
**Important Notes:**
- The base path must start with `/` and should NOT end with `/`
- Both API and Control Plane should use the same base path
- After setting environment variables, restart both services
- OpenAPI docs will be available at `<base-path>/docs` (e.g., `/hindsight/docs`)
**Complete Examples:**
See `docker/compose-examples/` directory for:
- Nginx configuration files (`simple.conf`, `api-and-control-plane.conf`)
- Docker Compose setups (`docker-compose.yml`, `reverse-proxy-only.yml`)
- Traefik and other reverse proxy examples
- Full deployment documentation
---
## Example .env File
```bash
# API Service
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public'
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
# Authentication (optional, recommended for production)
# HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
# HINDSIGHT_API_TENANT_API_KEY=your-secret-api-key
# File storage (optional, defaults to PostgreSQL native storage)
# HINDSIGHT_API_FILE_STORAGE_TYPE=s3
# HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-files
# HINDSIGHT_API_FILE_STORAGE_S3_REGION=us-east-1
# HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
# HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Control Plane
HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
```
---
For configuration issues not covered here, please [open an issue](https://github.com/vectorize-io/hindsight/issues) on GitHub.
references/developer/development.md
---
sidebar_position: 7
---
# Development Guide
Guide to setting up a local development environment for contributing to Hindsight.
## Prerequisites
- Python 3.11+
- [uv](https://docs.astral.sh/uv/) - Fast Python package manager
- Docker and Docker Compose
- An LLM provider credential, or a local/subscription-backed provider such as Ollama or GitHub Copilot
## Local Development Setup
### 1. Clone the Repository
```bash
git clone https://github.com/vectorize-io/hindsight.git
cd hindsight
```
### 2. Install Dependencies
```bash
uv sync
```
### 3. Start PostgreSQL
Start only the database via Docker:
```bash
cd docker && docker-compose up -d postgres
```
### 4. Configure Environment
```bash
cp .env.example .env
```
Edit `.env` with your LLM API key:
```bash
# Database (connects to Docker postgres)
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# LLM Provider (choose one)
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
HINDSIGHT_API_LLM_MODEL=llama-3.1-70b-versatile
```
### 5. Start the API Server
```bash
./scripts/start-server.sh --env local
```
The server will be available at http://localhost:8888.
## Running Tests
```bash
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_retrieval.py
# Run with verbose output
uv run pytest -v
```
## Code Generation
### Regenerate API Clients
When you modify the OpenAPI spec, regenerate the clients:
```bash
./scripts/generate-clients.sh
```
This generates:
- Python client in `hindsight-clients/python/`
- TypeScript client in `hindsight-clients/typescript/`
### Export OpenAPI Schema
```bash
./scripts/export-openapi.sh
```
## Project Structure
```
hindsight/
├── hindsight-api/ # Main API server
│ ├── hindsight_api/
│ │ ├── api/ # HTTP endpoints
│ │ ├── engine/ # Memory engine, retrieval, reasoning
│ │ └── web/ # Server entry point
│ └── tests/
├── hindsight-clients/ # Generated SDK clients
│ ├── python/
│ └── typescript/
├── hindsight-control-plane/ # Admin UI (Next.js)
├── docker/ # Docker Compose setup
└── scripts/ # Development scripts
```
## Contributing
Hindsight does not accept pull requests from outside the team. To report a bug or
ask for a feature, [open an issue](https://github.com/vectorize-io/hindsight/issues/new/choose).
See [CONTRIBUTING.md](https://github.com/vectorize-io/hindsight/blob/main/CONTRIBUTING.md)
for details.
## Troubleshooting
### Database Connection Issues
Ensure PostgreSQL is running:
```bash
docker-compose ps
```
Check database connectivity:
```bash
psql postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
```
### ML Model Download
On first run, Hindsight downloads embedding and reranking models. This may take a few minutes. Models are cached in `~/.cache/huggingface/`.
### Port Conflicts
If port 8888 is in use:
```bash
HINDSIGHT_API_PORT=8889 ./scripts/start-server.sh --env local
```
references/developer/extensions.md
# Extensions
Extensions allow you to customize and extend Hindsight behavior without modifying core code. They enable multi-tenancy, custom authentication, additional HTTP endpoints, and operation hooks.
---
## Available Extensions
### TenantExtension
Handles multi-tenancy and API key authentication. Validates incoming requests and determines which PostgreSQL schema to use for database operations, enabling tenant isolation at the database level.
**Built-in: ApiKeyTenantExtension**
A simple implementation that validates API keys against an environment variable and uses the `public` schema for all authenticated requests.
```bash
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
HINDSIGHT_API_TENANT_API_KEY=your-secret-key
```
**No longer built in: SupabaseTenantExtension**
Validates [Supabase](https://supabase.com) JWTs and gives each authenticated user their own PostgreSQL schema. It now lives in the [extensions registry](https://github.com/vectorize-io/hindsight/tree/main/hindsight-extensions/supabase-tenant), which documents its configuration and ships a Dockerfile that builds an image with it.
:::warning Breaking change in 0.9.3
Up to 0.9.2 this extension was built in, at `hindsight_api.extensions.builtin.supabase_tenant`. That path no longer exists, so an install still pointing at it fails at startup with `ModuleNotFoundError`. Add the extension to your image and set `HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension`. All `HINDSIGHT_API_TENANT_*` settings and the schema naming are unchanged.
:::
**External: StaticKeysTenantExtension**
A fully self-hosted multi-user mode: users and their API keys are declared in environment variables (no external identity provider, no users table). Each user maps to their own PostgreSQL schema (`{prefix}_{user_id}`), provisioned lazily on first access, giving database-level memory isolation between users. Multiple API keys may map to the same user and schema.
User IDs are case-insensitive: they are lowercased (and dashes normalized to underscores) before building the schema name, so `Rafael`, `rafael` and `RAFAEL` all resolve to the same tenant schema.
It lives in the [extensions registry](https://github.com/vectorize-io/hindsight/tree/main/hindsight-extensions/static-keys-tenant), which documents its configuration and ships a Dockerfile that builds an image with it.
For other multi-tenant setups with separate schemas per tenant (e.g., custom JWT-based auth), implement a custom `TenantExtension`.
---
### HttpExtension
Adds custom HTTP endpoints under the `/ext/` path prefix. Useful for adding domain-specific APIs that integrate with Hindsight's memory engine.
Provides two router methods:
- `get_router(memory)` — returns a FastAPI router mounted at `/ext/`
- `get_root_router(memory)` — returns a FastAPI router mounted at the application root (for well-known endpoints or other paths that must be at specific locations). Returns `None` by default.
**No built-in implementation** - implement your own to add custom endpoints.
```bash
HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtension
```
---
### OperationValidatorExtension
Hooks into retain/recall/reflect operations for validation and monitoring. Use cases include:
- Rate limiting and quota enforcement
- Permission checks and content filtering
- Audit logging and usage tracking
- Custom metrics collection
**No built-in implementation** - implement your own based on your requirements.
`self.context` is the process-wide extension context — use it for process-global handles such as `get_memory_engine()`. It holds no per-request state: take the tenant and bank from each hook's own argument (`ctx.bank_id` and `ctx.request_context`, the latter holding the identity resolved by the tenant extension).
```bash
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
```
---
### MCPExtension
Registers additional MCP (Model Context Protocol) tools on the Hindsight MCP server. Enables external packages to add custom tools without modifying core code.
**No built-in implementation** - implement your own to add custom MCP tools.
```bash
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtension
```
---
### LLMRequestExtension
Decides, per LLM call, which LLM the call goes to and with which credentials. Use it to:
- **Bill each customer on their own key.** Front your LLM spend with a gateway (LiteLLM proxy, Bifrost, OpenRouter, Portkey, ...) that gives each customer a virtual key, and send each bank's calls with its customer's key.
- **Pick the model per bank or per operation.** A cheap model for one bank's retain and a strong one for its reflect, a different provider or cloud account per customer.
It is asked once per LLM call with the tenant schema, the bank and the operation the call runs for — retain, reflect, consolidation, mental-model refresh — in the API process and in the worker alike. It answers with an `LLMRequestOverrides`, or `None` to use the server's own LLM. Raising refuses the call.
```bash
HINDSIGHT_API_LLM_REQUEST_EXTENSION=mypackage.llms:MyLLMs
```
An answer can carry:
| Field | Effect |
| --- | --- |
| `llm` | Send the call to this LLM instead of the server's: provider, model, base URL, key and the other per-LLM settings (`LLMMemberConfig`, the shape of a multi-LLM chain member). Fields left unset inherit the server's global LLM defaults, except its credentials (default headers, Vertex service account, Codex home, LiteLLM router config); timeouts and retries inherit the operation's. |
| `api_key` | The key for the LLM the call goes to (the server's, or `llm`'s — it fills an `llm` that has no key of its own). |
| `headers` | Extra request headers, e.g. a gateway's customer id. |
Things to know:
- **The choice is server-side.** Nothing comes from the API caller. If your extension builds an answer from something a caller can write (bank config, metadata), validate it — otherwise that is where an endpoint or a key would come from user input.
- **Credentials without `llm` cost no new client.** The key and headers go on a per-call copy of the existing SDK client that shares its connection pool. Supported on OpenAI-compatible endpoints (`openai`, `groq`, `deepseek`, `openrouter`, `fireworks`, `lmstudio`, any gateway set as `base_url`, ...), `anthropic` and `litellm` routed to a keyed backend (OpenAI, Anthropic, Azure, Gemini AI Studio, OpenRouter, a LiteLLM proxy, ...). A call to any other provider (`gemini`, `vertexai`, `ollama`, `bedrock`, `litellmrouter`, the OAuth/CLI providers) that the answer gives a key or headers to is **refused**, never sent on the configured credentials. To change the key of one of those, answer with a whole `llm` instead. A key is also refused when the server's default headers carry a credential (`Authorization`, `x-api-key`, `x-portkey-api-key`, ...) the answer does not override, since the SDK would send that one instead.
- **Asked once per LLM in a chain.** With a multi-LLM chain or a provider per operation, each LLM is asked separately; check `target.provider` before handing out a key meant for one of them.
- **Reflect prompt caching is off** while the extension is installed: Gemini's step-by-step reflect cache is created on the server's LLM, outside the calls the extension decides.
- **An `llm` answer is built once and reused**, up to 64 distinct LLMs per process (least recently used are dropped). An extension that answers with one LLM per user on a large user base should use a gateway and `api_key` instead.
- **No batch retain.** `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` with this extension fails at startup: batch jobs are submitted on the server's LLM.
- **LLM calls only.** Embeddings and reranking keep their own configuration.
- **Lifecycle:** `on_startup()` runs when the memory engine initializes, before any LLM call, and `on_shutdown()` when it closes.
- **Startup check:** the connection check at boot uses the server's own LLM. If it has no usable key (for example, the gateway has none for the server), set `HINDSIGHT_API_SKIP_LLM_VERIFICATION=true`.
Load the same extension in the API and in every worker: a worker without it runs consolidation and refreshes on the server's LLM.
The [`bank-llm-routing`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-extensions/bank-llm-routing) community extension routes on bank-id and operation patterns from configuration, and is a working example of the interface.
---
## Writing Custom Extensions
### Extension Basics
Extensions are Python classes loaded via environment variables:
```bash
HINDSIGHT_API_<TYPE>_EXTENSION=mypackage.module:MyExtensionClass
```
Configuration is passed via prefixed environment variables:
```bash
HINDSIGHT_API_<TYPE>_SOME_CONFIG=value
# Extension receives: {"some_config": "value"}
```
All extensions support lifecycle hooks:
- `on_startup()` - Called when the application starts
- `on_shutdown()` - Called when the application shuts down
Extensions have access to an `ExtensionContext` that provides:
- `run_migration(schema)` - Run database migrations for a schema
- `get_memory_engine()` - Get the MemoryEngine interface
### Shipping your own database migrations
An extension that keeps state of its own returns the directory holding its Alembic
revision files, and they are applied in the same migration run as Hindsight's own —
same ordering guarantees, same `alembic_version` table:
```python
class MyExtension(TenantExtension):
def alembic_version_locations(self) -> list[str]:
return [str(Path(__file__).parent / "alembic" / "versions")]
```
The directory holds revision files only — there is no `env.py`; Hindsight's own
configures the schema and the connection. The tree is independent of Hindsight's: give
its first revision `down_revision = None` and a `branch_labels` naming your extension,
so an operator can address it (`alembic upgrade <label>@head`).
Independent branches have no ordering between them. A revision that needs a Hindsight
table to exist first must say so explicitly, which orders it without making Hindsight's
revision its parent:
```python
depends_on = ("a1b2c3d4e5f6",) # a Hindsight revision id
```
A directory that does not exist is skipped with a warning rather than failing the
migration — a misconfigured extension must never leave a database unmigratable.
### Example: Custom TenantExtension with JWT
```python
import jwt
from hindsight_api.extensions import TenantExtension, TenantContext, AuthenticationError
class JwtTenantExtension(TenantExtension):
def __init__(self, config: dict[str, str]):
super().__init__(config)
self.jwt_secret = config.get("jwt_secret")
if not self.jwt_secret:
raise ValueError("HINDSIGHT_API_TENANT_JWT_SECRET is required")
async def authenticate(self, context: RequestContext) -> TenantContext:
token = context.api_key
if not token:
# Optional headers dict is forwarded in HTTP/MCP error responses
raise AuthenticationError("Bearer token required")
try:
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
tenant_id = payload.get("tenant_id")
if not tenant_id:
raise AuthenticationError("Missing tenant_id in token")
return TenantContext(schema_name=f"tenant_{tenant_id}")
except jwt.InvalidTokenError as e:
raise AuthenticationError(str(e))
```
`AuthenticationError` accepts an optional `headers` dict that is forwarded in both HTTP and MCP error responses. This is useful for returning custom headers like `WWW-Authenticate`:
```python
raise AuthenticationError(
"Authorization required",
headers={"WWW-Authenticate": 'Bearer realm="example"'},
)
```
### Reading additional request headers
`RequestContext` carries the `Authorization` header as `api_key`. To authenticate on a *different* header — for instance when a gateway terminates auth with one shared identity and forwards the per-caller identity separately — name the headers you want forwarded:
```bash
HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion
```
They are available as `context.extra_headers`, keyed by lower-cased name:
```python
async def authenticate(self, context: RequestContext) -> TenantContext:
assertion = context.extra_headers.get("x-user-assertion")
if not assertion:
raise AuthenticationError("x-user-assertion header required")
user_id = verify_assertion(assertion) # your verification
return TenantContext(schema_name=f"tenant_{user_id}")
```
This works on both the HTTP and MCP transports, and the same `RequestContext` is passed to `OperationValidatorExtension` hooks, so a validator can enforce rules against the identity resolved here.
Only headers you list are forwarded, and only when present on the request. The variable is unset by default, so extensions see no header data unless you opt in.
A header sent **more than once** is not forwarded at all, and a warning is logged. There is no safe way to choose between the copies — a proxy may append its trusted value either before or after a client-supplied one — so an extension reading it sees nothing and fails the request, rather than silently accepting a value that may be spoofed. Make sure your proxy *replaces* the identity header it injects instead of appending to it.
:::caution Deferred operations
`extra_headers` describes the request being served. Operations that run later — a queued retain, a scheduled consolidation, a mental-model refresh — are executed by a background worker with no request behind them, so their `RequestContext` carries no headers. Authorize on the header at request time; do not rely on it inside work that continues after the response.
:::
### Example: Custom HttpExtension
```python
from fastapi import APIRouter
from hindsight_api.extensions import HttpExtension
class MyHttpExtension(HttpExtension):
def get_router(self, memory: MemoryEngine) -> APIRouter:
router = APIRouter()
@router.get("/hello")
async def hello():
return {"message": "Hello from extension!"}
@router.post("/custom/{bank_id}/action")
async def custom_action(bank_id: str):
# Access memory engine for database operations
pool = await memory._get_pool()
# ... custom logic
return {"status": "ok"}
return router
def get_root_router(self, memory: MemoryEngine) -> APIRouter | None:
"""Optional: mount routes at the application root (not under /ext/)."""
router = APIRouter()
@router.get("/.well-known/my-metadata")
async def metadata():
return {"version": "1.0"}
return router
```
Routes from `get_router` are available at `/ext/hello`, `/ext/custom/{bank_id}/action`, etc.
Routes from `get_root_router` are mounted at the app root (e.g., `/.well-known/my-metadata`).
### Example: Custom OperationValidatorExtension
```python
from hindsight_api.extensions import (
OperationValidatorExtension,
ValidationResult,
PrecheckContext,
RetainContext,
RecallContext,
ReflectContext,
RetainResult,
)
class MyValidator(OperationValidatorExtension):
# Pre-body validation (optional)
async def precheck(self, ctx: PrecheckContext) -> ValidationResult:
if ctx.content_length is not None and ctx.content_length > 10_000_000:
return ValidationResult.reject("Payload is too large")
return ValidationResult.accept()
# Pre-operation validation (required)
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
# Implement your validation logic
return ValidationResult.accept()
# Or reject: return ValidationResult.reject("Reason")
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
return ValidationResult.accept()
# Post-operation hooks (optional)
async def on_retain_complete(self, result: RetainResult) -> None:
# Log usage, update metrics, send notifications, etc.
pass
```
`precheck` runs before the request body is read or deserialized. Its
`PrecheckContext.content_length` is the parsed `Content-Length` header as an
integer, or `None` when the header is missing or cannot be parsed (for example,
chunked transfer encoding). Use it for cheap size-aware quota or cost guards;
the full `validate_*` hooks still run after parsing and should enforce precise
per-operation limits.
#### Memory curation hooks
Curating a memory (`PATCH /v1/default/banks/{bank_id}/memories/{memory_id}`:
edit, invalidate, or revert) has its own pair of hooks, in addition to the
`validate_bank_write` access check that runs first:
- `validate_memory_update(ctx: MemoryUpdateContext)` runs before any work. The
context carries the requested `text` (when editing it), the requested `state`,
and `edits_fields`. Reject here to refuse the curation; the returned
`status_code` is passed through to the HTTP response.
- `on_memory_update_complete(result: MemoryUpdateResult)` runs once the change
has committed. `result.action` is `edit`, `invalidate`, `revert`, or `reason`,
and `result.reembedded_tokens` is the size of the text the engine embedded
again (0 for a plain invalidation or a reason-only update). An edit or revert
re-embeds the memory and re-consolidates the bank, so this is the figure to
meter if curation should cost the same as ingesting that text.
#### Transfer-archive import hooks
Importing a transfer archive (a document import, or a whole-bank restore) brings
pre-extracted facts into a bank without running fact extraction:
- `validate_import(ctx: ImportContext)` runs at submission, before the archive
is stored or queued. `ctx.documents` lists the archive's documents
(`ImportArchiveDocument`: `document_id` and the original `content`), so a validator can size the import
for a quota or cost check. A rejection reaches the caller directly. The
default accepts.
- Each imported document is then reported to `on_retain_complete` with
`result.source == RetainSource.IMPORT`, zero LLM token counts, and
`processed_content_tokens=0`. Use `source` to tell an import apart from a
re-retain whose chunks were all deduplicated, which also reports
`processed_content_tokens=0`; ordinary retains report `RetainSource.RETAIN`.
Cloning a bank within the same instance is not an import: it is neither gated
by `validate_import` nor reported to `on_retain_complete`.
#### Confining a caller to a tag scope
When several people share one bank and tags separate what each may see,
`resolve_tag_scope` pins every operation of a caller to a tag filter. Return
the tag groups the caller's reachable data must satisfy (they are AND-ed), or
`None` for no restriction:
```python
from hindsight_api.engine.search.tags import TagGroupLeaf
from hindsight_api.extensions import OperationValidatorExtension, TagScopeContext
class TeamScopes(OperationValidatorExtension):
async def resolve_tag_scope(self, ctx: TagScopeContext):
user = ctx.request_context.api_key_id # or whatever your tenant extension resolved
# Dan reads his own memories plus the shared team rules.
return [TagGroupLeaf(tags=[f"user:{user}", "kind:rule"], match="any_strict")]
```
The engine combines the scope with whatever filter the caller sends, so a
caller can narrow its view but never widen it:
- **Filtered reads** — recall, reflect (and every tool it runs), the memory,
document and mental-model lists, observation scopes, tag lists, the memory
graph, the timeseries, entities and the entity graph, and the knowledge-base
tree, search and export — only see rows inside the scope. Entity counts are
recounted over the visible memories.
- **Reads and writes by id** — a memory, a document and its chunks, a mental
model or knowledge page — answer `404` when the item is outside the scope.
A retain that names an existing document outside the scope is refused (`403`),
since replacing or appending to it would touch someone else's text.
- **Source text follows the document.** A memory can be visible through its own
tags (say, a `kind:rule` fact extracted from someone's private note) while the
note is not. Its chunk and document text are only returned when the
*document's* tags pass the filter. This applies to every tag-filtered recall
and reflect, with or without an extension.
- **Mental models and knowledge pages** created or updated by a scoped caller
must carry tags inside the scope (`403` otherwise — the caller could not see
what it made), and record the scope in their trigger (`scope_tag_groups`),
which every refresh AND-s with the model's own filter, so they can never be
built from memories their creator could not read. Background refreshes run
unscoped and rely on that recorded scope. A recorded scope only ever narrows
the model and is not editable through the API: to drop it, recreate the model.
Use a `_strict` match mode: the non-strict ones also admit untagged rows, and
an untagged mental model is built from the whole bank.
**Writing is a separate permission.** A caller can read a shared scope without
being allowed to change it. `resolve_write_tag_scope` returns the tags a caller
may write, as shell-style patterns (`user:dan`, `project:*`), or `None` for no
restriction (the default):
```python
class TeamScopes(OperationValidatorExtension):
async def resolve_write_tag_scope(self, ctx: TagScopeContext):
user = ctx.request_context.api_key_id
# Everyone writes their own memories; only Kate writes the team rules.
return [f"user:{user}", "kind:rule"] if user == "kate" else [f"user:{user}"]
```
Every tag on anything the caller writes or changes must match one of the
patterns, otherwise the write is refused with `403` (an item the caller cannot
even read still answers `404`). An untagged item belongs to everyone, so a
restricted writer cannot produce or change one. That covers:
- **retain** (text and files): each item's tags and the batch's `document_tags`,
plus every tag the retain strategy's [entity labels](./api/memory-banks.md#entity-labels)
with `tag: true` could add (`key:value` for each allowed value, `key:*` for an
open vocabulary), and explicit `observation_scopes` (`"shared"` writes untagged
observations). They are checked before extraction, so a refused retain costs
no LLM call;
- **memories and documents**: editing, invalidating or clearing the observations
of a memory; updating (including the new tags), reprocessing or deleting a
document;
- **mental models and knowledge pages**: creating one (its tags), updating,
refreshing, clearing or deleting one, and renaming, moving or deleting a
knowledge node (every page under it, and under the folder it moves into);
- **directives**: creating, updating or deleting one. An untagged directive
steers everyone's reflect, so a restricted writer cannot create one.
Directives are read like reflect reads them: untagged directives apply to
everyone, tagged ones only inside the caller's read scope.
Whole-bank operations reach every memory regardless of tags, so a caller with a
read or write scope cannot run them at all (`403`): export, clone and import,
clearing the bank's memories or observations, deleting the bank, changing or
resetting its config, mission or disposition, and running (or retrying failed)
consolidation on request. The consolidation the engine queues after a scoped
caller's own writes still runs. Operation status never returns the raw task payload to a
scoped caller. Reads that are not tag-scoped (stats, webhooks) still go through
`validate_bank_read` / `validate_bank_write`, which can only allow or deny them.
#### Deferring an operation
In addition to `accept` and `reject`, a `validate_*` hook can ask the
worker to **requeue** the operation for a future time by raising
`DeferOperation`. Use this for backpressure (rate-limited upstream,
quota window not yet open, dependency warming up) — unlike a retry, it
does not increment `retry_count` or write `error_message`. The worker
sets `next_retry_at` to your `exec_date` and the task is invisible to
claim queries until that time.
```python
from datetime import datetime, timedelta, timezone
from hindsight_api.extensions import (
DeferOperation,
OperationValidatorExtension,
RetainContext,
ValidationResult,
)
class QuotaAwareValidator(OperationValidatorExtension):
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
if not await self._quota_available(ctx.bank_id):
raise DeferOperation(
exec_date=datetime.now(timezone.utc) + timedelta(minutes=5),
reason="bank quota window exhausted",
)
return ValidationResult.accept()
```
`DeferOperation` is **worker-only**: do not raise it from
`validate_recall` or `validate_reflect` in synchronous HTTP request
paths — there is no queue to defer to and it will surface as a 500.
### Example: Custom MCPExtension
```python
from mcp.server.fastmcp import FastMCP
from hindsight_api.extensions import MCPExtension
from hindsight_api.engine import MemoryEngine
class MyMCPExtension(MCPExtension):
async def register_tools(self, mcp: FastMCP, memory: MemoryEngine) -> None:
@mcp.tool()
async def custom_search(query: str) -> str:
"""Custom MCP tool for specialized search."""
# Access memory engine for operations
pool = await memory._get_pool()
# ... custom logic
return f"Results for: {query}"
```
### Example: Custom LLMRequestExtension
```python
from hindsight_api.config import LLMMemberConfig
from hindsight_api.extensions import (
LLMRequestExtension,
LLMRequestOverrides,
LLMRequestRefused,
LLMRequestTarget,
)
class PerCustomerLLMs(LLMRequestExtension):
def __init__(self, config: dict[str, str]) -> None:
super().__init__(config)
self.keys = load_keys(config["keys_file"]) # your own store: {(schema, bank_id): key}
async def resolve(self, target: LLMRequestTarget) -> LLMRequestOverrides | None:
key = self.keys.get((target.schema, target.bank_id))
if key is None:
# Refuse rather than return None: None sends the call on the server's own LLM.
raise LLMRequestRefused(f"no LLM key for bank {target.bank_id!r}")
if target.operation == "reflect":
# A stronger model for reflect, on the customer's own account.
return LLMRequestOverrides(llm=LLMMemberConfig(provider="anthropic", model="claude-sonnet-5-5", api_key=key))
return LLMRequestOverrides(api_key=key, headers={"x-customer": target.bank_id})
```
`resolve` runs on every LLM call, so cache anything slow. Bank ids are unique per tenant schema only — a multi-tenant deployment must key on `target.schema` too, or one tenant can name a bank after another tenant's and be billed to its key. `LLMRequestOverrides` keeps the LLM and key out of its `repr`; keep them out of your own logs as well.
---
## Deploying Custom Extensions
### With Docker
Extensions are not bundled in the image. Build one on top of Hindsight that installs your extension's dependencies and copies it in. Install into the image's virtualenv explicitly — it was created by `uv sync` and ships no `pip` of its own, so a bare `pip install` lands where the server can't see it:
```dockerfile
FROM ghcr.io/vectorize-io/hindsight:latest
RUN uv pip install --python /app/api/.venv/bin/python --no-cache \
'PyJWT[crypto]>=2.12.0' 'httpx>=0.27.0'
COPY my_extension /app/extensions/my_extension
ENV PYTHONPATH=/app/extensions
# Fail the build, not the first request, if it isn't importable.
RUN /app/api/.venv/bin/python -c "import my_extension"
```
Then point the service at that image and pass the extension's variables as environment. Give the API and worker containers the same extension configuration — the worker uses the tenant extension to enumerate schemas for background consolidation.
See the [extensions registry README](https://github.com/vectorize-io/hindsight/blob/main/hindsight-extensions/README.md#docker-packaging) for the full recipe.
### Bare Metal
Install your extension package in the same Python environment as Hindsight:
```bash
# Install Hindsight
pip install hindsight-api
# Install your extension package
pip install ./my-extensions
# or
pip install my-extensions-package
# Configure
export HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
export HINDSIGHT_API_TENANT_JWT_SECRET=your-secret
# Run
hindsight-api
```
---
## Contributing Extensions
If you've built an extension that solves a common use case, for example:
- Authentication providers (OAuth, SAML, API gateways)
- Rate limiting or quota management
- Audit logging integrations
- Metrics exporters (Datadog, New Relic, etc.)
- Custom HTTP endpoints for specific platforms
[open a feature request](https://github.com/vectorize-io/hindsight/issues/new?template=feature_request.yml) with a link to your repository, and we can list it in the [extensions registry](https://github.com/vectorize-io/hindsight/blob/main/hindsight-extensions/README.md). Hindsight does not accept pull requests from outside the team, so the team adds registry entries. That README covers the extension layout, the development workflow, and Docker packaging.
Extensions live outside the server so that installing Hindsight does not pull in a vendor's client library, and so changing an extension does not require a Hindsight release. Only extensions that add no dependencies and are useful to any deployment (`ApiKeyTenantExtension`, `MemoryDefenseRegexExtension`) stay in `hindsight_api.extensions.builtin`.
references/developer/index.md
{/* The page's own `# Overview` H1 is suppressed via hide_title: this is the site
root, and the hero's headline is the H1 a visitor and a crawler should see.
The sidebar and page metadata still read "Overview" from the frontmatter. */}
<HomeHero />
## Benchmarks
<HomeBenchmarks />
{/* Retrieval accuracy only. The coding-agent chart opens inside the coding-agents
card above, where it is evidence for a path the reader has just been shown,
and is explained in full on /sdks/integrations/coding-agents. */}
## Why Hindsight?
AI agents forget everything between sessions. Every conversation starts from zero—no context about who you are, what you've discussed, or what the assistant has learned. This isn't just an implementation detail; it fundamentally limits what AI Agents can do.
**The problem is harder than it looks:**
- **Simple vector search isn't enough** — "What did Alice do last spring?" requires temporal reasoning, not just semantic similarity
- **Facts get disconnected** — Knowing "Alice works at Google" and "Google is in Mountain View" should let you answer "Where does Alice work?" even if you never stored that directly
- **AI Agents need to consolidate knowledge** — A coding assistant that remembers "the user prefers functional programming" should consolidate this into an observation and weigh it when making recommendations
- **Context matters** — The same information means different things to different memory banks with different personalities
Hindsight solves these problems with a memory system designed specifically for AI agents.
{/* Rendered here rather than by the DocBreadcrumbs wrapper (which serves every
other page): at the top it sat between the navbar and the hero band as a
bordered strip and broke the full-bleed, and it lands better as the answer
to the problem the section above just described. */}
<SkillBanner />
## How It Works
{/* The router: three kinds of visitor land on this page — one who has never
heard of agent memory, one looking for the coding-agent plugin, one looking
for the Hermes/OpenClaw plugin — and this is where each of them can see
their own path. It opens "How It Works" because the cards answer "what does
this look like for me" before the sections below answer "how". */}
<HomeFlow />
### Memory Types
Hindsight does not store conversations. It extracts what was said into typed
facts and then builds on them:
- **World fact** — an objective claim it was told. *"Alice works at Google."*
- **Experience fact** — something the bank itself did. *"I recommended Python to Bob."*
- **Observation** — a belief consolidated from many facts, with its evidence and
its history. *"User was a React enthusiast, has now switched to Vue."*
- **Mental model** — a curated summary you write for a question you ask often.
- **Knowledge page** — a living document the bank writes about itself.
Facts are not a list. Each is linked to the entities it mentions and to the other
facts that share them, which is what makes "where does Alice work?" answerable
from two facts that were never stored together — the graph at the top of this
page is a sample bank's; hover a memory to see what it links to.
### Multi-Strategy Retrieval
`recall()` runs four searches **in parallel**, because no single one handles
every question:
- **Semantic** — meaning rather than wording, so "Alice's job" finds "Alice works as a software engineer"
- **Keyword (BM25)** — the names and technical terms an embedding blurs together; five pluggable Postgres backends, including one that works on a Citus cluster
- **Graph** — entity links, so a fact reachable in two hops comes back even when it shares no words with the query
- **Temporal** — time expressions parsed into a window, then filled by relevance *and spread across the range*, so "what happened in 2023?" is not all from December
Then the part that matters more than any single arm:
- **Fused by rank, not score** — a memory several strategies agree on wins, and no arm's scoring scale can dominate the others
- **Re-ranked by a cross-encoder** that reads the query and the memory together
- **Cut to a token budget, not a top-k** — you say how much context you can afford and Hindsight fills it, because agents budget in tokens and not in result counts
See [Recall](retrieval.md) for the strategies in full.
### Observation Consolidation
A background worker keeps turning raw facts into durable beliefs:
- **Deduplicated** — overlapping facts merge into one observation instead of piling up as repeats
- **Evidence-grounded** — each observation points at the memories that support it, with exact quotes and a proof count
- **Refined, not overwritten** — new evidence updates an observation and its history is kept, so you can see a belief change
- **Freshness-aware** — when memories have landed but not yet been consolidated, `reflect` treats the affected observations as stale and checks them against raw facts before trusting them
### Knowledge Pages
Observations answer one question at a time. A **knowledge page** is a living
document the bank writes about itself — "What are the components here?", "What's
our error-handling convention?" — rewritten incrementally as consolidation
produces new knowledge in its scope:
- **A wiki, not a blob** — pages live in a tree of folders, browsable and searchable, each answering one question
- **Built from observations** — synthesized from consolidated beliefs rather than raw conversation, so a page is not a transcript summary
- **Never self-citing** — a page never reads another page, so they cannot cite each other into a feedback loop
- **Real files when you want them** — `hindsight fs mount` projects the tree onto disk as ordinary markdown, so `grep`, an editor or an agent's file tools all work with no SDK
See [Knowledge Pages](knowledge-pages.md) for the full model.
### Reflect
`recall()` returns memories. `reflect()` returns an **answer**, by running an
agentic loop over the bank rather than a single query:
- **It gathers its own evidence** — the agent decides what it needs and calls its own tools, up to ten rounds, and cannot answer before it has retrieved something
- **It checks sources in priority order** — mental models and knowledge pages first, then observations, and only then raw facts, so it reads the distilled answer before the transcript
- **It cites what it used** — and only IDs it actually retrieved can be cited
What makes two banks answer the same question differently is their configuration:
- **Mission** — the bank's identity in plain language, which tells it what to prioritise. *"I am a research assistant specializing in ML. I prefer simplicity over cutting-edge."*
- **Directives** — hard rules it must never break. *"Never recommend specific stocks."*
- **Disposition** — skepticism, literalism and empathy on a 1–5 scale, shaping how it interprets what it finds
These shape `reflect` only. `recall` returns the same memories whoever is asking.
See [Reflect](reflect.md) for the loop in detail.
## Architecture Deep Dive
Everything above, end to end and in motion: what a document turns into on the
way in, what each of the three operations touches, and what the worker changes
behind them. Play it, or step through it at your own pace.
**Figure: What Hindsight Does.** An animated diagram on the docs site; its narration, step by step:
- **retain()**
1. Your agent sends what happened: a conversation, a document, a transcript.
2. The original text is stored as a document.
3. It is split into chunks, so the exact passage can be handed back later.
4. An LLM pulls out facts: world facts about others, and experience facts about what the agent itself did. The bank already knew Alice worked at Microsoft.
5. Each fact is indexed four ways: by meaning, by its words, by the entities it links, and by when it happened.
6. retain() is done. The rest happens in the background.
7. Consolidation picks up the new facts and checks them against the observations the bank already holds. One disagrees: Microsoft or Google?
8. It updates that observation instead of adding a second one: Alice moved from Microsoft to Google in March. Both facts stay as its sources, so the history is kept.
9. When consolidation finishes, it queues a refresh for every mental model and page set to refresh after it that now has new memories…
10. …and each one re-runs its question through reflect and is rewritten.
- **recall()**
1. recall() finds the memories that matter for a query.
2. Searches run at once, each through its own index: meaning, exact words and the entity graph. The time search only joins when the query names a date.
3. The same indexes cover facts and observations, so both come back. They are merged and reranked; the old Microsoft fact falls below the cut.
4. The agent gets ranked memories it can put straight into its prompt.
- **reflect()**
1. reflect() answers a question by reasoning over everything in the bank.
2. An agent loop decides what to look up. It starts with the most refined knowledge: mental models and knowledge pages.
3. Then observations, searched through the same indexes as recall. If new facts are still waiting to be consolidated, they are marked stale.
4. Then raw facts through recall, for the details the summaries leave out.
5. When it needs the exact wording, it opens the chunk or document a fact came from.
6. It stops when it has enough evidence, and writes an answer shaped by the bank’s mission and disposition. It can only cite what it found.
7. The answer comes back with the memories it is based on.
## Integrations
**Coding agents** — [one install](../sdks/integrations/coding-agents.md) wires 20+
harnesses (Claude Code, Codex CLI, Cursor CLI, opencode, Copilot CLI and more) to
a per-repo memory bank.
**Personal agents** — [Hermes](../sdks/integrations/hermes.md) and
[OpenClaw](../sdks/integrations/openclaw.md) install Hindsight as their memory
provider: every turn recalls what matters before answering and retains what was
said afterwards. The Hermes desktop app configures it
[in Settings](../sdks/integrations/hermes-desktop.md), no terminal.
Everything else — frameworks, MCP servers, tools — is in the
Integrations Hub.
## Next Steps
### Getting Started
- [**Quick Start**](api/quickstart.md) — Install and get up and running in 60 seconds
- [**RAG vs Hindsight**](rag-vs-hindsight.md) — See how Hindsight differs from traditional RAG with real examples
### Core Concepts
- [**Retain**](retain.md) — How memories are stored with multi-dimensional facts
- [**Recall**](retrieval.md) — How the 4-way parallel search retrieves memories
- [**Reflect**](reflect.md) — How mission, directives, and disposition shape reasoning
### API Methods
- [**Retain**](api/retain.md) — Store information in memory banks
- [**Recall**](api/recall.md) — Search and retrieve memories
- [**Reflect**](api/reflect.md) — Agentic reasoning with memory
- [**Mental Models**](api/mental-models.md) — User-curated summaries for common queries
- [**Memory Banks**](api/memory-banks.md) — Configure mission, directives, and disposition
- [**Documents**](api/documents.md) — Manage document sources
- [**Operations**](api/operations.md) — Monitor async tasks
### Deployment
- [**Server Setup**](installation.md) — Deploy with Docker Compose, Helm, or pip
references/developer/installation.md
# Installation
Hindsight can be deployed in several ways depending on your infrastructure and requirements.
:::tip Don't want to manage infrastructure?
**[Hindsight Cloud](https://ui.hindsight.vectorize.io)** is a fully managed service that handles all infrastructure, scaling, and maintenance — [sign up here](https://ui.hindsight.vectorize.io).
:::
## Supported Platforms
Hindsight runs on **Linux**, **macOS**, and **Windows**:
| Platform | Docker | Bare Metal (pip) | Embedded DB (pg0) | Notes |
|----------|--------|------------------|--------------------|-------|
| **Linux** (x86_64, ARM64) | ✅ | ✅ | ✅ | Fully supported, recommended for production |
| **macOS** (Apple Silicon / arm64) | ✅ | ✅ | ✅ | Fully supported |
| **macOS** (Intel / x86_64) | ✅ | ⚠️ slim only | ✅ | Use `hindsight-all-slim` / `hindsight-api-slim`. The full bundle's local ML models (PyTorch, MLX) publish no Intel-Mac wheels, so `pip install hindsight-all` silently backtracks to a months-old release. Pair the slim bundle with a hosted embeddings/reranker provider or the in-process ONNX backend (`hindsight-api-slim[local-onnx]`). |
| **Windows** (x86_64) | ✅ | ✅ | ✅ | Fully supported — see [Windows setup](#windows) for external PostgreSQL option |
All platforms support the embedded database (pg0) for development. On Windows, you can also use an external PostgreSQL installation — see the [Windows](#windows) section for a step-by-step guide.
---
## Prerequisites
### PostgreSQL
Hindsight requires PostgreSQL 14+ with a vector extension for similarity search. The supported extensions are:
- **pgvector** (default)
- **pgvectorscale**
- **vchord**
- **scann** (AlloyDB)
Configure which one to use with `HINDSIGHT_API_VECTOR_EXTENSION`. See [Configuration](./configuration.md) for details.
**By default**, Hindsight uses **pg0** — an embedded PostgreSQL that runs locally on your machine. This is convenient for development but **not recommended for production**.
**For production**, use an external PostgreSQL with one of the supported vector extensions:
- **Supabase** — Managed PostgreSQL with pgvector built-in
- **Neon** — Serverless PostgreSQL with pgvector
- **Azure Database for PostgreSQL** — With pgvector and pgvectorscale support
- **Google AlloyDB** / **AlloyDB Omni** — With pgvector and ScaNN support
- **AWS RDS** / **Cloud SQL** — With pgvector extension enabled
- **Self-hosted** — PostgreSQL 14+ with your preferred vector extension
### LLM Provider
You need an LLM API key for fact extraction, entity resolution, and answer generation. See [Models](./models.md) for supported providers, model recommendations, and configuration.
### Hardware
Hindsight is designed to run on commodity hardware. The footprint depends mainly on whether the **full** image (which bundles local embedding and reranker models) or the **slim** image (which delegates those to external providers) is used.
| Component | Minimum RAM | Recommended RAM | Notes |
|-----------|-------------|-----------------|-------|
| **API — Full image** | 1.5 GB | 2 GB | Loads local BGE embedder (~130 MB) and MiniLM cross-encoder (~90 MB) into memory, plus PyTorch/ONNX runtime arenas. Idle RSS settles around 0.8–1.0 GB; expect 1.2–1.5 GB under load. |
| **API — Slim image** | 512 MB | 1 GB | No local models. Steady-state RSS is dominated by Python runtime and DB connections. Requires [external embedding and reranker providers](./configuration.md#embeddings) (e.g. TEI, OpenAI, Cohere). |
| **Control Plane (UI)** | 128 MB | 256 MB | Next.js process, lightweight. |
| **Worker** (if separated) | Same as API image variant | Same as API image variant | Workers load the same models as the API server. |
| **PostgreSQL** | 512 MB | 1 GB+ | Scales with the number of memories and indexes. |
:::tip Reducing the footprint
The bulk of the full image's memory comes from the bundled embedding and reranker models and their PyTorch/ONNX runtimes. To shrink the deployment to a few hundred MB of RAM, switch to the **slim** image and configure [external embedding and reranker providers](./configuration.md#embeddings).
:::
CPU vs GPU: 2 vCPUs on CPU-only is fine for development and basic workloads. For production traffic, the local reranker (cross-encoder) is the main bottleneck and typically benefits from a GPU to keep recall latency reasonable; alternatively, offload reranking to an [external reranker provider](./configuration.md#embeddings) (e.g. TEI, Cohere) on dedicated GPU hardware.
---
## Docker
**Best for**: Quick start, development, small deployments
Run everything in one container with embedded PostgreSQL:
```bash
export OPENAI_API_KEY=sk-xxx
docker run -it --pull always --name hindsight --restart unless-stopped --shm-size=1g -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-v hindsight-data:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- **API Server**: http://localhost:8888
- **Control Plane** (Web UI): http://localhost:9999
:::note Persisting data: named volume vs. host bind mount
The container runs as a non-root user (UID 1000). The `hindsight-data` **named volume** above is recommended — Docker creates it owned by the container user, so it works with no extra setup.
If you instead bind-mount a **host directory** (`-v $HOME/.hindsight-docker:/home/hindsight/.pg0`), that directory must be owned by UID 1000, or the embedded database fails to start with `Permission denied`:
```bash
sudo chown -R 1000:1000 $HOME/.hindsight-docker
```
Do **not** run the image under a different UID with `--user` to work around this. The image only defines the `hindsight` user (UID 1000), so any other UID has no `/etc/passwd` entry, and startup crashes in a library that looks the running user up by ID:
```
KeyError: 'getpwuid(): uid not found: 1042'
```
Running as UID 1000 (the default) with the directory chowned to match is the supported way to use a host bind mount. If you cannot chown the directory — for example on a NAS share mounted with a fixed `uid=` — use a named volume instead.
:::
All published images are [signed with Cosign](#verifying-image-signatures) — verification is optional.
:::tip Set a stable `HINDSIGHT_API_WORKER_ID` in production
The worker uses the container hostname as its identity, which Docker sets to the container ID by default. That value changes on every restart, so any task that was being processed when the container went down stays parked under the old ID with no way for the new container to recognize it as its own.
Set `HINDSIGHT_API_WORKER_ID` to a stable value (e.g., `-e HINDSIGHT_API_WORKER_ID=hindsight-prod`) so the worker keeps the same identity across restarts. This is recommended even for single-container deployments. For diagnosis and recovery commands, see [Admin CLI - Recovering stuck operations](./admin-cli.md#recovering-stuck-or-zombie-operations).
:::
### Docker Image Variants
| Variant | Size (AMD64) | Size (ARM64) | When to use |
|---------|--------------|--------------|-------------|
| **Full** (`latest`) | ~9 GB | ~3.7 GB | Default. Embeddings and reranking run in the image; the LLM is always external, including for local inference. |
| **Slim** (`slim`) | ~500 MB | ~500 MB | Use when you already rely on external services for embeddings and reranking (OpenAI, Cohere, TEI). Significantly smaller image, faster deploys. Requires [external providers](./configuration.md#embeddings). |
The slim image corresponds to the [`hindsight-api-slim`](#bare-metal-pip) pip package. See [Configuration](./configuration.md#embeddings) for external provider options.
Neither image bundles llama.cpp, so the built-in `llamacpp` provider is not available in Docker. To run inference locally, start llama.cpp (or Ollama, LM Studio, vLLM) alongside Hindsight and point `HINDSIGHT_API_LLM_BASE_URL` at it — see [`docker/docker-compose/local-llm/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/local-llm) for a working compose file.
### NVIDIA GPU Acceleration (CUDA)
To run in-process local embedding and reranker models on an NVIDIA GPU, build a custom CUDA-enabled image using the recipe in [`docker/docker-compose/cuda/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/cuda):
```bash
docker compose -f docker/docker-compose/cuda/docker-compose.yaml up --build
```
The recipe upgrades in-process PyTorch to CUDA 12.6 and configures GPU passthrough via the NVIDIA Container Toolkit. Build it on the machine that will run it — an emulated cross-architecture image cannot reach the GPU.
The CUDA runtime is additive: the base image's CPU PyTorch wheel stays in the lower layers, so the result is **roughly 11 GB on disk** against the ~9 GB Full image. This is why no CUDA image is published — the cost is only worth paying when you actually have a GPU to use.
See [`docker/docker-compose/cuda/README.md`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/cuda) for prerequisites, manual build steps, and verification.
### Bundling Custom Models in a Custom Image
:::tip Production deployments with non-default local models
If you use a non-default local embedder or reranker, bake the models into a custom image at build time rather than enabling the Helm `modelCache` PVC. See [`docker/docker-compose/custom-models/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/custom-models) for a runnable example.
:::
### Available Tags
```bash
# Standalone (API + Control Plane)
ghcr.io/vectorize-io/hindsight:latest # Full, latest release
ghcr.io/vectorize-io/hindsight:latest-slim # Slim, latest release
ghcr.io/vectorize-io/hindsight:0.4.9 # Full, specific version
ghcr.io/vectorize-io/hindsight:0.4.9-slim # Slim, specific version
# API only
ghcr.io/vectorize-io/hindsight-api:latest
ghcr.io/vectorize-io/hindsight-api:latest-slim
# Control Plane only
ghcr.io/vectorize-io/hindsight-control-plane:latest
```
### Verifying image signatures
Images are signed with [Cosign](https://docs.sigstore.dev/cosign/signing/overview/) keyless OIDC. To verify any tag:
```bash
cosign verify ghcr.io/vectorize-io/hindsight:<tag> \
--certificate-identity-regexp '^https://github\.com/vectorize-io/hindsight/\.github/workflows/(sign-images|release)\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
---
## Helm / Kubernetes
**Best for**: Production deployments, auto-scaling, cloud environments
```bash
# Install with built-in PostgreSQL
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set api.llm.provider=groq \
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
--set postgresql.enabled=true
# Or use external PostgreSQL
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set api.llm.provider=groq \
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
--set postgresql.enabled=false \
--set api.database.url=postgresql://user:pass@postgres.example.com:5432/hindsight
# Install a specific version
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight --version 0.1.3
# Upgrade to latest
helm upgrade hindsight oci://ghcr.io/vectorize-io/charts/hindsight
```
**Requirements**:
- Kubernetes cluster (GKE, EKS, AKS, or self-hosted)
- Helm 3.8+
### Distributed Workers
For high-throughput deployments, enable dedicated worker pods to scale task processing independently:
```bash
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set worker.enabled=true \
--set worker.replicaCount=3
```
The chart deploys workers as a StatefulSet, so each pod gets a stable name (e.g. `hindsight-worker-0`) that the worker uses as its `HINDSIGHT_API_WORKER_ID`. Tasks claimed by a pod are recognized as its own across restarts. If you swap the chart for a plain Deployment, set `HINDSIGHT_API_WORKER_ID` explicitly per replica — otherwise hostnames are randomized and previously-claimed tasks become orphaned. See [Admin CLI - Recovering stuck operations](./admin-cli.md#recovering-stuck-or-zombie-operations) for diagnosis.
See [Services - Worker Service](./services.md#worker-service) for configuration details and architecture.
See the [Helm chart values.yaml](https://github.com/vectorize-io/hindsight/tree/main/helm/hindsight/values.yaml) for all chart options.
---
## Bare Metal (pip)
**Best for**: Running Hindsight as a standalone service on a host machine.
### Install
```bash
pip install hindsight-api # Full — works out of the box
pip install hindsight-api-slim # Slim — requires external services for embeddings, reranking, and the database
```
When using `hindsight-api-slim`, you must configure external providers for all model operations. See [Configuration](./configuration.md#embeddings) for details.
### Run with Embedded Database
For development and testing, Hindsight can run with an embedded PostgreSQL (pg0):
```bash
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
hindsight-api
```
This creates a database in `~/.hindsight/data/` and starts the API on http://localhost:8888.
### Run with External PostgreSQL
For production, connect to your own PostgreSQL instance:
```bash
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
hindsight-api
```
**Note**: The database must exist and have pgvector enabled (`CREATE EXTENSION vector;`).
### CLI Options
```bash
hindsight-api --port 9000 # Custom port (default: 8888)
hindsight-api --host 127.0.0.1 # Bind to localhost only
hindsight-api --workers 4 # Multiple worker processes
hindsight-api --log-level debug # Verbose logging
```
### Control Plane
The Control Plane (Web UI) can be run standalone using npx:
```bash
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888
```
This connects to your running API server and provides a visual interface for managing memory banks, exploring entities, and testing queries.
#### Options
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
| `-p, --port` | `PORT` | 9999 | Port to listen on |
| `-H, --hostname` | `HOSTNAME` | 0.0.0.0 | Hostname to bind to |
| `-a, --api-url` | `HINDSIGHT_CP_DATAPLANE_API_URL` | http://localhost:8888 | Hindsight API URL |
| | `HINDSIGHT_CP_ACCESS_KEY` | *(none)* | Access key to protect the Control Plane UI. When set, users must enter this key to log in. |
#### Examples
```bash
# Run on custom port
npx @vectorize-io/hindsight-control-plane --port 9999 --api-url http://localhost:8888
# Using environment variables
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com
npx @vectorize-io/hindsight-control-plane
# Production deployment
PORT=80 HINDSIGHT_CP_DATAPLANE_API_URL=https://api.hindsight.io npx @vectorize-io/hindsight-control-plane
```
---
## Windows
**Best for**: Running Hindsight natively on Windows without Docker
Hindsight works on Windows with the embedded database (pg0) out of the box — just install and run:
```powershell
pip install hindsight-api
set HINDSIGHT_API_LLM_PROVIDER=openai
set HINDSIGHT_API_LLM_API_KEY=sk-xxx
set HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-api
```
### Using External PostgreSQL (optional)
If you prefer to use your own PostgreSQL instance instead of the embedded database:
```powershell
# Install PostgreSQL
winget install PostgreSQL.PostgreSQL.17
# Build pgvector (requires Visual Studio Build Tools)
git clone https://github.com/pgvector/pgvector.git
cd pgvector
# Open "x64 Native Tools Command Prompt for VS" and run:
set PGROOT=C:\Program Files\PostgreSQL\17
nmake /F Makefile.win
nmake /F Makefile.win install
# Create the database and enable the vector extension
psql -U postgres -c "CREATE DATABASE hindsight;"
psql -U postgres -d hindsight -c "CREATE EXTENSION vector;"
```
Then run Hindsight pointing to your database:
```powershell
pip install hindsight-api
set HINDSIGHT_API_DATABASE_URL=postgresql://postgres@localhost:5432/hindsight
set HINDSIGHT_API_LLM_PROVIDER=openai
set HINDSIGHT_API_LLM_API_KEY=sk-xxx
set HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-api
```
- **API Server**: http://localhost:8888
:::tip
You can also use the slim package (`pip install hindsight-api-slim`) if you configure external providers for embeddings and reranking. See [Configuration](./configuration.md#embeddings) for details.
:::
### Windows + China Network Notes
If you are running on Windows behind China network restrictions:
1. DeepSeek works well for `HINDSIGHT_API_LLM_PROVIDER`, but DeepSeek does not provide an embeddings endpoint.
2. Use local embeddings (recommended for privacy and reliability in restricted networks).
3. Set `HF_ENDPOINT=https://hf-mirror.com` before starting Hindsight so Hugging Face model downloads use a China-accessible mirror.
```powershell
set HF_ENDPOINT=https://hf-mirror.com
set HINDSIGHT_API_LLM_PROVIDER=deepseek
set HINDSIGHT_API_LLM_API_KEY=sk-your-deepseek-key
set HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
set HINDSIGHT_API_LLM_BASE_URL=https://api.deepseek.com
set HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
set HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
set HINDSIGHT_API_RERANKER_PROVIDER=flashrank
hindsight-api
```
The `HF_ENDPOINT` variable is used by Hugging Face tooling (`huggingface_hub`), not by Hindsight itself.
---
## Embedded in a Python Application
**Best for**: Using Hindsight programmatically from Python without running a separate server process.
```bash
pip install hindsight-all # Full — works out of the box (Linux, Windows, Apple Silicon Macs)
pip install hindsight-all-slim # Slim — requires external services for embeddings, reranking, and the database
```
On Intel (x86_64) Macs, install `hindsight-all-slim` — see [Supported Platforms](#supported-platforms).
`hindsight-all` supports two modes of embedding:
**In-process** (`HindsightServer`): the server runs in a background thread inside your application. Best when you want the tightest integration and are already managing your own process lifecycle.
```python
from hindsight import HindsightServer, HindsightClient
with HindsightServer(llm_provider="openai", llm_api_key="sk-xxx") as server:
client = HindsightClient(base_url=server.url)
client.retain(bank_id="alice", content="Alice prefers concise answers.")
results = client.recall(bank_id="alice", query="How should I respond to Alice?")
```
**Managed subprocess** (`HindsightEmbedded`): the server runs as a background daemon process, shared across multiple Python processes or sessions. The daemon starts on first use and runs until it is stopped.
```python
from hindsight import HindsightEmbedded
client = HindsightEmbedded(llm_provider="openai", llm_api_key="sk-xxx")
client.retain(bank_id="alice", content="Alice prefers concise answers.")
results = client.recall(bank_id="alice", query="How should I respond to Alice?")
```
See the [Python SDK](../sdks/python.md) for the full API reference.
---
## Next Steps
- [Configuration](./configuration.md) — Environment variables and settings
- [Models](./models.md) — ML models and providers
- [Monitoring](./monitoring.md) — Metrics and observability
references/developer/knowledge-pages.md
# Knowledge Pages
**Knowledge pages** are living documents a bank writes about itself. Each page answers one question — "What are the components here?", "What's our error-handling convention?" — and rewrites itself as the bank learns more. They're organized in folders, browsable, searchable, and can be projected onto disk as ordinary markdown files.
The shape is a wiki. The engine underneath is memory.
**Figure: Knowledge Pages.** An animated diagram on the docs site; its narration, step by step:
- **knowledge pages**
1. Hindsight keeps a wiki for your team. This page on deploys is written from what the bank knows.
2. Then something happens. Your tools send it to Hindsight: an incident, a Slack thread, a code change.
3. Hindsight stores it as new memories.
4. It learns from them…
5. …adds what is new, and corrects what is no longer true.
6. The page on deploys is now out of date, so Hindsight updates it.
7. Only what changed: the old advice is crossed out, the new facts land where they belong.
8. Later, another client asks: an AI agent, or a teammate.
9. Hindsight finds the page, already written and up to date.
10. The answer already includes what just happened. Nobody had to update the wiki.
---
## Mental Models, Simplified
A knowledge page *is* a [mental model](./mental-models.md). Same synthesis, same background refresh, same provenance.
What's different is how much you have to know to use one. A mental model exposes its mechanics — what it reads, when it rebuilds, how it edits itself. Nobody should have to think about synthesis scope and refresh triggers to keep a wiki. So a page comes with those decisions already made:
- It's built from the bank's [observations](./observations.md) — consolidated, deduplicated beliefs — rather than raw conversational detail.
- It refreshes incrementally whenever consolidation produces new knowledge in its scope, editing the document rather than regenerating it.
- It never reads other pages, so pages can't cite each other into a feedback loop.
- It gets a larger content budget, because it's a document rather than an answer.
You supply a name and a question. Everything else is a default you can override if you need to — every mental model setting still applies. See the [Knowledge Pages API](./api/knowledge-pages.md) for the exact defaults and how to change them.
---
## Organized Like a Wiki
Pages live in a tree of folders. Nesting is arbitrary, page names are unique within their folder, and deleting a folder deletes its subtree. That's the whole structural model — a hierarchy, the way anyone would organize documents by hand.
The tree is what makes a knowledge base navigable rather than a flat list of synthesized blobs: `Architecture/`, `Runbooks/`, `Decisions/`, each holding pages that stay current on their own.
---
## Projected as Real Files
Agents already know how to work with a filesystem. So the CLI can mirror a bank's knowledge base onto disk:
```bash
hindsight fs mount --bank my-bank
```
The folder tree becomes real directories, each page a real markdown file with YAML frontmatter, kept current by a background refresh loop. From there, everything ordinary works — `ls`, `cat`, `grep`, `rg`, `fzf`, your editor, an agent's file tools. No SDK, no API client, no new vocabulary.
The same content is available as a portable markdown bundle over the API, for exporting or committing elsewhere.
---
## Searchable
Pages are searchable at the **document level**: a query returns whole pages, ranked, with snippets. It combines full-text and semantic matching, fused server-side, with no reranking step — fast enough to be the first thing an agent reaches for.
That last part matters. Search is a tool an agent *chooses* to call, visible in the transcript, rather than content pushed into its context on every turn. Retrieval the agent asked for informs what it's doing; retrieval it didn't ask for tends to derail it.
This is a different path from [recall](./retrieval.md), which searches individual memories. Use page search to pick a document; use recall for a specific fact.
---
## Why Not Just Raw Files?
If the answer is documents in a tree, haven't we reinvented files — the thing a memory system exists to replace?
The difference is what sits underneath. A file is where information goes to age. Whoever wrote it last wins, contradictions accumulate quietly, and nothing ever reconciles them. Left alone, a hand-maintained wiki becomes a beautifully formatted lie — not because anyone lied, but because keeping it true is a chore, and chores lose.
A knowledge page is a **projected view** over processed memory, the way a database view is not a table. Before a page is written, Hindsight has already done the work files can't do for themselves: extracted facts from the raw sessions, commits, and documents; deduplicated them; and reconciled their contradictions through consolidation. So when a team decided X and later amended it to Y, the page says Y — and can say why — instead of preserving both a paragraph apart.
Your raw documents remain the source of truth about *what was said*. The pages are the reconciled truth about *what holds*.
This is also why pages heal themselves rather than rot: they aren't the storage, they're the rendering. Delete a page and nothing is lost — it re-projects from memory. Try deleting a wiki and getting it back.
---
## Working With Pages
- **Control plane** — the Knowledge Base view renders the tree, page contents, and which pages have fallen behind.
- **HTTP API** — see [Knowledge Pages API](./api/knowledge-pages.md) for the full endpoint surface.
- **CLI** — `hindsight fs` mirrors a bank to a local folder of markdown files.
- **Agent tools** — the Claude Code plugin exposes `agent_knowledge_*` MCP tools so an agent can list, read, create, and update its own pages during a session.
references/developer/mcp-server.md
---
sidebar_position: 5
---
# MCP Server
Hindsight includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that allows AI assistants to store and retrieve memories directly.
## Access
The MCP server is **enabled by default** and mounted at `/mcp` on the API server. Each memory bank has its own MCP endpoint:
```
http://localhost:8888/mcp/{bank_id}/
```
For example, to connect to the memory bank `alice`:
```
http://localhost:8888/mcp/alice/
```
To disable the MCP server, set the environment variable:
```bash
export HINDSIGHT_API_MCP_ENABLED=false
```
## Authentication
By default, the MCP endpoint is **open** (no authentication required).
To enable authentication, configure the API key tenant extension:
```bash
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
export HINDSIGHT_API_TENANT_API_KEY=your-secret-key
```
When authentication is enabled, include your API key in the `Authorization` header:
### Claude Code
```bash
claude mcp add --transport http hindsight http://localhost:8888/mcp \
--header "Authorization: Bearer your-secret-key" \
--header "X-Bank-Id: my-bank"
```
### Claude Desktop
Add to `~/.claude_desktop_config.json`:
```json
{
"mcpServers": {
"hindsight": {
"url": "http://localhost:8888/mcp",
"headers": {
"Authorization": "Bearer your-secret-key",
"X-Bank-Id": "my-bank"
}
}
}
}
```
### Direct HTTP Request
```bash
curl -X POST http://localhost:8888/mcp \
-H "Authorization: Bearer your-secret-key" \
-H "X-Bank-Id: my-bank" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
```
If the key is missing or invalid, requests will receive a `401 Unauthorized` response.
## Bank Selection
The memory bank is resolved in this priority order:
1. **URL path** (highest priority): `http://localhost:8888/mcp/my-bank/`
2. **X-Bank-Id header**: `--header "X-Bank-Id: my-bank"`
3. **Default**: Uses `HINDSIGHT_MCP_BANK_ID` env var (default: "default")
## Per-Bank Endpoints
Unlike traditional MCP servers where tools require explicit identifiers, Hindsight uses **per-bank endpoints**. The `bank_id` is part of the URL path, so tools don't need to specify which bank to use—it's implicit from the connection.
This design:
- **Simplifies tool usage** — no need to pass `bank_id` with every call
- **Enforces isolation** — each MCP connection is scoped to a single bank
- **Enables multi-tenant setups** — connect different users to different endpoints
## Two Modes
The MCP server operates in two modes depending on the URL:
| Mode | URL | Tools | bank_id |
|------|-----|-------|---------|
| **Single-bank** | `/mcp/{bank_id}/` | 27 tools (memory, mental models, directives, documents, operations, tags, bank management) | Implicit from URL |
| **Multi-bank** | `/mcp/` | All 30 tools including `list_banks`, `create_bank`, `get_bank_stats` | Explicit `bank_id` parameter on each tool |
**Single-bank mode** (recommended) scopes all operations to the bank in the URL. Tools don't expose a `bank_id` parameter.
**Multi-bank mode** exposes all tools with an optional `bank_id` parameter, plus bank management tools (`list_banks`, `create_bank`, `get_bank_stats`).
## Tool Metadata and Instructions
Hindsight can append deployment-specific guidance to the `retain` and `recall` MCP tool descriptions. Set `HINDSIGHT_API_MCP_INSTRUCTIONS` on the API server when clients should see local rules, such as which tags to use or which memories should be retained.
```bash
export HINDSIGHT_API_MCP_INSTRUCTIONS="Use project:<name> tags for project-specific memories."
```
MCP clients that read tool annotations also receive safety hints from the built-in tools:
- Read-only operations such as `recall`, `reflect`, `list_*`, and `get_*` are marked with `readOnlyHint: true`.
- Delete, clear, and invalidate operations are marked with `destructiveHint: true`.
- `openWorldHint` is `false` for the built-in tools because Hindsight operates on its configured memory store rather than the open internet.
- Write operations such as `retain`, `create_*`, `update_*`, `refresh_mental_model`, and `cancel_operation` are not marked destructive.
---
## Available Tools
### retain
Store information to long-term memory.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `content` | string | Yes | The fact or memory to store |
| `context` | string | No | Category for the memory (default: `general`) |
| `timestamp` | string | No | ISO 8601 timestamp for when the event occurred |
| `tags` | list[string] | No | Tags for organizing and filtering this memory |
| `metadata` | object | No | Key-value metadata to attach (e.g., `{"source": "slack"}`) |
| `document_id` | string | No | Associate this memory with an existing document |
**Example:**
```json
{
"name": "retain",
"arguments": {
"content": "User prefers Python over JavaScript for backend development",
"context": "programming_preferences",
"tags": ["user:alice", "preferences"]
}
}
```
**When to use:**
- User shares personal facts, preferences, or interests
- Important events or milestones are mentioned
- Decisions, opinions, or goals are stated
- Work context or project details are discussed
---
### sync_retain
Store information to long-term memory and wait for completion. Unlike [`retain`](#retain) (which is asynchronous), `sync_retain` blocks until the memory is fully stored and immediately available for recall — useful for read-after-write flows where you query right after storing.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `content` | string | Yes | The fact or memory to store |
| `context` | string | No | Category for the memory (default: `general`) |
| `timestamp` | string | No | ISO 8601 timestamp for when the event occurred |
| `tags` | list[string] | No | Tags for organizing and filtering this memory |
| `metadata` | object | No | Key-value metadata to attach (e.g., `{"source": "slack"}`) |
| `document_id` | string | No | Associate this memory with an existing document |
**Example:**
```json
{
"name": "sync_retain",
"arguments": {
"content": "User prefers Python over JavaScript for backend development",
"context": "programming_preferences",
"tags": ["user:alice", "preferences"]
}
}
```
**When to use:**
- You need the memory queryable immediately after storing (read-after-write)
- A workflow step depends on the stored memory being available before continuing
- Otherwise prefer `retain` (asynchronous) to avoid blocking on storage
---
### recall
Search memories to provide personalized responses.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Natural language search query |
| `max_tokens` | integer | No | Token budget for the facts' text (default: 4096). Per-result metadata (id, tags, scores, dates) is not counted, so the response is several times larger |
| `budget` | string | No | Search thoroughness: `low`, `mid`, or `high` (default: `high`) |
| `types` | list[string] | No | Filter by fact type: `world`, `experience`, `observation`. Defaults to all |
| `tags` | list[string] | No | Filter memories by tags. Omit for no filter |
| `tags_match` | string | No | `any` (default), `all`, `any_strict`, `all_strict`, or `exact`. With `exact`, pass `tags: []` to select the untagged/global scope |
| `tag_groups` | list[object] | No | Compound boolean tag filter. Mutually exclusive with `tags`; each leaf has its own `match` value |
| `query_timestamp` | string | No | ISO 8601 timestamp — recall as if asking at this point in time; anchors relative temporal expressions and recency scoring |
| `min_scores` | object | No | Optional per-stage score floors, e.g. `{"reranker": 0.5}`. Keys: `semantic`/`keyword` (retrieval-level cutoffs), `reranker`/`final` (post-ranking). All inclusive and AND-ed; omit for no filtering. Reranker scores aren't calibrated across queries — calibrate before use |
| `temporal_window` | object | No | An explicit `{"start": ISO, "end": ISO}` period to search over, used instead of reading dates out of the query text. Ranks memories dated inside the window higher; it does not drop the ones outside it, so it can't restrict results to a period |
| `include_chunks` | boolean | No | Also return the source text chunks the facts came from. Use it when you need exact strings (paths, IDs, commands) that extraction may have reworded. Default: `false` |
| `max_chunk_tokens` | integer | No | Max tokens of source chunks when `include_chunks` is set (default: 8192) |
| `include_entities` | boolean | No | Also return observations about the entities in the results. Default: `false` |
| `max_entity_tokens` | integer | No | Max tokens of entity observations when `include_entities` is set (default: 500) |
| `include_source_facts` | boolean | No | For observation results, also return the facts they were built from. Default: `false` |
| `max_source_facts_tokens` | integer | No | Max tokens of source facts when `include_source_facts` is set (default: 4096) |
**Example:**
```json
{
"name": "recall",
"arguments": {
"query": "What are the user's programming language preferences?",
"tags": ["preferences"],
"budget": "high"
}
}
```
**When to use:**
- Start of conversation to recall relevant context
- Before making recommendations
- When user asks about something they may have mentioned before
- To provide continuity across conversations
---
### reflect
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | The question or topic to reflect on |
| `context` | string | No | Optional context about why this reflection is needed |
| `budget` | string | No | Search budget: `low`, `mid`, or `high` (default: `low`) |
| `max_tokens` | integer | No | Maximum tokens in the response (default: 4096) |
| `response_schema` | object | No | JSON Schema for structured output. When provided, the response includes a `structured_output` field |
| `tags` | list[string] | No | Scope memories, observations, mental models, and tagged directives. Omitted tags leave memory retrieval unfiltered but load only untagged directives |
| `tags_match` | string | No | `any` (default), `all`, `any_strict`, `all_strict`, or `exact`. Untagged directives remain global in every mode |
| `include_trace` | boolean | No | Include `tool_trace` and `llm_trace` debugging output. Defaults to `false` to keep responses small |
The MCP tool forwards `tags_match` only when `tags` is present. Pass
`tags: []` with `tags_match: "exact"` to select the empty/global scope for raw
facts, observations, and mental models; directive loading also selects only
untagged directives.
**Example:**
```json
{
"name": "reflect",
"arguments": {
"query": "Based on my past decisions, what architectural style do I prefer?",
"budget": "mid",
"tags": ["architecture"]
}
}
```
**When to use:**
- When reasoned analysis is needed, not just fact retrieval
- Questions like "What should I do?" rather than "What did I say?"
- Synthesizing patterns across multiple memories
---
### create_mental_model
Create a mental model — a living document that stays current with your memories. Mental models are pre-computed reflections that get automatically refreshed as new memories are stored.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Human-readable name for the mental model |
| `source_query` | string | Yes | The query used to generate and refresh the model |
| `mental_model_id` | string | No | Custom ID (alphanumeric lowercase with hyphens). Auto-generated if not provided |
| `tags` | list[string] | No | Tags for organizing and filtering models |
| `tags_match` | string | No | How the model's tags are matched against memories on refresh: `any`, `all`, `any_strict`, `all_strict`, or `exact`. See the note below on the default |
| `trigger` | object | No | Refresh policy — see [Trigger settings](#trigger-settings) |
| `max_tokens` | integer | No | Maximum tokens for model content (default: 2048) |
| `trigger_refresh_after_consolidation` | boolean | No | Legacy shorthand for `trigger.refresh_after_consolidation` |
`trigger` is the preferred form for refresh settings; `tags_match` and
`trigger_refresh_after_consolidation` remain as shorthands for existing
integrations. Setting the same field both ways is an error rather than one
silently winning.
:::warning Tagged models default to `all_strict`
When a mental model has `tags` but no explicit `tags_match`, its refresh matches memories with **`all_strict`** — a memory must carry **every** one of the model's tags to be included. If your memories use narrow, single-topic tags (e.g. `["project:status"]`) while the model is tagged broadly (e.g. `["projects", "mental-model"]`), the refresh filters out everything and the content comes back empty.
Pass `tags_match: "any"` (the same default that `recall` and `reflect` use) to match memories that carry *any* of the model's tags:
```json
{
"name": "create_mental_model",
"arguments": {
"name": "Current projects",
"source_query": "Which projects is the user currently working on?",
"tags": ["projects", "mental-model"],
"tags_match": "any"
}
}
```
:::
**Example:**
```json
{
"name": "create_mental_model",
"arguments": {
"name": "Team Directory",
"source_query": "Who works here and what do they do?",
"tags": ["team", "people"],
"tags_match": "any"
}
}
```
Content generation runs asynchronously. The response includes an `operation_id` to track progress.
---
### list_mental_models
List all mental models in a bank, optionally filtered by tags.
Returns metadata only — `id`, `name`, `tags`, `is_stale` and timestamps. To read a
model's synthesized content, call `get_mental_model` with an id from this list.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tags` | list[string] | No | Filter models by tags |
---
### get_mental_model
Retrieve a specific mental model by ID, including its full content.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mental_model_id` | string | Yes | The ID of the mental model to retrieve |
---
### update_mental_model
Update a mental model's metadata or settings.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mental_model_id` | string | Yes | The ID of the mental model to update |
| `name` | string | No | New name |
| `source_query` | string | No | New source query |
| `tags` | list[string] | No | New tags |
| `max_tokens` | integer | No | New max tokens |
| `trigger` | object | No | Refresh-policy fields to change — see [Trigger settings](#trigger-settings). This is a patch: omitted fields keep their current values |
| `tags_match` | string | No | Legacy shorthand for `trigger.tags_match` |
| `trigger_refresh_after_consolidation` | boolean | No | Auto-refresh after consolidation. Only set when you want to change this setting |
---
### Trigger settings
`trigger` carries a mental model's (or knowledge page's) refresh policy: **when**
it rebuilds itself and **what** it rebuilds from. It accepts every field the HTTP
API accepts, so anything you can configure through `PATCH /mental_models/{id}`
you can also configure from an agent over MCP.
| Field | Type | Description |
|-------|------|-------------|
| `mode` | string | `full` regenerates the content from scratch on each refresh; `delta` makes surgical edits, preserving unchanged sections byte-for-byte. Delta falls back to full when there is no existing content or the source query changed |
| `refresh_after_consolidation` | boolean | Rebuild after each memory consolidation |
| `refresh_cron` | string | UTC five-field cron, e.g. `0 3 * * *` for daily at 03:00 UTC. Only runs when the model is stale, so an unchanged scope costs no LLM call |
| `full_refresh_cron` | string | UTC five-field cron for a scheduled **full** refresh of a delta model, e.g. `0 3 * * 0`. Independent of the two triggers above; skipped when no refresh ran since the last full one |
| `min_refresh_interval_seconds` | integer | Floor between two *automatic* refreshes. Triggers arriving sooner fold into one queued refresh, so a burst of retains costs one rebuild. Explicit refreshes ignore it |
| `fact_types` | list[string] | Which of `world`, `experience`, `observation` the refresh retrieves. Omit for all three |
| `exclude_mental_models` | boolean | Exclude **all** mental models from the refresh, so a model never reflects on its siblings |
| `exclude_mental_model_ids` | list[string] | Exclude specific mental models by ID |
| `tags_match` | string | How the model's tags select memories: `any`, `all`, `any_strict`, `all_strict`, `exact` |
| `tag_groups` | list[object] | Compound boolean tag expressions used *instead of* the model's flat tags |
| `include_chunks` | boolean | Whether the refresh's internal recall returns raw chunk text |
| `recall_max_tokens` | integer | Token budget for facts from the refresh's internal recall |
| `recall_chunks_max_tokens` | integer | Token budget for raw chunks from the refresh's internal recall |
| `budget` | string | How many agent steps a refresh may spend, as a multiple of the server's reflect iteration limit: `low` halves it, `mid` keeps it, `high` doubles it. Omit for `mid` |
| `response_schema` | object | JSON Schema for structured output, stored alongside the markdown under `reflect_response.structured_output` |
| `keep_trace` | boolean | Record how each refresh reached its result under `reflect_response.trace`. The only way to diagnose a cron- or consolidation-driven refresh after the fact |
`refresh_after_consolidation` and `refresh_cron` are **mutually exclusive** — a
model refreshes either after consolidation or on a schedule, never both. Setting
one clears the other.
**On create**, omitted fields take the engine defaults (`mode: full`, no
schedule, all fact types); for a knowledge page they take the page defaults
(`mode: delta`, observation-only, auto-refresh, siblings excluded).
**On update, `trigger` is a patch**: only the fields you send change, so putting
a model on a cron schedule keeps its `fact_types` and `mode`. Send an explicit
`null` to clear a nullable setting. Read the current policy back with
`get_mental_model` (detail `content` or `full`) if you want to inspect before
changing.
```json
{
"name": "update_mental_model",
"arguments": {
"mental_model_id": "team-conventions",
"trigger": { "refresh_cron": "0 3 * * *", "fact_types": ["observation"] }
}
}
```
---
### delete_mental_model
Permanently delete a mental model.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mental_model_id` | string | Yes | The ID of the mental model to delete |
---
### refresh_mental_model
Re-generate a mental model's content from the latest memories. Runs asynchronously.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mental_model_id` | string | Yes | The ID of the mental model to refresh |
---
### clear_mental_model
Clear a mental model's content while keeping its definition. After clearing, call `refresh_mental_model` to rebuild it from the latest memories.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `mental_model_id` | string | Yes | The ID of the mental model to clear |
---
### list_banks (multi-bank mode only)
List available memory banks, most recently written first. The response carries the
total number of matching banks alongside the page, so large deployments can be
walked with `offset`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | No | Case-insensitive substring matched against bank ID and name |
| `limit` | integer | No | Maximum number of banks to return (default: 100) |
| `offset` | integer | No | Number of banks to skip (default: 0) |
---
### create_bank (multi-bank mode only)
Create a new memory bank or retrieve an existing one.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `bank_id` | string | Yes | The ID for the new bank |
| `name` | string | No | Human-friendly name for the bank |
| `mission` | string | No | Mission describing who the agent is and what they're trying to accomplish |
---
### list_directives
List directives in a bank. This management tool does not use reflect's
directive-isolation behavior: omitting `tags` lists every directive, including
tagged directives.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tags` | list[string] | No | Filter using `any` matching. When present, returns untagged/global directives plus directives sharing at least one tag. When omitted or empty, returns all directives |
| `active_only` | boolean | No | Only return active directives (default: `true`) |
---
### create_directive
Create a new directive in a bank.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Human-readable name for the directive |
| `content` | string | Yes | The directive content/instruction |
| `priority` | integer | No | Priority level (higher = more important) |
| `is_active` | boolean | No | Whether the directive is active (default: `true`) |
| `tags` | list[string] | No | Execution scope for the directive. Empty/omitted (default) means global; non-empty means reflect must use a matching tag scope |
---
### delete_directive
Delete a directive by ID.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `directive_id` | string | Yes | The ID of the directive to delete |
---
### list_memories
Browse stored memories with optional filtering and pagination.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Filter by fact type: `world`, `experience`, or `observation` |
| `q` | string | No | Search query to filter memories |
| `limit` | integer | No | Maximum number of results (default: 100) |
| `offset` | integer | No | Number of results to skip for pagination (default: 0) |
---
### get_memory
Retrieve a specific memory by ID.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `memory_id` | string | Yes | The ID of the memory to retrieve |
---
### list_documents
List documents that have been ingested into the memory bank.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | No | Search query to filter documents |
| `limit` | integer | No | Maximum number of results (default: 100) |
| `offset` | integer | No | Number of results to skip for pagination (default: 0) |
---
### get_document
Retrieve a specific document by ID, including its metadata.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `document_id` | string | Yes | The ID of the document to retrieve |
---
### delete_document
Delete a document and all memories linked to it.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `document_id` | string | Yes | The ID of the document to delete |
---
### list_operations
List async operations (retain processing, mental model refresh, etc.) with optional status and type filtering.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter by status: `pending`, `running`, `completed`, `failed`, `cancelled` |
| `type` | string | No | Filter by type: `retain`, `consolidation`, `refresh_mental_model`, `file_convert_retain`, `webhook_delivery` |
| `limit` | integer | No | Maximum number of results, 1–100 (default: 20) |
| `offset` | integer | No | Number of results to skip for pagination (default: 0) |
| `exclude_parents` | boolean | No | Exclude parent batch operations (default: false) |
---
### get_operation
Get the status and details of an async operation.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `operation_id` | string | Yes | The ID of the operation to check |
---
### cancel_operation
Cancel a pending or running async operation.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `operation_id` | string | Yes | The ID of the operation to cancel |
---
### list_tags
List all unique tags used in a bank, optionally filtered by pattern.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | No | Glob pattern to filter tags (e.g., `project:*`) |
| `limit` | integer | No | Maximum number of results (default: 100) |
| `offset` | integer | No | Number of results to skip for pagination (default: 0) |
---
### get_bank
Get information about a memory bank, including its name, mission, and disposition.
---
### get_bank_stats (multi-bank mode only)
Get statistics for a memory bank (node/link counts).
---
### update_bank
Update a memory bank's configuration. Updates the bank's name and/or any bank-level configuration fields — only provided fields are updated; omitted fields remain unchanged.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | No | Human-friendly display name for the bank |
| `mission` | string | No | **Deprecated** — alias for `config_updates.reflect_mission` |
| `config_updates` | object | No | Dictionary of configuration fields to update. Supports all bank-configurable fields (see below). Non-configurable or credential fields are rejected |
The `config_updates` object accepts any bank-configurable field by its Python field name, including:
- `reflect_mission` — mission/context for Reflect operations
- `retain_mission` — steers what gets extracted during `retain()`
- `retain_extraction_mode` — `concise` (default), `verbose`, `custom`, `verbatim`, or `chunks`
- `retain_custom_instructions` — custom extraction prompt (active when mode is `custom`)
- `retain_chunk_size` — target maximum characters for each content chunk
- `retain_structured_chunk_size` — maximum characters for a single JSONL line or conversation turn to keep whole
- `retain_chunk_batch_size` — number of chunks to process in parallel
- `enable_observations` — toggle observation consolidation after `retain()`
- `observations_mission` — controls observation synthesis rules
- `disposition_skepticism` — critical evaluation level (1–5)
- `disposition_literalism` — literal vs. abstract interpretation (1–5)
- `disposition_empathy` — emotional context consideration (1–5)
- `entity_labels` — controlled vocabulary for entity classification
- `entities_allow_free_form` — allow labels outside `entity_labels`
- `recall_include_chunks` — include raw chunks in recall results
- `recall_prefer_observations` — default `prefer_observations` for recall calls that omit it
- `recall_max_tokens` — max tokens for recall results
- `mcp_enabled_tools` — tool allowlist for this bank
---
### delete_bank
Permanently delete a memory bank and all its data (memories, documents, entities, mental models).
---
### clear_memories
Clear all memories from a bank without deleting the bank itself. Optionally filter by fact type to only clear specific kinds of memories.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | No | Fact type to clear: `world`, `experience`, or `observation`. If not specified, clears all |
---
### get_knowledge_base_tree
Browse the knowledge base as a nested tree of folders and pages. Each page reports `is_stale`: `false` means it is provably up to date, `true` means the bank has been written to since the page last refreshed.
---
### search_knowledge_base
Find knowledge pages by relevance (hybrid BM25 + vector search over page names and content). Returns ranked pages with a snippet each.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | What to search for |
| `limit` | integer | No | Maximum pages to return, 1–50 (default: 10) |
---
### get_knowledge_page
Read a knowledge page as a markdown document (YAML frontmatter + synthesized body).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `page_id` | string | Yes | The ID of the page to read (a `kp-...` node id) |
---
### create_knowledge_folder
Create a folder in the knowledge base. Folders group pages and hold no content of their own.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Folder name |
| `parent_id` | string | No | Parent folder id (a `kf-...` node id). Omit to create at the top level |
---
### create_knowledge_page
Create a page — a living document whose content is synthesized from the bank's memories by running `source_query`. Content is generated asynchronously; use the returned `operation_id` to track completion.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Page name (unique within its folder) |
| `source_query` | string | Yes | The question this page answers and rebuilds itself from |
| `parent_id` | string | No | Parent folder id (a `kf-...` node id). Omit to create at the top level |
| `tags` | array | No | Tags scoping which memories the page is built from |
| `max_tokens` | integer | No | Maximum tokens for the generated content (default: 4096) |
| `trigger` | object | No | Refresh policy — see [Trigger settings](#trigger-settings). Omitted fields keep the knowledge-page defaults: `delta` rebuilds from consolidated observations after each consolidation, ignoring sibling pages |
| `refresh_after_consolidation` | boolean | No | Legacy shorthand for `trigger.refresh_after_consolidation` |
Set `trigger.refresh_cron` to move a page off consolidation-driven rebuilds and
onto a fixed UTC schedule — the two are mutually exclusive, so the cron clears
the auto-refresh while leaving the page's `mode` and `fact_types` alone.
---
### update_knowledge_node
Rename or move a folder/page, and/or update a page's options. Only the arguments you pass are changed. Changing `source_query` schedules an async refresh so the page rebuilds against the new question.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `node_id` | string | Yes | The folder (`kf-...`) or page (`kp-...`) to update |
| `name` | string | No | New name for the node |
| `parent_id` | string | No | Folder id to move the node into, or `"root"` to move it to the top level |
| `source_query` | string | No | Pages only — the new question the page answers |
| `tags` | array | No | Pages only — replacement tag list (pass `[]` to clear) |
| `max_tokens` | integer | No | Pages only — new maximum tokens for the generated content |
| `trigger` | object | No | Pages only — refresh-policy fields to change; see [Trigger settings](#trigger-settings). This is a patch: omitted fields keep their current values |
| `refresh_after_consolidation` | boolean | No | Pages only — legacy shorthand for `trigger.refresh_after_consolidation` |
---
### delete_knowledge_node
Delete a folder or page and its whole subtree. Each deleted page takes its backing mental model with it.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `node_id` | string | Yes | The folder (`kf-...`) or page (`kp-...`) to delete |
:::note
Exporting the knowledge base is deliberately not an MCP tool — it returns the whole
bank as a single markdown bundle. Use the HTTP endpoint
`GET /v1/default/banks/{bank_id}/knowledge-base/export` instead.
:::
---
## Integration with AI Assistants
The MCP server can be used with any MCP-compatible AI assistant. See the [Authentication](#authentication) section above for Claude Code and Claude Desktop configuration examples.
Each user can have their own configuration pointing to their personal memory bank using either:
- A bank-specific URL path like `/mcp/alice/` (recommended)
- The `X-Bank-Id` header
references/developer/memory-defense/index.md
---
sidebar_position: 95
---
# Memory Defense
Hindsight scrubs secrets and PII from retain content using a 45-pattern regex set. Each match is replaced with a `[REDACTED:type]` marker before content reaches memory units or the document body. The feature is configured per bank and disabled by default.
## How it works
Memory Defense is opt-in per bank. The extension is always present, but it sits dormant until you give a bank a policy that turns it on. When a policy is set, every memory the agent writes to that bank is scanned before it lands in storage. When the scanner recognizes a credential, an API key, a database connection string, or a known PII format, the matched substring is replaced with a redaction marker like `[REDACTED:github_token]`.
The scrubbed version is what actually gets stored. Memory units and document bodies persist the redacted text, so future recall responses, exports, and reflect operations never see the original secret.
A policy only affects future retain calls on the bank where it is set. Existing memories are not retroactively scanned when you add or change a policy.
## Configuring Memory Defense
Memory Defense is configured per bank via the bank's `memory_defense` config field. You can set the policy at bank creation time or update it later via `PATCH /v1/{tenant}/banks/{bank_id}/config`.
The open-source version implements the `sensitive_data` rule with two possible actions:
- **`redact`** — replace each matched secret with a `[REDACTED:type]` marker and store the scrubbed memory.
- **`block`** — drop any item that contains a match. If every item in a retain request is blocked, the call returns `422`.
A minimal policy:
```json
{
"memory_defense": {
"enabled": true,
"rules": [
{ "on": "sensitive_data", "action": "redact" }
]
}
}
```
Once that policy is on a bank, every retain to that bank is screened with the 45 redaction patterns documented below.
:::note Existing memories are not retroactively scanned
Enabling Memory Defense on a bank only affects future retain calls. Memories already in the bank are not re-scanned or modified when you add or change a policy. If you need to scrub a bank that already contains unredacted content, you have to re-ingest the affected memories or remove them manually.
:::
### Disabled by default
Memory Defense is off on every bank until you set a policy. A bank with no `memory_defense` field, with `enabled: false`, or with no `sensitive_data` rule is treated identically: the extension returns ALLOW and content passes through unchanged. To stop redacting on a bank that has it on, set `enabled: false` or remove the policy.
## Notifications
When an item is redacted or blocked, Hindsight fires a [`memory_defense.triggered` webhook](../api/webhooks.md#memory_defensetriggered) if a webhook on the bank is subscribed to that event type. The payload reports the action taken, the document ID, and which redaction patterns matched — useful for routing security alerts to a SIEM or Slack. Clean items fire no event.
The same redact/block decisions are also recorded as `memory_defense` entries in the [audit log](../configuration.md#audit-logging) (when audit logging is enabled), with the action and matched pattern labels in the entry metadata.
## Patterns covered
The 45 bundled patterns cover the categories below.
### AI and LLM providers
| Label | Catches |
|---|---|
| `anthropic_key` | `sk-ant-...` |
| `openai_key`, `openai_project_key`, `openai_admin_key` | `sk-...`, `sk-proj-...`, `sk-admin-...` |
| `google_api_key` | `AIza...` (39 chars) |
| `google_oauth_token` | `ya29.<token>` |
| `xai_key` | `xai-...` |
| `groq_key` | `gsk_...` |
| `huggingface_token` | `hf_...` |
| `replicate_token` | `r8_...` |
| `perplexity_key` | `pplx-...` |
| `databricks_token` | `dapi<hex32>` |
### Cloud providers
| Label | Catches |
|---|---|
| `aws_access_key` | `AKIA<16>` |
| `aws_session_token` | `ASIA<16>` |
| `digitalocean_token` | `dop_v1_<hex64>` |
### Source control and CI
| Label | Catches |
|---|---|
| `github_fg_pat` | `github_pat_...` |
| `github_token` | `ghp_<36>` |
| `github_app_token` | `ghs_<36>` |
| `github_user_token` | `ghu_<36>` |
| `github_refresh` | `ghr_<36>` |
| `github_oauth` | `gho_<36>` |
| `gitlab_pat` | `glpat-...` |
| `npm_token` | `npm_...` |
| `pypi_token` | `pypi-AgEIcHlwaS5vcmc...` |
### Payment processors
| Label | Catches |
|---|---|
| `stripe_secret` | `sk_live_...`, `sk_test_...` |
| `stripe_restricted` | `rk_live_...`, `rk_test_...` |
| `square_token` | `sq0...` |
| `braintree_token` | `access_token$production$...` |
### Communications and email
| Label | Catches |
|---|---|
| `slack_token` | `xoxb-`, `xoxp-`, `xoxa-`, `xoxr-` |
| `slack_webhook` | `https://hooks.slack.com/services/...` |
| `twilio_api_key` | `SK<hex32>` |
| `twilio_account_sid` | `AC<hex32>` |
| `sendgrid_key` | `SG.<22>.<43>` |
| `mailgun_key` | `key-<32>` |
| `discord_bot` | `<MNO><23>.<6>.<27>` |
| `telegram_bot` | `<8-10 digits>:<35>` |
### Commerce
| Label | Catches |
|---|---|
| `shopify_token` | `shpat_<hex32>` |
### Database connection strings
| Label | Catches |
|---|---|
| `db_url_postgres` | `postgres://user:pass@host` or `postgresql://...` |
| `db_url_mysql` | `mysql://user:pass@host` |
| `db_url_mongodb` | `mongodb://user:pass@host` or `mongodb+srv://...` |
### Private keys, JWTs, and generic credentials
| Label | Catches |
|---|---|
| `private_key_pem` | `-----BEGIN ... PRIVATE KEY-----` PEM blocks |
| `jwt` | `eyJ<header>.eyJ<payload>.<signature>` |
### PII (US defaults)
| Label | Catches |
|---|---|
| `credit_card` | 13 to 19 digits with regular separators |
| `ssn_us` | `123-45-6789` shape |
references/developer/mental-models.md
# Mental Models
A **mental model** is a standing answer to a question about a bank. You define the question once; Hindsight writes the answer, keeps it stored, and rewrites it in the background as the bank learns more.
Where [observations](./observations.md) are produced automatically and are atomic — one belief at a time — a mental model is deliberately curated: you decide which questions deserve a permanent, always-current answer.
**Figure: Mental Models.** An animated diagram on the docs site; its narration, step by step:
- **create**
1. You define the question once, plus how it should stay current.
2. Hindsight writes the answer with reflect, in the background. Tags limit which memories it may read.
3. It reads consolidated observations first, and raw facts to check details.
4. The answer is stored as a document, together with the evidence it was built from.
- **stay current**
1. The bank keeps learning. Consolidation writes a new observation inside this model’s scope.
2. The trigger checks first: is there a memory in this model’s scope newer than the newest one its last refresh read? Activity elsewhere in the bank does not count.
3. In delta mode the refresh reads only the memories that arrived since the last one.
4. Instead of rewriting, it applies edits: one bullet added to Research. Everything else is copied through untouched, and the old version goes to history.
- **read**
1. When your app needs the answer, it asks for the model by id.
2. That is a database read: no retrieval, no LLM call, no waiting.
3. Everyone asking gets the same document. Reflect reads mental models first too, and trusts one only while it is fresh.
---
## The Answer Is Already Written
The reason to use a mental model is speed. Reasoning over a bank's memory is expensive — retrieval, synthesis, an LLM writing an answer. A mental model moves all of that off the request path: the work happens in the background, ahead of time, and your application simply **reads the current version**.
Fetching a mental model is a database read. No retrieval, no synthesis, no LLM call, no waiting. An agent that boots by loading its mental models starts with a page of settled knowledge instead of spending its first few seconds rediscovering it.
This also makes answers **consistent**. Two users asking the same question get the same document, because there is only one document — not two independently generated answers that happen to disagree on the details.
Mental models are also the first thing [reflect](./reflect.md) reaches for. Its retrieval ladder goes:
| Layer | Produced by | Granularity |
|---|---|---|
| **Mental models** | You, explicitly | A whole document per question |
| **Observations** | Consolidation, automatically | One belief per fact cluster |
| **Raw facts** | Retain, automatically | One fact per statement |
Each layer is a cheaper, more settled version of the one below it. If the first step turns up a mental model that is fresh and covers the question, reflect can answer from it instead of descending through observations and raw facts — the same saving, applied inside the agentic loop.
---
## Always Current, Without Asking
A mental model is not a cached answer that goes stale silently. Hindsight tracks whether new memories have arrived that the model is supposed to cover, and rebuilds it when they have — either as soon as new knowledge is consolidated, or on a schedule you set.
The check comes first, and it is scoped: a rebuild only happens when something *within this model's own scope* actually changed. A busy bank does not cause unrelated models to churn, and a scheduled rebuild over an unchanged bank costs nothing.
When a model is handed to the reflect agent, it comes with a freshness signal — whether memories in its scope have landed since it was last written. A model that has fallen behind is still shown, but it no longer short-circuits retrieval, and the agent is expected to check it against the layers below rather than trust it blindly.
---
## Stable Across Rewrites
A document that is rewritten hundreds of times has a problem an LLM cannot solve by being asked nicely: told to "preserve the unchanged parts", it will still drift. Bullets become numbers, casing shifts, sentences get quietly paraphrased. Generating text is what the model does; copying it verbatim is not.
Hindsight can instead refresh a model **incrementally** — applying only the changes the new knowledge implies, and leaving everything else physically untouched rather than regenerating and hoping. A long-lived playbook stays the document you wrote, with the new parts added, instead of slowly becoming a different document that says roughly the same thing.
---
## Scope and Isolation
A mental model's tags decide two things: which memories it is allowed to read, and which callers are allowed to see it. A model scoped to one customer, team, or user is built only from that scope's memories and surfaces only for requests in that scope — the same isolation rules that govern the rest of the bank, applied to synthesized knowledge.
---
## Provenance
A mental model is not free-floating prose. It records the facts and observations it was built from, and it keeps the previous version of its content every time it changes. You can see what the model said last month and what evidence it was standing on — which matters when a model states something surprising and someone needs to know where it came from.
---
**See also:** [Mental Models API](./api/mental-models.md) — creating, refreshing, and configuring them, including refresh triggers, scoping options, and history.
references/developer/models.md
# Models
Hindsight uses several machine learning models for different tasks.
## Overview
- **LLM** — Fact extraction, reasoning, and generation. Provider-specific, fully configurable.
- **Embedding** — Vector representations for semantic search. Default: `BAAI/bge-small-en-v1.5`.
- **Cross-Encoder** — Reranking search results. Default: `cross-encoder/ms-marco-MiniLM-L-6-v2`.
Embedding and cross-encoder models are downloaded automatically from HuggingFace on first run.
---
## LLM
Used for fact extraction, entity resolution, mental model consolidation, and answer synthesis.
**Supported providers:**
- OpenAI
- OpenAI Responses
- Anthropic
- Google Gemini
- Vertex AI
- Groq
- Ollama
- Ollama Cloud
- LM Studio
- llama.cpp
- MiniMax
- DeepSeek
- z.ai
- opencode-go
- Atlas Cloud
- Meta Model API
- Volcano Engine
- OpenRouter
- Requesty
- OpenAI Codex
- ChatGPT
- Claude Code
- Cursor
- GitHub Copilot
- AWS Bedrock
- Fireworks AI
- Nous Portal
- SuperGrok (OAuth)
- OpenAI Compatible
- LiteLLM (100+)
Also supports **any OpenAI-compatible API** (e.g., Azure OpenAI, Together AI, Fireworks) and **100+ providers via LiteLLM** (e.g., AWS Bedrock, Azure OpenAI, Together AI).
> **💡 OpenAI-Compatible Providers**
>
Hindsight works with any provider that exposes an OpenAI-compatible API. Set `HINDSIGHT_API_LLM_PROVIDER=openai` and point `HINDSIGHT_API_LLM_BASE_URL` at the endpoint that serves `/chat/completions` — for most providers that is the URL ending in `/v1`, **not** the account or resource root.
**Azure OpenAI does not serve the API at the resource root**, so `https://<resource>.openai.azure.com` on its own returns `404 Resource not found`. See [Azure OpenAI Setup](#azure-openai-setup) for the two URL shapes that work.
The `openai` provider talks to the **Chat Completions API** (`/v1/chat/completions`). For the newer **Responses API** (`/v1/responses`), use `HINDSIGHT_API_LLM_PROVIDER=openai-responses` — see the tip below. Both accept a custom `HINDSIGHT_API_LLM_BASE_URL`, so an OpenAI-compatible endpoint that exposes `/v1/responses` works the same way as a Chat Completions one.
See [Configuration](./configuration.md#llm-provider) for setup examples.
> **💡 OpenAI Responses API (reasoning + tools together)**
>
Set `HINDSIGHT_API_LLM_PROVIDER=openai-responses` to call OpenAI's **Responses API** (`/v1/responses`) instead of Chat Completions.
Why it exists: some reasoning models — e.g. `gpt-5.6-terra` — **reject `reasoning_effort` when function tools are present** on Chat Completions (HTTP 400 unless `reasoning_effort="none"`). Reflect is a tool-calling loop, so on the `openai` (Completions) provider that forces the whole operation — including the final synthesis — to run with reasoning disabled. (Hindsight retries that 400 once with `reasoning_effort="none"` so the call still succeeds — but it succeeds without reasoning.) The Responses API keeps the model's chain-of-thought as a first-class reasoning item, so **reasoning and tools coexist**: reflect's search loop runs with a real `HINDSIGHT_API_LLM_REASONING_EFFORT` (e.g. `high`).
Recommended for reasoning models (gpt-5.x, o-series) that use tools. It also honors a custom `HINDSIGHT_API_LLM_BASE_URL`, so any OpenAI-compatible endpoint exposing `/v1/responses` (gateways, Azure-style deployments) can be used just like the Chat Completions path.
See [Configuration](./configuration.md#llm-provider) for setup examples.
> **ℹ️ Reasoning/thinking models and `max_tokens`**
>
On a thinking model (Gemini 2.5+/3.x, GPT-5/o-series, Grok reasoning, Claude extended thinking) the provider's output budget covers **reasoning tokens plus visible output** — the reasoning is billed against the same `max_output_tokens`/`max_completion_tokens` cap. A small cap can therefore be fully consumed by reasoning, leaving the visible answer truncated mid-word.
Hindsight keeps the reflect/mental-model `max_tokens` meaning **visible page length**: it is applied as a prompt-level target plus a post-hoc rewrite, **not** as a hard cap on the provider call. Reflect's synthesis call is uncapped by default so reasoning never starves the answer. If you want a hard cost ceiling on that call, set `HINDSIGHT_API_REFLECT_MAX_COMPLETION_TOKENS` — but leave enough headroom above your page length for reasoning, or thinking models will truncate again.
When a Gemini call does hit its cap, Hindsight logs a `truncated at max_output_tokens` warning instead of returning the half-written text as a silent success.
> **💡 AWS Bedrock**
>
Set `HINDSIGHT_API_LLM_PROVIDER=bedrock` to use AWS Bedrock models directly. Model names use Bedrock model IDs (e.g., `us.amazon.nova-2-lite-v1:0`). No API key is required — authentication uses AWS credentials (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION_NAME`) or IAM roles. For 50% cost savings on throughput, set `HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER=flex` (see [Configuration](./configuration.md#llm-provider)).
See [Configuration](./configuration.md#llm-provider) for setup examples.
> **💡 Built-in llama.cpp (fully local, no API key)**
>
Set `HINDSIGHT_API_LLM_PROVIDER=llamacpp` to run a built-in llama.cpp server with no external dependencies. A Gemma 4 E2B GGUF model (~3.5 GB) is auto-downloaded on first run. Requires the `local-llm` extra: `pip install 'hindsight-api-slim[local-llm]'`.
The published Docker image does not bundle `llama-cpp-python` (to keep the image small). For a runnable Docker setup that adds it on top, see [`docker/docker-compose/local-llm/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/local-llm).
See [Configuration](./configuration.md#built-in-llamacpp) for all options.
> **💡 LiteLLM Provider (Azure, Together AI, and more)**
>
Set `HINDSIGHT_API_LLM_PROVIDER=litellm` to use any model supported by [LiteLLM](https://docs.litellm.ai/docs/providers), including **Azure OpenAI**, **Together AI**, **Fireworks AI**, and many more. Model names use LiteLLM's provider prefix format (e.g., `azure/gpt-4o`).
See [Configuration](./configuration.md#llm-provider) for setup examples.
> **💡 LiteLLM Router (fallback chains, load-balancing, per-deployment limits)**
>
Set `HINDSIGHT_API_LLM_PROVIDER=litellmrouter` to run the default LLM through [LiteLLM's Router](https://docs.litellm.ai/docs/routing) — ordered fallback across deployments, load-balanced same-tier routing, weighted picks, per-deployment `rpm`/`tpm` limits, and cooldowns are all available via the [`Router` config](https://docs.litellm.ai/docs/routing#fallbacks). Hindsight passes the JSON config through verbatim.
See [Configuration](./configuration.md#llm-router-litellm-router) for setup.
### Provider Capabilities
Beyond basic generation, some providers support optional features that lower cost or latency. Hindsight uses each feature automatically when the configured provider supports it.
| Provider | Batch API | Explicit prompt caching |
|----------|:---------:|:-----------------------:|
| OpenAI (`openai`) | ✅ | — |
| OpenAI Responses (`openai-responses`) | — | — |
| Anthropic (`anthropic`) | — | — |
| Google Gemini (`gemini`) | ✅ | ✅ |
| Vertex AI (`vertexai`) | — | ✅ |
| Groq (`groq`) | ✅ | — |
| Ollama (`ollama`) | — | — |
| Ollama Cloud (`ollama-cloud`) | — | — |
| LM Studio (`lmstudio`) | — | — |
| llama.cpp (`llamacpp`) | — | — |
| MiniMax (`minimax`) | — | — |
| DeepSeek (`deepseek`) | — | — |
| z.ai (`zai`) | — | — |
| opencode-go (`opencode-go`) | — | — |
| Atlas Cloud (`atlas`) | — | — |
| Meta Model API (`meta`) | — | — |
| Volcano Engine (`volcano`) | — | — |
| OpenRouter (`openrouter`) | — | — |
| Requesty (`requesty`) | — | — |
| OpenAI Codex (`openai-codex`) | — | — |
| ChatGPT (`chatgpt`) | — | — |
| Claude Code (`claude-code`) | — | — |
| Cursor (`cursor`) | — | — |
| GitHub Copilot (`github-copilot`) | — | — |
| AWS Bedrock (`bedrock`) | — | — |
| Fireworks AI (`fireworks`) | ✅ | — |
| Nous Portal (`nous`) | — | — |
| SuperGrok (OAuth) (`xai-oauth`) | — | — |
| LiteLLM (100+) (`litellm`) | — | — |
- **Batch API** — submits bulk retain extraction through the provider's asynchronous batch endpoint, typically at ~50% lower cost. Used automatically when available; otherwise calls run synchronously.
- **Explicit prompt caching** — reuses the large, fixed system prefix that retain (fact extraction), consolidation, and the reflect tool-loop send on every call, billing it at the provider's cached-input rate. On Gemini/Vertex this uses the `CachedContent` API. **On by default**; disable with `HINDSIGHT_API_LLM_PROMPT_CACHE_ENABLED=false`. Hindsight structures these prompts so the cached prefix is **bank-agnostic** — one cache is shared across all banks rather than one per bank/mission, and creation soft-fails to an uncached call, so it never breaks a request.
> **📝 Note**
>
A blank "Explicit prompt caching" cell does not mean a provider has no caching. OpenAI, for example, caches a stable leading prompt prefix **automatically** server-side, so it benefits with no configuration; Anthropic supports caching via `cache_control` breakpoints which can be wired up through the same provider hook. The column tracks only Hindsight's explicit `get_or_create_cached_prefix` hook, which Gemini/Vertex implement today.
### Benchmarks
Not sure which model to use? The **[Model Leaderboard](https://benchmarks.hindsight.vectorize.io/)** benchmarks models across accuracy, speed, cost, and reliability for retain, reflect, and observation consolidation so you can pick the right trade-off for your use case.
[](https://benchmarks.hindsight.vectorize.io/)
### Tested Models
The following models have been tested and verified to work correctly with Hindsight:
| Provider | Model |
|----------|-------|
| **OpenAI** | `gpt-5.2` |
| **OpenAI** | `gpt-5` |
| **OpenAI** | `gpt-5-mini` |
| **OpenAI** | `gpt-5-nano` |
| **OpenAI** | `gpt-4.1-mini` |
| **OpenAI** | `gpt-4.1-nano` |
| **OpenAI** | `gpt-4o-mini` |
| **Anthropic** | `claude-sonnet-4-20250514` |
| **Anthropic** | `claude-3-5-sonnet-20241022` |
| **Gemini** | `gemini-3.5-flash` |
| **Gemini** | `gemini-3.1-pro-preview` |
| **Gemini** | `gemini-3.1-flash-lite` |
| **Groq** | `openai/gpt-oss-120b` |
| **Groq** | `openai/gpt-oss-20b` |
| **Meta** | `muse-spark-1.3` |
### Provider Default Models
Each provider has a recommended default model that's used when `HINDSIGHT_API_LLM_MODEL` is not explicitly set. This makes configuration simpler - just specify the provider and get a sensible default:
| Provider | Default Model |
|----------|--------------|
| `openai` | `gpt-4o-mini` |
| `openai-responses` | `gpt-5.6` |
| `anthropic` | `claude-haiku-4-5` |
| `gemini` | `gemini-3.5-flash` |
| `vertexai` | `google/gemini-3.1-flash-lite` |
| `groq` | `openai/gpt-oss-120b` |
| `ollama` | `gemma3:12b` |
| `ollama-cloud` | `gemma3:12b` |
| `lmstudio` | `local-model` |
| `llamacpp` | `gemma-4-e2b-it` (auto-downloaded GGUF) |
| `minimax` | `MiniMax-M3` |
| `deepseek` | `deepseek-v4-flash` |
| `zai` | `glm-4.5-flash` |
| `opencode-go` | `deepseek-v4-flash` |
| `atlas` | `deepseek-ai/deepseek-v4-pro` |
| `meta` | `muse-spark-1.3` |
| `volcano` | `doubao-pro-32k` |
| `openrouter` | `qwen/qwen3.5-9b` |
| `requesty` | `openai/gpt-4o-mini` |
| `openai-codex` | `gpt-5.6-luna` |
| `chatgpt` | `gpt-6-luna` |
| `claude-code` | `claude-sonnet-4-5-20250929` |
| `cursor` | `auto` |
| `github-copilot` | `gpt-5.6-terra` |
| `bedrock` | `us.amazon.nova-2-lite-v1:0` |
| `fireworks` | `accounts/fireworks/models/llama-v3p1-8b-instruct` |
| `nous` | `deepseek/deepseek-v4-flash` |
| `xai-oauth` | `grok-4.5` |
| `litellm` | `gpt-4o-mini` |
**Example:** Setting just the provider uses its default model:
```bash
# Uses claude-haiku-4-5 automatically
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
```
You can override the default by explicitly setting `HINDSIGHT_API_LLM_MODEL`:
```bash
# Override to use Sonnet instead
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929
```
This also applies to per-operation overrides:
```bash
# Global: OpenAI gpt-4o-mini (default)
export HINDSIGHT_API_LLM_PROVIDER=openai
# Retain: Anthropic claude-haiku-4-5 (default)
export HINDSIGHT_API_RETAIN_LLM_PROVIDER=anthropic
```
### Using Other Models
Other LLM models not listed above may work with Hindsight, but they must support **at least 65,000 output tokens** to ensure reliable fact extraction. If you need support for a specific model that doesn't meet this requirement, please [open an issue](https://github.com/hindsight-ai/hindsight/issues) to request an exception.
> **💡 Models with Limited Output Tokens**
>
If your model only supports 32k or fewer output tokens (e.g., some older models), you can reduce the retain completion token limit:
```bash
# For models that support 32k output tokens
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=32000
# For models that support 16k output tokens
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=16000
```
**Important:** `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` must be greater than `HINDSIGHT_API_RETAIN_CHUNK_SIZE` (default: 3000). The system will validate this on startup and provide an error message if the configuration is invalid.
> **⚠️ Groq free tier is not suitable for Hindsight**
>
Groq's free tier only allows 8,000 tokens per minute — far below what Hindsight needs for a single retain call (~64k). Free-tier Groq models therefore can't be used with Hindsight; use a paid Groq tier or a different provider.
### Configuration
```bash
# Groq (recommended)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
# OpenAI (Chat Completions API, /v1/chat/completions)
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# OpenAI Responses API (/v1/responses) — reasoning + tools together
export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-5.6 # reasoning model; e.g. gpt-5.6-terra
export HINDSIGHT_API_LLM_REASONING_EFFORT=high # sent alongside tools, unlike Completions
# Optional: point at any OpenAI-compatible endpoint exposing /v1/responses
# export HINDSIGHT_API_LLM_BASE_URL=https://your-gateway.example.com/v1
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-3.5-flash
# Anthropic
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Ollama (local)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3
# Ollama Cloud (hosted Ollama endpoint, requires API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama-cloud
export HINDSIGHT_API_LLM_API_KEY=your-ollama-cloud-api-key
export HINDSIGHT_API_LLM_MODEL=gemma3:12b
# LM Studio (local)
export HINDSIGHT_API_LLM_PROVIDER=lmstudio
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
export HINDSIGHT_API_LLM_MODEL=your-local-model
# MiniMax (1M context window)
export HINDSIGHT_API_LLM_PROVIDER=minimax
export HINDSIGHT_API_LLM_API_KEY=your-minimax-api-key
export HINDSIGHT_API_LLM_MODEL=MiniMax-M3 # or MiniMax-M2.7 for the previous generation
# MiniMax-M3.1-* always thinks (Hindsight sends thinking=adaptive) and defaults to max effort;
# lower it with: export HINDSIGHT_API_LLM_REASONING_EFFORT=low
# DeepSeek (https://api.deepseek.com)
export HINDSIGHT_API_LLM_PROVIDER=deepseek
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash # or deepseek-v4-pro / deepseek-chat / deepseek-reasoner
# z.ai (Zhipu GLM series, OpenAI-compatible, https://z.ai)
export HINDSIGHT_API_LLM_PROVIDER=zai
export HINDSIGHT_API_LLM_API_KEY=your-zai-api-key
export HINDSIGHT_API_LLM_MODEL=glm-4.5-flash # or glm-4.5-air for the paid tier
# opencode-go (OpenAI-compatible)
export HINDSIGHT_API_LLM_PROVIDER=opencode-go
export HINDSIGHT_API_LLM_API_KEY=your-opencode-go-api-key
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
# Atlas Cloud (OpenAI-compatible, https://www.atlascloud.ai)
export HINDSIGHT_API_LLM_PROVIDER=atlas
export HINDSIGHT_API_LLM_API_KEY=your-atlascloud-api-key # base_url defaults to https://api.atlascloud.ai/v1
export HINDSIGHT_API_LLM_MODEL=deepseek-ai/deepseek-v4-pro # reasoning model; also Qwen / GLM / Kimi / MiniMax, etc.
# Meta Model API (OpenAI-compatible, https://ai.developer.meta.com)
export HINDSIGHT_API_LLM_PROVIDER=meta
export HINDSIGHT_API_LLM_API_KEY=your-meta-model-api-key # base_url defaults to https://api.meta.ai/v1
export HINDSIGHT_API_LLM_MODEL=muse-spark-1.3 # or muse-spark-1.2 / -contributor variants
# Muse Spark always reasons — raise the deadlines (see "Meta Model API Setup" below)
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=300
export HINDSIGHT_API_LLM_TIMEOUT=300
# Nous Portal (OpenAI-compatible; no API key — uses your `hermes portal` login)
export HINDSIGHT_API_LLM_PROVIDER=nous
export HINDSIGHT_API_LLM_MODEL=deepseek/deepseek-v4-flash # any Nous-hosted slug
# No API key needed — reads a rotating JWT from ~/.hermes/auth.json (see "Nous Portal Setup" below)
# Cursor subscription via the cursor-agent CLI (no API key — uses `cursor-agent login`)
export HINDSIGHT_API_LLM_PROVIDER=cursor
export HINDSIGHT_API_LLM_MODEL=auto # or any id from `cursor-agent --list-models`
# An agent CLI turn takes 15-30s — raise the timeouts (see "Cursor Setup" below)
export HINDSIGHT_API_LLM_TIMEOUT=300
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=180
# SuperGrok subscription via device-code OAuth (no API key; the subscription lane, not
# xAI API support — for an api.x.ai API key use `openai` with a base URL instead)
export HINDSIGHT_API_LLM_PROVIDER=xai-oauth
# export HINDSIGHT_API_LLM_MODEL=grok-4.5 # defaults to grok-4.5
# No API key needed — reads an OAuth grant from ~/.hindsight/xai_oauth.json (see "SuperGrok Subscription Setup" below)
# Vertex AI (Google Cloud)
export HINDSIGHT_API_LLM_PROVIDER=vertexai
export HINDSIGHT_API_LLM_MODEL=gemini-3.1-flash-lite
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-gcp-project-id
# Optional: region (default: us-central1)
# export HINDSIGHT_API_LLM_VERTEXAI_REGION=us-central1
# Optional: service account key (otherwise uses ADC)
# export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json
```
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance.md) for optimization strategies.
---
### GitHub Copilot Setup
Use a GitHub Copilot subscription for Hindsight's extraction, consolidation, and reflection calls through the official GitHub Copilot SDK.
**Prerequisites:**
- An active GitHub Copilot entitlement
- Copilot CLI signed in under the same operating-system user that runs Hindsight
**Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=github-copilot
export HINDSIGHT_API_LLM_MODEL=gpt-5.6-terra
# No HINDSIGHT_API_LLM_API_KEY is needed.
```
The provider starts one shared Copilot runtime for all Hindsight LLM lanes. That runtime uses a hook-free temporary `COPILOT_HOME` containing only the signed-in account selection; the credential itself is resolved by the runtime from the system keychain or an existing `gh` CLI login. Each call then uses an isolated transient session with repository instructions, skills, Copilot memory, built-in tools, and the cross-session store disabled. This prevents the memory integration from recursively retaining its own extraction calls.
For automation, the Copilot SDK also accepts `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN`. With one of those set, Copilot CLI never has to have been run on the host, so containers and CI images work with no `~/.copilot` at all. GitHub Actions and server-to-server deployments must have the appropriate Copilot organization policy and token permissions.
**Important notes:**
- Usage counts against the authenticated account or organization's Copilot allowance and AI Credits.
- `HINDSIGHT_API_LLM_BASE_URL` optionally points to an existing headless Copilot runtime, such as `http://127.0.0.1:4321`; it is not an LLM-provider endpoint for this provider.
- GitHub-hosted Copilot sessions do not expose temperature or maximum-output-token controls through the SDK, so those Hindsight settings are not applied.
- Embeddings and reranking continue to use Hindsight's separately configured providers; their local defaults require no API key.
---
### ChatGPT Setup (Sign in with ChatGPT)
The `chatgpt` provider runs Hindsight's LLM calls on your ChatGPT plan through
OpenAI's documented [Sign in with ChatGPT flow for open-source apps](https://developers.openai.com/siwc/token-sharing-open-source).
Hindsight registers itself as its own OAuth client on first sign-in, and every call goes to the public
`https://api.openai.com/v1/responses` endpoint with your OAuth access token. It does not use an API key.
How it differs from `openai-codex`: that provider reuses the Codex CLI's sign-in and calls ChatGPT's private
`chatgpt.com/backend-api/codex` endpoint. `chatgpt` uses the supported route and keeps its own
credentials, so it never shares or rotates the Codex CLI's tokens.
**Prerequisites:**
- A ChatGPT plan that allows plan usage in third-party apps
- A browser on the machine where you run the login
**Setup on a machine with a browser (for example a laptop):**
1. **Sign in:**
```bash
hindsight-admin chatgpt login
```
This opens your browser and waits for the callback on `http://127.0.0.1:1455/auth/callback`
(`--port` changes the port; `--no-browser` prints the URL instead). Approve the request, including
use of your ChatGPT plan. The command validates the ID token and the granted scopes, then writes
`~/.hindsight/chatgpt/credentials.json` (mode `0600`) next to this host's `host_id`.
If you declined plan usage, run `login --enable-plan-usage` to be asked again.
2. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=chatgpt
# export HINDSIGHT_API_LLM_MODEL=gpt-6-luna # the default
# export HINDSIGHT_API_LLM_REASONING_EFFORT=medium # sent as reasoning.effort
```
3. **Check the stored session** (prints account, client id, scopes and expiry, never token values):
```bash
hindsight-admin chatgpt status
```
**Self-hosted server or VM (no browser):** the `127.0.0.1` callback reaches the machine running the
browser, so sign in on your laptop and move the credential file to the server.
1. On the laptop: run `login` as above.
2. Copy the file over SSH and install it on the server with `login --from-file`, as the user that runs Hindsight (add
`--home <dir>` before `login` if the server sets `HINDSIGHT_API_LLM_CHATGPT_HOME`).
`--from-file` keeps the server's own `host_id` (creating one on first use) instead of the laptop's:
```bash
scp ~/.hindsight/chatgpt/credentials.json server:/tmp/chatgpt.json
ssh server 'hindsight-admin chatgpt login --from-file /tmp/chatgpt.json && rm /tmp/chatgpt.json'
```
3. On the laptop, stop using that copy without revoking it:
```bash
hindsight-admin chatgpt logout --local-only
```
Refresh tokens rotate, so after the copy only the server may use and refresh this session. A plain
`logout` would revoke it and end the server's session too. `--local-only` clears the laptop's
tokens but keeps its client registration for a later `login`. `--from-file` refuses to replace a
different account's registration on the server unless you pass `--force`.
The server then owns refreshes. Sign in again on the laptop later if you also want Hindsight there.
**Important notes:**
- Access tokens last one hour and are refreshed automatically (five minutes before expiry, and once on
an HTTP 401). Refresh tokens rotate; refreshes take a file lock and re-read the file first, so
several Hindsight processes can share one credential directory.
- When the refresh token is no longer valid, calls fail with the `login` command to run.
- On ChatGPT Plus, the five-hour usage limit is shared with every other app that uses your plan,
including Codex. When the limit is reached, Hindsight defers retain and consolidation work and
retries it 30 minutes later instead of failing it. Review usage and per-app limits at
[ChatGPT Settings → Usage](https://chatgpt.com/settings/usage).
- The route is a preview: Hindsight sends `store: false` and `stream: true`, omits fields it rejects
(`temperature`, `max_output_tokens`, `user`, and others), and sends system prompts as developer
messages.
- `logout` revokes the session and clears local tokens but keeps the client registration and host id
for the next `login`. `logout --local-only` clears the tokens without revoking.
- `HINDSIGHT_API_LLM_CHATGPT_HOME` relocates the credential directory. Use one directory per
ChatGPT account. The directory must be writable, because each refresh writes the rotated token
back; in a container, mount a writable volume there. If a write fails, the process keeps the new
tokens in memory, but other processes and restarts will not see them.
### OpenAI Codex Setup (ChatGPT Plus/Pro)
Use your ChatGPT Plus or Pro subscription for Hindsight without separate OpenAI Platform API costs.
**Prerequisites:**
- Active ChatGPT Plus or Pro subscription
- Node.js/npm installed (for Codex CLI)
**Setup Steps:**
1. **Install Codex CLI:**
```bash
npm install -g @openai/codex
```
2. **Login with ChatGPT credentials:**
```bash
codex auth login
```
This opens a browser window to authenticate with your ChatGPT account and saves OAuth tokens to `~/.codex/auth.json`.
3. **Verify authentication:**
```bash
ls ~/.codex/auth.json # Should show the auth file exists
```
4. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
# export HINDSIGHT_API_LLM_MODEL=gpt-5.6-luna # the default
# No API key needed - reads from ~/.codex/auth.json automatically
```
5. **Start Hindsight:**
```bash
hindsight-api
```
You can use any model supported by OpenAI Codex CLI
**Important Notes:**
- OAuth tokens are stored in `~/.codex/auth.json`
- Tokens refresh automatically when needed
- Usage is billed to your ChatGPT subscription (not separate API costs)
- For personal development use only (see ChatGPT Terms of Service)
#### Isolating Codex auth for long-running services
By default Hindsight reads Codex credentials from `~/.codex/auth.json` — the
same file the `@openai/codex` CLI, editor plugins, and other agent runtimes use.
This is convenient for local development but can cause a subtle failure mode when
Hindsight runs as a **long-lived service** (systemd unit, container, background
daemon) alongside another Codex process:
- Codex refresh tokens are single-use and rotate on refresh.
- If another process refreshes the shared token, Hindsight's long-running
process is left holding a stale refresh token.
- Recall and `/health` keep working (the database and API are fine), but
`/reflect` fails with an error such as:
```text
Codex refresh_token is permanently invalid (error.code=refresh_token_reused).
Run 'codex auth login' to re-authenticate.
```
To avoid this, give the Hindsight service its **own dedicated Codex auth home**
via the `CODEX_HOME` environment variable. Hindsight honors `CODEX_HOME` exactly
like the `@openai/codex` CLI: when set, it reads `$CODEX_HOME/auth.json` instead
of `~/.codex/auth.json`.
```bash
# Dedicated credentials directory for the Hindsight service
export CODEX_HOME=/var/lib/hindsight/codex
# One-time login into that isolated home (opens a browser / device-code flow)
codex auth login # writes $CODEX_HOME/auth.json
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
hindsight-api
```
For a systemd unit, set it in the service definition so it never shares auth
with an interactive Codex session:
```ini
[Service]
Environment=CODEX_HOME=/var/lib/hindsight/codex
```
After a fresh login into the dedicated home and restarting only the Hindsight
service, `/reflect` uses its own token that other Codex processes will not
rotate out from under it.
`CODEX_HOME` is also honored by the `openai-codex` embeddings provider.
#### Two Codex profiles in one process
`CODEX_HOME` is process-wide, so every Codex provider a Hindsight process builds
resolves the same `auth.json`. To run more than one independently authorized
ChatGPT account — for example, to fail over when the preferred account hits its
usage limit — give each one its own credentials directory with
`HINDSIGHT_API_LLM_CODEX_HOME` (primary) and `HINDSIGHT_API_LLM_<n>_CODEX_HOME`
(indexed [multi-LLM chain](./configuration.md#multi-llm-strategies-failover--round-robin)
members). Each falls back to `CODEX_HOME`, then `~/.codex`, when unset.
```bash
# Two profiles, logged in separately
CODEX_HOME=/var/lib/hindsight/codex-a codex auth login
CODEX_HOME=/var/lib/hindsight/codex-b codex auth login
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_CODEX_HOME=/var/lib/hindsight/codex-a
export HINDSIGHT_API_LLM_1_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_1_CODEX_HOME=/var/lib/hindsight/codex-b
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover"}'
```
Token refresh is coordinated per auth-file path, so the two profiles refresh
independently and never overwrite each other's tokens. Failover is the generic
multi-LLM behaviour: a member is tried after the previous one has exhausted its
own retries and raised — there is no separate quota classifier or cooldown, so a
rate-limited primary is re-tried (and fails) at the head of each request before
the fallback serves it.
---
### Meta Model API Setup
[Meta Model API](https://ai.developer.meta.com) serves the Muse Spark models over an
OpenAI-compatible endpoint. Get a key from the Model API dashboard, then:
```bash
export HINDSIGHT_API_LLM_PROVIDER=meta
export HINDSIGHT_API_LLM_API_KEY=your-meta-model-api-key
export HINDSIGHT_API_LLM_MODEL=muse-spark-1.3
```
The base URL defaults to `https://api.meta.ai/v1`. Available models are
`muse-spark-1.3` (recommended), `muse-spark-1.2`, `muse-spark-1.1`, and the
discounted `-contributor` variants of 1.3 and 1.2, which permit training on your
prompts and completions. All share a 1,048,576-token context window.
#### Required knobs
Muse Spark **always reasons** before it replies. That single property drives every
setting below, so treat these as required rather than optional tuning:
| Variable | Set it to | Why |
|----------|-----------|-----|
| `HINDSIGHT_API_REFLECT_LLM_TIMEOUT` | `300` | Reflect's default is 30s. Muse Spark's final synthesis exceeds that, and the call fails after its retries rather than degrading — reflect returns nothing. |
| `HINDSIGHT_API_LLM_TIMEOUT` | `300` | The global deadline (default 120s) covers retain and consolidation, which are slower here than on a non-reasoning model. |
| `HINDSIGHT_API_LLM_REASONING_EFFORT` | unset, or `minimal`/`low`/`medium`/`high`/`xhigh` | `none` is rejected with `HTTP 400`. Leave it unset to let the model choose its own depth. |
| `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` | leave at the `64000` default | Reasoning tokens are billed against the **output** budget. Lower this too far and a reply comes back with no content at all. |
#### Good to know
- **Prompt caching is automatic.** There is no key, flag, or breakpoint to set — Meta
reuses a matching prompt prefix on its own, so the capability table below lists no
explicit prompt-caching support even though the benefit applies.
- **No batch API and no embeddings endpoint.** Embeddings continue to come from
whichever `HINDSIGHT_API_EMBEDDINGS_PROVIDER` you configure.
- **Recursive JSON schemas are rejected** with `HTTP 400`. No Hindsight code path
sends one, so this only matters if you add a self-referencing response model.
- **Expect slower calls.** A trivial prompt can spend more tokens reasoning than it
returns as output.
---
### Nous Portal Setup (Hermes)
Use your [Nous Portal](https://portal.nousresearch.com) subscription for Hindsight via the Hermes CLI login — no static API key required.
**Prerequisites:**
- A Nous Portal account
- The [Hermes](https://hermes-agent.nousresearch.com) CLI installed
**Setup Steps:**
1. **Log in to Nous Portal:**
```bash
hermes portal
```
This opens a browser to authenticate with Nous Portal and saves OAuth credentials to `~/.hermes/auth.json`.
2. **Verify authentication:**
```bash
hermes portal status # should show "Auth: ✓ logged in"
```
3. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=nous
# export HINDSIGHT_API_LLM_MODEL=deepseek/deepseek-v4-flash # defaults to deepseek/deepseek-v4-flash
# No API key needed — reads from ~/.hermes/auth.json automatically
```
4. **Start Hindsight:**
```bash
hindsight-api
```
You can use any model hosted on the Nous Portal inference API.
**Important Notes:**
- Credentials are read from `~/.hermes/auth.json` (the same store the Hermes agent uses) — no static API key in Hindsight's config.
- The short-lived inference JWT is refreshed automatically, before expiry and reactively on a 401.
- Refreshes coordinate with a running Hermes agent through the shared auth store, so the two never disrupt each other's session.
- Default base URL: `https://inference-api.nousresearch.com/v1` (override with `HINDSIGHT_API_LLM_BASE_URL`).
---
### Claude Code Setup (Claude Pro/Max)
Use your Claude Pro or Max subscription for Hindsight without separate Anthropic API costs.
> **⚠️ Terms of Service Notice**
>
This integration uses the Claude Agent SDK with your personal Claude Pro/Max subscription
credentials. You must be logged into Claude Code on your own machine before using this provider.
**Please be aware:**
- Anthropic's [Agent SDK documentation](https://docs.claude.com/en/api/agent-sdk/overview)
states that third-party developers should not offer claude.ai login or rate limits for
their products. Hindsight does **not** perform any login on your behalf — it uses
credentials you've already authenticated via `claude auth login`.
- In January 2026, Anthropic [enforced restrictions](https://paddo.dev/blog/anthropic-walled-garden-crackdown/)
against third-party tools using Claude subscription OAuth tokens. Those restrictions
targeted tools that **spoofed the Claude Code client identity** — Hindsight uses the
official Claude Agent SDK instead.
- This provider is intended for **local, personal development use only**. Do not use it
in production deployments or shared environments.
- Anthropic's terms may change. If you want guaranteed compliance, use the `anthropic`
provider with an API key instead.
- Usage counts against your Claude Pro/Max subscription limits.
For production or team use, we recommend using `HINDSIGHT_API_LLM_PROVIDER=anthropic` with
an API key from the [Anthropic Console](https://console.anthropic.com/).
**Prerequisites:**
- Active Claude Pro or Max subscription
- Claude Code CLI installed
**Setup Steps:**
1. **Install Claude Code CLI:**
```bash
npm install -g @anthropics/claude-code
# Or via Homebrew
brew install anthropics/claude-code/claude-code
```
2. **Login with Claude credentials:**
```bash
claude auth login
```
This opens a browser window to authenticate with your Claude account. Authentication is automatically managed by the Claude Agent SDK.
3. **Verify authentication:**
```bash
claude --version
# Should show version without errors
```
4. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# No API key needed - uses claude auth login credentials
```
**Server-held token (optional, for a personal machine without a browser login):** instead of
relying on the host's `claude auth login` session, you can generate a long-lived token once and
give it to Hindsight. This is still subscription access, so the Terms of Service notice above
applies in full: it does not make the provider suitable for servers, Docker deployments or shared
hosts. For those, use the `anthropic` provider with an API key.
```bash
claude setup-token # prints sk-ant-oat01-...
export HINDSIGHT_API_LLM_API_KEY=sk-ant-oat01-...
```
Hindsight passes it to every spawned CLI as `CLAUDE_CODE_OAUTH_TOKEN`, so the auth session lives
in the server's config (and can be set per operation or per bank like any other LLM key).
With no key set, the CLI's own login is used.
5. **Start Hindsight:**
```bash
hindsight-api
```
You can use any model supported by Claude Code CLI.
**Important Notes:**
- Authentication handled by Claude Agent SDK (uses bundled CLI)
- Credentials managed securely by Claude Code
- Usage billed to your Claude subscription (not separate API costs)
- For personal development use only (see Claude Terms of Service)
---
### Cursor Setup (Cursor subscription)
Serve Hindsight's extraction, consolidation and reflection calls from a **Cursor
subscription**, by driving the `cursor-agent` CLI in its headless print mode. No API key
and no second per-token billing relationship.
This is the opposite direction from the [Cursor integration](../sdks/integrations/cursor.md),
which makes Cursor a *client* of Hindsight. Here Cursor is the model backend Hindsight calls.
**Prerequisites:**
- An active Cursor subscription (a free plan works, but only with `auto` — see below)
- `cursor-agent` installed and signed in under the same OS user that runs Hindsight
**Setup Steps:**
1. **Install the CLI:**
```bash
curl https://cursor.com/install -fsS | bash
```
2. **Log in:**
```bash
cursor-agent login
```
3. **Verify:**
```bash
cursor-agent --version
cursor-agent --list-models
```
4. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=cursor
export HINDSIGHT_API_LLM_MODEL=auto
# An agent CLI turn takes 15-30s, well past the 30s reflect default.
export HINDSIGHT_API_LLM_TIMEOUT=300
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=180
# No API key needed. To authenticate with a key instead:
# export HINDSIGHT_API_LLM_API_KEY=... # or CURSOR_API_KEY
```
**Important notes:**
- **Raise the timeouts.** Each call spawns a `cursor-agent` turn, which takes 15-30s
against `auto` — slower than a chat-completions request and past the 30s
`HINDSIGHT_API_REFLECT_LLM_TIMEOUT` default. Left at the default, reflect spends its
first iteration on timeout retries before recovering.
- **Structured output and tool calling are prompt-level emulations.** The CLI exposes no
`response_format`, JSON-schema, temperature or tool-definition flag — only
`--output-format text|json|stream-json`, which describes the envelope. Hindsight puts
the schema (or the tool list) in the prompt and parses the model's JSON back out,
retrying on a parse failure. That is less reliable than a native schema, so prefer a
strong named model over `auto` for reflect, the operation that leans hardest on tools.
- **Free plans are limited to `auto`.** A named `--model` returns
`Named models unavailable Free plans can only use Auto`. Paid plans can use any id from
`cursor-agent --list-models`.
- **The agent's own tools are turned off.** `cursor-agent` is an agent, not a completions
endpoint: it ships Shell, Read, Write, Delete, web fetch and more, and `--mode ask` is
*not* a tool switch — a headless run in ask mode will still read files out of its
working directory. Hindsight therefore runs the CLI in an empty scratch workspace, with
its own `CURSOR_CONFIG_DIR` (so it never sees your `~/.cursor` config, hooks or session
history), and writes a `cli-config.json` there that denies every tool by name. That is
the equivalent of the `tools=[]` the `claude-code` provider passes its SDK, and it
matters because retain prompts are built from whatever text you store in Hindsight.
- Usage counts against your Cursor subscription.
- Reasoning effort is not supported — the CLI has no such flag, and Hindsight warns once
at startup if `HINDSIGHT_API_LLM_REASONING_EFFORT` is set.
- Embeddings and reranking continue to use Hindsight's separately configured providers.
---
### SuperGrok Subscription Setup (device-code OAuth)
Serve LLM calls from a **SuperGrok subscription** (device-code OAuth) — no API
key. For API-key access to `api.x.ai`, use `provider: openai` with
`HINDSIGHT_API_LLM_BASE_URL=https://api.x.ai/v1`; **this provider is the
subscription lane**, the same category as `openai-codex` (ChatGPT subscription)
and `claude-code` (Claude subscription). Both routes reach the same published
endpoint, so the credential is the whole difference: a flat-rate consumer
subscription authorized once in a browser, instead of a per-token metered key.
Authentication is xAI's own OIDC issuer at `https://auth.x.ai` using the RFC
8628 device-code flow, with the public OAuth client id published in xAI's
Apache-2.0 Grok CLI sources. Hindsight keeps its own credential file and never
reads or writes the Grok CLI's `~/.grok/auth.json`.
> **📝 Subscription entitlement**
>
xAI may restrict `api.x.ai` access by SuperGrok subscription tier: an OAuth
grant can be valid and still be refused with HTTP 403. Hindsight reports that
as a distinct entitlement error naming the cause rather than as a credential
failure, and it neither re-tries nor discards the token. If you hit it, verify
the account tier or use an API-key provider instead.
Usage counts against your SuperGrok subscription limits. When the account's
spending limit stops a call, Hindsight raises a distinct quota error and leaves
the credential alone.
**Prerequisites:**
- An active SuperGrok subscription
- A browser on the machine you run the login from (the device-code flow needs
an interactive approval; the running service never performs one)
**Setup steps:**
1. **Log in once, on the host that will own the credential:**
```bash
python -m hindsight_api.engine.providers.xai_oauth_auth login
```
The command prints a verification URL and a user code, waits for approval,
then writes `~/.hindsight/xai_oauth.json` with owner-only permissions.
2. **Configure Hindsight:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=xai-oauth
# export HINDSIGHT_API_LLM_MODEL=grok-4.5 # defaults to grok-4.5
# No API key needed
```
3. **Start Hindsight:**
```bash
hindsight-api
```
**Important notes:**
- The access token is refreshed automatically: proactively 60 seconds before
expiry (or before the configured request timeout, whichever is longer), and
once reactively on an HTTP 401. Several configured lanes share one credential
file safely — the refresh takes an advisory lock and re-reads the file under
it, so siblings do not each refresh.
- A running service **never** starts the device-code flow. When only a login can
restore service, calls fail with the exact command to run.
- Unlike reaching `api.x.ai` through `provider: openai`, this provider sends the
member's configured `reasoning_effort`, so that setting takes effect here.
The one value not forwarded is `none`: xAI rejects it outright (HTTP 400),
so this lane expresses it the way the API expects — by omitting the field.
- Only non-streaming calls are implemented. Streaming responses omit
`prompt_tokens_details`, so cached-token accounting would read low.
**Optional environment overrides:**
| Variable | Purpose | Default |
|---|---|---|
| `HINDSIGHT_API_XAI_OAUTH_BASE_URL` | Point at a different upstream. Takes precedence over `HINDSIGHT_API_LLM_BASE_URL`. | `https://api.x.ai/v1` |
| `HINDSIGHT_API_XAI_OAUTH_TOKEN_PATH` | Relocate the credential store. Both the read and the write follow it. | `~/.hindsight/xai_oauth.json` |
| `HINDSIGHT_API_XAI_OAUTH_CLIENT_ID` | Override the OAuth client id used for login and refresh. | xAI's published public client id |
| `HINDSIGHT_API_XAI_OAUTH_SCOPE` | Override the scope string requested at login. | `openid profile email offline_access grok-cli:access api:access` |
| `HINDSIGHT_API_XAI_OAUTH_REFRESH_SKEW_SECONDS` | Refresh this many seconds before expiry. Widen it for deployments that touch the provider rarely. | `60` |
| `HINDSIGHT_API_XAI_OAUTH_REFRESH_TIMEOUT_SECONDS` | Per-request timeout for discovery, login and refresh calls. | `20` |
#### Deployment note
The credential is a host-local file, and only the interactive login can create
it. A containerized engine therefore needs the store mounted from a host where
the login was run (or the login run inside the container once, interactively)
and `HINDSIGHT_API_XAI_OAUTH_TOKEN_PATH` pointed at it. The mount must be
**writable**: unlike the Grok CLI's own file, this store is where Hindsight
persists each rotated token, so a read-only mount degrades to failing once the
current token expires.
xAI issues a **new refresh token on every refresh** and retires the one it
replaces. Replicas must therefore share the one credential file rather than
each starting from its own copy of a single login's output: a copy stops
working as soon as another replica refreshes, and the login has to be repeated.
A single shared writable volume is the supported shape; if you cannot provide
one, run one replica on this provider and give the others an API-key lane.
---
### Azure OpenAI Setup
Azure OpenAI is reached through the **`openai`** provider — there is no `azure`
provider, and setting one fails at startup with
`Invalid LLM provider: azure`.
The one thing that trips people up is the base URL. Azure does not serve the
OpenAI API at the resource root, so the endpoint shown in the Azure portal is
not usable on its own:
| `HINDSIGHT_API_LLM_BASE_URL` | Result |
|---|---|
| `https://<resource>.openai.azure.com` | `404 Resource not found` |
| `https://<resource>.openai.azure.com/openai/deployments/<deployment>` | `404 Resource not found` (no `api-version`) |
| `https://<resource>.openai.azure.com/openai/v1` | works |
| `https://<resource>.openai.azure.com/openai/deployments/<deployment>?api-version=<version>` | works |
**Recommended — the v1 surface:**
```bash
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=<azure-openai-resource-key>
export HINDSIGHT_API_LLM_MODEL=<deployment-name>
export HINDSIGHT_API_LLM_BASE_URL=https://<resource>.openai.azure.com/openai/v1
```
**Or the deployment-scoped form.** Keep the `api-version` query string — Hindsight
parses it out of the base URL and passes it to the SDK:
```bash
export HINDSIGHT_API_LLM_BASE_URL=https://<resource>.openai.azure.com/openai/deployments/<deployment>?api-version=2025-01-01-preview
```
**Important notes:**
- `HINDSIGHT_API_LLM_MODEL` is your **deployment name**, not the model name. A
`gpt-4o` deployed as `my-gpt4o` is configured as `my-gpt4o`.
- The key is the Azure OpenAI **resource** key (`az cognitiveservices account
keys list -n <resource> -g <group>`). An API Management subscription key is a
different credential: with APIM in front, the base URL must be the APIM route
and APIM has to forward the `api-key` header. Test against the Azure endpoint
directly first to isolate which layer is failing.
- Gateways and proxies must preserve the same path shape (`/openai/v1` or
`/openai/deployments/...?api-version=`).
- Azure OpenAI accepts `prompt_cache_key` on GPT deployments (it improves cache
routing on GPT-5.6 and later), but the same `*.openai.azure.com` endpoint also
serves non-OpenAI Foundry models (DeepSeek, Llama, Mistral) that reject it with
`unrecognized_request_argument`. The host alone can't tell the two apart, so
[`HINDSIGHT_API_LLM_CACHE_AFFINITY`](./configuration.md#llm-provider) resolves
`auto` to `none` for Azure hosts. If your deployment serves an OpenAI model,
set it explicitly to `openai_prompt_cache_key`.
---
### Vertex AI Setup (Google Cloud)
Google Cloud's Vertex AI provides access to Gemini models via the native Google GenAI SDK.
**Prerequisites:**
- GCP project with Vertex AI API enabled
- IAM role `roles/aiplatform.user` for your credentials
**Environment Variables:**
| Variable | Description | Required |
|----------|-------------|----------|
| `HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID` | Your GCP project ID | Yes |
| `HINDSIGHT_API_LLM_VERTEXAI_REGION` | GCP region (e.g., `us-central1`) | No (default: `us-central1`) |
| `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY` | Path to service account JSON key file | No (uses ADC if not set) |
**Authentication Methods:**
1. **Application Default Credentials (ADC)** - Recommended for development
```bash
# Setup ADC
gcloud auth application-default login
# Configure Hindsight
export HINDSIGHT_API_LLM_PROVIDER=vertexai
export HINDSIGHT_API_LLM_MODEL=gemini-3.1-flash-lite
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-id
```
2. **Service Account Key** - Recommended for production
```bash
# Create service account and download key
gcloud iam service-accounts create hindsight-api
gcloud projects add-iam-policy-binding your-project-id \
--member="serviceAccount:hindsight-api@your-project-id.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"
gcloud iam service-accounts keys create key.json \
--iam-account=hindsight-api@your-project-id.iam.gserviceaccount.com
# Configure Hindsight
export HINDSIGHT_API_LLM_PROVIDER=vertexai
export HINDSIGHT_API_LLM_MODEL=gemini-3.1-flash-lite
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-id
export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json
```
**Notes:**
- Model names can optionally include the `google/` prefix (e.g., `google/gemini-3.1-flash-lite`) — it will be stripped automatically
- The native SDK handles token refresh automatically
- Uses service account credentials if provided, otherwise falls back to ADC
---
## Embedding Model
Converts text into dense vector representations for semantic similarity search.
**Default:** `BAAI/bge-small-en-v1.5` (384 dimensions, ~130MB)
### Supported Providers
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers (default) | Development, low latency |
| `onnx` | In-process ONNX Runtime embedder (no Ollama/TEI/API sidecar) | Lightweight local CPU, multilingual |
| `openai` | OpenAI embeddings API | Production, high quality |
| `openai-codex` | OpenAI embeddings via Codex OAuth (ChatGPT Plus/Pro, no API key) | Existing ChatGPT/Codex subscribers |
| `openrouter` | OpenRouter embeddings (OpenAI-compatible gateway) | Multi-provider setups |
| `cohere` | Cohere embeddings API | Production, multilingual |
| `google` | Google embeddings (Gemini API or Vertex AI) | Production, multilingual, high quality |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `zeroentropy` | ZeroEntropy zembed-1 | Production, high quality retrieval |
| `nvidia` | NVIDIA NIM retrieval embeddings (`input_type`-aware) | Users on NVIDIA NIM / build.nvidia.com |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| `litellm-sdk` | LiteLLM SDK (direct API, no proxy) | Multi-provider, simpler setup |
### Local Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
### OpenAI Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `text-embedding-3-small` | 1536 | Default OpenAI, cost-effective |
| `text-embedding-3-large` | 3072 | Higher quality, more expensive |
| `text-embedding-ada-002` | 1536 | Legacy model |
### Google Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `gemini-embedding-001` | 768 (configurable) | Default Google, general purpose |
| `gemini-embedding-2-preview` | 768 (configurable) | Gemini Embedding 2 family; multimodal, one vector per input |
Google's `gemini-embedding-001` supports configurable output dimensionality via truncation, google recommend using: 768, 1536, 3072, via `HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY`. Default is 768.
The `gemini-embedding-2` family, including `gemini-embedding-2-preview`, is supported on both the Gemini API and Vertex AI. These models aggregate multi-input requests, so Hindsight automatically embeds one input per call to keep per-fact vectors aligned.
### Cohere Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `embed-english-v3.0` | 1024 | English text |
| `embed-multilingual-v3.0` | 1024 | 100+ languages |
### ZeroEntropy Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `zembed-1` | 1280 default (2560/1280/640/320/160/80/40 configurable) | High quality asymmetric retrieval |
Hindsight sends retained memory text to ZeroEntropy as `document` inputs and recall/search text as `query` inputs. ZeroEntropy's API default is 2560 dimensions; Hindsight defaults to 1280 so pgvector HNSW works without changing the vector extension.
> **⚠️ Embedding Dimensions**
>
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
**Configuration Examples:**
```bash
# Local provider (default)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# ONNX provider (in-process local CPU, no Ollama/TEI/API sidecar; pip install hindsight-api-slim[local-onnx])
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=intfloat/multilingual-e5-small
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
# OpenAI
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
# Cohere
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
# Google (API key auth)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001
# Google (Vertex AI auth - auto-detected when project ID is set)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001
export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_PROJECT_ID=your-gcp-project-id
# TEI (self-hosted)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
# ZeroEntropy
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=zeroentropy
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL=zembed-1
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_DIMENSIONS=1280
# NVIDIA NIM
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=nvidia
export HINDSIGHT_API_EMBEDDINGS_NVIDIA_API_KEY=nvapi-your-key
export HINDSIGHT_API_EMBEDDINGS_NVIDIA_MODEL=nvidia/llama-nemotron-embed-vl-1b-v2 # default, can omit
# LiteLLM proxy
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small
# LiteLLM SDK (direct, no proxy)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=openai/text-embedding-3-small
# AWS Bedrock (via LiteLLM SDK; credentials come from the environment or an IAM role)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=bedrock/amazon.titan-embed-text-v2:0
```
> **💡 AWS Bedrock application inference profiles**
>
If your org's Service Control Policy denies `bedrock:InvokeModel` on the bare model id once an [application inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) exists, keep `..._MODEL` as-is and add the profile ARN separately:
```bash
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
# Stays a recognizable model id — this is what picks the request format
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=bedrock/amazon.titan-embed-text-v2:0
# The profile actually invoked
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL_ID=arn:aws:bedrock:eu-west-1:123456789012:application-inference-profile/abc123
```
The two are separate because Bedrock's embedding request and response formats differ per model family (Titan, Cohere, TwelveLabs, Nova), and the family is read off the model id. A profile ARN is opaque and account-scoped, so it can't be used for that — put it in `..._MODEL_ID` and leave `..._MODEL` naming the real model.
Chat models don't need this: set `HINDSIGHT_API_LLM_MODEL=bedrock/converse/<arn>`, since the Converse API takes one format for every model.
See [Configuration](./configuration.md#embeddings) for all options including Azure OpenAI and custom endpoints.
For a runnable Docker Compose setup that serves both embeddings and reranking from self-hosted TEI sidecars, see [`docker/docker-compose/tei/`](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/tei).
---
## Cross-Encoder (Reranker)
Reranks initial search results to improve precision.
**Default:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (~85MB)
### Supported Providers
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers CrossEncoder (default) | Development, low latency |
| `cohere` | Cohere rerank API | Production, high quality |
| `openrouter` | OpenRouter rerank API (Cohere-compatible gateway) | Multi-provider setups |
| `zeroentropy` | ZeroEntropy rerank API (zerank-2) | Production, state-of-the-art accuracy |
| `siliconflow` | SiliconFlow rerank API (Cohere-compatible `/rerank` endpoint) | Users in China or anyone on SiliconFlow's platform |
| `nvidia` | NVIDIA NIM reranking API (native `/retrieval/{model}/reranking` endpoint) | Users on NVIDIA NIM / build.nvidia.com |
| `typesafe` | TypeSafe typed-decision API (Jev) — ranks the whole pool in one question | Production, highest ranking quality; can also return only the relevant candidates |
| `alibaba` | Alibaba Cloud DashScope rerank API (qwen3-rerank) | Users on Alibaba Cloud / DashScope |
| `google` | Google Discovery Engine ranking API (REST + Google auth) | Production, GCP integration |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `flashrank` | FlashRank (lightweight, fast) | Resource-constrained environments |
| `onnx` | Any cross-encoder exported to ONNX, in-process via ONNX Runtime | Custom models without PyTorch or a TEI server |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| `litellm-sdk` | LiteLLM SDK (direct API, no proxy) | Multi-provider, simpler setup |
| `jina-mlx` | Jina rerank v3 via Apple Silicon MLX (local, no API key) | Apple Silicon (M1+) local inference |
| `rrf` | RRF-only (no neural reranking) | Testing, minimal resources |
### Local Models
| Model | Use Case |
|-------|----------|
| `cross-encoder/ms-marco-MiniLM-L-6-v2` | Default, fast |
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
### Cohere Models
| Model | Use Case |
|-------|----------|
| `rerank-english-v3.0` | English text |
| `rerank-multilingual-v3.0` | 100+ languages |
### ZeroEntropy Models
| Model | Use Case |
|-------|----------|
| `zerank-2` | Flagship multilingual reranker (default) |
| `zerank-2-small` | Faster, lighter variant |
### SiliconFlow Models
SiliconFlow hosts a range of open-weight rerankers behind a Cohere-compatible `/rerank` endpoint:
| Model | Use Case |
|-------|----------|
| `BAAI/bge-reranker-v2-m3` | Multilingual, strong default |
| `Qwen/Qwen3-Reranker-8B` | Larger, higher accuracy |
### TypeSafe Models
TypeSafe is not a `/rerank` endpoint — it answers typed *questions* against a *state*.
Hindsight makes the candidates the options of a Choice question, so the returned
probability distribution is the ranking. When the candidate pool fits within the 250-option
limit and context token budget, a single call ranks the entire pool. Larger pools or pools
with long documents are partitioned into groups whose top candidates advance to a finals
ranking round, with token truncation applied to fit context windows.
| Model | Use Case |
|-------|----------|
| `jev-latest` | Default; tracks the current Jev release |
Setting `HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true` adds a second question
that cuts the ranked list where relevance ends, so recall returns the relevant
candidates and nothing else. It is off by default because it meaningfully shrinks what
recall returns. See [Configuration](configuration.md#typesafe) for the full
behaviour and its trade-offs.
TypeSafe's scores are rank positions within one recall, not relevance scores, so recall
publishes no `scores.reranker` under it and rejects `min_scores.reranker` with HTTP 400.
### Alibaba Cloud Models
Alibaba Cloud DashScope exposes `qwen3-rerank` via a Cohere-compatible `/reranks` endpoint:
| Model | Use Case |
|-------|----------|
| `qwen3-rerank` | 100+ languages, default |
### LiteLLM Supported Providers
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
| Provider | Model Example |
|----------|---------------|
| Cohere | `cohere/rerank-english-v3.0` |
| Together AI | `together_ai/...` |
| Voyage AI | `voyage/rerank-2` |
| Jina AI | `jina_ai/...` |
| AWS Bedrock | `bedrock/...` |
**Configuration Examples:**
```bash
# Local provider (default)
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Cohere
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# Cohere-compatible endpoint (Azure AI Foundry, Jina, Voyage, self-hosted BGE, ...)
# Setting COHERE_BASE_URL switches the provider off the Cohere SDK and onto a
# plain HTTP client that speaks the standard rerank wire format:
# POST {base_url} Authorization: Bearer <key>
# {"model","query","documents","return_documents":false}
# -> {"results":[{"index","relevance_score"}, ...]}
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-v3.5 # whatever model the endpoint serves
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-endpoint.example/rerank
# ZeroEntropy (state-of-the-art accuracy)
export HINDSIGHT_API_RERANKER_PROVIDER=zeroentropy
export HINDSIGHT_API_RERANKER_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_ZEROENTROPY_MODEL=zerank-2 # default, can omit
# SiliconFlow (Cohere-compatible /rerank endpoint)
export HINDSIGHT_API_RERANKER_PROVIDER=siliconflow
export HINDSIGHT_API_RERANKER_SILICONFLOW_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_SILICONFLOW_MODEL=BAAI/bge-reranker-v2-m3 # default, can omit
# NVIDIA NIM (native reranking API, not Cohere-compatible)
export HINDSIGHT_API_RERANKER_PROVIDER=nvidia
export HINDSIGHT_API_RERANKER_NVIDIA_API_KEY=nvapi-your-key
export HINDSIGHT_API_RERANKER_NVIDIA_MODEL=nvidia/llama-nemotron-rerank-vl-1b-v2 # default, can omit
# TypeSafe (typed-decision API, ranks the whole pool in one question)
export HINDSIGHT_API_RERANKER_PROVIDER=typesafe
export HINDSIGHT_API_RERANKER_TYPESAFE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_TYPESAFE_MODEL=jev-latest # default, can omit
# export HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true # return only the relevant ones
# Alibaba Cloud DashScope (qwen3-rerank)
export HINDSIGHT_API_RERANKER_PROVIDER=alibaba
export HINDSIGHT_API_RERANKER_ALIBABA_API_KEY=your-dashscope-api-key
export HINDSIGHT_API_RERANKER_ALIBABA_MODEL=qwen3-rerank # default, can omit
# TEI (self-hosted)
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
# FlashRank (lightweight)
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
# LiteLLM proxy
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0
# RRF-only (no neural reranking)
export HINDSIGHT_API_RERANKER_PROVIDER=rrf
```
See [Configuration](./configuration.md#reranker) for all options including Azure-hosted endpoints and batch settings.
references/developer/monitoring.md
# Monitoring
Hindsight provides comprehensive observability through Prometheus metrics, OpenTelemetry distributed tracing, and pre-built Grafana dashboards.
## Local Development
For local observability, use the Grafana LGTM (Loki, Grafana, Tempo, Mimir) all-in-one stack:
```bash
./scripts/dev/start-monitoring.sh
```
This starts a single Docker container providing:
- **Grafana UI**: http://localhost:3000 (anonymous admin access)
- **Traces (Tempo)**: OTLP endpoint at http://localhost:4318 (HTTP) and http://localhost:4317 (gRPC)
- **Metrics (Prometheus/Mimir)**: Scrapes http://localhost:8888/metrics automatically
- **Logs (Loki)**: Available for log aggregation
- **Pre-built Dashboards**: Hindsight Operations, LLM Metrics, API Service
**Enable tracing in your API:**
```bash
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
```
:::note Production Deployment
The local monitoring stack is for development only. In production, deploy Grafana LGTM separately or use commercial platforms (Grafana Cloud, DataDog, New Relic, etc.).
:::
## Grafana Dashboards
Pre-built dashboards are available in [`monitoring/grafana/dashboards/`](https://github.com/anthropics/hindsight/tree/main/monitoring/grafana/dashboards). Import these JSON files into your Grafana instance:
| Dashboard | Description |
|-----------|-------------|
| **Hindsight Operations** | Operation rates, latency percentiles, per-bank metrics |
| **Hindsight LLM Metrics** | LLM calls, token usage, latency by scope/provider |
| **Hindsight API Service** | HTTP requests, error rates, DB pool, process metrics |
The dashboards are automatically provisioned when using the monitoring stack script.
## Metrics Endpoint
Hindsight exposes Prometheus metrics at `/metrics`:
```bash
curl http://localhost:8888/metrics
```
## Health Endpoints
The API server (port 8888) and every worker (port 8889) expose the same three
endpoints. They answer two different questions, and pointing a probe at the wrong
one is the difference between riding out a database blip and turning it into an
outage:
| Endpoint | Checks the database? | Use for |
|----------|----------------------|---------|
| `/health/live` | No | Liveness probes |
| `/health/ready` | Yes | Readiness probes |
| `/health` | Yes | Readiness — alias of `/health/ready`, kept for compatibility |
**`/health/live`** returns 200 whenever the process can serve a request, and does
no I/O to get there:
```json
{ "status": "alive", "version": "0.4.0", "uptime_seconds": 812.4 }
```
Answering at all is the check. Hindsight runs request handlers and task work on a
single event loop, so a loop wedged by a blocking call cannot respond inside the
probe timeout — which is exactly the failure a restart fixes. The worker's payload
adds `worker_id`, `is_shutdown`, and `seconds_since_last_poll` (the age of its last
completed claim cycle, `null` before the first one). That last field is there to
alert on: it never changes the status code, because a poller stalled behind a
saturated database is the case where restarting makes things worse.
**`/health` and `/health/ready`** acquire a pooled connection and run `SELECT 1`,
returning 200 when the database is reachable and 503 when it is not. The payload
separates the two ways that can go wrong — `db_acquire_ms` and `db_pool_waiting`
point at pool exhaustion, a slow query at the database itself:
```json
{ "status": "healthy", "database": "connected", "db_acquire_ms": 0.4, "db_pool_waiting": 0 }
```
:::warning Never point a liveness probe at a dependency check
A liveness failure means "restart this process." If your liveness probe checks the
database, then a slow database restarts every pod at once: in-flight requests are
dropped, claimed async operations are requeued with `retry_count` incremented
toward the permanent-failure cliff, and each restarted pod reconnects to re-warm
its pool against a database that is already struggling. Readiness failing is the
correct response — it takes the pod out of the Service and puts it back when the
database recovers.
The bundled Helm chart is wired this way already (`livenessProbe` → `/health/live`,
`readinessProbe` → `/health`). If you wrote your own manifests against an older
version, move the liveness path over.
:::
## Available Metrics
:::note High-cardinality labels are opt-in
`bank_id` and `tenant` (the tenant schema) are left off every metric by default, because each
one adds a series set per bank or tenant. Turn them on with
`HINDSIGHT_API_METRICS_INCLUDE_BANK_ID=true` / `HINDSIGHT_API_METRICS_INCLUDE_TENANT=true`
only on deployments with few banks or tenants. The backlog gauges always carry `tenant`.
:::
### Operation Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.operation.duration` | Histogram | operation, bank_id, source, budget, max_tokens, success, memories_backend | Duration of operations in seconds |
| `hindsight.operation.total` | Counter | operation, bank_id, source, budget, max_tokens, success, memories_backend | Total number of operations executed |
**Labels:**
- `operation`: Operation type (`retain`, `recall`, `reflect`, plus async worker task types such as `consolidation`)
- `bank_id`: Memory bank identifier
- `source`: Where the operation was triggered from (`api`, `reflect`, `internal`, `worker`)
- `budget`: Budget level if specified (`low`, `mid`, `high`)
- `max_tokens`: Max tokens if specified
- `success`: Whether the operation succeeded (`true`, `false`)
- `memories_backend`: The store serving the bank, when the memories extension names one
(`MemoriesExtension.backend_name_for`). Absent by default, so existing series are unchanged; a
deployment whose banks live in different stores uses it to compare their latency without
turning on `tenant`. Recall phase metrics recorded inside the operation carry it too.
The `source` label allows distinguishing between:
- `api`: Direct API calls from clients
- `reflect`: Internal recall calls made during reflect operations
- `internal`: Other internal operations
- `worker`: Async worker completions recorded when a claimed task reaches a terminal outcome
For `source="worker"`, the `success` label is a completion-throughput signal:
`false` means the task raised out to the poller after retries were exhausted or
an unexpected error occurred. Failures handled inside the executor and returned
normally still record `success="true"` here; use
`hindsight_async_operations{status="failed"}` for authoritative async operation
failure status.
### Retain Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.retain.documents.total` | Counter | outcome, bank_id | Documents processed by retain, by extraction outcome |
**Labels:**
- `outcome`: `facts` when the document has memory units after the retain, `no_facts` when it has none
- `bank_id`: Memory bank identifier
`outcome="no_facts"` is the signal that a document was stored but produced no memories. Those documents are invisible to `recall` and `reflect` until they are [reprocessed](retain.md#when-a-mission-excludes-everything-in-a-document) — nothing else reports them, because the retain itself succeeded. A rising share usually means a [retain mission](retain.md#steering-extraction-with-a-mission) is excluding more than intended:
```promql
sum(rate(hindsight_retain_documents_total{outcome="no_facts"}[15m]))
/ sum(rate(hindsight_retain_documents_total[15m]))
```
### LLM Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.llm.duration` | Histogram | provider, model, scope, success | Duration of LLM API calls in seconds |
| `hindsight.llm.calls.total` | Counter | provider, model, scope, success | Total number of LLM API calls |
| `hindsight.llm.tokens.input` | Counter | provider, model, scope, success, token_bucket | Input tokens for LLM calls |
| `hindsight.llm.tokens.output` | Counter | provider, model, scope, success, token_bucket | Output tokens from LLM calls |
**Labels:**
- `provider`: LLM provider (`openai`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`, `bedrock`, `litellm`)
- `model`: Model name (e.g., `gpt-4`, `claude-3-sonnet`)
- `scope`: What the LLM call is for (`memory`, `reflect`, `consolidation`, `answer`)
- `success`: Whether the call succeeded (`true`, `false`)
- `token_bucket`: Token count bucket for cardinality control (`0-100`, `100-500`, `500-1k`, `1k-5k`, `5k-10k`, `10k-50k`, `50k+`)
### Consolidation Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.consolidation.batch_failures` | Counter | failure_class, error_type | Consolidation LLM batch calls that failed, including those whose facts were later recovered |
**Labels:**
- `failure_class`: How the call was treated (`fail_fast` — the model returned something the response schema rejects, so a re-send of the same payload cannot help; `retry` — transport-shaped, an unchanged re-send may succeed; `propagate` — not a batch failure, re-raised to the task handler)
- `error_type`: Exception class name (e.g. `ValidationError`, `JSONDecodeError`)
This is not the same signal as the `failed_consolidation` field in bank stats. That
field counts *facts* still waiting to be consolidated after a failure; when a batch
call fails, consolidation halves the batch and retries, so a call that failed at
batch size 8 and succeeded at size 1 leaves `failed_consolidation` at 0. Everything
the failed response asked for is dropped, though — including any observations it
wanted to delete — so a bank can look completely healthy while its
supersession cleanup does nothing.
Alert on the schema-rejection rate, which no other metric exposes:
```promql
sum(rate(hindsight_consolidation_batch_failures_total{failure_class="fail_fast"}[15m]))
```
A sustained non-zero rate usually means the consolidation model does not reliably
emit valid JSON for the response schema. Switching to a model with stronger
structured output, or enabling `HINDSIGHT_API_LLM_STRICT_SCHEMA_CONSOLIDATION` if
the provider supports grammar-enforced schemas, is the usual fix.
The same count is reported per run as `llm_batch_failures` in the consolidation
operation's result, and the consolidation log summary prints a warning line
whenever it is non-zero. Both count *attempts* — one batch call retried three times
contributes three — so they are not bounded by the number of batches in the run.
### HTTP Request Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.http.duration` | Histogram | method, endpoint, status_code, status_class | Duration of HTTP requests in seconds |
| `hindsight.http.requests.total` | Counter | method, endpoint, status_code, status_class | Total number of HTTP requests |
| `hindsight.http.requests.in_progress` | UpDownCounter | method, endpoint | Number of HTTP requests currently being processed |
**Labels:**
- `method`: HTTP method (`GET`, `POST`, `PUT`, `DELETE`)
- `endpoint`: Template of the route the request matched, with path parameters named rather than filled in (e.g. `/v1/default/banks/{bank_id}/documents/{document_id}`, `/v1/default/banks/{bank_id}/operations/{operation_id}`), so the label has one value per route however many banks, documents or ids are requested. The template does not include a `base_path` / root-path prefix. Requests that match no route (404s) are labelled `unmatched`; a request matched by a route that has no path template is labelled `unknown_route`.
- `status_code`: HTTP status code (`200`, `400`, `500`, etc.)
- `status_class`: Status code class (`2xx`, `4xx`, `5xx`)
### Database Pool Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.db.pool.size` | Gauge | - | Current number of connections in the pool |
| `hindsight.db.pool.idle` | Gauge | - | Number of idle connections in the pool |
| `hindsight.db.pool.min` | Gauge | - | Minimum pool size |
| `hindsight.db.pool.max` | Gauge | - | Maximum pool size |
### Process Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `hindsight.process.cpu.seconds` | Gauge | type | Process CPU time in seconds |
| `hindsight.process.memory.bytes` | Gauge | type | Process memory usage in bytes |
| `hindsight.process.open_fds` | Gauge | - | Number of open file descriptors |
| `hindsight.process.threads` | Gauge | - | Number of active threads |
**Labels:**
- `type` (CPU): `user` or `system`
- `type` (Memory): `rss_max` (maximum resident set size)
### Histogram Buckets
Custom bucket boundaries are configured for better percentile accuracy:
**Operation Duration Buckets (seconds):**
```
0.1, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 60.0, 120.0
```
**LLM Duration Buckets (seconds):**
```
0.1, 0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0
```
**HTTP Duration Buckets (seconds):**
```
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0
```
## Prometheus Configuration
```yaml
scrape_configs:
- job_name: 'hindsight'
static_configs:
- targets: ['localhost:8888']
```
## Example Queries
### Average operation latency by type
```promql
rate(hindsight_operation_duration_sum[5m]) / rate(hindsight_operation_duration_count[5m])
```
### LLM calls per minute by provider
```promql
rate(hindsight_llm_calls_total[1m]) * 60
```
### P95 LLM latency
```promql
histogram_quantile(0.95, rate(hindsight_llm_duration_bucket[5m]))
```
### Total tokens consumed by model
```promql
sum by (model) (hindsight_llm_tokens_input_total + hindsight_llm_tokens_output_total)
```
### Internal vs API recall operations
```promql
sum by (source) (rate(hindsight_operation_total{operation="recall"}[5m]))
```
### HTTP requests per second by endpoint
```promql
sum by (endpoint) (rate(hindsight_http_requests_total[1m]))
```
### HTTP error rate (5xx)
```promql
sum(rate(hindsight_http_requests_total{status_class="5xx"}[5m])) / sum(rate(hindsight_http_requests_total[5m]))
```
### P95 HTTP latency
```promql
histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))
```
### Database pool utilization
```promql
hindsight_db_pool_size / hindsight_db_pool_max
```
### Active database connections
```promql
hindsight_db_pool_size - hindsight_db_pool_idle
```
### CPU usage rate
```promql
rate(hindsight_process_cpu_seconds{type="user"}[1m])
```
---
## Distributed Tracing
Hindsight supports OpenTelemetry distributed tracing for memory operations and LLM calls, following GenAI semantic conventions v1.37+.
### Configuration
See [Configuration - OpenTelemetry Tracing](./configuration.md#opentelemetry-tracing) for environment variables.
**Quick Start:**
```bash
# Enable tracing
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# View traces with Grafana LGTM (local dev)
./scripts/dev/start-monitoring.sh
# Open http://localhost:3000 → Explore → Tempo
```
Supports any OTLP-compatible backend (Grafana LGTM, Langfuse, OpenLIT, DataDog, New Relic, Honeycomb, [Pydantic Logfire](https://logfire.pydantic.dev), etc.).
### Span Hierarchy
**Parent Spans (Operations):**
- `hindsight.retain` - Memory ingestion
- `hindsight.recall` - Memory retrieval
- `hindsight.recall_embedding` - Query embedding
- `hindsight.recall_retrieval` - Parallel search (semantic, BM25, graph, temporal)
- `hindsight.recall_fusion` - Reciprocal Rank Fusion
- `hindsight.recall_rerank` - Cross-encoder reranking
- `hindsight.reflect` - Agentic reasoning
- `hindsight.reflect_tool_call` - Tool execution (recall, lookup, etc.)
- `hindsight.consolidation` - Observation synthesis
- `hindsight.mental_model_refresh` - Mental model updates
**Child Spans (LLM Calls):**
- Named by scope (e.g., `hindsight.memory`, `hindsight.reflect`)
- Contain full prompts/completions as events
- Follow GenAI semantic conventions for attributes
### Span Attributes
**Operation Spans:**
- `hindsight.operation` - Operation type
- `hindsight.bank_id` - Memory bank ID
- `hindsight.query` - Query text (truncated to 100 chars)
- `hindsight.fact_types` - Fact types for recall
- `hindsight.thinking_budget` - Budget allocation
- `hindsight.max_tokens` - Token limit
**LLM Spans (GenAI Semantic Conventions):**
- `gen_ai.operation.name` - Always `"chat"`
- `gen_ai.provider.name` - Provider (`openai`, `anthropic`, `google`, etc.)
- `gen_ai.request.model` - Model name
- `gen_ai.usage.input_tokens` - Input tokens
- `gen_ai.usage.output_tokens` - Output tokens
- `hindsight.scope` - LLM call purpose (`memory`, `reflect`, `consolidation`, etc.)
**Events:**
- `gen_ai.client.inference.operation.details` - Full prompts and completions
### Trace Context Propagation
Hindsight participates in your existing traces rather than starting parallel ones. When a caller sends W3C trace context (the standard `traceparent` header, which most OpenTelemetry HTTP client instrumentations add automatically), Hindsight continues that trace: the API request appears as a server span under the caller, and every memory operation and LLM call it triggers nests beneath it.
Requests that arrive without trace context still start their own trace, so nothing changes for un-instrumented callers.
Health and metrics endpoints are excluded from tracing so probe traffic doesn't drown out real work. Set `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` to a comma-separated list of URL patterns to override this.
### Worker Processes
Standalone worker processes honour the same `HINDSIGHT_API_OTEL_*` variables as the API and export their own spans. This matters for deployments that run dedicated workers, since consolidation, background retain and mental model refresh — most of the long-running work and token spend — happen there.
Give the worker its own `HINDSIGHT_API_OTEL_SERVICE_NAME` to separate it from the API in your tracing backend. When left unset, workers report themselves as `hindsight-worker`.
Worker spans are currently their own traces: a background operation is not linked to the request that queued it, because it runs long after that request has returned.
references/developer/multilingual.md
# Multilingual Support
Hindsight automatically detects the language of your input and responds in the same language. This means facts, entities, and reflect responses are preserved in their original language without translation to English.
## How It Works
**Figure: Multilingual Support.** An animated diagram on the docs site; its narration, step by step:
- **retain()**
1. Your agent retains Chinese text that mentions English company names.
2. There is no separate language detector. The LLM is told to write facts in the language of the input, and to keep names as they are.
3. Facts are stored in Chinese. 王芳 stays 王芳, not “Wang Fang”, and Google stays Google.
4. Indexing is where language matters. The default embedding model and keyword index are English-only; pick a multilingual model and a tokenizer that can split Chinese.
5. Stored, in the language it came in.
- **recall()**
1. A query in Chinese.
2. The multilingual embeddings match by meaning; the tokenizer lets the exact name 王芳 match as a word.
3. Both point to the Chinese facts. The reranker that then orders them is English-only by default too, so pick a multilingual one.
4. The facts come back exactly as stored. Nothing was translated on the way.
- **reflect()**
1. A question in Chinese.
2. Reflect searches the bank like before…
3. …through the same indexes…
4. …and finds both facts.
5. The answer is written in the language of the question.
6. To force one language everywhere instead, set HINDSIGHT_API_LLM_OUTPUT_LANGUAGE.
When you retain content or reflect on a query, Hindsight:
1. **Detects the input language** automatically from the content
2. **Extracts facts in the original language** - preserving nuance and meaning
3. **Stores entities in their native script** - 张伟 stays 张伟, not "Zhang Wei"
4. **Responds in the same language** - queries in Chinese get Chinese answers
---
## Retain with Non-English Content
When you retain content in any language, Hindsight extracts and stores facts in that same language.
### Example: Chinese Content
```python
from hindsight_client import Hindsight
hindsight = Hindsight(base_url=HINDSIGHT_URL)
# Retain Chinese content
hindsight.retain(
bank_id="multilingual-user",
content="""
张伟是一位资深软件工程师,在腾讯工作了五年。
他专门研究分布式系统,并领导了公司微服务架构的开发。
""",
context="团队概述"
)
# Query in Chinese - get Chinese results
results = hindsight.recall(
bank_id="multilingual-user",
query="告诉我关于张伟的信息"
)
# Facts are returned in Chinese:
# - 张伟是一位资深软件工程师,在腾讯工作了五年
# - 张伟专门研究分布式系统,并领导了公司微服务架构的开发
```
### Example: Japanese Content
```python
hindsight.retain(
bank_id="multilingual-user",
content="""
田中さんはソフトウェアエンジニアで、東京のスタートアップで働いています。
彼女はPythonとTypeScriptが得意で、毎日コードレビューをしています。
""",
context="チームプロフィール"
)
# Query in Japanese
results = hindsight.recall(
bank_id="multilingual-user",
query="田中さんについて教えてください"
)
```
---
## Reflect with Non-English Queries
The `reflect` operation also respects the input language, generating thoughtful responses in the same language as the query.
### Example: Chinese Reflection
```python
# Store facts about team members (in Chinese)
hindsight.retain(
bank_id="multilingual-team-eval",
content="张伟是一位优秀的软件工程师,完成了五个重大项目。他总是按时交付,代码整洁有良好的文档。",
context="绩效评估"
)
hindsight.retain(
bank_id="multilingual-team-eval",
content="李明最近加入团队。他错过了第一个截止日期,代码有很多bug。",
context="绩效评估"
)
# Reflect in Chinese
result = hindsight.reflect(
bank_id="multilingual-team-eval",
query="谁是更可靠的工程师?"
)
# Response is in Chinese:
# "我认为张伟更可靠。张伟完成了五个重大项目,按时交付,代码质量高..."
```
---
## Mixed Language Content
Hindsight handles mixed-language content gracefully, preserving both languages where appropriate.
### Example: Chinese Text with English Company Names
```python
hindsight.retain(
bank_id="multilingual-user",
content="""
王芳在Google北京办公室工作,她是一名高级产品经理。
之前她在Microsoft和Amazon工作过。
她负责管理YouTube在中国市场的推广策略。
""",
context="员工资料"
)
# Facts preserve both languages:
# - 王芳在Google北京办公室工作,担任高级产品经理
# - 王芳曾在Microsoft和Amazon工作过
# - 王芳负责管理YouTube在中国市场的推广策略
```
---
## Supported Languages
**Hindsight's multilingual support depends entirely on your LLM's language capabilities.** Hindsight instructs the LLM to detect the input language and respond in that same language. If your LLM supports a language, Hindsight will work with it.
Most modern LLMs (GPT-4, Claude, Gemini, Llama 3, etc.) support dozens of languages including:
- **East Asian**: Chinese (Simplified/Traditional), Japanese, Korean
- **European**: Spanish, French, German, Italian, Portuguese, Dutch, Polish, Russian
- **Middle Eastern**: Arabic, Hebrew, Turkish
- **South Asian**: Hindi, Bengali, Tamil
- **Southeast Asian**: Thai, Vietnamese, Indonesian
**To verify support for your target language**, test your LLM directly with content in that language. If the LLM can understand and generate text in the language, Hindsight will preserve it correctly.
---
## Configuring for Multilingual Use
For optimal multilingual performance, configure all four components of the pipeline:
### 1. LLM (Required)
Your LLM must support the target languages. Most modern LLMs do, but verify with your specific model.
### 2. Embedding Model (Recommended)
The default embedding model (`BAAI/bge-small-en-v1.5`) is **English-only**. For multilingual content, use a multilingual embedding model:
```bash
# In your .env file
HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-m3
```
**Recommended multilingual embedding models:**
| Model | Languages | Notes |
|-------|-----------|-------|
| `BAAI/bge-m3` | 100+ | Best overall multilingual performance |
| `intfloat/multilingual-e5-large` | 100+ | Good alternative |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 50+ | Lighter weight |
### 3. Reranker Model (Recommended)
The default reranker (`cross-encoder/ms-marco-MiniLM-L-6-v2`) is **English-only**. For multilingual content, use a multilingual reranker:
```bash
# In your .env file
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3
```
**Recommended multilingual reranker models:**
| Model | Languages | Notes |
|-------|-----------|-------|
| `BAAI/bge-reranker-v2-m3` | 100+ | Best multilingual reranking |
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | 14 | Lighter alternative |
### 4. BM25 / Full-Text Search Backend
The semantic (embedding) arm covers cross-lingual matches by meaning. Hindsight runs a BM25 keyword arm in parallel, and **BM25 is inherently within-language** — it's character/token matching against a tokenizer's lexemes. The default `native` backend uses PostgreSQL's English dictionary, which produces poor results for non-English content (and no useful tokenization at all for Chinese / Japanese / Korean, which lack whitespace word boundaries).
There are two knobs that interact:
- `HINDSIGHT_API_TEXT_SEARCH_EXTENSION` — selects the backend (`native`, `vchord`, `pg_textsearch`, `pgroonga`, or `pg_search`).
- `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE` — selects the PostgreSQL dictionary used by the `native` backend (default: `english`).
Pick the backend based on the languages your bank stores:
| Backend | Multilingual / CJK | Notes |
|---------|--------------------|-------|
| `native` | European languages only (English, French, German, Spanish, Italian, Portuguese, Russian, Dutch, Swedish, Norwegian, Danish, Finnish, Hungarian, Turkish, Arabic, plus `simple`). CJK requires a third-party dictionary like `zhparser`. | Stock PostgreSQL — no extra extensions. Configure the language via `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE`. |
| `vchord` | Multilingual via `llmlingua2` tokenizer. | Best when you're already using vchord for vector search. |
| `pg_textsearch` | English only (hardcoded). | Industry-standard BM25 ranking + Block-Max WAND. |
| `pgroonga` | **Yes — out of the box.** Single index handles English, CJK, and mixed-script content via the `TokenBigram` polyglot tokenizer + `NormalizerNFKC150` Unicode normalization. | Recommended for non-English / mixed-language banks. Requires the `pgroonga` extension. See `docker/docker-compose/pgroonga/`. |
| `pg_search` | Multilingual via configurable tokenizer (e.g. `chinese_compatible`, `jieba`, `chinese_lindera`, `japanese_lindera`, `korean_lindera`, `ngram`). | ParadeDB `pg_search` extension; the only Citus-compatible BM25 backend. Tokenizer set via `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER`. See `docker/docker-compose/pg_search/`. |
**Choosing for a single-language bank** (e.g. all Spanish content):
```bash
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=native
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE=spanish
```
**Choosing for a CJK or mixed-language bank**:
```bash
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pgroonga
```
> **📝 External PostgreSQL required**
>
The `pgroonga` and `pg_search` backends depend on PostgreSQL extensions that are **not** bundled with the embedded `pg0` database Hindsight uses when `HINDSIGHT_API_DATABASE_URL` is unset. On that default single-container setup neither backend can be enabled — point `HINDSIGHT_API_DATABASE_URL` at an external PostgreSQL that has the extension installed, as the `docker/docker-compose/pgroonga/` and `docker/docker-compose/pg_search/` recipes do.
The `native` and `pgroonga` knobs do not apply to each other — `pgroonga`'s tokenizer is set at index creation and ignores `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE`.
#### Forcing the LLM Output Language
Independent from the BM25 backend, `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` forces every LLM-generated artifact into a single language regardless of the source content. This applies uniformly to:
- **Retain** — fact text, context, and entity names extracted from source documents.
- **Consolidation** — observations / mental models synthesized from those facts.
- **Reflect** — the final natural-language response returned by the reflect API.
```bash
# Every LLM call (retain, consolidation, reflect) emits Spanish regardless of source language.
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=Spanish
```
Common patterns:
- **Aligned, single-language bank**: `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE=spanish` + `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=Spanish` — store, index, and respond in Spanish even when sources are mixed.
- **Mixed-language bank with multilingual indexing**: `HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pgroonga` + leave `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` unset — preserve source-language facts; pgroonga handles all of them in one index; reflect responds in the query's language.
- **Cross-lingual unification**: `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=English` — every fact, observation, and reflect response in English regardless of source. Useful when the consumer (an English-only LLM, dashboard, or downstream pipeline) needs uniform output.
Leave `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` unset to preserve the source/query language across the pipeline (the default).
#### Default behaviour when no output language is set
With `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` unset, retain and consolidation are both instructed to keep their output in the language of the source material. For observations specifically:
- **Language is decided per observation**, from the facts that observation is built on — not from the batch. A batch mixing Chinese and English facts produces Chinese observations for the Chinese facts and English observations for the English ones. When a single observation merges facts written in several languages, the majority language of those facts wins.
- **Updates follow the new facts.** When an existing observation is written in a different language from the facts updating it, the whole observation is rewritten in the new facts' language. A bank whose observations previously drifted into the wrong language converges back as new facts arrive.
- **Names and technical terms are never translated** — proper nouns, product and place names, identifiers, code, and units stay as written in the source facts, whatever the surrounding language.
This is prompt-level guidance, not a hard guarantee: a model that ignores instructions can still emit the wrong language. Set `HINDSIGHT_API_LLM_OUTPUT_LANGUAGE` explicitly when a bank must be single-language no matter what its sources look like.
---
## Best Practices
### 1. Use Multilingual Models for Non-English Content
If you primarily work with non-English content, configure multilingual embedding and reranker models. English-only models will still store your content correctly, but semantic search quality will be degraded.
### 2. Keep Content in One Language Per Retain Call
While mixed content works, keeping each `retain` call in a single language produces more consistent results.
### 3. Query in the Same Language as Your Content
For best results, query using the same language as your stored content. Cross-language queries (e.g., English query for Chinese content) may work but results can vary depending on your embedding model.
---
## Technical Details
Multilingual support is implemented through LLM prompt instructions rather than external language detection libraries. This approach:
- **Requires no additional dependencies**
- **Works with any LLM** that supports multiple languages
- **Handles edge cases** like mixed-language content naturally
- **Preserves semantic meaning** better than rule-based translation
The LLM is instructed to:
1. Detect the input language
2. Extract all facts, entities, and descriptions in that same language
3. Never translate to English unless the input is in English
references/developer/observations.md
# Observations: Knowledge Consolidation
After memories are retained, Hindsight automatically consolidates related facts into **observations** — deduplicated, evidence-grounded beliefs the bank has built up from multiple memories. Each observation tracks its supporting evidence (with exact quotes) and a proof count, and is refined rather than overwritten when new evidence arrives.
**Figure: Observation Consolidation.** An animated diagram on the docs site; its narration, step by step:
- **refine**
1. A new fact lands. Until it is consolidated, observation searches are flagged stale, so reflect checks them against the raw facts.
2. Consolidation runs in the background after retain. For each new fact it recalls related observations, only within the same tag scope.
3. One LLM call sees the new facts next to those observations and decides, facet by facet: create, update or delete.
4. Before anything is written, each new or rewritten observation is compared with its closest neighbours. Only a near-identical one gets a merge-or-keep check.
5. The observation is rewritten with the fact attached as evidence, so its proof count goes up. The previous wording is kept in history.
6. The same write marks the fact consolidated, so the observation is fresh again.
- **contradict**
1. Now a fact that contradicts what the bank believes.
2. A change of state is not a reason to delete. The LLM updates the belief so it records what changed, with dates when it has them.
3. The observation now tells the whole journey, not just “prefers Vue”. It rests on all three facts, and both older versions stay in history.
- **something new**
1. A fact about something the bank has no belief on yet.
2. Nothing covers this facet, so the LLM creates a new observation instead of bending an unrelated one.
3. The near-duplicate check keeps it: different facets stay separate observations.
4. The new observation starts with one source. It will gain evidence as more facts repeat it.
---
## What Are Observations?
Observations are **consolidated knowledge** built from multiple facts. Unlike raw facts — which are individual pieces of information — observations represent deduplicated beliefs, preferences, and learnings grounded in accumulated evidence. They are not summaries the LLM invents on the fly: each observation is backed by specific source memories, carries a proof count, and evolves as new evidence supports, contradicts, or extends it.
| Raw Facts | Observation |
|-----------|--------------|
| "Alice prefers Python" | "Alice is a Python-focused developer who values readability and simplicity" |
| "Alice dislikes verbose code" | |
| "Alice recommends type hints" | |
Observations provide:
- **Deduplication**: One durable belief instead of many overlapping facts
- **Grounding**: Every observation references the specific memories (with quotes) that support it
- **Evolution**: Refined as evidence strengthens, weakens, or contradicts it — history is preserved
- **Freshness awareness**: when newer memories haven't been consolidated yet, `reflect` treats the affected observations as stale and verifies them against raw facts
- **Efficiency**: Condensed knowledge for faster retrieval
---
## How Consolidation Works
### Automatic Background Processing
After `retain()` completes, the consolidation engine runs automatically:
1. **New facts analyzed** — Each new fact is compared against existing observations
2. **Pattern detection** — Related facts are grouped and synthesized
3. **Observation creation/update** — New observations are created or existing ones refined
4. **Evidence tracking** — Each observation maintains references to supporting facts
### Near-Duplicate Reconciliation
Consolidation can still produce two observations that say the same thing in slightly different words — for example when a weaker model writes a near-identical observation instead of refining the existing one, or when refining an observation reshapes its wording so it overlaps another one. Left alone, these near-duplicates clutter recall with redundant beliefs.
When enabled, Hindsight reconciles them automatically. Whenever an observation is created **or** updated, it is compared against the existing observations it most closely resembles. If one is highly similar, a focused check decides whether to **merge** them into a single belief (folding both sets of supporting evidence together) or **keep** them separate. Because the check reads the full text of both, observations that differ in a meaningful detail — a number, a negation, a named entity or language — are correctly kept apart rather than collapsed.
This is controlled by the [`HINDSIGHT_API_CONSOLIDATION_DEDUP_THRESHOLD`](configuration.md#observations) setting: the cosine similarity at or above which two observations are reconciled. It is **enabled by default** (`0.97`); a lower value reconciles more aggressively, and `1.0` disables it. Reconciliation runs on PostgreSQL deployments only — it is skipped on Oracle regardless of the threshold.
Reconciliation only compares observations **within the same tag scope**. If you tag retains with a unique per-call value (e.g. a `session-id`), each session lands in its own scope and never dedups against the others — producing one near-identical observation per session. To consolidate across those volatile tags, retain with [`observation_scopes: "shared"`](api/retain.md#shared), which scopes observations to one global, untagged belief while leaving the session tag on the source facts for recall filtering.
### Disabling Auto-Consolidation
Set `HINDSIGHT_API_ENABLE_AUTO_CONSOLIDATION=false` (or configure per-bank via the [bank config API](api/memory-banks.md#observations-configuration)) to prevent consolidation from running automatically after retain, delete, and update operations. When disabled, consolidation only runs when you explicitly call the [consolidate endpoint](#trigger-consolidation).
This is useful when you want full control over consolidation timing — for example, batching many retains before consolidating, or running consolidation only for specific scopes.
### Targeted Consolidation
By default, consolidation processes **all** unconsolidated memories in a bank. You can scope it to specific tag sets using the `observation_scopes` parameter on the consolidate endpoint:
```python
# Consolidate only memories tagged with user:alice
await client.banks.trigger_consolidation(
bank_id=BANK_ID,
consolidation_request=ConsolidationRequest(observation_scopes=[["user:alice"]]),
)
# Consolidate memories for alice OR the engineering team
await client.banks.trigger_consolidation(
bank_id=BANK_ID,
consolidation_request=ConsolidationRequest(
observation_scopes=[["user:alice"], ["team:engineering"]]
),
)
```
Each scope is a list of tags. A memory matches a scope if its tags **contain all** tags in that scope. For example, scope `["user:alice"]` matches memories tagged `["user:alice", "team:eng"]`.
When `observation_scopes` is omitted, all unconsolidated memories are processed (backward compatible).
### Evidence-Based Evolution
Observations evolve as new evidence arrives:
| Event | What the bank learns | Observation state |
|-------|---------------------|----------------|
| **Day 1** | "Redis is open source under BSD license" | "Redis is excellent for caching — fast, reliable, and OSS-friendly" (2 supporting facts) |
| **Day 2** | "Redis has great community support" | Observation reinforced (3 supporting facts) |
| **Day 30** | "Redis changed license to SSPL" | Observation refined: "Redis is technically strong, but has license concerns for cloud" |
| **Day 45** | "Valkey forked Redis under BSD" | New observation: "Consider Valkey for new projects requiring true OSS" |
### Handling Contradictory Evidence
What happens when a new fact contradicts an existing observation?
The consolidation engine doesn't blindly overwrite — it **reconciles** the contradiction by capturing the evolution:
**Example: User preference changes**
| Time | Fact | Observation |
|------|------|--------------|
| Week 1 | "User says they love React" | "User prefers React for frontend development" |
| Week 2 | "User praises React's component model" | "User is enthusiastic about React, particularly its component model" |
| Week 3 | "User says they've switched to Vue and won't use React anymore" | "User was previously a React enthusiast who appreciated its component model, but has now switched to Vue and no longer uses React" |
Notice how the final observation captures the **full journey** — not just "User prefers Vue" but the complete evolution of their preference. This nuanced understanding means:
- Your agent won't recommend React tutorials to someone who explicitly moved away from it
- Your agent understands *why* this matters (they were enthusiastic before, so this is a deliberate choice)
- Your agent can reference this history when relevant ("I know you used to work with React...")
The system:
1. **Detects the conflict** — New fact contradicts existing observation
2. **Preserves history** — Incorporates the previous understanding into the new observation
3. **Creates nuanced observation** — Synthesizes a richer understanding that captures the change
4. **Updates freshness** — Marks the observation as recently updated
**Example: Correcting misinformation**
| Time | Fact | Observation |
|------|------|--------------|
| Day 1 | "Alice works at Google" | "Alice is a Google employee" |
| Day 10 | "Alice actually works at Meta, not Google" | "Alice works at Meta (previously thought to work at Google)" |
When a fact explicitly corrects previous information, the observation is updated to reflect the correction while noting the previous understanding. The raw facts are always preserved, so you can trace back to see what was originally stated and when it was corrected.
---
## Observations in Retrieval
Observations are automatically included in both `recall()` and `reflect()` operations:
### In Recall
Observations are returned alongside raw facts, filtered by the `types` parameter:
```python
# Include observations in recall
results = client.recall(
bank_id="my-bank",
query="What programming languages does Alice prefer?",
types=["world", "experience", "observation"]
)
# Observations only
observations = client.recall(
bank_id="my-bank",
query="What patterns have I learned?",
types=["observation"]
)
```
### In Reflect
The reflect agent uses **hierarchical retrieval**:
1. **[Mental Models](api/mental-models.md)** — User-curated summaries (highest priority)
2. **Observations** — Consolidated knowledge with freshness awareness
3. **Raw Facts** — Ground truth for verification
The agent automatically queries observations and uses them to inform its reasoning.
---
## Freshness Awareness
Observations track when they were last updated. During reflect, the agent considers freshness:
- **Fresh observations**: Used directly for reasoning
- **Stale observations**: Agent verifies against current facts before relying on them
This ensures responses stay accurate even as the underlying data changes.
---
## Observation Scopes
By default, observations are scoped to all of a memory's tags combined. The `observation_scopes` retain parameter lets you control this — building separate observations per tag, per combination, or with a custom list of scopes. This is key when a single memory carries multiple tags and you want each tag to accumulate its own observations independently.
See [`observation_scopes` in the Retain API](./api/retain.md#observation_scopes) for the full explanation and options.
To inspect the scopes that already exist in a bank, call `GET /v1/default/banks/{bank_id}/observations/scopes`. The response lists each exact tag set with its observation count; the empty tag list is the global scope. The listing is paged (`limit`, default 100, and `offset`), with `total` reporting how many distinct scopes the bank holds. Use a returned scope as `tags` with `tags_match: "exact"` when you need to filter to that precise observation scope without also matching observations that carry extra tags. To recall **only** the global scope — the untagged observations written by `observation_scopes: "shared"` — pass an empty list with exact matching: `tags: []`, `tags_match: "exact"`.
### Changing the Scopes of Existing Documents {#changing-scopes}
The scoping is stored on each memory when it is retained. To move documents that are already in the bank, for example a bank retained with per-session tags under the default `combined` scoping that should now consolidate across sessions, send the new value on the document:
```python
# Move a document retained under its session tag to the shared scope
await client.documents.update_document(
bank_id=BANK_ID,
document_id="session-123",
update_document_request=UpdateDocumentRequest(
observation_scopes=UpdateDocumentObservationScopes(actual_instance="shared")
),
)
```
It takes the same values as retain (`"combined"` returns to the default). Nothing is re-extracted, and the memories keep their tags. A memory is consolidated again only if the change moves the scopes it resolves to. In that case its observations are deleted and rebuilt in the new scope by the next consolidation, and so are observations that other documents' memories shared with it. A change that resolves to the same scopes deletes nothing and queues nothing. Examples: `combined` over a single tag versus `[["that-tag"]]`, or re-sending the current value. The new value is also saved on the document, so a later reprocess keeps it.
To move a whole bank, list its documents (filtering by tag if needed) and send one PATCH per document. Two things to know first:
- **Update the client first.** A retain that appends to a document sends its own `observation_scopes`. If your client still retains without the new value, the next append moves that document back.
- **Expect a rebuild.** Every moved memory loses its observations until consolidation runs again. On a large bank that is one LLM pass over most of its facts, and recall returns fewer observations meanwhile. Mental models citing the deleted observations refresh as usual. Moving documents in batches spreads the cost.
---
## Observations Mission
You can define exactly what this bank should synthesise by setting an **observations mission** (`observations_mission`). This replaces the built-in durable-knowledge rules with your own instructions, letting you control what shape observations take.
```
e.g. Observations are stable facts about people and projects.
Always include preferences, skills, and recurring patterns.
Ignore one-off events and ephemeral state.
```
Leave it blank to use the server default — durable, specific facts that stay true over time (preferences, skills, relationships, recurring patterns), with ephemeral state filtered out.
**Examples:**
| `observations_mission` | What gets synthesised |
|------------------------|----------------------|
| *(unset — default)* | Durable facts: preferences, skills, relationships, recurring patterns |
| *"Observations are weekly summaries of sprint outcomes and blockers"* | Broad event summaries grouped by time period |
| *"Observations are stable facts about named individuals only"* | Person-centric knowledge, tied to specific people |
| *"Observations are recurring patterns in customer support interactions"* | Failure modes, common requests, pain points |
Set `observations_mission` via the [bank config API](api/memory-banks.md#observations-configuration) or the [`HINDSIGHT_API_OBSERVATIONS_MISSION`](configuration.md#observations) environment variable.
---
## Consolidation Strategies
The mission, the observation cap and the source-facts token limits apply to every scope in the bank. **Consolidation strategies** (`consolidation_strategies`) let specific scopes use their own instead.
This is what makes one bank work across several audiences. Say each memory is tagged with its author, their team and the company, and retained with `observation_scopes: [["user:dana"], ["team:exec"], ["company:acme"]]`. Each scope builds its own observations. But with a single mission, the company-wide scope gets the same detail as Dana's own — names, deal sizes, anything said in confidence — just visible to more people. A strategy fixes that:
```json
[
{
"scopes": [{"tags": ["company:*"]}],
"observations_mission": "This scope is shared with the whole company. Record only general, industry-level trends. Never name a specific company, person, deal size or funding stage.",
"max_observations_per_scope": 20
},
{
"scopes": [{"tags": ["team:*"]}],
"observations_mission": "Record decisions the team must act on."
}
]
```
With that in place, Dana's scope keeps "Acme Robotics signed a 3-year lease for a 4,000 GPU cluster", while the company scope gets "Companies are leasing GPU clusters to run open-weight models".
How a strategy is matched:
- **`scopes`** is a list of alternatives. Each is `{"tags": [...], "tags_match": "all" | "exact"}`, where `tags` are [fnmatch](https://docs.python.org/3/library/fnmatch.html) patterns (`*` matches any text). A strategy applies to a consolidation scope when **any** of its alternatives matches it (OR); the tags inside one alternative must all be present (AND).
- **`tags_match`**, set per alternative, decides whether other tags are allowed:
| `tags_match` | `{"tags": ["company:*", "team:*"]}` matches… | …but not |
|---|---|---|
| `"all"` *(default)* | `{company:acme, team:exec}`, `{user:dana, team:exec, company:acme}` | `{company:acme}` (no team) |
| `"exact"` | `{company:acme, team:exec}` only | `{user:dana, team:exec, company:acme}` (extra `user:` tag) |
Because the mode is per alternative, one strategy can mix them — `[{"tags": ["company:*"], "tags_match": "exact"}, {"tags": ["team:*"]}]` claims scopes that are *only* a company, or that have a team among other tags. To match *any one* of several tags, give each its own alternative: `[{"tags": ["company:*"]}, {"tags": ["team:*"]}]`. Excluding a tag ("has a company but no user") is not expressible.
> **🚨 The untagged scope can never be claimed**
>
No pattern matches a scope with no tags, including a bare `["*"]`. That means a bank using
[`observation_scopes: "shared"`](./api/retain.md#observation_scopes), which consolidates over the single untagged
scope, has one scope that no strategy can reach: it always uses the bank-wide values. If you need a
differently-briefed catch-all, give it a real tag and target that instead of relying on `shared`.
Remember that a strategy matches **observation scopes** — the tag sets consolidation groups observations under, set by [`observation_scopes`](./api/retain.md#observation_scopes) at retain time — not the tags of individual memories. A memory tagged with a user, a team and a company but retained with `observation_scopes: [["user:dana"], ["team:exec"], ["company:acme"]]` produces three single-tag scopes, none of which has both a company and a team.
- **What a strategy can set:** `observations_mission`, `max_observations_per_scope`, `consolidation_source_facts_max_tokens` and `consolidation_source_facts_max_tokens_per_observation`. Every one is optional.
- **Anything a strategy leaves unset, and every scope no strategy matches,** uses the bank-wide value. The control plane shows these bank-wide values as the **Default** strategy.
### When two strategies match the same scope
**The first one in the list wins, whole.** Exactly one strategy applies to a scope. List order is priority order — left to right in the control plane's tabs.
The strategies are never mixed. If the winning strategy leaves a setting empty, that setting comes from **Default**, not from a later strategy that also matches. For example, with:
```json
[
{"scopes": [{"tags": ["company:acme"]}], "max_observations_per_scope": 5},
{"scopes": [{"tags": ["company:*"]}], "observations_mission": "Record only general trends."}
]
```
the scope `company:acme` gets a cap of 5 and the **bank-wide mission**, not "Record only general trends". The second strategy still applies to every other `company:*` scope. To give `company:acme` both, put both settings on the first strategy.
To see which strategy a scope uses, open the strategy in the control plane: each rule shows how many existing scopes it matches and how many an earlier strategy takes, with examples. The same answer is available from `POST /v1/default/banks/{bank_id}/consolidation-strategies/preview`, which takes a draft `strategies` list (nothing is saved) and reports, per strategy and rule, the matching scopes and which strategy actually handles each — computed with consolidation's own matching. It scans up to 10,000 distinct scopes; beyond that `complete` is `false` and the counts are lower bounds. Only scopes that already have observations exist to be previewed.
A strategy that sets nothing is ignored: it matches no scope and doesn't block later ones.
A malformed strategy is rejected when you save it through the control plane or the bank config API — an unknown key
(`"scope"` for `"scopes"`), a tag list that is not a list of strings, an unknown `tags_match` — with a 400 naming the
offending entry. The `HINDSIGHT_API_CONSOLIDATION_STRATEGIES` environment variable is **not** validated this way: a
malformed entry is dropped silently, and an unrecognised `tags_match` falls back to `"all"` rather than erroring. If you
configure strategies by environment variable, check the preview afterwards to confirm each one claims what you expect. An *incomplete* one is accepted, because the control plane saves strategies as you type them: a rule with no tags yet, or a strategy with no setting yet, is stored and simply ignored until you finish it. Strategies survive [bank template](api/memory-banks.md) export and import unchanged.
Each scope is consolidated in its own LLM call, so one scope's mission never reaches another scope's call.
Set `consolidation_strategies` in the control plane (bank **Configuration → Observations**), via the [bank config API](api/memory-banks.md#observations-configuration), or with the [`HINDSIGHT_API_CONSOLIDATION_STRATEGIES`](configuration.md#observations) environment variable. It replaces the older `observation_scope_limits`, which still works but is checked only after the strategies, and only
ever carried the observation cap.
> **📝 Porting a rule from `observation_scope_limits`**
>
The two fields match differently. `observation_scope_limits` is exact-cover only: its pattern must account for every tag
in the scope. A strategy defaults to `tags_match: "all"`, which allows extra tags. Copying a pattern across unchanged
therefore widens what it matches. Add `"tags_match": "exact"` to keep the old behaviour.
Note also that a strategy claiming a scope without setting `max_observations_per_scope` leaves the cap to
`observation_scope_limits`, falling back past any later strategy that would have set one.
---
## Observation Lifecycle & Invalidation
### When Memories Are Deleted
Observations are derived from source memories. When source memories are removed, Hindsight automatically keeps observations consistent:
| Action | Effect on observations |
|--------|----------------------|
| Delete a document | All observations derived from the document's memories are deleted |
| Delete individual memories (by type) | Observations sourced from those memories are deleted |
| Delete an entire bank | All observations are deleted along with everything else |
After deletion, the **remaining source memories** that fed the affected observations have their consolidation state reset, so they will be re-consolidated on the next consolidation run and produce fresh observations.
### Clearing Observations for a Specific Memory
You can clear all observations derived from a single memory without deleting the memory itself. This is useful when you want to force re-synthesis of a memory's contribution to consolidated knowledge.
Use the `DELETE /v1/default/banks/{bank_id}/memories/{memory_id}/observations` endpoint. This will:
1. Delete all observations that list the memory as a source
2. Reset `consolidated_at` on the memory itself and any other source memories that contributed to those observations
3. Trigger a consolidation job so fresh observations are produced automatically
### Resetting All Observations
To wipe all consolidated knowledge and start over:
```python
# Clear all observations for a bank
await client.banks.clear_observations(bank_id=BANK_ID)
```
This resets the consolidation state for all source memories in the bank, so the next consolidation run will re-derive all observations from scratch.
---
## Trigger Consolidation {#trigger-consolidation}
Use the consolidate endpoint to manually trigger consolidation:
```http
POST /v1/default/banks/{bank_id}/consolidate
Content-Type: application/json
{
"observation_scopes": [["user:alice"], ["team:engineering"]]
}
```
The request body is optional. When omitted (or sent as an empty body), all unconsolidated memories in the bank are processed.
| Parameter | Type | Description |
|-----------|------|-------------|
| `observation_scopes` | `list[list[str]]` \| `null` | Optional list of tag scopes. Only memories whose tags contain all tags in at least one scope are processed. Omit for a full-bank sweep. |
## Configuration
Observation consolidation runs automatically by default. You can disable auto-consolidation with [`HINDSIGHT_API_ENABLE_AUTO_CONSOLIDATION`](configuration.md#observations) and trigger it on-demand via the [consolidate endpoint](#trigger-consolidation). Monitor consolidation progress via the [Operations API](./api/operations.md).
---
## Next Steps
- [**Retain**](./retain.md) — How facts are stored and trigger consolidation
- [**Recall**](./retrieval.md) — How observations are retrieved
- [**Reflect**](./reflect.md) — How the agentic loop uses observations
- [**Mental Models**](./api/mental-models.md) — User-curated summaries for common queries
references/developer/oracle.md
# Oracle Database
Hindsight uses PostgreSQL as its default storage backend, but it also runs on
**Oracle Database 23ai** for organizations that standardize on Oracle
infrastructure. All memory operations — retain, recall, and reflect — work the
same way on Oracle; the backend is selected with a single environment variable.
This guide covers everything needed to run Hindsight against Oracle: the
prerequisites, the driver, a local quick start, provisioning a production
database, running migrations, and the handful of behavioural differences from
PostgreSQL.
:::info When to use Oracle
Oracle is the right choice when your organization already runs Oracle and needs
Hindsight to live inside that footprint. For everything else, the default
PostgreSQL backend is simpler to operate — see [Storage](./storage.md) for the
rationale. Oracle and PostgreSQL are configured independently; you pick one per
deployment.
:::
## Requirements
| Requirement | Details |
|-------------|---------|
| Oracle Database | **23ai** (23.4+). [Oracle Database Free 23ai](https://www.oracle.com/database/free/) works for development. |
| `VECTOR` type | Used for embeddings. Requires the schema to live in an **ASSM tablespace** (see below). |
| Oracle Text | Full-text search uses Oracle Text indexes. The schema user needs the `CTXAPP` role. |
| Driver | [`python-oracledb`](https://python-oracledb.readthedocs.io/) ≥ 2.5.0, running in **thin mode** — pure Python, no Oracle Instant Client required. |
:::warning The schema must use an ASSM tablespace
Oracle's `SYSTEM` tablespace uses *manual* segment space management (MSSM),
which **does not support `VECTOR` columns**. Create the Hindsight user in a
tablespace with **Automatic Segment Space Management (ASSM)** — otherwise
migrations fail when they create embedding columns. The provisioning SQL below
does this for you.
:::
## Install the driver
The Oracle driver is an optional extra — it is not bundled with the default
packages. Install it alongside Hindsight:
```bash
# With the packaged extra
pip install "hindsight-api-slim[oracle]"
# Or add the driver to an existing install (e.g. the full hindsight-api package)
pip install hindsight-api oracledb
```
If the driver is missing at startup, Hindsight fails with:
`python-oracledb is required for Oracle backend. Install it with: pip install oracledb`.
## Quick start (local Oracle)
The fastest way to try Hindsight on Oracle is the bundled helper script, which
starts a local **Oracle Database Free 23ai** container, provisions the test
user with the correct tablespace and grants, and prints a ready-to-use
connection URL:
```bash
# Start Oracle Free in Docker and bootstrap the hindsight_test user
./scripts/dev/start-oracle.sh
# ...prints:
# export HINDSIGHT_API_DATABASE_BACKEND=oracle
# export HINDSIGHT_API_DATABASE_URL='oracle+oracledb://hindsight_test:hindsight_test@localhost:1521/FREEPDB1'
# Stop and remove the container when done
./scripts/dev/stop-oracle.sh
```
A cold start takes 60–120s while the database initializes. If the first run
reports a provisioning error, the database was still starting up — just re-run
`./scripts/dev/start-oracle.sh` once the container is healthy (it is idempotent).
Once the script prints the connection URL, export the variables it shows, and
**also set the schema** to the Oracle user it created:
```bash
export HINDSIGHT_API_DATABASE_SCHEMA=HINDSIGHT_TEST
```
Then run migrations and start the API (see the steps below). Setting the schema
is required on Oracle — see [step 3](#3-configure-hindsight). This is the same
setup Hindsight's CI uses to test the Oracle backend.
## Production setup
### 1. Provision the schema user
Connect to your pluggable database as a privileged user (for example `SYSTEM`)
and create a dedicated tablespace and user for Hindsight. The tablespace **must**
use ASSM so `VECTOR` columns are supported:
```sql
-- ASSM tablespace (required for VECTOR columns). Size to your data volume.
CREATE BIGFILE TABLESPACE hindsight_ts
DATAFILE 'hindsight_ts.dbf' SIZE 2G AUTOEXTEND ON NEXT 500M MAXSIZE UNLIMITED
EXTENT MANAGEMENT LOCAL
SEGMENT SPACE MANAGEMENT AUTO;
-- Dedicated schema user
CREATE USER hindsight IDENTIFIED BY "<strong-password>"
DEFAULT TABLESPACE hindsight_ts
TEMPORARY TABLESPACE temp
QUOTA UNLIMITED ON hindsight_ts;
-- Object privileges Hindsight's migrations need
GRANT CONNECT, RESOURCE, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW, CREATE PROCEDURE TO hindsight;
-- Oracle Text (full-text search indexes)
GRANT CTXAPP TO hindsight;
```
:::note Least privilege
`CONNECT` and `RESOURCE` cover the basics; the explicit `CREATE TABLE / SEQUENCE
/ VIEW / PROCEDURE` grants and `CTXAPP` are what the schema migrations require.
No `DBA` role is needed. On a managed service where `CREATE TABLESPACE` is not
available directly, provision the schema through the platform's admin tooling —
the requirements are unchanged: an **ASSM** default tablespace (needed for
`VECTOR` columns) plus the `CTXAPP` role.
:::
### 2. Build the connection URL
Hindsight uses SQLAlchemy-style URLs. The Oracle form is:
```
oracle+oracledb://USER:PASSWORD@HOST:PORT/SERVICE_NAME
```
| Part | Example | Notes |
|------|---------|-------|
| `USER` / `PASSWORD` | `hindsight` / `s3cret` | The schema user from step 1. URL-encode reserved characters (`@`, `/`, `:`) in the password. |
| `HOST:PORT` | `db.internal:1521` | The listener host and port (Oracle default is `1521`). |
| `SERVICE_NAME` | `FREEPDB1` | The **service name** of your pluggable database (not the SID). `FREEPDB1` for Oracle Free. |
Example:
```
oracle+oracledb://hindsight:s3cret@db.internal:1521/ORCLPDB1
```
#### Full connect descriptors and TNS aliases
The form above is Easy Connect, which can only express `host:port/service`. When you
need more — Autonomous Database over TCPS, `retry_count`, `ssl_server_dn_match`, or a
TNS alias from a `tnsnames.ora` — leave the host empty and pass the descriptor as a
`dsn` query parameter:
```
oracle+oracledb://USER:PASSWORD@/?dsn=DESCRIPTOR_OR_TNS_ALIAS
```
The descriptor has to be percent-encoded, like the password. For example, the
Autonomous Database descriptor
```
(description=(retry_count=20)(address=(protocol=tcps)(port=1522)(host=adb.example.com))(connect_data=(service_name=abc_low))(security=(ssl_server_dn_match=yes)))
```
becomes
```bash
export HINDSIGHT_API_DATABASE_URL="oracle+oracledb://hindsight:s3cret@/?dsn=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.stdin.read().strip()))' <<< '(description=(retry_count=20)(address=(protocol=tcps)(port=1522)(host=adb.example.com))(connect_data=(service_name=abc_low))(security=(ssl_server_dn_match=yes)))')"
```
:::warning Wallet-based mTLS is still not supported
Hindsight passes no wallet directory or wallet password to the driver, so
connections that require a wallet (mTLS) do not work. TCPS with
`ssl_server_dn_match` does. Otherwise secure the connection at the network layer
(private networking, VPN, or a TLS-terminating proxy).
:::
### 3. Configure Hindsight
Point Hindsight at Oracle with two environment variables:
```bash
export HINDSIGHT_API_DATABASE_BACKEND=oracle
export HINDSIGHT_API_DATABASE_URL='oracle+oracledb://hindsight:s3cret@db.internal:1521/ORCLPDB1'
export HINDSIGHT_API_DATABASE_SCHEMA=HINDSIGHT # the Oracle user from step 1
```
`HINDSIGHT_API_DATABASE_BACKEND` defaults to `postgresql`; set it to `oracle` to
select the Oracle backend.
:::warning Set `DATABASE_SCHEMA` to your Oracle user
`HINDSIGHT_API_DATABASE_SCHEMA` defaults to `public` — a PostgreSQL concept. On
Oracle a schema **is a user**, there is no `public` schema, and leaving the
default makes migrations fail with `ORA-01435: user does not exist`. Set it to
the schema user you created in step 1, spelled exactly as Oracle stores it —
**uppercase** (e.g. `HINDSIGHT`) unless you created the user with a quoted
lower-case name.
:::
See [Configuration → Database](./configuration.md#database) for the full list of
database variables.
### 4. Run migrations
Hindsight runs the same schema migrations on Oracle as on PostgreSQL. By default
the API applies them automatically on startup
(`HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=true`). To run them explicitly — for
example in a controlled deploy step — use:
```bash
hindsight-admin run-db-migration
```
This routes through the dialect-aware migration runner and creates the Oracle
schema. (Unlike the admin CLI's data-movement commands, `run-db-migration`
is fully supported on Oracle — see [Limitations](#limitations-vs-postgresql).)
:::warning Migrate with your runtime embedding dimension
The embedding `VECTOR` columns are sized to the dimension of the configured
embeddings model. Run migrations with the **same embeddings provider/model you
will serve with** — otherwise the column dimension won't match the vectors the
API produces and retain fails with `ORA-51803: Vector dimension count must
match…` (for example, a schema built for a 384-dim local model rejects the
1536-dim vectors from OpenAI `text-embedding-3-small`). If you change the
embeddings model later, re-run migrations with `--embedding-dimension <N>` to
resize the columns.
:::
### 5. Start the API
```bash
hindsight-api
```
On startup Hindsight logs the resolved database (with credentials masked); it
should show your Oracle host and confirm the Oracle backend is active.
## Configuration reference
Oracle-relevant settings, all documented in full on the
[Configuration](./configuration.md) page:
| Variable | Purpose |
|----------|---------|
| `HINDSIGHT_API_DATABASE_BACKEND` | `postgresql` (default) or `oracle`. |
| `HINDSIGHT_API_DATABASE_URL` | `oracle+oracledb://…` connection URL. |
| `HINDSIGHT_API_DATABASE_SCHEMA` | Schema/user for the tables. On Oracle set this to your schema user (uppercase); the `public` default fails. |
| `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Auto-apply migrations when the API boots (default `true`). |
## Limitations vs PostgreSQL
Memory operations behave identically on Oracle, but a few operational and
internal details differ:
- **Admin CLI data commands are PostgreSQL-only.** `hindsight-admin` backup,
restore, bank export/import, and worker-status use asyncpg binary `COPY` and
`TRUNCATE`, which are PostgreSQL-specific and not available on Oracle.
Schema migrations (`run-db-migration`) *are* supported on Oracle.
- **No embedded database.** The `pg0` embedded PostgreSQL used for zero-config
local development has no Oracle equivalent — Oracle always requires a running
instance (use the [quick-start script](#quick-start-local-oracle) locally).
- **Consolidation reconciliation is skipped.** The similarity-based
near-duplicate reconciliation pass in consolidation
(`HINDSIGHT_API_CONSOLIDATION_DEDUP_THRESHOLD`) is a PostgreSQL-only path;
consolidation still runs on Oracle, without that extra reconciliation step.
- **Entity resolution uses Oracle fuzzy matching.** Fuzzy entity lookup during
retain uses Oracle's text matching rather than PostgreSQL's `pg_trgm` trigram
matching. Behaviour is equivalent; the underlying mechanism differs.
## Troubleshooting
| Symptom | Cause / Fix |
|---------|-------------|
| `python-oracledb is required for Oracle backend` | The driver isn't installed. Run `pip install oracledb` (or install the `[oracle]` extra). |
| `ORA-01435: user does not exist` on migration | `HINDSIGHT_API_DATABASE_SCHEMA` is unset (defaults to `public`) or misspelled. Set it to your Oracle schema user, uppercase (e.g. `HINDSIGHT`). |
| `ORA-51803: Vector dimension count must match` on retain | The schema was migrated with a different embedding dimension than the running embeddings model. Migrate with the same embeddings config, or re-run `run-db-migration --embedding-dimension <N>`. |
| Migration errors when creating embedding/`VECTOR` columns | The schema user's default tablespace is not ASSM (often the `SYSTEM` tablespace). Recreate the user in an ASSM tablespace as shown above. |
| Full-text search errors / missing Oracle Text index | The schema user is missing the `CTXAPP` role. Run `GRANT CTXAPP TO <user>;`. |
| `ORA-12514` / service not found | The URL uses a SID or wrong service name. Use the pluggable database **service name** (e.g. `FREEPDB1`), not the SID. |
| Login works manually but fails from Hindsight | A reserved character in the password isn't URL-encoded. Encode `@ / : ?` in the `DATABASE_URL`. |
## See also
- [Storage](./storage.md) — why PostgreSQL is the default, and how Oracle fits in
- [Configuration](./configuration.md#database) — all database environment variables
- [Installation](./installation.md) — packaging and deployment options
- [Admin CLI](./admin-cli.md) — administrative commands (PostgreSQL-only data operations)
references/developer/performance.md
# Performance
Hindsight is designed for high-performance semantic memory operations at scale. This page covers performance characteristics, optimization strategies, and best practices.
## Overview
Hindsight's performance is optimized across three key operations:
- **Retain (Ingestion)**: Batch processing with async operations for large-scale memory storage
- **Recall (Search)**: Sub-second semantic search with configurable thinking budgets
- **Reflect (Reasoning)**: Disposition-aware answer generation with controllable compute
## Design Philosophy: Optimized for Fast Reads
Hindsight is **architected from the ground up to prioritize read performance over write performance**. This design decision reflects the typical usage pattern of memory systems: memories are written once but read many times.
The system makes deliberate trade-offs to ensure **sub-second recall operations**:
- **Pre-computed embeddings**: All memory embeddings are generated and indexed during retention
- **Optimized vector search**: HNSW indexes enable fast approximate nearest neighbor search
- **Fact extraction at write time**: Complex LLM-based fact extraction happens during retention, not retrieval
- **Structured memory graphs**: Relationships and temporal information are resolved upfront
This means **Recall (search) operations are blazingly fast** because all the heavy lifting has already been done.
### Performance Comparison
| Operation | Typical Latency | Primary Bottleneck | Optimization Strategy |
|-----------|----------------|-------------------|----------------------------------|
| **Recall** | 100-600ms | Re-ranker (on CPU) | Use GPU for re-ranking, or reduce budget |
| **Reflect** | 800-3000ms | LLM generation | Use faster LLM |
| **Retain** | 500ms-2000ms per batch | **LLM fact extraction** | Use high-throughput LLM provider |
Hindsight is designed to ensure your **application's read path (recall/reflect) is always fast**, even if it means spending more time upfront during writes. This is the right trade-off for memory systems where:
- Memories are retained in background processes or during low-traffic periods
- Memories are queried frequently in user-facing, latency-sensitive contexts
- The ratio of reads to writes is high (typically 10:1 or higher)
---
## Retain Performance
**Retain (write) operations are inherently slower** because they involve LLM-based fact extraction, entity recognition, temporal reasoning, relationship mapping, and embedding generation. **The LLM is the primary bottleneck for write latency.**
### Hindsight Doesn't Need a Smart Model
The fact extraction process is structured and well-defined, so smaller, faster models work extremely well. Our recommended model is `gpt-oss-20b` (available via Groq and other providers).
To maximize retention throughput:
1. **Use high-throughput LLM providers**: Choose providers with high requests-per-minute (RPM) limits and low latency
- **Fast**: [Groq](https://groq.com) with `gpt-oss-20b` or other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI)
- **Slow**: Standard cloud LLM providers with rate limits
2. **Batch your operations**: Group related content into batch requests. Send as much data as you want in a single request — the only limit is the HTTP payload size.
3. **Use async mode for large datasets**: Queue operations in the background
4. **Parallel processing**: For very large datasets, use multiple concurrent retention requests with different `document_id` values
### Automatic Batch Optimization
**When using async retain, Hindsight automatically handles batch sizing for you.** You don't need to manually tune batch sizes or worry about optimal chunking.
How it works:
- **Send large batches**: Submit hundreds or thousands of items in a single async retain request
- **Automatic splitting**: Hindsight automatically splits large batches (>10,000 tokens) into optimized sub-batches
- **Parallel processing**: Sub-batches are processed concurrently in the background
- **Status tracking**: Parent operation aggregates status from all sub-batches
- **Token-based**: Batching counts real tokens, not characters
Benefits:
- Send entire documents or datasets in one API call
- Let Hindsight optimize the processing strategy
- Track overall progress via the parent operation status
- No need to manually split data into small batches
### Throughput
Factors affecting throughput:
- Document size and complexity
- LLM provider rate limits (for fact extraction)
- Database write performance
- Available CPU/memory resources
---
## Tuning for Local & Small Environments
Hindsight's defaults are tuned for cloud LLM providers and multi-core servers. When you run it on a laptop, a single GPU box, or against a **local LLM server** (llama.cpp, vLLM, LM Studio, Ollama) with a small fixed slot pool, those defaults can saturate the backend, time out, or thrash the CPU. This section collects the knobs that matter for low-resource setups.
### LLM concurrency
The default `HINDSIGHT_API_LLM_MAX_CONCURRENT=32` assumes a cloud provider that can absorb dozens of parallel requests. A local server with a handful of slots cannot — Hindsight will fill every slot and **starve any other client sharing the endpoint** (your main agent, another app, or a second Hindsight operation).
```bash
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2
```
A value of `2` lets retain and consolidation run concurrently without blocking each other. If the endpoint is **shared** with other clients (other applications, agents, or workflows hitting the same llama-server / vLLM / LM Studio instance), reserve slots for them by lowering further — leave at least one slot free per shared client.
You can also split the budget per operation so background work never crowds out live reads. The per-operation caps compose *on top of* the global cap:
```bash
# global=4, with retain/consolidation capped low so reflect always has headroom
export HINDSIGHT_API_LLM_MAX_CONCURRENT=4
export HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=1
export HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT=1
```
### Timeouts and retries
Small models on modest hardware generate tokens slowly, and the first request after startup pays a model-load cost. The default `HINDSIGHT_API_LLM_TIMEOUT=120` (seconds) can be too tight for a large local model on CPU — raise it to avoid spurious timeouts and wasted retries:
```bash
export HINDSIGHT_API_LLM_TIMEOUT=300 # allow slow local generation
export HINDSIGHT_API_LLM_MAX_RETRIES=2 # fail faster locally — retries rarely help a slow box
```
A local endpoint isn't rate-limited, so aggressive retry/backoff mostly adds latency on real failures. Lower retries and let genuine errors surface quickly.
### Smaller, faster models — and reasoning effort
Retain (fact extraction) is structured work that does not need a frontier model; reflect can use a lighter model still. On a constrained box, point each operation at the smallest model that holds up:
```bash
# Reflect on a small/fast model; retain on a slightly stronger structured-output model
export HINDSIGHT_API_REFLECT_LLM_MODEL=<small-fast-model>
export HINDSIGHT_API_RETAIN_LLM_MODEL=<structured-output-model>
```
If your model exposes a reasoning/thinking budget, set it low — extra reasoning tokens are pure latency for the extraction and consolidation paths. Nothing is sent unless you set it, so an unconfigured deployment runs at whatever effort the model defaults to (`none` is the value that switches thinking off entirely, where the backend supports it):
```bash
export HINDSIGHT_API_LLM_REASONING_EFFORT=low
```
Consolidation sends multiple facts to the LLM in a single call (default 8). On a small model with a limited context window, a large batch produces an oversized prompt and a long, error-prone response. Shrink the batch so each consolidation call stays small and reliable:
```bash
export HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=2 # default 8; lower = smaller prompts, more calls
```
### Built-in llama.cpp tuning
The bundled `llamacpp` provider runs a llama.cpp server as a managed subprocess — no external server needed. Key knobs for small machines:
```bash
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2 # retain + consolidation without blocking
export HINDSIGHT_API_LLAMACPP_GPU_LAYERS=-1 # -1 = offload all layers to GPU; 0 = CPU only
export HINDSIGHT_API_LLAMACPP_CONTEXT_SIZE=8192 # lower to save RAM/VRAM; raise for big batches
export HINDSIGHT_API_LLAMACPP_EXTRA_ARGS="--n_threads 8" # match physical cores on CPU-only boxes
# export HINDSIGHT_API_LLAMACPP_NO_GRAMMAR=true # faster, but less reliable JSON output
```
See [Built-in llama.cpp](./configuration.md#built-in-llamacpp) for the full option list.
### Reranker on CPU
Recall's bottleneck on a machine without a GPU is the cross-encoder reranker. The local reranker has several CPU/Apple-Silicon knobs that are quality-neutral but materially faster:
```bash
# Apple Silicon (MPS): half precision is 27–36% faster, quality-identical
export HINDSIGHT_API_RERANKER_LOCAL_FP16=true
# Sort pairs by length before batching — 36–54% faster, quality-identical by construction
export HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=true
# Cap reranker parallelism so it doesn't thrash a small CPU under load (default 4)
export HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=2
# On macOS, force CPU if MPS/XPC causes instability
# export HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=true
```
The biggest single win on CPU is reranking fewer candidates. By default Hindsight reranks up to 300 candidates per recall — shrink that pool to cut cross-encoder work proportionally:
```bash
export HINDSIGHT_API_RERANKER_MAX_CANDIDATES=100 # default 300; RRF pre-filters the rest
```
For a pure-CPU box that struggles with the cross-encoder, `flashrank` is a lighter ONNX-based reranker:
```bash
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
```
You can also reduce recall work directly: use a lower `budget` (`low`/`mid`) for everyday queries and reserve `high` for comprehensive reasoning. See [Recall Performance](#recall-performance) below.
---
## Recall Performance
### Budget
The `budget` parameter controls the search depth and quality. Choose based on query complexity — comprehensive questions that need thorough analysis benefit from higher budgets:
| Budget | Use Case |
|--------|----------|
| `low` | Quick lookups, real-time chat |
| `mid` | Standard queries, balanced performance |
| `high` | Comprehensive questions, thorough analysis |
### Optimization
1. **Appropriate budgets**: Use lower budgets for simple queries, higher for comprehensive reasoning
2. **Limit result tokens**: Set `max_tokens` to control response size (default: 4096)
3. **Include chunks**: Use `include_chunks` to retrieve the raw text that generated memories when you need additional context
### Database Performance
Hindsight uses PostgreSQL with pgvector for efficient vector search:
- **Index type**: HNSW for approximate nearest neighbor search
- **Typical query time**: 10-50ms for vector search on 100K+ facts
- **Scalability**: Tested with millions of facts per bank
## Reflect Performance
### Performance Characteristics
| Component | Latency | Description |
|-----------|----------------|-------------|
| Memory search | 100-600ms | Based on budget (low/mid/high) |
| LLM generation | 500-2000ms | Depends on provider and response length |
| **Total** | **600-2600ms** | Typical end-to-end latency |
### Optimization Strategies
1. **Budget selection**: Use lower budgets when context is sufficient
2. **Context provision**: Provide relevant `context` to reduce recall requirements and steer towards more focused answers
## Best Practices
### Operations
- **Use appropriate budgets**: Don't over-provision for simple queries; use higher budgets for comprehensive reasoning
- **Batch retain operations**: Group related content together for better efficiency
- **Cache frequent queries**: Cache at the application level for repeated queries
- **Profile with trace**: Use the `trace` parameter to identify slow operations
### Scaling
- **Horizontal scaling**: Deploy multiple API instances behind a load balancer with shared PostgreSQL
- **Concurrency**: 100+ simultaneous requests supported; memory search scales with CPU cores
- **LLM rate limits**: Distribute load across multiple API keys/providers (typically 60-500 RPM per key)
### Cost Optimization
- **Use efficient models**: `gpt-oss-20b` via Groq for retain — Hindsight doesn't need frontier models
- **Enable provider Batch API**: Set `HINDSIGHT_API_RETAIN_BATCH_ENABLED=true` with async retain to cut LLM fact-extraction costs by 50% (supported on OpenAI and Groq; results delivered within 24 hours)
- **Control token budgets**: Limit `max_tokens` for recall, use lower budgets when possible
- **Optimize chunks**: Larger chunks (1000-2000 tokens) are more efficient than many small ones
### Monitoring
- **Prometheus metrics**: Available at `/metrics` — track latency percentiles, throughput, and error rates
- **Key metrics**: `hindsight_recall_duration_seconds`, `hindsight_reflect_duration_seconds`, `hindsight_retain_items_total`
references/developer/rag-vs-hindsight.md
---
sidebar_position: 2
---
# RAG vs Memory
Traditional RAG (Retrieval-Augmented Generation) retrieves documents similar to a query. Hindsight provides structured memory with temporal reasoning, entity understanding, and belief formation.
## Capability Comparison
| Capability | RAG | Hindsight |
|------------|-----|-----------|
| **Search strategy** | Semantic similarity only | Semantic + keyword + graph + temporal |
| **Multi-hop reasoning** | Limited to retrieved chunks | Graph traversal across entity relationships |
| **Temporal queries** | Keyword matching ("spring") | Date parsing and range filtering |
| **Entity understanding** | None | Entity resolution, co-occurrence tracking |
| **Knowledge consolidation** | Stateless | Mental models that synthesize and evolve |
| **Disposition** | None | 3 traits (skepticism, literalism, empathy) influence interpretation |
## Architecture Comparison
### RAG
| Step | Operation |
|------|-----------|
| 1 | Embed query |
| 2 | Vector similarity search |
| 3 | Return top-k chunks |
| 4 | Generate response |
Single retrieval strategy. No state between queries.
### Hindsight
| Step | Operation |
|------|-----------|
| 1 | Parse query (extract temporal expressions, entities) |
| 2 | Execute 4 parallel retrievals: semantic, BM25, graph, temporal |
| 3 | Fuse results with RRF |
| 4 | Rerank with cross-encoder |
| 5 | Apply disposition traits |
| 6 | Generate response |
Multiple retrieval strategies. Persistent state across sessions.
## Example Scenarios
### Multi-Hop Reasoning
**Stored facts:**
- "Alice is the tech lead on Project Atlas"
- "Project Atlas uses Kubernetes"
- "Kubernetes cluster had an outage Tuesday"
**Query:** "Was Alice affected by recent issues?"
| System | Result |
|--------|--------|
| RAG | Retrieves facts about Alice only (no semantic similarity to "issues") |
| Hindsight | Traverses Alice → Project Atlas → Kubernetes → outage via entity links |
### Temporal Queries
**Stored facts with timestamps:**
- March: "Alice started microservices migration"
- April: "Alice completed auth service"
- October: "Alice focusing on performance"
**Query:** "What did Alice do last spring?"
| System | Result |
|--------|--------|
| RAG | Returns all Alice facts regardless of date |
| Hindsight | Parses "last spring" → March-May, filters to that range |
### Entity Understanding
**Stored facts about a user across sessions:**
- "Pro subscription"
- "Mobile app crashes in settings"
- "Switched to annual billing"
- "Desktop app working fine"
**Query:** "What do you know about my account?"
| System | Result |
|--------|--------|
| RAG | Lists disconnected facts |
| Hindsight | Returns connected facts via entity graph: subscription status, billing, known issues |
### Knowledge Evolution
**Week 1:** User struggles with async Python, succeeds with threads
**Week 3:** User asks about asyncio, implements async database calls
| System | Behavior |
|--------|----------|
| RAG | No memory of progression |
| Hindsight | Consolidates mental model "user prefers sync" → refines to "user growing comfortable with async" |
## When to Use Each
| Use Case | Recommended |
|----------|-------------|
| Document Q&A over static corpus | RAG |
| Search with no temporal requirements | RAG |
| AI assistants with persistent memory | Hindsight |
| Applications requiring entity tracking | Hindsight |
| Systems needing consistent disposition | Hindsight |
| Temporal queries ("last month", "in 2023") | Hindsight |
references/developer/reflect.md
# Reflect: Agentic Reasoning with Disposition
When you call `reflect()`, Hindsight runs an **agentic loop** that autonomously gathers evidence and reasons through the lens of the bank's disposition to generate contextual responses.
**Figure: Reflect Agent Loop.** An animated diagram on the docs site; its narration, step by step:
- **reflect()**
1. reflect() gets a question that needs reasoning, not just lookup.
2. The bank’s mission, disposition and directives go into the agent’s instructions before it looks anything up.
3. Each turn is one tool call. Retrieval starts at the top, with mental models: curated summaries kept up to date.
4. A fresh mental model can be enough to answer. This one covers the team, not the ML project, so the agent keeps looking.
5. Then observations: beliefs consolidated from many facts.
6. The result is flagged stale: the bank has facts not yet consolidated into observations. So the agent must check the raw facts.
7. recall() runs the full four-way search over raw facts: the ground truth.
8. The newer fact is what the stale observation was missing: Alice asked to move to ML herself.
9. recall already returns the chunk each fact came from. expand goes further: the whole source document, for the full context.
10. The agent calls done. Every cited ID is checked against what it actually retrieved; anything else is dropped. It must gather evidence first, and stops after 10 iterations at most.
11. The answer follows the directives and the bank’s disposition, and comes back with the memories it is based on.
---
## How It Works
Unlike simple retrieval, reflect is an **agentic system** that:
1. **Autonomously gathers evidence** — The agent decides what information it needs and calls appropriate tools
2. **Uses hierarchical retrieval** — Checks mental models first, then observations, then raw facts
3. **Applies disposition** — Shapes reasoning based on the bank's personality traits
4. **Enforces applicable directives** — Global rules and rules matching the reflect tag scope
5. **Cites sources** — Returns which memories and observations were used
### The Agentic Loop
The reflect agent runs in a loop with access to these tools:
| Tool | Purpose | Priority |
|------|---------|----------|
| `search_mental_models` | User-curated summaries — best match in full, a snippet of the others | Highest (check first) |
| `read_mental_models` | Full text of the pages returned as snippets | When a snippet looks relevant |
| `search_observations` | Consolidated knowledge | High |
| `recall` | Raw facts (ground truth) | Fallback |
| `expand` | Get more context for a memory | As needed |
| `done` | Complete with final answer | When ready |
The agent:
- **Must gather evidence** before answering (guardrail prevents empty responses)
- **Runs up to 10 iterations** to find relevant information
- **Validates citations** — only IDs that were actually retrieved can be cited
### Hierarchical Retrieval Strategy
The agent uses a smart retrieval hierarchy:
1. **[Mental Models](api/mental-models.md)** — User-curated summaries you've pre-computed for common queries
2. **[Observations](observations.md)** — Consolidated knowledge with freshness awareness
3. **Raw Facts** — Ground truth for verification when observations are stale
**Mental models** are saved reflect responses that you create for frequently asked questions. They're checked first because they represent explicitly curated knowledge. See the [Mental Models API](api/mental-models.md) for how to create and manage them.
If an observation is marked as **stale**, the agent automatically verifies it against current facts.
---
## Why Reflect?
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way.
### The Problem
Without reflect:
- **No consistent character**: Same question gets different answers each time
- **No knowledge synthesis**: System never connects related facts
- **No reasoning context**: Responses don't reflect accumulated knowledge
- **Generic responses**: Every AI sounds the same
### The Value
With reflect:
- **Consistent character**: A "detail-oriented, cautious" bank emphasizes risks and thorough planning
- **Evolving knowledge**: Observations strengthen and adapt as evidence accumulates
- **Contextual reasoning**: "Based on what I know about your team's remote work success..."
- **Differentiated behavior**: Support bots sound diplomatic, code reviewers sound direct
### When to Use Reflect
| Use `recall()` when... | Use `reflect()` when... |
|------------------------|-------------------------|
| You need raw facts | You need reasoned interpretation |
| You're building your own reasoning | You want disposition-consistent responses |
| You need maximum control | You want the bank to "think" for itself |
| Simple fact lookup | Forming recommendations |
**Example:**
- `recall("Alice")` → Returns all Alice facts and relevant mental models
- `reflect("Should we hire Alice?")` → Agent gathers evidence about Alice, reasons about fit, returns answer with citations
---
## Disposition Traits
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and reasons during `reflect()`:
| Trait | Scale | Low (1) | High (5) |
|-------|-------|---------|----------|
| **Skepticism** | 1-5 | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
| **Literalism** | 1-5 | Flexible interpretation, reads between the lines | Literal interpretation, takes things at face value |
| **Empathy** | 1-5 | Detached, focuses on facts | Empathetic, considers emotional context |
### Mission: Natural Language Identity
Beyond numeric traits, you can provide a natural language **mission** that describes the bank's identity and reasoning context:
```python
client.create_bank(bank_id="architect-bank")
client.update_bank_config(
"architect-bank",
reflect_mission="You're a senior software architect - keep track of system designs, "
"technology decisions, and architectural patterns. Prefer simplicity over cutting-edge.",
disposition_skepticism=4, # Questions new technologies
disposition_literalism=4, # Focuses on concrete specs
disposition_empathy=2, # Prioritizes technical facts
)
```
The reflect mission frames how the agent reasons and responds:
- Provides identity context: who the agent is and what it cares about
- Shapes how disposition traits are applied in practice
- Keeps reasoning consistent across conversations
> **ℹ️ Per-operation missions**
>
The reflect mission only affects `reflect()`. To steer what gets extracted during `retain()`, use [`retain_mission`](api/memory-banks.md#retain-configuration). To control what gets synthesised into observations, use [`observations_mission`](api/memory-banks.md#observations-configuration).
---
## Disposition Shapes Reasoning
Two banks with different dispositions, given identical facts about remote work:
**Bank A** (low skepticism, high empathy):
> "Remote work enables flexibility and work-life balance. The team seems happier and more productive when they can choose their environment."
**Bank B** (high skepticism, low empathy):
> "Remote work claims need verification. What are the actual productivity metrics? The anecdotal benefits may not translate to measurable outcomes."
**Same facts → Different conclusions** because disposition shapes interpretation.
---
## Disposition Presets by Use Case
Different use cases benefit from different disposition configurations:
| Use Case | Recommended Traits | Why |
|----------|-------------------|-----|
| **Customer Support** | skepticism: 2, literalism: 2, empathy: 5 | Trusting, flexible, understanding |
| **Code Review** | skepticism: 4, literalism: 5, empathy: 2 | Questions assumptions, precise, direct |
| **Legal Analysis** | skepticism: 5, literalism: 5, empathy: 2 | Highly skeptical, exact interpretation |
| **Therapist/Coach** | skepticism: 2, literalism: 2, empathy: 5 | Supportive, reads between lines |
| **Research Assistant** | skepticism: 4, literalism: 3, empathy: 3 | Questions claims, balanced interpretation |
---
## Directives: Hard Rules
While disposition traits *influence* reasoning style, **directives** are hard
rules that the agent *must* follow when they apply to the current reflect scope.
Untagged directives are global; tagged directives require matching reflect
tags. See [Memory Banks: Directives](api/memory-banks.md#directives) for
the full matching and default-behavior tables.
### When to Use Directives
Use directives for constraints that must never be violated:
- **Compliance rules**: "Never recommend specific stocks or financial products"
- **Privacy constraints**: "Never share personal data with third parties"
- **Style requirements**: "Always respond in formal English"
- **Domain guardrails**: "Always cite sources when making factual claims"
### Directive Scope and Tags
Directive `tags` scope when a directive applies, just like they scope memories. Untagged directives always apply; tagged directives apply only when the `reflect` request carries matching tags. A `reflect` with no tags therefore applies only the untagged directives. Set `apply_all_directives: true` on the request to enforce **every** active directive regardless of tags. See [Memory Banks: Directive Scope and Tags](api/memory-banks.md#directive-scope-and-tags).
### Directives vs Disposition
| Aspect | Disposition | Directives |
|--------|-------------|------------|
| **Nature** | Soft influence | Hard rules |
| **Effect** | Shapes interpretation and tone | Must be followed exactly |
| **Violation** | Acceptable (it's a tendency) | Not acceptable |
| **Example** | High skepticism → questions claims | "Never make medical diagnoses" |
> **💡 Tip**
>
Use disposition for personality and character. Use directives for compliance and guardrails.
See [Memory Banks: Directives](api/memory-banks.md#directives) for how to create and manage directives.
---
## What You Get from Reflect
When you call `reflect()`:
**Returns:**
- **Response text** — Disposition-influenced answer from the agent
- **based_on** — Evidence used: memories, mental models, and directives that grounded the response
- **trace** — Tool calls, LLM calls, and observations accessed (when `include.tool_calls=True`)
- **structured_output** — Parsed response if `response_schema` was provided
- **usage** — Token usage metrics
**Example:**
```json
{
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit for the research team lead position...",
"based_on": {
"memories": [
{"id": "mem-123", "text": "Alice has 5 years of ML experience", "type": "world"},
{"id": "mem-456", "text": "Alice worked at Google on search ranking", "type": "experience"}
],
"mental_models": [],
"directives": [
{"id": "dir-001", "name": "Formal Language", "rules": ["Always respond in formal English"]}
]
},
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000}
}
```
The agent automatically gathers evidence, validates citations, and generates a grounded response.
---
## Structured Output
Reflect returns prose by default. Pass a `response_schema` (a JSON Schema **object** with a
`properties` map) to *also* get a machine-readable version of the same answer:
```json
{
"query": "How does Alice fit the research lead role?",
"response_schema": {
"type": "object",
"properties": {
"recommendation": { "type": "string" },
"strengths": { "type": "array", "items": { "type": "string" } },
"confidence": { "type": "number" }
},
"required": ["recommendation"]
}
}
```
The response then carries **both** the prose and the structured view:
```json
{
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit...",
"structured_output": {
"recommendation": "Strong fit for research lead",
"strengths": ["5 years of ML experience", "Search ranking at Google"],
"confidence": 0.9
}
}
```
**How it works.** The agent first reasons to a natural-language answer, then a second pass
extracts that answer into JSON matching your schema. `structured_output` is therefore always a
faithful projection of `text` — you get the readable answer **and** a typed object to program
against, never one instead of the other.
**When extraction fails.** The reflect still returns `200` with the prose answer, no
`structured_output`, and a `structured_output_error` field saying why (a provider error, a timeout,
unparseable output). A missing `structured_output` *without* that field means the answer simply held
nothing matching your schema — so you can retry the broken case without retrying the ordinary one.
**Schema rules.** The schema must be an object with at least one property. Each property's `type`
is one of `string`, `number`, `integer`, `boolean`, `array`, or `object`, and `required` lists
property names. Invalid schemas are rejected before the call runs, so a malformed schema fails
fast instead of silently returning nothing.
### Structured output for mental models
A pinned mental model can carry its own `response_schema` in its trigger config. Every refresh
then stores a `structured_output` next to the markdown content — extracted from the **final
stored document**. This matters for delta-mode (surgical) refreshes: the structured view reflects
the whole merged document, not just the facts that changed in that refresh.
### Building a schema (no code)
In the control plane, the reflect view and the mental-model dialogs include a **Build schema**
editor with a **Visual** mode (add fields — name, type, required, description) and a **Code** mode
(raw JSON). The page itself shows only whether a schema is set; all editing happens in the
builder, so you never hand-write JSON unless you want to.
---
## Why Disposition Matters
Without disposition, all AI assistants sound the same. With disposition:
- **Customer support bots** can be diplomatic and empathetic
- **Code review assistants** can be direct and thorough
- **Creative assistants** can be open to unconventional ideas
- **Risk analysts** can be appropriately cautious
Disposition creates **consistent character** across conversations while observations **evolve with evidence**.
---
## Next Steps
- [**Observations**](./observations.md) — How knowledge is consolidated
- [**Retain**](./retain.md) — How rich facts are stored
- [**Recall**](./retrieval.md) — How multi-strategy search works
- [**Reflect API**](./api/reflect.md) — Code examples and parameters
references/developer/retain.md
---
sidebar_position: 2
---
# Retain: How Hindsight Stores Memories
When you call `retain()`, Hindsight transforms conversations and documents into structured, searchable memories that preserve meaning and context.
## What Retain Does
**Figure: What Retain Does.** An animated diagram on the docs site; its narration, step by step:
- **new facts**
1. You send content, a context that says who is speaking, and when it was said.
2. The original is kept as a document and split into chunks, so the exact passage can be handed back later.
3. An LLM reads each chunk and pulls out facts with what, when, where, who and why. It keeps the reason, not just the event. Each fact is embedded right away.
4. Entity resolution decides who each name is. It compares each name with the entities the bank already has. Neither is known yet, so both become new entities.
5. Entities are stored once, and every fact that mentions them points to them.
6. The facts are world facts: Bob is talking about Alice, not about the agent. Each keeps two times: when it happened and when Hindsight learned it.
7. Finally the facts are linked: through shared entities, closeness in time, similar meaning (from the embeddings), and cause and effect.
8. retain() is done. Observations are built from these facts later, in the background.
- **same person, new name**
1. Two months later, Bob mentions “Alice C.”
2. Same path: stored as a new document, split into chunks…
3. …and read by the LLM. The fact names “Alice C.”, a name the bank has never seen.
4. The name is close to Alice Chen, and the same fact names the Zurich office she is already linked to. Together that is enough: same person.
5. No new entity is created: this mention of “Alice C.” counts as Alice Chen.
6. The fact keeps its own wording, but it points to Alice Chen, so it joins everything else about her. Ask about Alice later and you get all of it.
7. Its entity link ties it to the three facts from January.
8. Done: one more fact about the same Alice.
---
## Rich Fact Extraction
Hindsight doesn't just store what was said — it captures **why**, **how**, and **what it means**.
### What Gets Captured
When you retain "Alice joined Google last spring and was thrilled about the research opportunities", Hindsight extracts:
**The core facts:**
- Alice joined Google
- This happened last spring
**The emotions and meaning:**
- She was thrilled
- It represented an important opportunity
**The reasoning:**
- She chose it for the research opportunities
This rich extraction means you can later ask "Why did Alice join Google?" and get a meaningful answer, not just "she joined Google."
### Preserving Context
Traditional systems fragment information:
- "Bob suggested Summer Vibes"
- "Alice wanted something unique"
- "They chose Beach Beats"
Hindsight preserves the full narrative:
- "Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy, but Alice wanted something unique. They ultimately decided on 'Beach Beats' for its playful tone."
This means search results include the full context, not disconnected fragments.
---
## Two Types of Facts
Every fact is classified by **whose perspective it captures** — the agent that owns the bank, or the outside world:
| Type | What it captures | Example |
|----------------|------------------------------------------------------------------------------|---------|
| **experience** | The bank's own agent acting, observing, or interacting — its first-person history | "I recommended Python to Alice" |
| **world** | Facts about other people, places, things, and events | "Alice works at Google" |
The split is decided by **who is speaking**, not by grammar. A first-person statement is an `experience` only when the speaker *is* the bank's agent. The same words said by someone else are a `world` fact about that person:
- Agent's own log — "I patched the auth bug" → **experience** (the agent did it).
- A user talking to the agent — "I bought a Tesla" → **world** (a fact about the *user*, not the agent).
**Describe the speaker in each item's `context`** to steer this correctly. When retaining transcripts or third-party content, a context like *"Customer Maria is speaking"* ensures her first-person statements are stored as `world` facts about Maria rather than mistaken for the agent's own experiences. For the agent's own logs, a context like *"The assistant is speaking"* attributes its first-person statements to the agent as `experience` facts.
**Note:** Observations are consolidated automatically in the background after `retain()` operations complete. This consolidation process synthesizes patterns from new facts into the bank's knowledge base.
---
## Entity Recognition
Hindsight automatically identifies and tracks **entities** — the people, organizations, and concepts that matter.
### What Gets Recognized
- **People:** "Alice", "Dr. Smith", "Bob Chen"
- **Organizations:** "Google", "MIT", "OpenAI"
- **Places:** "Paris", "Central Park", "California"
- **Products & Concepts:** "Python", "TensorFlow", "machine learning"
### Entity Resolution
The same entity mentioned different ways gets unified through **fuzzy name matching**, reinforced by co-occurrence and temporal proximity:
- "Alice" + "Alice Chen" + "Alice C." → one person
Because resolution keys off name similarity, close variants merge automatically. Names that do not resemble each other (a nickname and an unrelated formal name, for example) are not unified on the name alone, though shared co-occurring entities can still link them.
**Why it matters:** You can ask "What do I know about Alice?" and get everything, even if she was mentioned as "Alice Chen" in some conversations.
Resolution is a judgement call, so it can go the other way too: on a bank with a lot of history, a short new name that resembles an existing entity — and that turns up alongside entities the existing one is already associated with — can be absorbed into it instead of becoming its own entity. If you see facts about a new person attached to an unrelated entity, [How entity resolution decides](configuration.md#how-entity-resolution-decides) explains what is being compared and which setting makes matching stricter.
### Context-Aware Disambiguation
If "Alice" appears with "Google" and "Stanford" multiple times, a new "Alice" mentioning those is likely the same person. Hindsight uses co-occurrence patterns to disambiguate common names.
### Entity Labels
You can define a controlled vocabulary of `key:value` classification labels (e.g. `pedagogy:scaffolding`, `engagement:active`) that are extracted at retain time and stored as entities. Because labels become entities, they automatically link related memories in the knowledge graph and improve both semantic and keyword retrieval. Labels can optionally also write to the memory unit's tags, enabling standard tag-based filtering during recall and reflect.
Unlike regular entities, label entities never merge by name similarity — distinct label values must stay distinct, so they resolve by exact match only and are excluded from fuzzy name matching altogether.
See [entity_labels in the bank config](api/memory-banks.md#entity-labels) for full configuration details.
---
## Building Connections
Memories aren't isolated — Hindsight creates a **knowledge graph** with four types of connections:
### Entity Connections
All facts mentioning the same entity are linked together.
**Enables:** "Tell me everything about Alice" → retrieves all Alice-related facts
### Time-Based Connections
Facts close in time are connected, with stronger links for closer dates.
**Enables:** "What else happened around then?" → finds contextually related events
### Meaning-Based Connections
Semantically similar facts are linked, even if they use different words.
**Enables:** "Tell me about similar topics" → finds thematically related information
### Causal Connections
Cause-effect relationships are explicitly tracked.
**Enables:** "Why did this happen?" → trace reasoning chains
**Example:** "Alice felt burned out" ← caused by ← "She worked 80-hour weeks"
---
## Understanding Time
Hindsight tracks **two temporal dimensions**:
### When It Happened
For events (meetings, trips, milestones), Hindsight records when they occurred.
- "Alice got married in June 2024" → occurred in June 2024
For general facts (preferences, characteristics), there's no specific occurrence time.
- "Alice prefers Python" → ongoing preference
### When You Learned It
Hindsight also tracks when you told it each fact.
**Why both?**
Imagine in January 2025, someone tells you "Alice got married in June 2024":
- **Historical queries** work: "What did Alice do in 2024?" → finds the marriage
- **Recency ranking** works: Recent mentions get priority in search
- **Temporal reasoning** works: "What happened before her marriage?" → finds earlier events
Without this distinction, old information would either be unsearchable by date or treated as irrelevant.
---
## Tagging Memories
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
- **Item tags**: Tag individual memories with specific scopes
- **Document tags**: Apply tags to all items in a batch
- **Tag filtering**: Filter during recall/reflect by tags
See [Retain API](./api/retain.md) for code examples and [Recall API](./api/recall.md) for filtering options.
---
## Images and Files in Your Content
A lot of what a document actually says lives in its pictures: the screenshot of
the button an instruction refers to, the diagram holding an escalation path, the
chart that *is* the data. `content` accepts an ordered list of blocks so those sit
where they belong, instead of being summarised into a caption beforehand:
```json
{
"content": [
{"type": "text", "text": "To reset the VPN, click the button shown:"},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}},
{"type": "text", "text": "...then reconnect."}
]
}
```
A plain string still works exactly as before — nothing about text-only retain
changes.
The point is **position**. Extraction sends the text and the pictures together,
in order, so the model reads the screenshot beside the sentence that introduces
it. "Click the button shown below" is meaningless on its own; next to the
picture it becomes a fact that names the button.
### What comes back
Facts read naturally — `[image: image/png]` marks where an attachment was, never
a content hash — and each memory carries the attachments **it was drawn from**,
with a URL you can fetch:
```json
{
"text": "The escalation path for a stuck sync begins by contacting Tier 3 Platform.",
"attachments": [
{"id": "c414cd0e204d", "kind": "image", "media_type": "image/png",
"url": "/v1/default/banks/my-bank/attachments/c414cd0e204d"}
]
}
```
Facts that came from the prose have no `attachments`, even when the same document
is full of pictures. So an attachment shown next to a memory means the model
looked at it to produce that memory — it is evidence, not decoration.
An observation carries the attachments of the facts it was consolidated from, so
a screenshot still reaches you when recall or reflect answers from the
observation rather than the raw fact. Reflect's `based_on` (with
`include.facts`) returns each cited memory with its `attachments`, plus the
`document_id`, `chunk_id`, `tags` and `metadata` it was stored with.
### What to expect from charts and tables
An attachment carrying structured data is transcribed rather than summarised:
each row, bar or labelled value becomes its own fact, carrying its label, its
figure, and how it is drawn. A chart therefore produces many more memories than a
screenshot does — that is deliberate, since a summary of a twenty-bar chart keeps
three values and silently drops seventeen, and later questions ask about the ones
it dropped.
Very dense pages are still sampled rather than exhausted: a single extraction pass
over an infographic listing a hundred entries will capture a large share of them,
not all of them.
### Requirements
- A model that can read images. By default that is the bank's retain model, but
it does not have to be: `HINDSIGHT_API_VLM_MODEL` names a **vision slot** used
only for the chunks that actually carry an attachment, leaving every text-only
chunk on the retain LLM. So a bank whose documents are mostly prose can keep a
cheap text model and pay for vision only where there is something to look at.
- If that model cannot read images — or if Hindsight cannot tell, which is the
case for gateway backends serving mixed catalogues — the retain is refused
with `422` rather than dropping the attachment silently. See
[`HINDSIGHT_API_LLM_VISION`](configuration.md#llm-provider).
- Batch retain (`HINDSIGHT_API_RETAIN_BATCH_ENABLED`) cannot carry attachments,
and also refuses with `422`.
This is different from [`POST /files/retain`](configuration.md#file-processing),
which converts a whole file to markdown as its **own** document. That is still the
right tool for a scanned report you want parsed — but it separates the file from
the prose that referred to it, which is exactly what inline attachments avoid.
For storage, size limits and accepted media types, see
[Inline attachments in retain](configuration.md#inline-attachments-in-retain).
---
## What You Get
After `retain()` completes:
- **Structured facts** that preserve meaning, emotions, and reasoning
- **Unified entities** that resolve different name variations
- **Knowledge graph** with entity, temporal, semantic, and causal links
- **Temporal grounding** for both historical and recency-based queries
- **Optional tags** for filtering during recall
All stored in your isolated **memory bank**, ready for `recall()` and `reflect()`.
---
## Steering Extraction with a Mission
By default, `retain()` extracts all significant facts from the content. You can narrow this focus with a **retain mission** (`retain_mission`) — a plain-language description of what this bank should pay attention to.
```
e.g. Always include technical decisions, API design choices, and architectural trade-offs.
Ignore meeting logistics, greetings, and social exchanges.
```
The mission is injected into the extraction prompt alongside the built-in rules — it steers the LLM without replacing the extraction logic. It works with the LLM-based extraction modes (`concise`, `verbose`, `verbatim`, `custom`) and is ignored in `chunks` mode.
For finer control, you can also change the **extraction mode**:
| Mode | When to use |
|------|-------------|
| `concise` *(default)* | General-purpose — selective, fast |
| `verbose` | When you need richer facts with full context and relationships |
| `custom` | When you want to write your own extraction rules entirely |
| `verbatim` | When you need the original chunk text preserved, with LLM-extracted metadata such as entities and dates |
| `chunks` | When you want to store chunks as-is with no LLM call or extracted metadata |
Set `retain_mission` and `retain_extraction_mode` via the [bank config API](api/memory-banks.md#retain-configuration), or with the [`HINDSIGHT_API_RETAIN_MISSION`](configuration.md#retain) and [`HINDSIGHT_API_RETAIN_EXTRACTION_MODE`](configuration.md#retain) environment variables.
### When a mission excludes everything in a document
A mission narrows what becomes a memory — and content that produces no facts produces no memories at all. The document itself is still stored, but `recall` and `reflect` search memories, so a document with zero memories cannot be found by either. Tightening a mission therefore trades away retrieval of the raw source, not just fact creation.
This is a normal outcome, not an error: the retain succeeds and the operation is reported as completed. Two signals tell you it happened:
| Where | What to look for |
|-------|------------------|
| [`retain.completed` webhook](api/webhooks.md#retaincompleted) | `data.memory_unit_count: 0` |
| [Metrics](monitoring.md#retain-metrics) | `hindsight.retain.documents.total{outcome="no_facts"}` |
You can also audit after the fact: `GET /documents` returns `memory_unit_count` per document, so filtering for `0` lists everything currently unreachable.
Extraction is not fully deterministic — a borderline document can yield facts on one run and none on the next. Treat a zero as "this document needs another pass" rather than as a permanent verdict.
To recover a document, widen the mission and reprocess it — the stored text is re-extracted, and no re-upload is needed:
```
POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocess
```
---
## Observation Consolidation
After `retain()` completes, Hindsight automatically triggers **observation consolidation** in the background. This process:
1. Analyzes new facts against existing observations
2. Creates new observations when patterns emerge
3. Refines existing observations with new evidence
4. Tracks which facts support each observation
This happens asynchronously — your `retain()` call returns immediately while consolidation runs in the background.
See [Observations](./observations.md) for details on how consolidation works.
---
## Memory Defense and Source Provenance
### receipt_uri (optional)
Type: `string`.
Optional pointer into an external receipt or co-signature system. Stored as-is and surfaced in `security_events.receipt_uri` for any Memory Defense decision on this item.
### 422 — Memory Defense violation
When Memory Defense is enabled on the target bank and **every** item in the batch is blocked by policy, the request returns 422 with a violation list:
```json
{
"detail": {
"violations": [
{ "index": 0, "detector": "prompt_injection", "severity": "high", "message": "..." }
]
}
}
```
Partial-block batches return 200 with the un-blocked items processed; blocked items are silently dropped from the result with their decisions recorded in `security_events`.
See [Memory Defense](./memory-defense/index.md) for the full guide.
---
## Next Steps
- [**Observations**](./observations.md) — How knowledge is consolidated after retain
- [**Recall**](./retrieval.md) — How multi-strategy search retrieves relevant memories
- [**Reflect**](./reflect.md) — How the agentic loop uses observations
- [**Retain API**](./api/retain.md) — Code examples and parameters
references/developer/retrieval.md
---
sidebar_position: 3
---
# Recall: How Hindsight Retrieves Memories
When you call `recall()`, Hindsight uses multiple search strategies in parallel to find the most relevant memories, regardless of how you phrase your query.
**Figure: Multi-Strategy Retrieval (TEMPR).** An animated diagram on the docs site; its narration, step by step:
- **recall()**
1. recall() gets a query. Nothing is decided yet about which kind of search fits it best, so every arm that applies runs.
2. Each arm searches its own index: meaning (vectors), exact words (BM25), the entity graph, and time. “March 2026” becomes a date range; a query with no date skips the time arm.
3. All four point into the same memories. Each arm returns its own ranked list, and facts and observations compete in every one. The mark shows how many arms found each.
4. RRF fusion merges the lists by rank, not raw score: a memory found near the top by several arms beats one found by a single arm.
5. The top candidates (up to 300) go to a cross-encoder, which reads the query and each memory together and scores how well they match.
6. Small multiplicative boosts nudge the score: recent memories, memories inside the asked time range, and observations backed by more evidence.
7. Results are packed best-first until max_tokens is used up. Only the memory text counts toward the budget.
8. The agent gets a short, ranked list it can put straight into its prompt.
---
## The Challenge of Memory Recall
Different queries need different search approaches:
- **"Alice works at Google"** → needs exact name matching
- **"Where does Alice work?"** → needs semantic understanding
- **"What did Alice do last spring?"** → needs temporal reasoning
- **"Why did Alice leave?"** → needs causal relationship tracing
No single search method handles all these well. Hindsight runs **four complementary strategies in parallel** and fuses their results.
---
## Four Search Strategies
### Semantic Search
**What it does:** Understands the *meaning* behind words, not just the words themselves.
**Best for:**
- Conceptual matches: "Alice's job" → "Alice works as a software engineer"
- Paraphrasing: "Bob's expertise" → "Bob specializes in machine learning"
- Synonyms: "meeting" matches "conference", "discussion", "gathering"
**Why it matters:** You can ask questions naturally without matching exact keywords.
---
### Keyword Search
**What it does:** Finds exact terms and names, even when they're spelled uniquely.
**Best for:**
- Proper nouns: "Google", "Alice Chen", "MIT"
- Technical terms: "PostgreSQL", "HNSW", "TensorFlow"
- Unique identifiers: URLs, product names, specific phrases
**Why it matters:** Ensures you never miss results that mention specific names or terms, even if they're semantically distant from your query.
**Backends:** Hindsight ships five pluggable BM25 backends, selected via
`HINDSIGHT_API_TEXT_SEARCH_EXTENSION`:
| Backend | What it uses | Citus-compatible? |
|---|---|---|
| `native` | PostgreSQL `tsvector` + `ts_rank_cd` (TF-IDF, not true BM25) | Yes |
| `vchord` | `vchord_bm25` extension | No |
| `pg_textsearch` | Timescale `pg_textsearch` extension | No |
| `pgroonga` | PGroonga (Groonga) full-text extension, `TokenBigram` polyglot tokenizer | No |
| `pg_search` | ParadeDB `pg_search` extension, configurable tokenizer (e.g. `jieba`, `chinese_compatible`, `ngram`) via `HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER` | Yes |
If you need true BM25 ranking on a horizontally scaled Postgres (Citus) cluster,
`pg_search` is the only option. See the [`pg_search` docker-compose example](https://github.com/vectorize-io/hindsight/tree/main/docker/docker-compose/pg_search).
---
### Graph Traversal
**What it does:** Follows connections between entities to find indirectly related information.
**Best for:**
- Indirect relationships: "What does Alice do?" → Alice → Google → Google's products
- Entity exploration: "Bob's colleagues" → Bob → co-workers → shared projects
- Multi-hop reasoning: "Alice's team's achievements"
**Why it matters:** Retrieves facts that aren't semantically or lexically similar but are **structurally connected** through the knowledge graph.
**Example:** Even if Alice and her manager are never mentioned together, graph traversal can find the manager through shared projects or team relationships.
---
### Temporal Search
**What it does:** Understands time expressions and filters by when events occurred.
**Best for:**
- Historical queries: "What did Alice do in 2023?"
- Time ranges: "What happened last spring?"
- Relative time: "What did Bob work on last year?"
- Before/after: "What happened before Alice joined Google?"
**How it works:** When the query contains a time reference, Hindsight parses it into a date window, then retrieves the memories whose time overlaps that window. Within the window it selects candidates by **semantic relevance to the query** — not by recency — so the most relevant in-window memory is never dropped just because other memories happen to be more recent. It then **spreads the selection across the window's range**: the window is divided into time-buckets and the strongest match from each populated bucket is taken first, so a "what happened in 2023?" query surfaces memories from across the whole year rather than clustering on whichever stretch is densest. Time is also used as a *scoring* signal — memories closer to the center of the window get a small boost (see [Temporal proximity signal](#temporal-proximity-signal)).
**Why it matters:** Enables precise historical queries that stay relevant *and* representative of the whole period. It also stays fast and meaningful on memory banks whose timestamps are densely clustered (for example when a large batch is ingested with one date): because selection is relevance-first, a time window that happens to match most of the bank still returns the best matches rather than an arbitrary slice.
---
## Result Fusion
After the four strategies run, results are **fused together**:
- Memories appearing in **multiple strategies** rank higher (consensus)
- **Rank matters more than score** (robust across different scoring systems)
- Final results are **re-ranked** using a neural model that considers query-memory interaction
**Why fusion matters:** A fact that's both semantically similar AND mentions the right entity will rank higher than one that's only semantically similar.
---
## Why Multiple Strategies?
Consider the query: **"What did Alice say about Python last spring?"**
- **Semantic** finds facts about Alice's views on programming
- **Keyword** ensures "Python" is actually mentioned
- **Graph** connects Alice → programming languages → related entities
- **Temporal** filters to "last spring" timeframe
The **fusion** of all four gives you exactly what you're looking for, even though no single strategy would suffice.
---
## Token Budget Management
Hindsight is built for AI agents, not humans. Traditional search systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
**How it works:**
- Top-ranked memories selected first
- Stops when token budget is exhausted
- You specify context budget, Hindsight fills it with the most relevant memories
**Parameters you control:**
- `max_tokens`: How much memory content to return (default: 4096 tokens)
- `budget`: Search depth level (low, mid, high)
- `types`: Filter by world, experience, observation, or all
- `tags`: Filter memories by visibility tags
- `tags_match`: How to match tags (see [Recall API](./api/recall.md) for all options)
### Expanding Context: Chunks
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material:
**Chunks** return the raw text that generated each memory—useful when the distilled fact loses important nuance:
```
Memory: "Alice prefers Python over JavaScript"
Chunk: "Alice mentioned she prefers Python over JavaScript, mainly because
of its data science ecosystem, though she admits JS is better for
frontend work and she's been learning TypeScript lately."
```
Use `include_chunks=True` with `max_chunk_tokens` to control the token budget for chunks. This is useful when generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?").
---
## Tuning Recall: Quality vs Latency
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
### Budget: Search Depth
Controls how thoroughly Hindsight explores the memory bank—affecting graph traversal depth, candidate pool size, and cross-encoder re-ranking:
| Budget | Best For | Trade-off |
|--------|----------|-----------|
| **low** | Quick lookups, simple queries | Fast, may miss indirect connections |
| **mid** | Most queries, balanced | Good coverage, reasonable speed |
| **high** | Complex queries requiring deep exploration | Thorough, slower |
**Example:** "What did Alice's manager's team work on?" benefits from high budget to traverse multiple hops (Alice → manager → team → projects) and evaluate more candidates.
### Max Tokens: Context Window Size
Controls how much memory content to return:
| Max Tokens | ~Pages of Text | Best For | Trade-off |
|------------|----------------|----------|-----------|
| **2048** | ~2 pages | Focused answers, fast LLM | Fewer memories, faster |
| **4096** (default) | ~4 pages | Balanced context | Good coverage, standard |
| **8192** | ~8 pages | Comprehensive context | More memories, slower LLM |
**Example:** "Summarize everything about Alice" benefits from higher max_tokens to include more facts.
### Two Independent Dimensions
Budget and max_tokens control different aspects of recall:
| Parameter | What it controls | Latency impact | Example |
|-----------|------------------|----------------|---------|
| **Budget** | How thoroughly to explore memories | Search time | High budget finds Alice → manager → team → projects |
| **Max Tokens** | How much context to return | LLM processing time | High tokens returns more memories to the agent |
**They're independent.** Common combinations:
| Budget | Max Tokens | Use Case |
|--------|------------|----------|
| high | low | Deep search, return only the best results |
| low | high | Quick search, return everything found |
| high | high | Comprehensive research queries |
| low | low | Fast chatbot responses |
### Recommended Configurations
| Use Case | Budget | Max Tokens | Why |
|----------|--------|------------|-----|
| **Chatbot replies** | low | 2048 | Fast responses, focused context |
| **Document Q&A** | mid | 4096 | Balanced coverage and speed |
| **Research queries** | high | 8192 | Comprehensive, multi-hop reasoning |
| **Real-time search** | low | 2048 | Minimize latency |
---
## Scoring & Ranking Deep Dive
This section explains exactly how Hindsight turns raw retrieval results into a final ranked list. The pipeline has three stages: **RRF fusion**, **cross-encoder reranking**, and **combined scoring**.
### Stage 1: Reciprocal Rank Fusion (RRF)
After all strategies run in parallel, their results are merged using [Reciprocal Rank Fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf). RRF combines ranked lists by rewarding items that appear highly ranked across multiple strategies, without relying on raw scores (which aren't comparable across different retrieval methods).
**Formula:**
```
score(d) = Σ 1 / (k + rank_i(d))
i
```
Where:
- **k = 60** (smoothing constant — prevents top-ranked items from dominating)
- **rank_i(d)** = position of document *d* in strategy *i* (1-indexed)
- The sum runs over all strategies where *d* appears
**Within RRF, all four strategies are weighted equally** — fusion uses rank position, not the source, so no strategy gets an implicit multiplier. You can, however, deliberately bias a source with [`HINDSIGHT_API_RECALL_STRATEGY_BOOSTS`](./configuration.md): that boost is applied at a separate stage — before the reranking pre-filter cap, and again after a cross-encoder rerank — not inside the RRF fusion above. The pre-cap step runs only when the merged pool exceeds the cap, and it does not rewrite RRF scores. A passthrough reranker (mode `rrf`, an `rrf` provider, or a failover chain that has degraded to `rrf`) skips the post-rerank nudge: within the cap the setting then does nothing, and above the cap it only changes who is kept.
**Why RRF over raw score merging?** Each retrieval strategy produces scores on a different scale (cosine similarity, BM25 tf-idf, graph activation). These scores aren't comparable — a BM25 score of 12.5 and a cosine similarity of 0.85 don't mean the same thing. RRF sidesteps this by using only rank positions, making it robust across any scoring system without requiring calibration.
**Example:** A memory ranked #1 in semantic and #5 in BM25:
```
RRF score = 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318
```
A memory ranked #1 in semantic only:
```
RRF score = 1/(60+1) = 0.0164
```
The first memory ranks higher because it has **consensus** across strategies.
---
### Stage 2: Cross-Encoder Reranking
RRF gives a good initial ranking, but it's based on positions, not on deep query-document understanding. The cross-encoder evaluates each candidate against the query as a pair, producing a relevance score.
**Pre-filtering:** Before reranking, candidates are trimmed to the top **300** (by RRF score) to limit computational cost. This is configurable via `HINDSIGHT_API_RERANKER_MAX_CANDIDATES`. If [`HINDSIGHT_API_RECALL_STRATEGY_BOOSTS`](./configuration.md) is set, the boost is applied before this cut, so candidates from a favoured source are more likely to survive it. The boost promotes the favoured arm in *rank* space (its rank is divided by the level's divisor before the RRF contribution is computed) rather than scaling its score, so it reaches deeper into that arm without evicting the top-ranked hits of the others — including on banks whose merged pool is many times the cap. When `trace: true` is requested, the `rerank_prefilter` phase reports how many candidates were kept and dropped, the cap in force, the active boosts, and the per-arm composition of the survivors.
**Why rerank after RRF?** RRF is position-based — it knows a memory ranked well across strategies, but it never actually reads the query and the memory together. The cross-encoder does: it takes the query and each candidate as a pair and produces a relevance score based on their full interaction. This catches nuances that position-based fusion misses, like a memory that ranked #1 in keyword search because it matched a common term but is actually irrelevant to the query's intent.
**Score normalization:** Cross-encoders output raw logits (which can be negative). Scores that already fall within [0, 1] — as returned by calibrated external API rerankers (e.g. Cohere, SiliconFlow, ZeroEntropy, Alibaba, Jina) — are passed through unchanged to preserve their absolute confidence. Raw logits outside [0, 1] are normalized to [0, 1] using the sigmoid function:
```
CE_normalized = 1 / (1 + e^(-raw_logit))
```
**Batch processing:** Candidates are scored in batches — **32 pairs** for the local reranker, **128 pairs** for TEI.
:::tip No cross-encoder?
When running without a cross-encoder (e.g., slim image with no external reranker), the system falls back to RRF-derived scores: candidates are assigned synthetic scores spread across [0.1, 1.0] based on their RRF rank, so the combined scoring boosts below still work meaningfully. `HINDSIGHT_API_RECALL_STRATEGY_BOOSTS` does not add its post-rerank nudge on this path. If the merged pool is within the reranker cap, the setting changes nothing; if the pool is larger, it only changes which candidates are kept, and their order still follows these RRF-derived scores plus recency, temporal and proof.
:::
---
### Stage 3: Combined Scoring (Boosts)
The normalized cross-encoder score is adjusted by three **multiplicative boosts** that incorporate signals the cross-encoder can't see: recency, temporal proximity, and evidence strength.
**Why multiplicative instead of additive?** Additive boosts (e.g., `CE + 0.1 × recency`) would give the same absolute bonus to every candidate regardless of relevance. A barely-relevant memory could leapfrog a highly-relevant one just by being recent. Multiplicative boosts keep adjustments proportional to the base relevance score — a +10% nudge on a high-relevance memory is a bigger absolute change than +10% on a low-relevance one. This ensures secondary signals never overpower the primary relevance judgment.
**Formula:**
```
final_score = CE_normalized × recency_boost × temporal_boost × proof_count_boost
```
Each boost is centered at 1.0 (neutral) and controlled by an alpha that caps how much it can swing:
```
boost = 1 + α × (signal - 0.5)
```
| Boost | α | Max adjustment | What it rewards |
|-------|---|----------------|-----------------|
| **Recency** | 0.2 | ±10% | Recent memories over older ones |
| **Temporal proximity** | 0.2 | ±10% | Memories close to a queried time window |
| **Proof count** | 0.1 | ±5% | Observations backed by more evidence |
#### Recency signal
Linear decay over 365 days from the memory's occurrence date:
```
recency = clamp(1.0 - days_ago / 365, 0.1, 1.0)
```
A memory from the query timestamp has recency 1.0 (+10% boost). A memory from 6 months before the query timestamp has recency ~0.5 (neutral). A memory more than a year before the query timestamp has recency 0.1 (-8% penalty). If no `query_timestamp` is provided, the server's current time is used. Memories without dates get 0.5 (neutral — no boost or penalty).
#### Temporal proximity signal
Only active when the query contains a time reference (e.g., "last spring", "in 2023"). Measures how close a memory's date is to the center of the queried time window:
```
temporal_proximity = 1.0 - min(days_from_center / (window_days / 2), 1.0)
```
A memory at the center of the window gets 1.0 (+10% boost). A memory at the edge gets 0.0 (-10% penalty). For non-temporal queries, all memories get 0.5 (neutral).
#### Proof count signal
For observation-type memories, rewards those backed by more evidence using a logarithmic curve:
```
proof_norm = clamp(0.5 + ln(proof_count) / 10, 0.0, 1.0)
```
| Proof count | proof_norm | Boost |
|-------------|-----------|-------|
| 1 | 0.5 | Neutral |
| 3 | 0.61 | +1.1% |
| 10 | 0.73 | +2.3% |
| 150+ | 1.0 | +5% (max) |
#### Maximum combined range
With all boosts at their extremes:
- **Best case:** ×1.10 × 1.10 × 1.05 ≈ **+27%**
- **Worst case:** ×0.90 × 0.90 × 0.95 ≈ **-23%**
The boosts are intentionally conservative — they nudge the ranking without overriding cross-encoder relevance.
---
### Stage 4: Token Truncation
After scoring, results are sorted by `final_score` and selected top-down until the `max_tokens` budget is exhausted. Only the memory text counts toward the budget — metadata is free. A result too long for what is left of the budget is skipped and the packing continues with the next one, so a single long fact does not cost you the shorter ones ranked behind it. If nothing fits at all, the top-ranked result is still returned whole, so a query that matched something is never answered with an empty list.
---
### How Budget Maps to Pipeline Parameters
The `budget` parameter (low/mid/high) controls **search depth** — how many candidates each strategy considers. Each level maps to a **recall budget** number that flows through every pipeline stage:
| Budget | Recall budget (fixed mode) | Env var override |
|--------|---------------------------|-----------------|
| **low** | 100 | `HINDSIGHT_API_RECALL_BUDGET_FIXED_LOW` |
| **mid** | 300 (default) | `HINDSIGHT_API_RECALL_BUDGET_FIXED_MID` |
| **high** | 1000 | `HINDSIGHT_API_RECALL_BUDGET_FIXED_HIGH` |
This recall budget flows through the pipeline as follows:
| Pipeline stage | How the recall budget is used |
|----------------|-------------------------------|
| **Semantic search** | `LIMIT recall_budget` in SQL, with the ANN candidate list sized to match for the query (on pgvector, `hnsw.ef_search`, capped at that setting's maximum of 1000) |
| **BM25 search** | `LIMIT recall_budget` in SQL |
| **Graph traversal** | Explores up to recall_budget nodes |
| **Temporal spreading** | Activates up to recall_budget nodes via links |
| **Result consideration** | Top recall_budget × 2 results considered for token filtering |
Reranking pre-filter (300 candidates) is **independent** of budget — it's a separate knob (`HINDSIGHT_API_RERANKER_MAX_CANDIDATES`).
:::info Adaptive budgeting
An alternative budget mode scales the recall budget with `max_tokens` instead of using fixed values:
```
recall_budget = clamp(max_tokens × ratio, min, max)
```
| Budget | Ratio | Env var override |
|--------|-------|-----------------|
| low | 2.5% of max_tokens | `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_LOW` |
| mid | 7.5% of max_tokens | `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_MID` |
| high | 25% of max_tokens | `HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_HIGH` |
The result is clamped to a floor of **20** (`HINDSIGHT_API_RECALL_BUDGET_MIN`) and a ceiling of **2000** (`HINDSIGHT_API_RECALL_BUDGET_MAX`).
Enable with `HINDSIGHT_API_RECALL_BUDGET_FUNCTION=adaptive`.
:::
---
### Graph Scoring Detail
The graph traversal (link expansion) combines three independent signals additively for each candidate:
| Signal | Score formula | Range |
|--------|--------------|-------|
| **Entity overlap** | tanh(shared_entity_count × 0.5) | [0, ~1.0] |
| **Semantic link** | Precomputed kNN link weight | [0.7, 1.0] |
| **Causal link** | Causal link weight | [0, 1.0] |
```
graph_score = entity_score + semantic_score + causal_score ∈ [0, 3]
```
The additive combination rewards **convergent evidence** — a memory connected to the query through multiple signal types ranks higher than one connected through a single strong signal.
**Why tanh for entity scores?** Raw shared-entity count is unbounded — a high-fanout entity like "user" could produce counts of 50+, drowning out the other two signals. `tanh(count × 0.5)` saturates naturally: the first few shared entities matter a lot (1→0.46, 2→0.76, 3→0.91), but additional ones contribute diminishing returns, keeping the entity signal in [0, 1] alongside semantic and causal scores.
**Why additive instead of multiplicative here?** Unlike the combined scoring boosts, graph signals are independent evidence channels, not adjustments to a base score. A memory might be connected only through causal links (no shared entities, no semantic similarity) — multiplicative combination would zero it out. Additive scoring lets each signal contribute independently, and the outer RRF fusion handles ranking across strategies.
**Entity signal example:** A memory sharing 1 entity with the query scores tanh(0.5) ≈ 0.46. Two shared entities score tanh(1.0) ≈ 0.76. Three or more saturate near 0.91+.
---
## Next Steps
- [**Retain**](./retain.md) — How memories are stored with rich context
- [**Reflect**](./reflect.md) — How disposition influences reasoning
- [**Recall API**](./api/recall.md) — Code examples, parameters, and tag filtering
references/developer/services.md
# Services
Hindsight consists of three services that can run together or separately depending on your deployment needs.
**Figure: Services.** An animated diagram on the docs site; its narration, step by step:
- **retain · async**
1. A retain is slow work: an LLM has to read it. With async=true the API does not do it in the request.
2. It writes a task row into async_operations — the queue is just a table in the same database.
3. And answers right away with the operation id.
4. Every worker polls the table (every 500 ms). The claim locks the row, so exactly one worker gets it — the others skip it.
5. The worker calls the LLM to extract facts and embeds them.
6. Then writes them to the bank.
7. The task is marked completed. New facts queue their own follow-up: consolidation (when auto-consolidation is on), plus graph and index upkeep.
8. The Control Plane talks to the same API, so you can watch the operation there.
- **background**
1. Background work goes through the same queue. This time worker-2 wins the claim.
2. Consolidation asks the LLM to merge new facts into observations.
3. Mental models that refresh after consolidation — and are now stale — get a refresh task of their own.
4. The API’s internal worker claims from the same table. Set HINDSIGHT_API_WORKER_ENABLED=false and only dedicated workers do.
5. Any worker can run any task: they share the package, the image and the database, so you add more to scale.
- **recall · sync**
1. Later, a recall. Like reflect and any read, it is answered inside the API process. Nothing is queued.
2. The query is embedded — by a model loaded in the process itself, or a remote embeddings service.
3. The four searches run as queries against PostgreSQL, where every bank lives.
4. Candidates are reranked by the cross-encoder, local or remote like the embeddings.
5. The API keeps no state of its own, so you can run as many copies as you like behind a load balancer.
## API Service
The core memory engine. Handles all memory operations:
- **Retain**: Ingests content, extracts facts, builds knowledge graph
- **Recall**: Semantic search across memories
- **Reflect**: Disposition-aware answer generation
```bash
hindsight-api # Default port: 8888
```
The API service is stateless and can be horizontally scaled behind a load balancer. All state is stored in PostgreSQL.
By default, the API also processes background tasks (mental model consolidation) internally. For high-throughput deployments, you can disable this and run dedicated workers instead.
## Worker Service
Dedicated task processor for background operations. Uses the **same package and Docker image** as the API service, just with a different entry point.
```bash
hindsight-worker # Default metrics port: 8889
```
Workers use PostgreSQL as a task broker, polling for pending tasks. Multiple workers can run simultaneously without conflicts.
| Deployment | Internal Worker | Dedicated Workers |
|------------|-----------------|-------------------|
| **Development** | ✅ Simple, all-in-one | ❌ Overkill |
| **Small production** | ✅ Less infrastructure | ❌ Overkill |
| **High throughput** | ❌ API bottleneck | ✅ Scale independently |
| **Long-running tasks** | ❌ Blocks API resources | ✅ Isolated processing |
To use dedicated workers, disable the internal worker in the API and start worker processes:
```bash
# Disable internal worker in API
HINDSIGHT_API_WORKER_ENABLED=false hindsight-api
# Start dedicated workers (run multiple instances)
hindsight-worker --worker-id worker-1
hindsight-worker --worker-id worker-2
```
Each worker exposes `/health/live` (liveness, no database access), `/health` and
`/health/ready` (readiness, checks the database), and `/metrics` for monitoring.
See [Monitoring - Health Endpoints](./monitoring.md#health-endpoints).
Before scaling down or removing workers, release their tasks with `hindsight-admin decommission-worker <worker-id>`.
See [Configuration - Distributed Workers](./configuration.md#distributed-workers) for all worker settings and [Installation - Helm](./installation.md#distributed-workers) for Kubernetes deployment.
## Control Plane
Web UI for managing and exploring your memory banks:
- Browse agents and memory banks
- Explore entities and relationships
- View ingestion history and operations
- Test recall queries interactively
The Control Plane connects to the API service and provides a visual interface for development and debugging.
For bare metal deployments, you can run the Control Plane standalone using npx. See [Installation - Bare Metal](./installation.md#control-plane) for details.
references/developer/storage.md
# Storage
Hindsight uses PostgreSQL as its primary storage backend, with Oracle AI Database available as an alternative for enterprise deployments.
## Why PostgreSQL?
PostgreSQL provides all capabilities required for a semantic memory system in a single database:
| Capability | Implementation |
|------------|----------------|
| Vector search | pgvector extension with HNSW indexes |
| Full-text search | Built-in tsvector with GIN indexes |
| Relational data | Native PostgreSQL |
| JSON documents | JSONB with indexing |
| Graph queries | Recursive CTEs |
### Reduced System Dependencies
Building exclusively for PostgreSQL simplifies deployment and operations:
- Single connection string to configure
- Single backup and restore strategy
- Single monitoring target
- ACID transactions across all data types
- Single upgrade path
### No Storage Abstraction
Hindsight does not abstract storage behind a generic interface. This is a deliberate trade-off.
We believe PostgreSQL is becoming the standard database API. Its popularity, extension ecosystem, and modularity mean that PostgreSQL-compatible interfaces are appearing everywhere—from serverless offerings to distributed databases. Building for PostgreSQL today means compatibility with a growing ecosystem tomorrow.
Supporting multiple databases would increase flexibility but conflict with our core goals: Hindsight is fully open source and designed to be as simple as possible to run and use. Adding database abstractions introduces complexity in code, testing, documentation, and operations—complexity that we pass on to users.
By building on PostgreSQL, we keep the system simple:
- One set of deployment instructions
- One set of performance characteristics to understand
- One codebase optimized for one backend
- No configuration decisions about which database to use
### Oracle AI Database Support
For enterprise deployments, Hindsight also supports Oracle AI Database with full feature parity. All memory operations—retain, recall, and reflect—work identically on Oracle, making it a drop-in option for organizations that standardize on Oracle infrastructure.
See the [Oracle Database guide](./oracle.md) for setup: prerequisites, provisioning, connection URLs, migrations, and the differences from PostgreSQL.
## Development with pg0
For local development, Hindsight uses **[pg0](https://github.com/vectorize-io/pg0)**—an embedded PostgreSQL distribution.
### What is pg0?
pg0 is a single binary containing:
- PostgreSQL server
- pgvector extension (pre-installed)
- Automatic initialization
### Behavior
When no `HINDSIGHT_API_DATABASE_URL` is configured, Hindsight:
1. Starts an embedded PostgreSQL instance on port 5555
2. Initializes the schema
3. Stores data in `~/.hindsight/pg0/`
### Environments
| Environment | Database | Configuration |
|-------------|----------|---------------|
| Development | pg0 (embedded) | Automatic |
| Production | PostgreSQL 15+ | `HINDSIGHT_API_DATABASE_URL` environment variable |
## Requirements
- PostgreSQL 15 or later
- pgvector 0.5.0 or later
Any PostgreSQL instance that satisfies these requirements should work. If you encounter issues with a specific setup, [open a GitHub issue](https://github.com/hindsight-ai/hindsight/issues).
### Tested Managed Services
- AWS RDS (PostgreSQL 15+)
- Google Cloud SQL
- Azure Database for PostgreSQL
- Supabase
- Neon
references/faq.md
# FAQ
Common questions and answers about Hindsight.
**Contents**
- [What is Hindsight and how does it differ from RAG?](#what-is-hindsight-and-how-does-it-differ-from-rag)
- [Why use Hindsight instead of other solutions?](#why-use-hindsight-instead-of-other-solutions)
- [Supported clients, integrations, and LLM providers](#which-clients-and-languages-are-supported)
- [Which model should I use?](#which-model-should-i-use-with-hindsight)
- [Hosting and system requirements](#do-i-need-to-host-my-own-infrastructure)
- [What are "zombie" operations and how do I recover them?](#what-are-zombie-operations-and-how-do-i-recover-them)
- [How do I isolate user data?](#how-do-i-isolate-user-data)
- [Retain, recall, and reflect — what's the difference?](#whats-the-difference-between-retain-recall-and-reflect)
- [When should I use recall vs reflect?](#when-should-i-use-recall-vs-reflect)
- [When should I use mental models?](#when-should-i-use-mental-models)
- [Mental model vs. knowledge page](#whats-the-difference-between-a-mental-model-and-a-knowledge-page)
- [Latency expectations](#whats-the-typical-latency-for-recall-operations)
- [Tags, metadata, and entity labels](#does-hindsight-support-metadata-filtering)
- [Controlling which memory types are recalled](#how-do-i-control-which-types-of-memories-are-recalled)
- [Recommended format for conversations](#what-is-the-recommended-format-for-retaining-conversations)
- [How is Hindsight's graph different from a traditional knowledge graph?](#how-is-hindsights-graph-different-from-a-traditional-knowledge-graph)
---
### What is Hindsight and how does it differ from RAG?
Hindsight is an agent memory system that provides long-term memory for AI agents using biomimetic data structures. Unlike traditional RAG (Retrieval-Augmented Generation), Hindsight:
- **Stores structured facts** instead of raw document chunks
- **Builds mental models** that consolidate knowledge over time
- **Uses graph-based relationships** between entities and concepts
- **Supports temporal reasoning** with time-aware retrieval
- **Enables disposition-aware reflection** for nuanced reasoning
For a detailed comparison, see [RAG vs Memory](developer/rag-vs-hindsight.md).
---
### Why use Hindsight instead of other solutions?
Hindsight is purpose-built for agent memory with unique advantages:
- **State-of-the-art accuracy**: Ranked #1 LongMemEval benchmarks for agent memory (see [details](https://benchmarks.hindsight.vectorize.io/))
- **Built on proven technology**: PostgreSQL - battle-tested, reliable, and widely understood
- **Cloud-native architecture**: Designed for modern cloud deployments with horizontal scalability
- **Flexible deployment**: Self-host or use Hindsight Cloud - works with any LLM provider
- **True long-term memory**: Builds mental models that consolidate knowledge over time, not just retrieval
- **Graph-based reasoning**: Understands relationships between entities and concepts for richer context
- **Production-ready**: Scales to millions of memories with 50-500ms recall latency
- **Developer-friendly**: Simple APIs (retain, recall, reflect), SDKs for Python/TypeScript/Go/Rust, integrations with LiteLLM/Vercel AI SDK
Unlike vector databases (just search) or RAG systems (document retrieval), Hindsight provides **living memory** that evolves with your users.
---
### Which clients and languages are supported?
<ClientsGrid />
---
### Which integrations are supported?
Browse all supported integrations in the Integrations Hub.
---
### Which LLM providers are supported?
- OpenAI
- OpenAI Responses
- Anthropic
- Google Gemini
- Vertex AI
- Groq
- Ollama
- Ollama Cloud
- LM Studio
- llama.cpp
- MiniMax
- DeepSeek
- z.ai
- opencode-go
- Atlas Cloud
- Meta Model API
- Volcano Engine
- OpenRouter
- Requesty
- OpenAI Codex
- ChatGPT
- Claude Code
- Cursor
- GitHub Copilot
- AWS Bedrock
- Fireworks AI
- Nous Portal
- SuperGrok (OAuth)
- OpenAI Compatible
- LiteLLM (100+)
See [Models](developer/models.md) for the full list of supported providers, recommended models, and configuration examples.
---
### Which model should I use with Hindsight?
The **[Model Leaderboard](https://benchmarks.hindsight.vectorize.io/)** benchmarks models across accuracy, speed, cost, and reliability for retain, reflect, and observation consolidation — it's the best place to find the right trade-off for your use case.
[](https://benchmarks.hindsight.vectorize.io/)
See [Models](developer/models.md) for the full list of supported and tested models, provider defaults, and configuration examples.
---
### Do I need to host my own infrastructure?
No! You have two options:
1. **Hindsight Cloud** - Fully managed service at [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io)
2. **Self-hosted** - Deploy on your own infrastructure using Docker or direct installation
See [Installation](developer/installation.md) for self-hosting instructions.
---
### What are the minimum system requirements for self-hosting?
For running the Hindsight API server locally:
- Python 3.11+
- 4GB RAM minimum (8GB recommended for production)
- LLM API key (OpenAI, Anthropic, etc.) or local LLM setup
See [Installation](developer/installation.md) for setup instructions.
---
### What are "zombie" operations and how do I recover them?
A **zombie operation** is a background task stuck in `processing` indefinitely because the worker that claimed it is gone — typically after a Docker container restart. The symptom is a `pending_consolidation` (or similar) counter that never decreases on `/banks/{bank_id}/stats`, even though the worker logs show plenty of free slots.
The root cause is almost always an unstable `HINDSIGHT_API_WORKER_ID`. By default the worker uses the container hostname as its identity, and Docker rotates that on every restart — so the new container has a different ID and won't recognize the old worker's claims as its own.
**Recover** with the admin CLI:
```bash
# If you know which worker is dead:
hindsight-admin decommission-worker <old-worker-id>
# Or, fleet-wide release across all workers:
hindsight-admin decommission-workers
```
**Prevent it** by setting `HINDSIGHT_API_WORKER_ID` to a stable value (Docker `-e HINDSIGHT_API_WORKER_ID=...`, or `--worker-id` on bare metal). The Helm chart already handles this — its StatefulSet wires the pod name automatically.
See [Admin CLI — Recovering stuck operations](developer/admin-cli.md#recovering-stuck-or-zombie-operations) for the full diagnosis and recovery flow.
---
### How do I isolate user data?
A **memory bank** is an isolated memory store (like a "brain") that contains its own memories, entities, relationships, and optional disposition traits (skepticism, literalism, empathy). Banks are completely isolated from each other with no data leakage.
There are two approaches for multi-user applications:
**1. Per-user memory banks** (recommended for most use cases)
- Create one bank per user (e.g., `bank_id="user-123"`)
- Easiest setup and strongest data isolation
- Perfect for per-user queries and personalization
- Each bank can have unique disposition traits and background context
- **Limitation**: Cannot perform cross-user analysis (e.g., "What is the most mentioned topic across all users?")
**2. Single bank with tags** (for applications needing aggregated insights)
- Use one bank for the entire application
- Tag memories with user identifiers during retain (e.g., `tags={"user_id": "user-123"}`)
- Filter by tags during recall/reflect for per-user queries
- **Advantage**: Enables both per-user AND cross-user queries (e.g., analyze specific users or aggregate across all users)
Choose per-user banks for simplicity and privacy, or single bank with tags if you need holistic reasoning across users. See [Memory Banks](developer/api/memory-banks.md) for management details.
---
### What's the difference between retain, recall, and reflect?
Hindsight has three core operations:
- **Retain**: Store data (facts, entities, relationships)
- **Recall**: Search and retrieve raw memory data based on a query
- **Reflect**: Use an AI agent to answer a query using retrieved memories
See [Operations](developer/api/operations.md) for API details.
---
### When should I use recall vs reflect?
**Use recall when:**
- You want raw facts to feed into your own reasoning or prompt
- You need maximum control over how memories are interpreted
- You're doing simple fact lookup (e.g., "What did Alice say about X?")
- Latency is critical — recall is significantly faster (50-500ms vs 1-10s)
- You want to build your own answer synthesis layer on top of retrieved memories
**Use reflect when:**
- You want a ready-to-use answer generated from memories (no extra LLM call needed)
- You need disposition-aware responses shaped by the bank's personality traits (skepticism, literalism, empathy)
- The query requires multi-step reasoning across facts, observations, and mental models
- You need structured output (via `response_schema`) from memory-grounded reasoning
- You want citations — reflect returns which memories, mental models, and directives informed the answer
**Key difference**: Recall returns data; reflect returns an answer. Recall gives you raw materials, reflect does the reasoning for you using the bank's disposition and an autonomous search loop.
```
recall("What food does Alice like?")
→ ["Alice loves sushi", "Alice prefers vegetarian options"] # raw facts
reflect("What should I order for Alice?")
→ "I'd recommend a vegetarian sushi platter — Alice loves sushi and prefers vegetarian options." # grounded answer
```
See [Recall](developer/api/recall.md) and [Reflect](developer/reflect.md) for full API details.
---
### When should I use mental models?
**Mental models** are standing answers to questions you define — synthesized from the bank's knowledge and rewritten in the background as it changes. Use them when you need:
- Higher-level understanding beyond raw facts (e.g., "User prefers functional programming patterns")
- Long-term behavioral patterns (e.g., "Customer is price-sensitive but values quality")
- An answer available instantly, with no reasoning on the request path
- Context for AI agent reasoning during **reflect** operations
You create a mental model with the question that defines it; Hindsight builds the content and keeps it current. Reflect checks mental models first, so a fresh one can answer without descending into observations and raw facts. See [Mental Models](developer/api/mental-models.md).
---
### What's the difference between a mental model and a knowledge page?
A **knowledge page is a mental model** — the same engine, the same refresh behaviour — with two additions: a place in a folder tree, and a set of defaults tuned for documents rather than answers (built from observations only, refreshed incrementally after each consolidation, never influenced by other pages, and a larger content budget).
| | Mental model | Knowledge page |
|---|---|---|
| Shape | A standing answer to a question | A markdown document with frontmatter |
| Organization | Flat list, scoped by tags | Nested folders and pages |
| Sources | All fact types by default | Observations only by default |
| Refresh | Off by default | Delta refresh after each consolidation |
| Typical consumer | Your application or an agent, by lookup | A person or agent browsing and reading |
Use a plain mental model when something in your system looks up the answer. Use a knowledge page when the result is meant to be browsed and read directly, like a wiki. Anything you can configure on a mental model can be configured on a page.
See [Mental Models](developer/api/mental-models.md) and the Knowledge Pages section of the developer docs.
---
### What's the typical latency for recall operations?
Typical latencies:
- **Without reranking**: 50-100ms
- **With reranking**: 200-500ms (depends on reranker model and installation)
See [Performance](developer/performance.md) for tuning options.
---
### Does Hindsight support metadata filtering?
Yes — through **Tags**. Tags are string labels attached to memories at retain time and used as a visibility filter at recall/reflect time. Only memories tagged with a matching value are returned.
```python
# Tag memories at retain time
client.retain(bank_id="my-bank", items=[{
"content": "...",
"tags": ["user:alice"],
}])
# Filter by tag at recall time
client.recall(bank_id="my-bank", query="...", tags=["user:alice"])
```
See [Tags](developer/api/retain.md#tags-and-document_tags) for full details including document-level tagging.
**What about filtering by entities?**
Entities (people, places, concepts) extracted from memories are stored in the knowledge graph and drive graph-based retrieval — so querying "tell me about Alice" will naturally surface Alice-related memories without any manual filtering.
If you need explicit tag-based filtering on entity-like values, use **entity labels** with `tag: true`. Entity labels let you define a controlled vocabulary of `key:value` classifiers (e.g. `user:alice`, `topic:algebra`) extracted at retain time. Setting `tag: true` on a label group automatically writes each extracted label as a tag on the memory unit, making them available for standard `tags`/`tags_match` filtering:
```python
# Bank config: entity label group with tag: true
{
"entity_labels": [{
"key": "user",
"type": "text",
"tag": True,
"description": "The user this memory belongs to"
}]
}
# The label "user:alice" is extracted and also written as a tag
# Filter at recall time using the standard tags parameter
client.recall(bank_id="my-bank", query="...", tags=["user:alice"])
```
See [Entity Labels](developer/retain.md#entity-labels) for configuration details.
**What about document `metadata`?**
Document metadata (the `metadata` key-value pairs on a retain item) serves a different purpose. It is:
- **Included in the fact extraction prompt**, so the LLM can use it as additional context when extracting facts — for example, knowing the document title or source can improve accuracy.
- **Returned with every recalled memory** as-is, so your application can link memories back to source systems (e.g. a URL, thread ID, or ticket number) without extra lookups.
Metadata is not a filter — use tags when you need recall to be scoped to a subset of documents.
---
### How do I control which types of memories are recalled?
If your bank mixes different shapes of memory (e.g., concise rules and detailed procedures) and recall surfaces the wrong shape for a given query, use **entity labels** with `tag: true` to classify facts during retain and hard-filter them during recall.
1. Define a label group on the bank with `tag: true` and a controlled vocabulary (e.g., `rule` vs `procedure`)
2. Retain normally — the LLM classifies each extracted fact automatically
3. Pass `tags=["memory_type:rule"]` and `tags_match="any_strict"` at recall time to deterministically include only matching memories
This is a SQL-level filter applied before ranking, not a scoring signal — the excluded memories never enter the retrieval pipeline. This is more reliable than adjusting ranking weights, which only nudge continuous scores and cannot guarantee ordering.
See [Best Practices — Filtering by Memory Shape](best-practices.md#filtering-by-memory-shape-with-entity-labels) for a full walkthrough, or [Entity Labels](developer/api/memory-banks.md#entity-labels) for the configuration reference.
---
### What is the recommended format for retaining conversations?
Pass the **entire conversation as a single document** and upsert it as the conversation grows — Hindsight chunks it automatically, so you don't need to split it yourself.
**Preferred format: JSON array**
```json
[
{"role": "user", "content": "I moved to Berlin last month."},
{"role": "assistant", "content": "How are you finding it?"},
{"role": "user", "content": "Love it, especially the food scene."}
]
```
Hindsight has internal chunking optimizations for the JSON array format, since it's the most common conversation shape.
**Alternative: prefixed plain text**
```
[2025-06-01T10:32:00Z] user: I moved to Berlin last month.
[2025-06-01T10:32:05Z] assistant: How are you finding it?
[2025-06-01T10:32:20Z] user: Love it, especially the food scene.
```
Adding a username and timestamp prefix to each message improves extraction quality — the LLM uses those signals to attribute facts correctly and reason about timing.
**Use a stable document ID to upsert:**
```python
await client.retain(
bank_id="my-bank",
documents=[{
"id": "chat-session-abc123", # stable ID enables upsert
"content": conversation, # full conversation so far
}]
)
```
Re-retaining with the same `id` replaces the old document and its facts, so you won't accumulate duplicates as the conversation grows.
**Don't pre-summarize or pre-extract facts.** Hindsight does this automatically and needs the full conversation for context — a message like "yes, exactly" or "I'll go with option 2" is meaningless without the surrounding exchange.
---
### How is Hindsight's graph different from a traditional knowledge graph?
Hindsight uses an **event-centric graph** that organizes data around moments (conversations, observations, memories) rather than static, isolated facts. Memories are the anchor: entities attach to the memories they appear in, and memories themselves can also be linked directly to each other (more on that below). The closest analogy is a map versus a scrapbook.
**A traditional graph (like Neo4j) is a map.** It shows how places connect directly to each other via roads, mapping rigid structural facts about the world:
```
[Toronto] ──IS_IN──> [Canada]
```
**A Hindsight graph is a scrapbook.** Each page is a single memory. On that page, you slap stickers of all the people, concepts, and labels present in that specific moment. The stickers don't touch each other — they just live on the same page together:
```
Scrapbook page: "Math Class"
[Alice] [Acme] [pedagogy:scaffolding]
```
#### How do the two handle change over time?
This is where traditional graphs run into major limitations when used for AI memory.
**Traditional graphs struggle with change.** If Alice leaves Acme Corp to work at Stark Industries, you have to manually delete or rewrite the old `[WORKS_AT]` arrow. If you don't, the graph contradicts reality by stating she works at both places simultaneously. Traditional graphs are built for the absolute present; they lose history when data changes.
**Hindsight handles change effortlessly because history is preserved.** If Alice changes jobs, you don't rewrite the past. You simply create a new scrapbook page — `Memory: Tuesday's Call` with a sticker for `[Stark Industries]`. The old page from last year still accurately links her to `[Acme Corp]`. The graph naturally tracks how the world evolves without complex database maintenance.
#### Where do the stickers come from? How do I control them?
The creation of stickers happens through a mix of AI automation and developer control:
- **Open-world automation (default):** Hindsight's LLM pipeline automatically detects and extracts standard entities — people, places, dates — from raw text and turns them into stickers.
- **Developer control via `entity_labels`:** You can define a custom schema using [entity labels](developer/retain.md#entity-labels). This forces the LLM to categorize memories using a strict, predefined vocabulary. Instead of letting the AI invent random descriptors, you choose the exact names and parameters for the stickers — for example, forcing a `pedagogy` key to only choose from `scaffolding`, `direct_instruction`, or `socratic_questioning`. To lock the bank to *only* your configured labels and turn off the open-world automation above, also set `entities_allow_free_form: false` on the bank config — the LLM then skips free-form named entities entirely and emits only entries that match your schema.
#### Can I build direct entity-to-entity pipelines?
No. In Hindsight, entities (people, places, concepts, or classification labels) do not have direct arrows connecting them to one another. Instead, they interact by anchoring to the same parent memory unit — the same scrapbook page.
#### How does Hindsight find connections if entities aren't directly linked?
Recall starts with semantic search — finding the memories most relevant to your query as seed pages — and then expands from those seeds through three parallel signals:
- **Shared entities.** If Memory A and Memory B both anchor to the same entity node — like `[pedagogy:scaffolding]` — Hindsight traverses `Memory A → [pedagogy:scaffolding] → Memory B`. This is the scrapbook-pages-with-the-same-stickers case.
- **Semantic links between memories.** When a new memory is retained, Hindsight precomputes links to its nearest neighbors in embedding space, so semantically related pages connect directly without needing a shared sticker.
- **Causal links between memories.** Hindsight also extracts explicit causal relationships (`causes`, `caused_by`, `enables`, `prevents`) between memories, so cause-and-effect chains are first-class edges rather than something the model has to infer.
The semantic layer picks the starting points; the graph fills in the connections through whichever of those three signals is strongest for each candidate.
#### Does this graph structure help reduce hallucinations in my agent?
Yes — by giving the consuming LLM better-grounded context to reason from. Three properties of the scrapbook model contribute directly:
- **History is preserved, not collapsed.** The model sees time-bounded facts (Alice worked at Acme last year, Stark Industries now) rather than a contradictory present-tense edge that forces it to reconcile or guess.
- **Connections come from recorded links, not inference.** When recall surfaces two related memories, it's because they share an entity, a precomputed semantic neighbor, or an explicit causal edge — the link appears in the retrieved context, so the model doesn't have to invent one.
- **Convergent evidence accumulates.** Multiple memories anchoring to the same entity reinforce each other instead of competing, giving the model corroborated claims rather than singular unsupported ones.
---
## Still have questions?
Join our [Slack community](https://vectorize.io/slack) or report issues on [GitHub](https://github.com/vectorize-io/hindsight/issues).
references/sdks/cli.md
# CLI Reference
The Hindsight CLI provides command-line access to memory operations and bank management. All commands follow the [OpenAPI specification](../openapi.json), so you can use `--help` on any command to see all available options.
## Installation
```bash
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
```
## Configuration
Configure the API URL:
```bash
# Set directly
hindsight configure --api-url http://localhost:8888
# With API key for authentication
hindsight configure --api-url http://localhost:8888 --api-key your-api-key
```
You can also run `hindsight configure` with no flags to be prompted interactively, or use environment variables (highest priority):
```bash
export HINDSIGHT_API_URL=http://localhost:8888
export HINDSIGHT_API_KEY=your-api-key
```
### Named Profiles
When you need to switch between multiple Hindsight deployments (e.g. local,
staging, production) without constantly rewriting `~/.hindsight/config`, use
named profiles. Each profile is a TOML file at
`~/.hindsight/cli-profiles/<name>.toml` and is selected per-invocation with
`-p/--profile` (or by setting `$HINDSIGHT_PROFILE`).
```bash
# Create (or overwrite) a profile
hindsight profile create prod \
--api-url https://api.hindsight.vectorize.io \
--api-key hsk_...
# List and inspect profiles
hindsight profile list
hindsight profile show prod
# Use a profile for a single command
hindsight -p prod bank list
# Or make it sticky for the current shell
export HINDSIGHT_PROFILE=prod
hindsight bank list
# Remove a profile
hindsight profile delete prod -y
```
Profile files are written with `0600` permissions on Unix so the API key is
only readable by the owner.
**Configuration precedence** (highest first):
1. Environment variables (`HINDSIGHT_API_URL`, `HINDSIGHT_API_KEY`)
2. Named profile — explicit `-p <name>`, otherwise `$HINDSIGHT_PROFILE`
3. Shared config file (`~/.hindsight/config`, written by `hindsight configure`)
4. Default (`http://localhost:8888`)
`HINDSIGHT_API_URL` / `HINDSIGHT_API_KEY` always override profile values, which
makes it safe to use `-p` in scripts while letting CI inject credentials via
environment.
## Core Commands
### Retain (Store Memory)
Store a single memory:
```bash
hindsight memory retain my-cli-bank "Alice works at Google as a software engineer"
# With context
hindsight memory retain my-cli-bank "Bob loves hiking" --context "hobby discussion"
# Queue for background processing
hindsight memory retain my-cli-bank "Meeting notes" --async
# With an event date (ISO 8601 datetime or date)
hindsight memory retain my-cli-bank "Project launched" --timestamp 2024-01-15
# Store without a timestamp (overrides the default of "now")
hindsight memory retain my-cli-bank "Background fact" --timestamp unset
```
### Retain Files
Bulk import from files:
```bash
# Single file
hindsight memory retain-files my-cli-bank notes.txt
# Directory (recursive by default)
hindsight memory retain-files my-cli-bank ./documents/
# With context
hindsight memory retain-files my-cli-bank meeting-notes.txt --context "team meeting"
# With a named retain strategy (see retain_strategies in bank config)
hindsight memory retain-files my-cli-bank ./documents/ --strategy conversations
# Background processing
hindsight memory retain-files my-cli-bank ./data/ --async
```
### Recall (Search)
Search memories using semantic similarity:
```bash
hindsight memory recall my-cli-bank "What does Alice do?"
# With options
hindsight memory recall my-cli-bank "hiking recommendations" \
--budget high \
--max-tokens 8192
# Filter by fact type
hindsight memory recall my-cli-bank "query" --fact-type world,observation
# Filter by tags
hindsight memory recall my-cli-bank "query" --tags work,project \
--tags-match all
# Pin results to a specific time
hindsight memory recall my-cli-bank "query" --query-timestamp "2026-01-15T00:00:00Z"
# Show trace information
hindsight memory recall my-cli-bank "query" --trace
```
### Reflect (Generate Response)
Generate a response using memories and bank disposition:
```bash
hindsight memory reflect my-cli-bank "What do you know about Alice?"
# With additional context
hindsight memory reflect my-cli-bank "Should I learn Python?" --context "career advice"
# Higher budget for complex questions
hindsight memory reflect my-cli-bank "Summarize my week" --budget high
# Filter by fact type
hindsight memory reflect my-cli-bank "query" \
--fact-types world,experience \
--exclude-mental-models
```
### Memory History
View the observation history for a specific memory unit:
```bash
hindsight memory history my-cli-bank "$MEMORY_ID"
```
### Clear Observations
Remove all observations for a memory unit, keeping the core fact:
```bash
hindsight memory clear-observations my-cli-bank "$MEMORY_ID"
# Skip confirmation prompt
hindsight memory clear-observations my-cli-bank "$MEMORY_ID" -y
```
## Bank Management
### List Banks
```bash
hindsight bank list
```
### View Disposition
```bash
hindsight bank disposition my-cli-bank
```
### Set Disposition
```bash
hindsight bank set-disposition my-cli-bank --skepticism 3 --literalism 4 --empathy 5
```
### View Statistics
```bash
hindsight bank stats my-cli-bank
```
### Set Bank Name
```bash
hindsight bank name my-cli-bank "My Assistant"
```
### Set Mission
```bash
hindsight bank mission my-cli-bank "I am a helpful AI assistant interested in technology"
```
### Clear Observations (Bank-wide)
Remove all observations across the entire bank:
```bash
hindsight bank clear-observations my-cli-bank
# Skip confirmation prompt
hindsight bank clear-observations my-cli-bank -y
```
### Recover Consolidation
Recover from a failed or stuck consolidation:
```bash
hindsight bank consolidation-recover my-cli-bank
```
## Document Management
```bash
# List documents
hindsight document list my-cli-bank
# Get document details
hindsight document get my-cli-bank "$DOCUMENT_ID"
# Replace a document's tags
hindsight document update my-cli-bank "$DOCUMENT_ID" --tags project-x,meetings
# Delete document and its memories
hindsight document delete my-cli-bank "$DOCUMENT_ID"
```
## Entity Management
```bash
# List entities
hindsight entity list my-cli-bank
# Get entity details
hindsight entity get my-cli-bank "$ENTITY_ID"
```
## Operation Management
Track and manage async operations (retain-files, consolidation, etc.):
```bash
# List operations
hindsight operation list my-cli-bank
# Get operation status
hindsight operation get my-cli-bank "$OPERATION_ID"
# Cancel a pending operation
hindsight operation cancel my-cli-bank "$OPERATION_ID"
# Retry a failed operation
hindsight operation retry my-cli-bank "$OPERATION_ID"
```
## Webhook Management
Configure event delivery hooks for bank activity:
```bash
# List webhooks
hindsight webhook list my-cli-bank
# Create a webhook (defaults to consolidation.completed events)
hindsight webhook create my-cli-bank https://example.com/hook
# Create with specific events and signing secret
hindsight webhook create my-cli-bank https://example.com/hook \
--event-types retain.completed,consolidation.completed \
--secret my-hmac-secret
# Update a webhook
hindsight webhook update my-cli-bank "$WEBHOOK_ID" --url https://new-url.com
# View delivery history
hindsight webhook deliveries my-cli-bank "$WEBHOOK_ID"
# Delete a webhook
hindsight webhook delete my-cli-bank "$WEBHOOK_ID" -y
```
## Knowledge Base
Manage a bank's knowledge pages — living documents organized in a folder tree. See [Knowledge Pages](../developer/api/knowledge-pages.md) for what they are and how they refresh.
```bash
# Show the folder/page tree (pages that have fallen behind are marked stale)
hindsight knowledge-base tree my-cli-bank
```
```bash
# Create a folder, optionally nested under another
hindsight knowledge-base create-folder my-cli-bank "Operations"
```
```bash
hindsight knowledge-base create-folder my-cli-bank "Runbooks" --parent-id "$FOLDER_ID"
```
```bash
# Create a page — content is generated in the background
hindsight knowledge-base create-page my-cli-bank \
"Deploying the API" \
"How is the API deployed?" \
--parent-id "$FOLDER_ID" \
--tags ops,type:runbook
# Build a page from raw facts instead of the observation-only default
hindsight knowledge-base create-page my-cli-bank "Recent Incidents" \
"What incidents happened recently?" \
--fact-types experience,world --mode full
```
```bash
# Read a page as a markdown document
hindsight knowledge-base get-page my-cli-bank "$PAGE_ID"
# Hybrid search (full-text + vector) over whole pages
hindsight knowledge-base search my-cli-bank "how do we deploy" --limit 5
# Rename, move, or reconfigure a node
hindsight knowledge-base update my-cli-bank "$NODE_ID" --name "New name"
hindsight knowledge-base update my-cli-bank "$PAGE_ID" --source-query "New question?"
# Export the whole knowledge base as a markdown bundle
hindsight knowledge-base export my-cli-bank
# Delete a folder or page and everything under it
hindsight knowledge-base delete my-cli-bank "$NODE_ID" -y
```
> **💡 Tip**
>
`hindsight fs mount --bank <bank_id>` mirrors the same knowledge base onto disk as
real markdown files, kept current by a background refresh loop — handy when you'd
rather use `grep`, `rg`, or your editor than the commands above.
## Audit Logs
Inspect the audit trail for a bank:
```bash
# List audit entries
hindsight audit list my-cli-bank
# Filter by action and transport
hindsight audit list my-cli-bank --action recall --transport mcp
# Filter by date range
hindsight audit list my-cli-bank \
--start-date "2026-04-01T00:00:00Z" \
--end-date "2026-04-10T00:00:00Z"
# Pagination
hindsight audit list my-cli-bank --limit 50 --offset 100
```
## Output Formats
```bash
# Pretty (default)
hindsight memory recall my-cli-bank "query"
# JSON
hindsight memory recall my-cli-bank "query" -o json
# YAML
hindsight memory recall my-cli-bank "query" -o yaml
```
## Global Options
| Flag | Description |
|------|-------------|
| `-v, --verbose` | Show detailed output including request/response |
| `-o, --output <format>` | Output format: pretty, json, yaml |
| `-p, --profile <name>` | Use a named profile (see [Named Profiles](#named-profiles)) |
| `--help` | Show help |
| `--version` | Show version |
## Control Plane UI
Launch the web-based Control Plane UI directly from the CLI:
```bash
hindsight ui
```
This runs the Control Plane locally on port 9999 using the API URL from your configuration. The UI provides:
- **Memory bank management** — Browse and manage all your banks
- **Entity explorer** — Visualize the knowledge graph
- **Query testing** — Interactive recall and reflect testing
- **Operation history** — View ingestion and processing logs
> **💡 Tip**
>
The UI command requires Node.js to be installed. It automatically downloads and runs the `@vectorize-io/hindsight-control-plane` package via npx.
## Interactive Explorer
Launch the TUI explorer for visual navigation of your memory banks:
```bash
hindsight explore
```
The explorer provides an interactive terminal interface to:
- **Browse memory banks** — View all banks and their statistics
- **Search memories** — Run recall queries with real-time results
- **Inspect memory details** — Open a memory to view its text, metadata, entities, and full JSON fields
- **Inspect entities** — Explore the knowledge graph and entity relationships
- **View facts** — Browse world facts, experiences, and observations
- **Navigate documents** — See source documents and their extracted memories
### Keyboard Shortcuts
| Key | Action |
|-----|--------|
| `↑/↓` | Navigate items |
| `Enter` | Select item / view details |
| `Tab` | Switch panels |
| `/` | Search |
| `q` | Quit |
{/* Screenshot placeholder: explore command TUI */}
## Example Workflow
```bash
# Configure API URL
hindsight configure --api-url http://localhost:8888
# Store some memories
hindsight memory retain cli-demo "Alice works at Google"
hindsight memory retain cli-demo "Bob is a data scientist"
hindsight memory retain cli-demo "Alice and Bob are colleagues"
# Search memories
hindsight memory recall cli-demo "Who works with Alice?"
# Generate a response
hindsight memory reflect cli-demo "What do you know about the team?"
# Check bank disposition
hindsight bank disposition cli-demo
```
references/sdks/embed.md
---
sidebar_position: 5
---
# Daemon CLI (hindsight-embed)
Zero-configuration local memory system with automatic daemon management. Perfect for development, prototyping, and single-user applications.
## Overview
`hindsight-embed` is a zero-configuration SDK that wraps the Hindsight API and PostgreSQL database into a single auto-managed local daemon. It's designed for development, prototyping, and single-user applications where you want memory capabilities without infrastructure overhead.
**How it works:**
1. **First command triggers startup**: When you run any `hindsight-embed` command, it checks if a local daemon is running
2. **Auto-daemon management**: If no daemon exists, it automatically spawns `hindsight-api --daemon` in the background
3. **Embedded database**: The daemon uses `pg0` (embedded PostgreSQL) — no separate database installation required
4. **Command forwarding**: Your command is forwarded to the local daemon via HTTP (localhost:8888)
5. **Auto-shutdown**: After 5 minutes of inactivity (configurable), the daemon gracefully shuts down to free resources
**Key features:**
- **Zero setup** — One `configure` command and you're ready
- **Automatic lifecycle** — Daemon starts on-demand and is shared by every client
- **Isolated storage** — Each bank gets its own embedded PostgreSQL database
- **Local-only** — Binds to `127.0.0.1:8888`, not accessible from network
- **Production-grade engine** — Uses the same memory engine as the full API service
Think of it as SQLite for long-term memory — all the power of Hindsight without managing servers.
## Installation
Install via `uvx` (recommended - always latest version):
```bash
# Run directly without installation
uvx hindsight-embed@latest configure
# Or use pipx for persistent installation
pipx install hindsight-embed
```
## Quick Start
### 1. Configure
```bash
# Interactive configuration
hindsight-embed configure
# Or non-interactive via environment variables
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-embed configure
```
Configuration is saved to `~/.hindsight/embed`:
```bash
HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
# Daemon settings (macOS: force CPU to avoid MPS/XPC issues)
HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1
HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1
```
### 2. Use Memory Operations
```bash
# Store a memory
hindsight-embed memory retain default "User prefers dark mode"
# Query memories
hindsight-embed memory recall default "user preferences"
# Reasoning with memory
hindsight-embed memory reflect default "What color scheme should I use?"
```
The daemon starts automatically on first use!
### 3. Open the Control Center (optional)
Use the local control center when you want a browser-based configuration wizard and daemon supervisor:
```bash
# Launch the control center and open the browser
hindsight-embed control start
# Or use the browser wizard instead of terminal prompts during setup
hindsight-embed configure --ui
```
The control center listens on localhost only (`http://localhost:7878` by default) and prints a tokenized URL. It stores the local access token at `~/.hindsight/control.token` and writes logs to `~/.hindsight/control.log`.
```bash
# Pick a different control-center port for this launch
hindsight-embed control start --port 7879
# Start without opening a browser automatically
hindsight-embed control start --no-open
# Check, inspect, or stop the control center
hindsight-embed control status
hindsight-embed control logs -f
hindsight-embed control stop
```
The control center runs as a separate process from the memory daemon. Stopping or restarting the control center does not stop a running daemon.
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLM_API_KEY` | **Required**. API key for LLM provider | - |
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama` | `openai` |
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-4o-mini` |
| `HINDSIGHT_EMBED_DAEMON_LOG_MAX_BYTES` | Rotate the daemon log at startup when it reaches this size; `0` disables rotation | `10485760` (10 MiB) |
| `HINDSIGHT_EMBED_DAEMON_LOG_BACKUP_COUNT` | Retained backups; `0` truncates a full log at startup | `3` |
| `HINDSIGHT_EMBED_CONTROL_PORT` | Default port for `hindsight-embed control start` | `7878` |
The size is checked only when a daemon starts, so a single uninterrupted run is never truncated and can
grow past `MAX_BYTES` — and at the next start that whole file is kept as the first backup. Retained size
is therefore around `MAX_BYTES × (BACKUP_COUNT + 1)` (40 MiB by default) only for daemons that restart
regularly; a daemon left running for weeks keeps whatever it wrote. Restart it to keep the bound
meaningful.
**Provider Examples:**
```bash
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Groq (fast inference)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=llama-3.3-70b-versatile
# Anthropic
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
```
## Daemon Management
The daemon runs until you stop it — it never exits on its own, so a long retain, reflect or
consolidation is never cut short.
### Daemon Commands
```bash
# Check daemon status
hindsight-embed daemon status
# View daemon logs in real-time
hindsight-embed daemon logs -f
# Stop daemon manually
hindsight-embed daemon stop
```
### Reclaiming Disk Space
When the daemon is launched with `uvx` (the default for the npm package and the editor plugins), every Hindsight version it has run keeps its own cached Python environment — around 1.5 GB each — and these are not removed automatically. If disk space gets tight, stop the daemon before clearing the cache: unused entries can only be reclaimed while no daemon is running.
```bash
hindsight-embed daemon stop
uv cache prune
hindsight-embed daemon status # starts fresh on the current version
```
### Control Center Commands
```bash
# Start or reuse the local browser control center
hindsight-embed control start
# Check whether it is running
hindsight-embed control status
# View control-center logs
hindsight-embed control logs -f
# Stop the control-center process
hindsight-embed control stop
```
## Commands
All memory operations follow the same interface as the CLI:
### Retain (Store Memory)
```bash
hindsight-embed memory retain <bank_id> "content"
# With context
hindsight-embed memory retain <bank_id> "content" --context "source information"
# Background processing
hindsight-embed memory retain <bank_id> "content" --async
```
### Recall (Search)
```bash
hindsight-embed memory recall <bank_id> "query"
# With budget control
hindsight-embed memory recall <bank_id> "query" --budget high
# Show trace
hindsight-embed memory recall <bank_id> "query" --trace
```
### Reflect (Generate Response)
```bash
hindsight-embed memory reflect <bank_id> "prompt"
# With additional context
hindsight-embed memory reflect <bank_id> "prompt" --context "additional info"
```
### Bank Management
```bash
# List all banks
hindsight-embed bank list
# View bank stats
hindsight-embed bank stats <bank_id>
# Set bank name
hindsight-embed bank name <bank_id> "My Assistant"
# Set bank mission
hindsight-embed bank mission <bank_id> "I am a helpful AI assistant"
```
## Troubleshooting
### Daemon Won't Start
Check the daemon logs:
```bash
hindsight-embed daemon logs
# Or watch in real-time
hindsight-embed daemon logs -f
```
Common issues:
- **Missing API key**: Set `HINDSIGHT_API_LLM_API_KEY`
- **Port conflict**: Another service using port 8888
- **Permissions**: Check `~/.hindsight/` directory permissions
### Daemon Exits Immediately
The daemon never stops itself, so an exit is a crash or a failed startup. Check the log:
```bash
hindsight-embed daemon status
tail -n 100 ~/.hindsight/daemon.log
```
### Reset Configuration
```bash
# Remove config file and reconfigure
rm ~/.hindsight/embed
hindsight-embed configure
```
## When to Use
**Perfect for:**
- Development and prototyping
- Single-user applications
- Local-first tools
- Quick experiments with Hindsight
**Not suitable for:**
- Production multi-user deployments
- Network-accessible services
- High-availability requirements
- Multi-tenant applications
For production deployments, use the [API Service](../developer/services.md) with external PostgreSQL instead.
references/sdks/go.md
---
sidebar_position: 3
---
# Go Client
Official Go client for the Hindsight API, generated from the OpenAPI 3.1 spec using [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator).
import CodeSnippet from '@site/src/components/CodeSnippet';
import quickstartGo from '!!raw-loader!@site/examples/api/quickstart.go';
## Installation
```bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go
```
Requires Go 1.23+.
## Quick Start
<CodeSnippet code={quickstartGo} section="quickstart-full" language="go" />
## API Structure
The Go client provides access to all Hindsight API operations through structured namespaces:
- **`client.MemoryAPI`** - Retain, recall, reflect operations
- **`client.BanksAPI`** - Bank management
- **`client.DirectivesAPI`** - Directive management
- **`client.MentalModelsAPI`** - Mental model management
- **`client.DocumentsAPI`** - Document operations
- **`client.EntitiesAPI`** - Entity operations
- **`client.OperationsAPI`** - Async operation monitoring
## Working with Nullable Fields
The Go client uses `NullableString`, `NullableTime`, and similar types for optional fields:
<CodeSnippet code={quickstartGo} section="nullable-fields" language="go" />
## Error Handling
<CodeSnippet code={quickstartGo} section="error-handling" language="go" />
## More Examples
For detailed examples of all operations, see:
- [Python SDK documentation](./python.md) - API concepts are the same
- [Node.js SDK documentation](./nodejs.md) - API concepts are the same
- [OpenAPI specification](https://hindsight.dev/openapi.json) - Complete API reference
references/sdks/hindsight-all-npm.md
---
sidebar_position: 6
---
# Programmatic API (Node.js)
The `@vectorize-io/hindsight-all` npm package is the Node.js equivalent of the Python [`hindsight-all`](./hindsight-all.md) package. It lets your Node code spawn and supervise a local Hindsight daemon without deploying any server infrastructure — pair it with [`@vectorize-io/hindsight-client`](./nodejs.md) for memory operations.
The daemon runs as a **separate OS process** on `127.0.0.1` (not in your Node process). Your code talks to it over HTTP via `HindsightClient`.
This package **does not ship an HTTP client** — it only owns the server process. Once the daemon is running, talk to it with [`@vectorize-io/hindsight-client`](./nodejs.md) against `server.getBaseUrl()`. The two packages compose: one owns the process, the other owns the API surface.
## How it works
1. `server.start()` resolves the underlying `hindsight-embed` command (via `uvx` from PyPI, or `uv run --directory <path>` for a local checkout).
2. Runs `profile create <name> --merge --port <port> [--env KEY=VALUE ...]` with every entry from `options.env` forwarded as `--env`.
3. Runs `daemon --profile <name> start`.
4. Polls `http://host:port/health` until it returns `200` or the `readyTimeoutMs` budget is exhausted.
5. `server.stop()` runs `daemon --profile <name> stop`.
The server is intentionally transparent: new daemon env vars or CLI flags never require a wrapper release — pass them through `env`, `extraProfileCreateArgs`, or `extraDaemonStartArgs`.
## Requirements
- **Node.js ≥ 22** — uses global `fetch` and `AbortSignal.timeout`.
- **`uv` / `uvx`** on `PATH` — used to download and run the Hindsight daemon. Install via [docs.astral.sh/uv](https://docs.astral.sh/uv/).
## Install
```bash
npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-client
```
## Example
```ts
import { HindsightServer, consoleLogger } from '@vectorize-io/hindsight-all';
import { HindsightClient } from '@vectorize-io/hindsight-client';
const server = new HindsightServer({
profile: 'my-app',
port: 9077,
env: {
HINDSIGHT_API_LLM_PROVIDER: 'anthropic',
HINDSIGHT_API_LLM_API_KEY: process.env.ANTHROPIC_API_KEY,
HINDSIGHT_API_LLM_MODEL: 'claude-sonnet-4-20250514',
},
logger: consoleLogger,
});
await server.start();
const client = new HindsightClient({ baseUrl: server.getBaseUrl() });
await client.retain('user-123', 'User prefers dark mode.');
const recall = await client.recall('user-123', 'what are the user preferences?');
await server.stop();
```
For a remote Hindsight API, skip the server entirely and point `HindsightClient` directly at the remote URL.
## `HindsightServerOptions`
| Option | Type | Default | Description |
|---|---|---|---|
| `profile` | `string` | `"default"` | Profile name passed to `--profile` on every sub-command. |
| `port` | `number` | `8888` | TCP port the daemon listens on. |
| `host` | `string` | `"127.0.0.1"` | Hostname the daemon binds to (used for health checks). |
| `embedVersion` | `string` | `"latest"` | Version of the underlying `hindsight-embed` package to run via `uvx`. |
| `embedPackagePath` | `string` | — | Local checkout path — takes precedence over `embedVersion`. Uses `uv run --directory` instead of `uvx`. |
| `env` | `Record<string, string \| undefined>` | `{}` | Environment variables passed to the daemon process **and** written into the profile config via `--env KEY=VALUE`. The preferred way to surface any `HINDSIGHT_API_*` / `HINDSIGHT_EMBED_*` setting. |
| `extraProfileCreateArgs` | `string[]` | `[]` | Extra args appended verbatim to `profile create`. |
| `extraDaemonStartArgs` | `string[]` | `[]` | Extra args appended verbatim to `daemon start`. |
| `platformCpuWorkaround` | `boolean` | `true` on macOS | Auto-set `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1` and `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1` to avoid Metal/MPS crashes. Caller-supplied `env` values win over the auto-applied ones. |
| `readyTimeoutMs` | `number` | `30000` | Max time to wait for `/health` to return 200. |
| `readyPollIntervalMs` | `number` | `1000` | Polling interval while waiting for `/health`. |
| `logger` | `Logger` | silent | Pluggable logger (`debug`/`info`/`warn`/`error`). `consoleLogger` and `silentLogger` helpers are exported. |
## Server methods
| Method | Returns | Description |
|---|---|---|
| `start()` | `Promise<void>` | Configure profile, spawn the daemon, wait for `/health`. Idempotent — safe to re-run. |
| `stop()` | `Promise<void>` | Stop the daemon. Never throws; logs and resolves even on failure. |
| `checkHealth()` | `Promise<boolean>` | One-shot `/health` probe with a 2 s timeout. |
| `getBaseUrl()` | `string` | `http://host:port` — pass this straight to `HindsightClient`. |
| `getProfile()` | `string` | The profile name this server operates on. |
For memory operations (retain, recall, reflect, bank management) use [`@vectorize-io/hindsight-client`](./nodejs.md).
references/sdks/hindsight-all.md
---
sidebar_position: 2
---
# Programmatic API (Python)
The `hindsight-all` Python package lets your code spawn and manage a local Hindsight daemon without deploying any server infrastructure. It bundles the Hindsight API server, embedded PostgreSQL, and the Python client into one install — `pip install hindsight-all` and you can start a fully-functional Hindsight instance from a few lines of Python.
The daemon runs as a **separate OS process** on `127.0.0.1` (not in your Python process memory). Your code talks to it over HTTP via the bundled `HindsightClient`.
If you already have a Hindsight server running and just need a client, use [Python Client (hindsight-client)](./python.md) instead.
## How it works
`hindsight-all` exposes two primary APIs:
- **`HindsightServer`** — explicit lifecycle. Use it as a context manager when you want deterministic startup/shutdown (e.g. in tests).
- **`HindsightEmbedded`** — auto-managed. Starts a daemon on first use and reuses it across calls. Easiest for application code that doesn't want to think about lifecycle.
Both end up talking to the same underlying daemon via the same `HindsightClient` HTTP interface — the difference is only how the server process is managed.
## Installation
```bash
pip install hindsight-all
```
The `hindsight-all` wheel bundles `hindsight-api-slim`, `hindsight-client`, and `hindsight-embed` as dependencies, so one `pip install` gets you everything.
On Intel (x86_64) Macs, install `hindsight-all-slim` instead — the full bundle's local ML models have no Intel-Mac wheels. See [Supported Platforms](../developer/installation.md#supported-platforms).
## `HindsightServer` — explicit lifecycle
Use `HindsightServer` as a context manager when you want the server to start immediately, run for the duration of a block, and shut down cleanly afterwards. Ideal for tests and short-lived scripts.
```python
import os
from hindsight import HindsightServer, HindsightClient
with HindsightServer(
llm_provider="openai",
llm_model="gpt-4o-mini",
llm_api_key=os.environ["OPENAI_API_KEY"],
) as server:
client = HindsightClient(base_url=server.url)
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="What does Alice do?")
for r in results:
print(r.text)
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
print(answer.text)
# Server is stopped here
```
## `HindsightEmbedded` — auto-managed
`HindsightEmbedded` is the simplest way to use Hindsight in Python. It automatically manages a background daemon for you — starts on first use and stays alive across calls.
```python
from hindsight import HindsightEmbedded
import os
# Server starts automatically on first call
client = HindsightEmbedded(
profile="myapp", # Profile for data isolation
llm_provider="openai",
llm_model="gpt-4o-mini",
llm_api_key=os.environ["OPENAI_API_KEY"],
)
# Use immediately - no manual server management needed
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="What does Alice do?")
# Server continues running (stop it with `hindsight-embed daemon stop`)
# Or explicitly stop it:
client.close(stop_daemon=True)
```
### What's a Profile?
A profile is an isolated Hindsight environment. Each profile gets its own embedded PostgreSQL database (stored in `~/.pg0/instances/hindsight-embed-{profile}/`) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.
### When to use which
| Use case | Pick |
|---|---|
| Tests, short-lived scripts, deterministic startup/shutdown | `HindsightServer` (context manager) |
| Long-running application, auto-start on first use, don't want to manage lifecycle | `HindsightEmbedded` |
| Existing Hindsight server running elsewhere | [`hindsight-client`](./python.md) directly |
## API namespaces
Both `HindsightEmbedded` and `HindsightClient` expose organized API namespaces for bank management, mental models, directives, and memories:
```python
from hindsight import HindsightEmbedded
import os
embedded = HindsightEmbedded(
profile="myapp",
llm_provider="openai",
llm_api_key=os.environ["OPENAI_API_KEY"],
)
# Core operations
embedded.retain(bank_id="test", content="Hello")
results = embedded.recall(bank_id="test", query="Hello")
# Bank management
embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
embedded.banks.set_mission(bank_id="test", mission="Updated mission")
embedded.banks.delete(bank_id="test")
# Mental models
embedded.mental_models.create(
bank_id="test",
name="User Preferences",
content="User prefers dark mode"
)
models = embedded.mental_models.list(bank_id="test")
# Directives
embedded.directives.create(
bank_id="test",
name="Response Style",
content="Be concise and friendly"
)
directives = embedded.directives.list(bank_id="test")
# List memories
memories = embedded.memories.list(bank_id="test", type="world", limit=50)
```
API namespaces ensure the daemon is running before each call, so daemon crashes are handled gracefully:
```python
# ✅ GOOD - Uses API namespace (daemon restarts handled)
embedded.banks.create(bank_id="test", name="Test")
# ❌ BAD - Direct client access (daemon crashes NOT handled)
client = embedded.client
client.create_bank(bank_id="test", name="Test") # Fails if daemon crashed
```
For the full reference of retain/recall/reflect methods and their options (which work the same regardless of how you obtain the client) see the [Python Client page](./python.md).
references/sdks/integrations/ag2.md
# AG2
Persistent long-term memory for [AG2](https://ag2.ai) agents (community AutoGen fork). Give your agents retain/recall/reflect tools that persist across conversations.
[View Changelog →](../../changelog/integrations/ag2.md)
## Features
- **Drop-in Tools** — `register_hindsight_tools()` registers retain, recall, and reflect in one line
- **AG2-native** — Uses `Annotated` type hints compatible with AG2's `@register_for_llm` / `@register_for_execution` pattern
- **GroupChat Support** — Multiple agents can share a single memory bank
- **Selective Tools** — Include only the tools you need (`include_retain`, `include_recall`, `include_reflect`)
- **Simple Configuration** — Configure once globally or override per tool set
## Installation
```bash
pip install hindsight-ag2
```
## Quick Start
```python
from autogen import AssistantAgent, UserProxyAgent, LLMConfig
from hindsight_ag2 import register_hindsight_tools
llm_config = LLMConfig(api_type="openai", model="gpt-4o-mini")
with llm_config:
assistant = AssistantAgent(
name="assistant",
system_message="You are a helpful assistant with long-term memory.",
)
user_proxy = UserProxyAgent(
name="user",
human_input_mode="NEVER",
)
# Register Hindsight memory tools on both agents
register_hindsight_tools(
assistant, user_proxy,
bank_id="my-bank",
hindsight_api_url="http://localhost:8888",
)
# The assistant can now use hindsight_retain, hindsight_recall, hindsight_reflect
result = user_proxy.initiate_chat(
assistant,
message="Remember that I prefer Python over JavaScript.",
)
```
That's it. The assistant can now store and retrieve memories across conversations.
## How It Works
The integration provides three AG2-compatible tool functions backed by Hindsight's API:
| Tool | Hindsight | What happens |
|------|-----------|--------------|
| `hindsight_retain(content)` | `retain(bank_id, content, ...)` | Content is stored. Hindsight extracts facts, entities, and relationships from the raw text. |
| `hindsight_recall(query)` | `recall(bank_id, query, ...)` | Hindsight runs semantic search, BM25, graph traversal, and reranking. Returns a numbered list of matching memories. |
| `hindsight_reflect(query)` | `reflect(bank_id, query, ...)` | Hindsight synthesizes a reasoned answer from all relevant memories, using the bank's disposition traits. |
Tools are plain Python functions with `Annotated` type hints. AG2 uses these hints to generate the tool schema that the LLM sees.
## Configuration
### Global Configuration
```python
from hindsight_ag2 import configure
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-key", # or set HINDSIGHT_API_KEY env var
budget="mid", # low / mid / high
max_tokens=4096,
tags=["source:ag2"], # default tags for retain
)
```
### Per-Tool Overrides
Constructor arguments override global configuration:
```python
from hindsight_ag2 import create_hindsight_tools
tools = create_hindsight_tools(
bank_id="my-bank",
hindsight_api_url="http://localhost:8888",
budget="high",
max_tokens=8192,
tags=["team:alpha"],
)
```
## GroupChat with Shared Memory
Multiple agents can share a single memory bank in a GroupChat:
```python
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager, LLMConfig
from hindsight_ag2 import register_hindsight_tools
llm_config = LLMConfig(api_type="openai", model="gpt-4o-mini")
with llm_config:
researcher = AssistantAgent(name="researcher", system_message="You research topics.")
writer = AssistantAgent(name="writer", system_message="You write content.")
executor = UserProxyAgent(name="executor", human_input_mode="NEVER")
# All agents share the same memory bank
for agent in [researcher, writer]:
register_hindsight_tools(agent, executor, bank_id="team-memory")
group_chat = GroupChat(agents=[researcher, writer, executor], messages=[])
manager = GroupChatManager(groupchat=group_chat)
```
## Manual Registration
For full control over how tools are registered:
```python
from hindsight_ag2 import create_hindsight_tools
tools = create_hindsight_tools(
bank_id="my-bank",
hindsight_api_url="http://localhost:8888",
)
for tool_fn in tools:
assistant.register_for_llm(description=tool_fn.__doc__)(tool_fn)
user_proxy.register_for_execution()(tool_fn)
```
## API Reference
### Configuration
| Function | Description |
|----------|-------------|
| `configure(...)` | Set global connection and default settings |
| `get_config()` | Get current configuration |
| `reset_config()` | Reset configuration to None |
### create_hindsight_tools
| Parameter | Default | Description |
|-----------|---------|-------------|
| `bank_id` | required | Hindsight memory bank ID |
| `client` | `None` | Pre-configured `Hindsight` client |
| `hindsight_api_url` | from config | Hindsight API URL |
| `api_key` | from config | API key |
| `budget` | `"mid"` | Recall/reflect budget (low/mid/high) |
| `max_tokens` | `4096` | Max tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode (any/all/any_strict/all_strict) |
| `retain_metadata` | `None` | Metadata dict for retain operations |
| `retain_document_id` | `None` | Document ID for retain (groups/upserts memories) |
| `recall_types` | `None` | Fact types to filter (world, experience, observation) |
| `recall_include_entities` | `False` | Include entity information in recall results |
| `reflect_context` | `None` | Additional context for reflect operations |
| `reflect_max_tokens` | `max_tokens` | Max tokens for reflect results |
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
| `reflect_tags` | `recall_tags` | Tags to filter memories used in reflect |
| `reflect_tags_match` | `recall_tags_match` | Tag matching for reflect |
| `include_retain` | `True` | Include the retain tool |
| `include_recall` | `True` | Include the recall tool |
| `include_reflect` | `True` | Include the reflect tool |
## Requirements
- Python >= 3.10
- ag2 >= 0.9.0
- A running Hindsight API server
references/sdks/integrations/agent-framework.md
# Microsoft Agent Framework
[View Changelog →](../../changelog/integrations/agent-framework.md)
Persistent memory for [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) — the successor to Semantic Kernel — using [Hindsight](https://vectorize.io/hindsight). The integration plugs in as a **context provider**, so every agent run automatically recalls relevant memories into the agent's context and retains the conversation afterward. No MCP, and no tools the model has to remember to call.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting required.
```bash
pip install hindsight-agent-framework
export HINDSIGHT_API_KEY=your-hindsight-key
```
```python
from agent_framework.openai import OpenAIChatClient
from hindsight_agent_framework import HindsightProvider
agent = OpenAIChatClient().as_agent(
name="assistant",
instructions="You are a helpful assistant.",
context_providers=[HindsightProvider(bank_id="user-123")],
)
session = agent.create_session()
await agent.run("Remember that I prefer vegetarian food.", session=session)
await agent.run("Suggest a recipe.", session=session) # recalls the preference
```
## How It Works
| Hook | Behavior |
| --- | --- |
| `before_run` | Recall memories relevant to the user's message and inject them as a `## Memories` block in the agent's instructions. |
| `after_run` | Retain the user input + agent response so future runs build on them. |
Memories live in a Hindsight **bank** — one per user, agent, or session (you choose via `bank_id`). Recall and retain are best-effort: a memory hiccup never blocks the agent.
## Configuration
`HindsightProvider(bank_id, ...)` accepts `hindsight_api_url`, `api_key`, `budget` (low/mid/high), `max_tokens`, `tags`, `recall_tags`, `mission`, `auto_recall`, `auto_retain`, and more. Process-wide defaults can be set with `configure(...)`.
## Self-Hosting
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api # http://localhost:8888
```
```python
HindsightProvider(bank_id="user-123", hindsight_api_url="http://localhost:8888")
```
See the [integration source](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/agent-framework) for full details.
references/sdks/integrations/agent-plugin.md
# Agent Plugins
Portable long-term memory for any [Agent Plugins](https://agent-plugins.org) client, powered by [Hindsight](https://vectorize.io/hindsight).
[Agent Plugins](https://agent-plugins.org) is the vendor-neutral open standard (developed with Amazon, Cursor, Microsoft, OpenAI, and Vercel) for packaging **Agent Skills + MCP servers** into a single distributable plugin. Instead of a separate integration per tool, Hindsight ships **one** plugin that every compatible client can load — at launch: **ChatGPT / Codex, Cursor, GitHub Copilot, Kiro, and VS Code**.
> **⚠️ Codex**
>
Codex's Agent Plugins loader drops this plugin's `Authorization` header, so the plugin cannot authenticate from Codex. Use the [Coding Agents](coding-agents.md) integration instead — see [Codex](#codex) below.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
1. Get your `hsk_...` API key from [ui.hindsight.vectorize.io/connect](https://ui.hindsight.vectorize.io/connect).
2. Set the environment variables the plugin reads:
```bash
export HINDSIGHT_API_KEY="hsk_your_token"
export HINDSIGHT_BANK_ID="my-project" # optional; see "Bank selection" below
```
3. Install the plugin in your client (through its plugin/MCP UI, or by pointing it at the plugin directory — installation is client-specific per the standard).
Once installed, ask the agent something that depends on past context, or tell it a durable preference — it calls `recall` and `retain` automatically, guided by the bundled skill.
## What's in the plugin
The plugin is a thin, transport-only wrapper — all memory logic stays server-side in Hindsight. It follows the Agent Plugins `1.0.0` layout:
```
agent-plugin/
├── plugin.json # manifest ($schema + name + metadata)
├── mcp.json # Hindsight MCP server (Streamable HTTP)
└── skills/
└── hindsight-memory/
└── SKILL.md # teaches the agent when to recall / retain / reflect
```
- **`mcp.json`** connects the client to Hindsight's built-in [MCP server](../../developer/mcp-server.md) over Streamable HTTP.
- **`skills/hindsight-memory/SKILL.md`** is loaded into the agent's context so it knows *when* to reach for memory, not just that the tools exist.
## Memory tools
Via the MCP server, the agent gets Hindsight's full memory surface. The three it reaches for most:
| Tool | When | What it does |
|------|------|--------------|
| `recall` | Before answering, when past context could help | Semantic + keyword + graph + temporal retrieval over the bank |
| `retain` | After learning a durable, reusable fact | Stores the fact for future sessions |
| `reflect` | When a lookup is too shallow and you need synthesized reasoning | Disposition-aware reasoning over everything remembered |
Additional tools (knowledge pages, mental models, documents, tags) are exposed too — see the [MCP Server reference](../../developer/mcp-server.md).
## Configuration
The plugin reads two environment variables, interpolated into `mcp.json`:
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| API key | `HINDSIGHT_API_KEY` | — | Your `hsk_...` key. Sent as `Authorization: Bearer`. Required for Hindsight Cloud. |
| Memory bank | `HINDSIGHT_BANK_ID` | see below | Bank to read from and write to (sent as `X-Bank-Id`). Use one bank per user, project, or team for isolation. |
> **📝 Env-var syntax varies by client**
>
Most clients substitute `${VAR}`; some (VS Code, Cursor) use `${env:VAR}`. If your client doesn't interpolate, paste the literal key and bank id into `mcp.json`. This does not work in Codex, which removes the `Authorization` header regardless of its value.
**Bank selection when `HINDSIGHT_BANK_ID` is unset.** A client that substitutes variables sends an empty `X-Bank-Id`, and the server uses the `default` bank. A client that does *not* substitute sends the literal text `${HINDSIGHT_BANK_ID}`, which Hindsight treats as a bank name: reads fail with bank-not-found, and the first `retain` creates a bank with that name. Set the variable, or paste the bank id literally, to avoid this.
**Self-hosting:** replace the host in `mcp.json` (`https://api.hindsight.vectorize.io`) with your deployment's URL. A local server with the MCP endpoint open needs no API key.
## Codex
Codex's Agent Plugins loader treats `Authorization` as a header only the client may set, so it removes the one in this plugin's `mcp.json`. It also copies the other headers verbatim, without expanding `${VAR}` placeholders. The portable plugin therefore cannot authenticate to Hindsight Cloud from Codex, and pasting a literal key does not help because the header is removed whatever its value.
For Codex, use the [Coding Agents](coding-agents.md) integration. It writes Codex's native MCP configuration and adds automatic recall/retain hooks:
```bash
npx @vectorize-io/hindsight-coding-agents install codex
```
## Explicit tools vs. automatic capture
Agent Plugins `1.0.0` standardizes **Skills + MCP**, not session lifecycle hooks. This plugin therefore delivers **explicit, tool-driven** memory that works identically across every supported client.
For the fully automatic experience — recall injected before every prompt and transcripts retained on session end — use the hook-based [Coding Agents](coding-agents.md) integration, which covers Claude Code, Codex and many other coding agents. It shares the same Hindsight banks, so memory captured by the hook-based integration is recalled through the Agent Plugin, and vice versa.
## Troubleshooting
**No memories recalled**: `recall` returns results only after something has been retained. Retain a fact first, or seed the bank via the [API](../../developer/api/quickstart.md).
**401 Unauthorized**: Check `HINDSIGHT_API_KEY` is set and your client is interpolating it into the `Authorization` header (see the env-var syntax note above). In Codex this is expected: the header is removed, so use the [Codex](#codex) path instead.
**A bank named `${HINDSIGHT_BANK_ID}` appears**: your client sent the placeholder without substituting it. Set the variable or paste the bank id into `mcp.json`, then delete the stray bank.
**Wrong or empty memory**: Confirm `HINDSIGHT_BANK_ID` points at the bank you expect. Different tools writing to different banks won't share memory.
## Learn more
- [Agent Plugins standard](https://agent-plugins.org)
- [Hindsight MCP Server reference](../../developer/mcp-server.md)
- [Hindsight Cloud sign-up](https://ui.hindsight.vectorize.io)
references/sdks/integrations/agentcore.md
# Amazon Bedrock AgentCore Runtime
Persistent memory for [Amazon Bedrock AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-sessions.html) agents using Hindsight.
AgentCore Runtime sessions are explicitly ephemeral — they terminate on inactivity and reprovision fresh environments. `hindsight-agentcore` adds durable cross-session memory so agents remember users, decisions, and learned patterns across any number of Runtime sessions.
[View Changelog →](../../changelog/integrations/agentcore.md)
## How It Works
```
AgentCore Runtime invocation
│
▼
before_turn() ← Recall relevant memories from Hindsight
│
▼
Agent executes ← Prompt enriched with prior context
│
▼
after_turn() ← Retain output to Hindsight (async)
```
Memory is keyed to stable user identity — **not** the `runtimeSessionId`. Banks survive session churn.
Default bank format:
```
tenant:{tenant_id}:user:{user_id}:agent:{agent_name}
```
## Installation
```bash
pip install hindsight-agentcore
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
import os
from hindsight_agentcore import HindsightRuntimeAdapter, TurnContext, configure
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key=os.environ["HINDSIGHT_API_KEY"],
)
adapter = HindsightRuntimeAdapter(agent_name="support-agent")
# Your AgentCore Runtime handler
async def handler(event: dict) -> dict:
context = TurnContext(
runtime_session_id=event["sessionId"],
user_id=event["userId"], # from validated auth — never client-supplied
agent_name="support-agent",
tenant_id=event.get("tenantId"),
request_id=event.get("requestId"),
)
result = await adapter.run_turn(
context=context,
payload={"prompt": event["prompt"]},
agent_callable=run_my_agent,
)
return result
async def run_my_agent(payload: dict, memory_context: str) -> dict:
prompt = payload["prompt"]
if memory_context:
prompt = f"Past context:\n{memory_context}\n\nCurrent request: {prompt}"
output = await call_bedrock(prompt)
return {"output": output}
```
## Lower-Level Hooks
For direct control over the recall → execute → retain cycle:
```python
memory_context = await adapter.before_turn(context, query=user_message)
result = await run_my_agent(payload, memory_context=memory_context)
await adapter.after_turn(context, result=result["output"], query=user_message)
```
## Retrieval Modes
### Recall (default)
Fast multi-strategy retrieval (semantic + keyword + graph + temporal):
```python
from hindsight_agentcore import RecallPolicy
adapter = HindsightRuntimeAdapter(
recall_policy=RecallPolicy(mode="recall", budget="mid", max_tokens=1500)
)
```
### Reflect
LLM-synthesized context for complex reasoning tasks:
```python
adapter = HindsightRuntimeAdapter(
recall_policy=RecallPolicy(mode="reflect")
)
```
Use `reflect` selectively — it's slower. Reserve it for explicit planning steps or routing decisions.
## Async Retention
By default, `after_turn()` fires retention as a background task — the user turn is never delayed:
```python
configure(retain_async=True) # default
configure(retain_async=False) # await retention before returning
```
## Long-Running Workflows
For jobs spanning multiple Runtime sessions, retain at start and completion:
```python
# Task start
await adapter.after_turn(
context,
result="Started QBR analysis for Acme Corp",
query=task_description,
)
# ... long-running work across potentially multiple sessions ...
# Task completion
await adapter.after_turn(
context,
result=f"Completed QBR analysis. Finding: {summary}",
query=task_description,
)
```
## Identity and Auth
**Never use `runtimeSessionId` as the bank ID.** Sessions expire. Memory must survive session churn.
Preferred identity sources (in order):
1. Validated user ID from AgentCore JWT/OAuth context
2. `X-Amzn-Bedrock-AgentCore-Runtime-User-Id` header
3. Application-supplied user ID in trusted server-side deployments
```python
context = TurnContext(
runtime_session_id=event["sessionId"],
user_id=jwt_claims["sub"], # stable identity from validated token
agent_name="support-agent",
tenant_id=jwt_claims.get("tenant"),
)
```
## Custom Bank Resolution
Override the default `tenant:user:agent` format:
```python
from hindsight_agentcore import TurnContext
def my_resolver(context: TurnContext) -> str:
return f"acme:{context.user_id}:{context.agent_name}"
adapter = HindsightRuntimeAdapter(bank_resolver=my_resolver)
```
**Security rule:** The resolver must fail closed (`BankResolutionError`) rather than leak memory across users when identity is missing.
## Configuration Reference
| Option | Env Variable | Default | Description |
|---|---|---|---|
| `hindsight_api_url` | `HINDSIGHT_API_URL` | Hindsight Cloud | Hindsight server URL |
| `api_key` | `HINDSIGHT_API_KEY` | — | API key for Hindsight Cloud |
| `recall_budget` | — | `"mid"` | Search depth: `low`, `mid`, `high` |
| `recall_max_tokens` | — | `1500` | Max tokens recalled |
| `retain_async` | — | `True` | Non-blocking retention |
| `timeout` | — | `15.0` | HTTP timeout in seconds |
| `tags` | — | `[]` | Tags applied to all retained memories |
| `verbose` | — | `False` | Log memory operations |
## Failure Modes
| Failure | Behavior |
|---|---|
| Hindsight unavailable | `before_turn()` returns `""`, agent continues |
| Recall timeout | Returns `""`, agent continues |
| Retain failure | Logged as warning, user turn unaffected |
| Bad bank resolution | Fails closed — no cross-user memory leakage |
## Requirements
- Python 3.10+
- `hindsight-client>=0.4.0`
references/sdks/integrations/agno.md
# Agno
Persistent memory tools for [Agno](https://github.com/agno-agi/agno) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Agno's native Toolkit pattern.
[View Changelog →](../../changelog/integrations/agno.md)
## Features
- **Native Toolkit** - Extends Agno's `Toolkit` base class, just like `Mem0Tools`
- **Memory Instructions** - Pre-recall memories for injection into `Agent(instructions=[...])`
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Flexible Bank Resolution** - Static bank ID, `RunContext.user_id`, or custom resolver
- **Simple Configuration** - Configure once globally, or pass a client directly
## Installation
```bash
pip install hindsight-agno
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from hindsight_agno import HindsightTools
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)],
)
agent.print_response("Remember that I prefer dark mode")
agent.print_response("What are my preferences?")
```
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL to `http://localhost:8888`. See the [installation guide](../../developer/installation.md) for setup.
The agent now has three tools it can call:
- **`retain_memory`** — Store information to long-term memory
- **`recall_memory`** — Search long-term memory for relevant facts
- **`reflect_on_memory`** — Synthesize a reasoned answer from memories
## With Memory Instructions
Pre-recall relevant memories and inject them into the system prompt:
```python
from hindsight_agno import HindsightTools, memory_instructions
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
)],
instructions=[memory_instructions(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
)],
)
```
## Selecting Tools
Include only the tools you need:
```python
tools = [HindsightTools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
enable_retain=True,
enable_recall=True,
enable_reflect=False, # Omit reflect
)]
```
## Bank Resolution
The bank ID is resolved in order:
1. **`bank_resolver`** — Custom callable `(RunContext) -> str`
2. **`bank_id`** — Static bank ID passed to constructor
3. **`run_context.user_id`** — Automatic per-user banks
```python
# Per-user banks from RunContext
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(hindsight_api_url="https://api.hindsight.vectorize.io")],
user_id="user-123", # Used as bank_id
)
# Custom resolver
def resolve_bank(ctx):
return f"team-{ctx.user_id}"
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_resolver=resolve_bank,
hindsight_api_url="https://api.hindsight.vectorize.io",
)],
)
```
## Global Configuration
Instead of passing connection details to every toolkit, configure once:
```python
from hindsight_agno import configure, HindsightTools
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create toolkit without passing connection details
tools = [HindsightTools(bank_id="user-123")]
```
## Configuration Reference
### `HindsightTools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | `None` | Static Hindsight memory bank ID |
| `bank_resolver` | `None` | Callable `(RunContext) -> str` for dynamic bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `enable_retain` | `True` | Include the retain (store) tool |
| `enable_recall` | `True` | Include the recall (search) tool |
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | Recall query for memory injection |
| `budget` | `"low"` | Recall budget level |
| `max_results` | `5` | Maximum memories to inject |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Hindsight Cloud (`https://api.hindsight.vectorize.io`) | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
## Requirements
- Python >= 3.10
- agno
- hindsight-client >= 0.4.0
- A running Hindsight API server
references/sdks/integrations/ai-sdk.md
# Vercel AI SDK
The `@vectorize-io/hindsight-ai-sdk` package integrates [Hindsight](https://hindsight.vectorize.io) memory with the [Vercel AI SDK](https://ai-sdk.dev). It provides five ready-to-use tools for retaining, recalling, and reflecting on long-term memories.
[View Changelog →](../../changelog/integrations/ai-sdk.md)
## Installation
```bash
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai
```
## Setup
Create a Hindsight client and pass it to `createHindsightTools` along with a `bankId`. The `bankId` identifies the memory store for this session—typically a user ID.
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
const tools = createHindsightTools({
client,
bankId: 'user-123',
});
```
> **💡 Per-request bank IDs**
>
In multi-user applications, create `tools` inside your request handler so each request closes over the correct `bankId`. See the [Next.js example](#in-a-nextjs-route-handler) below.
## Usage
### With `generateText`
```typescript
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { text } = await generateText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'Remember that I prefer dark mode and large fonts.',
});
```
### With `streamText`
```typescript
import { streamText } from 'ai';
const result = streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'What are my display preferences?',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
### With `ToolLoopAgent`
```typescript
import { generateText, ToolLoopAgent, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: process.env.HINDSIGHT_API_URL! });
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: createHindsightTools({ client, bankId: 'user-123' }),
stopWhen: stepCountIs(10),
system: 'You are a helpful assistant with long-term memory.',
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
```
### In a Next.js Route Handler
```typescript
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const hindsightClient = new HindsightClient({
baseUrl: process.env.HINDSIGHT_API_URL!,
});
export async function POST(req: Request) {
const { messages, userId } = await req.json();
// Tools are created per-request, closing over the current user's bankId
const tools = createHindsightTools({
client: hindsightClient,
bankId: userId,
});
return streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
messages,
}).toDataStreamResponse();
}
```
---
## Tools Reference
Five tools are registered. The `bankId` is fixed at creation time—the agent cannot change it.
| Tool | What the agent provides | What the constructor controls |
|------|------------------------|-------------------------------|
| `retain` | `content`, `documentId`, `timestamp`, `context` | `async`, `tags`, `metadata` |
| `recall` | `query`, `queryTimestamp` | `budget`, `types`, `maxTokens`, `includeEntities`, `includeChunks` |
| `reflect` | `query`, `context` | `budget` |
| `getMentalModel` | `mentalModelId` | — |
| `getDocument` | `documentId` | — |
**Why this split?** Semantic inputs (what to remember, what to search for) belong to the agent. Infrastructure concerns (cost budget, tagging strategy, async mode) belong to the application.
---
## Constructor Options
All options except `client` and `bankId` are optional. Each tool's options are grouped under the tool name.
```typescript
const tools = createHindsightTools({
client,
bankId: userId,
retain: {
async: true, // fire-and-forget (default: false)
tags: ['env:prod', 'app:support'], // always attached to every retained memory
metadata: { version: '2.0' }, // always attached to every retained memory
},
recall: {
budget: 'high', // processing depth: low | mid | high (default: 'mid')
types: ['experience', 'world'], // restrict to these fact types (default: all)
maxTokens: 2048, // cap token budget (default: API default)
includeEntities: true, // include entity observations (default: false)
includeChunks: true, // include raw source chunks (default: false)
},
reflect: {
budget: 'mid', // processing depth (default: 'mid')
},
});
```
### `retain`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `async` | `boolean` | `false` | Fire-and-forget — do not wait for ingestion to complete |
| `tags` | `string[]` | — | Tags attached to every retained memory |
| `metadata` | `Record<string, string>` | — | Metadata attached to every retained memory |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `recall`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls retrieval depth and latency |
| `types` | `('world' \| 'experience' \| 'observation')[]` | all | Restrict results to these fact types |
| `maxTokens` | `number` | API default | Cap the total tokens returned |
| `includeEntities` | `boolean` | `false` | Include entity observations in results |
| `includeChunks` | `boolean` | `false` | Include raw source chunks in results |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `reflect`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls synthesis depth and latency |
| `maxTokens` | `number` | API default | Maximum tokens for the response |
| `description` | `string` | built-in | Override the tool description shown to the model |
references/sdks/integrations/aider.md
# Aider
Persistent long-term memory for [Aider](https://aider.chat), powered by [Hindsight](https://vectorize.io/hindsight). `hindsight-aider` is a drop-in wrapper for the `aider` command: it **recalls relevant project memory before each session** (injected into Aider's context via a read-only file) and **retains the session transcript after** — so each Aider session starts with what you've learned and saves what it learns. Memory is scoped **per git repo**.
[View Changelog →](../../changelog/integrations/aider.md)
## How It Works
Aider has no MCP client or per-prompt hook, but it loads **read-only context files** and writes a **chat-history file**. The wrapper uses both:
- **Recall (before):** queries Hindsight, writes the results to `.aider.hindsight-memory.md`, and launches `aider --read .aider.hindsight-memory.md …` so the memory is in context. `aider -m "fix the auth bug"` uses that message as the recall query; otherwise a general project-context query.
- **Retain (after):** when Aider exits, the wrapper reads the slice of the chat-history file written during the session and retains it to the repo's bank.
Recall is once per session (Aider can't be hooked mid-conversation), which fits its session-oriented workflow.
## Setup
```bash
pip install hindsight-aider aider-chat
export HINDSIGHT_API_TOKEN=hsk_...
```
Use it exactly like `aider` — all arguments pass through:
```bash
hindsight-aider # interactive, project memory loaded
hindsight-aider -m "add retry logic" # one-shot; recall uses the message
hindsight-aider src/app.py # any aider args
```
Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or a self-hosted server with `HINDSIGHT_API_URL=http://localhost:8888`. The bank defaults to the git repo name, so a project's memory is shared with the other Hindsight editor integrations on the same repo.
See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/aider) for full configuration options.
references/sdks/integrations/autogen.md
# AutoGen
Persistent long-term memory for [AutoGen](https://microsoft.github.io/autogen/) agents via Hindsight. Provides `FunctionTool` instances that plug directly into AutoGen's `AssistantAgent`.
[View Changelog →](../../changelog/integrations/autogen.md)
## Features
- **Memory Tools** — retain, recall, and reflect as AutoGen `FunctionTool` instances compatible with `AssistantAgent(tools=[...])`
- **Async-Native** — Uses `aretain`, `arecall`, `areflect` directly — works seamlessly in AutoGen's async runtime
- **Selective Tools** — Include only the tools you need with `include_retain/recall/reflect` flags
- **Tag-Based Scoping** — Partition memories by topic, session, or user with tags
- **Global Configuration** — Configure once with `configure()`, create tools anywhere
## Installation
```bash
pip install hindsight-autogen autogen-agentchat "autogen-ext[openai]"
```
`hindsight-autogen` pulls in `autogen-core` and `hindsight-client`. You also need `autogen-agentchat` for `AssistantAgent` and `autogen-ext[openai]` for the OpenAI model client.
## Quick Start
```python
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from hindsight_client import Hindsight
from hindsight_autogen import create_hindsight_tools
async def main():
client = Hindsight(base_url="http://localhost:8888")
await client.acreate_bank(bank_id="user-123")
model_client = OpenAIChatCompletionClient(model="gpt-4o")
tools = create_hindsight_tools(client=client, bank_id="user-123")
agent = AssistantAgent(
name="assistant",
model_client=model_client,
tools=tools,
)
# Store a memory
result = await agent.run(task="Remember that I prefer dark mode")
print(result.messages[-1].content)
# Hindsight processes retained content asynchronously (fact extraction,
# entity resolution, embeddings). A brief pause ensures memories are
# searchable before the next recall. In production, this delay is only
# needed when retain and recall happen back-to-back in the same script.
await asyncio.sleep(3)
# Recall it later
result = await agent.run(task="What are my UI preferences?")
print(result.messages[-1].content)
# Clean up
await client.aclose()
await model_client.close()
asyncio.run(main())
```
> **💡 Jupyter Notebooks**
>
If you're running in a Jupyter notebook, you don't need `asyncio.run()` — just use `await` directly in cells since the notebook already has an active event loop.
The agent gets three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
## Selecting Tools
Include only the tools you need:
```python
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=True,
include_recall=True,
include_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing a client to every call, configure once:
```python
from hindsight_autogen import configure, create_hindsight_tools
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode
)
# Now create tools without passing client — uses global config
tools = create_hindsight_tools(bank_id="user-123")
```
## Memory Scoping with Tags
Use tags to partition memories by topic, session, or user:
```python
# Store memories tagged by source
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
tags=["source:chat", "session:abc"],
recall_tags=["source:chat"],
recall_tags_match="any",
)
```
## Production Patterns
### Error Handling
Tools raise `HindsightError` on failure, which AutoGen surfaces to the agent as a tool error. Wrap agent calls for graceful degradation:
```python
from hindsight_autogen.errors import HindsightError
try:
result = await agent.run(task="What do you remember about me?")
except HindsightError as e:
print(f"Memory operation failed: {e}")
```
### Bank Lifecycle
Create banks before first use and clean up when done:
```python
async def main():
client = Hindsight(base_url="http://localhost:8888")
# Create bank (idempotent)
await client.acreate_bank(bank_id="user-123")
tools = create_hindsight_tools(client=client, bank_id="user-123")
# ... use tools ...
# Optional: delete bank when no longer needed
await client.adelete_bank(bank_id="user-123")
```
### Multi-Agent Teams
Give each agent its own memory bank, or share a bank across a team:
```python
# Per-agent memory
researcher_tools = create_hindsight_tools(client=client, bank_id="researcher-memory")
writer_tools = create_hindsight_tools(client=client, bank_id="writer-memory")
# Shared team memory
shared_tools = create_hindsight_tools(
client=client,
bank_id="team-shared",
tags=["team:content"],
)
```
## API Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode (any/all/any\_strict/all\_strict) |
| `retain_metadata` | `None` | Default metadata dict for retain operations |
| `retain_document_id` | `None` | Default document\_id for retain (groups/upserts memories) |
| `recall_types` | `None` | Fact types to filter (world, experience, observation) |
| `recall_include_entities` | `False` | Include entity information in recall results |
| `reflect_context` | `None` | Additional context for reflect operations |
| `reflect_max_tokens` | `None` | Max tokens for reflect results (defaults to `max_tokens`) |
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
| `reflect_tags` | `None` | Tags to filter memories used in reflect (defaults to `recall_tags`) |
| `reflect_tags_match` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
| `include_retain` | `True` | Include the retain (store) tool |
| `include_recall` | `True` | Include the recall (search) tool |
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Production API | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
## Requirements
- Python >= 3.10
- autogen-core >= 0.4.0
- hindsight-client >= 0.4.0
references/sdks/integrations/chat.md
# Vercel Chat SDK
We built `@vectorize-io/hindsight-chat` to give [Vercel Chat SDK](https://github.com/vercel/chat) bots persistent, per-user memory with a single handler wrapper. The integration works across Slack, Discord, Teams, Google Chat, GitHub, and Linear — no custom plumbing required.
[View Changelog →](../../changelog/integrations/chat.md)
## Setup
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — get an API key instantly, no infrastructure to run. Self-hosting? See the [installation guide](../../developer/installation.md).
## Installation
```bash
npm install @vectorize-io/hindsight-chat
```
## Quick Start
```typescript
import { Chat } from 'chat';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { withHindsightChat } from '@vectorize-io/hindsight-chat';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const chat = new Chat({ connectors: [/* your connectors */] });
const hindsight = new HindsightClient({ apiKey: process.env.HINDSIGHT_API_KEY });
chat.onNewMention(
withHindsightChat(
{
client: hindsight,
bankId: (msg) => msg.author.userId, // per-user memory
},
async (thread, message, ctx) => {
await thread.subscribe();
const result = await streamText({
model: openai('gpt-4o'),
system: ctx.memoriesAsSystemPrompt(),
messages: [{ role: 'user', content: message.text }],
});
// Stream the response
const chunks: string[] = [];
for await (const chunk of result.textStream) {
chunks.push(chunk);
}
const fullResponse = chunks.join('');
await thread.post(fullResponse);
// Store the conversation in memory
await ctx.retain(
`User: ${message.text}\nAssistant: ${fullResponse}`
);
}
)
);
```
## Configuration
### `withHindsightChat(options, handler)`
`withHindsightChat` wraps your existing Chat SDK handler and injects memory context automatically. It returns a standard handler `(thread, message) => Promise<void>` so it drops in without changing your handler signature.
#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `client` | `HindsightClient` | *required* | Hindsight client instance |
| `bankId` | `string \| (msg) => string` | *required* | Memory bank ID or resolver function |
| `recall.enabled` | `boolean` | `true` | Auto-recall memories before handler |
| `recall.budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Processing budget for recall |
| `recall.maxTokens` | `number` | API default | Max tokens for recall results |
| `recall.types` | `FactType[]` | all | Filter to specific fact types |
| `recall.includeEntities` | `boolean` | `true` | Include entity observations |
| `retain.enabled` | `boolean` | `false` | Auto-retain inbound messages |
| `retain.async` | `boolean` | `true` | Fire-and-forget retain |
| `retain.tags` | `string[]` | – | Tags for retained memories |
| `retain.metadata` | `Record<string, string>` | – | Metadata for retained memories |
### Context (`ctx`)
We inject a third `ctx` argument into your handler that exposes the full Hindsight memory API scoped to the current user's bank:
| Property/Method | Description |
|----------------|-------------|
| `ctx.bankId` | Resolved bank ID |
| `ctx.memories` | Array of recalled memories |
| `ctx.entities` | Entity observations (or null) |
| `ctx.memoriesAsSystemPrompt(options?)` | Format memories for LLM system prompt |
| `ctx.retain(content, options?)` | Store content in memory |
| `ctx.recall(query, options?)` | Search memories |
| `ctx.reflect(query, options?)` | Reason over memories |
## Examples
### Subscribed Message Handler
```typescript
chat.onSubscribedMessage(
withHindsightChat(
{
client: hindsight,
bankId: (msg) => msg.author.userId,
recall: { budget: 'high', maxTokens: 1000 },
},
async (thread, message, ctx) => {
const result = await generateText({
model: openai('gpt-4o'),
system: ctx.memoriesAsSystemPrompt(),
messages: [{ role: 'user', content: message.text }],
});
await thread.post(result.text);
}
)
);
```
### Auto-Retain Inbound Messages
```typescript
chat.onNewMention(
withHindsightChat(
{
client: hindsight,
bankId: (msg) => msg.author.userId,
retain: { enabled: true, tags: ['slack', 'inbound'] },
},
async (thread, message, ctx) => {
// Inbound message is already being retained automatically
const result = await generateText({
model: openai('gpt-4o'),
system: ctx.memoriesAsSystemPrompt(),
messages: [{ role: 'user', content: message.text }],
});
await thread.post(result.text);
// Retain the assistant response separately
await ctx.retain(`Assistant: ${result.text}`, {
tags: ['slack', 'outbound'],
});
}
)
);
```
### Static Bank ID (Shared Memory)
```typescript
// All users share the same memory bank
chat.onNewMention(
withHindsightChat(
{ client: hindsight, bankId: 'shared-team-memory' },
async (thread, message, ctx) => {
// ...
}
)
);
```
## Error Handling
We designed the integration so that memory failures never break your bot. Auto-recall and auto-retain errors are caught internally, logged as warnings, and the handler continues with empty memories. Manual `ctx.retain()`, `ctx.recall()`, and `ctx.reflect()` calls propagate errors normally so you can handle them as needed.
references/sdks/integrations/chatgpt.md
# ChatGPT
Add persistent, searchable memory to [ChatGPT](https://chatgpt.com) using [Hindsight](https://vectorize.io/hindsight). Store insights from your conversations and automatically recall relevant context in future sessions—all secured with OAuth.
## Overview
ChatGPT's built-in memory helps with preferences, but knowledge from specific conversations (research, code, decisions) is lost when you start a new chat. Hindsight solves this by providing:
- **Cross-session memory** — Knowledge from one conversation persists to the next
- **Smart recall** — Hindsight automatically retrieves relevant memories when you need them
- **No API keys** — OAuth handles authentication securely
- **Custom instructions** — Aggressive auto-retain/recall via system prompts
## Quick Start
### 1. Create a Hindsight Cloud Account
[Sign up free](https://ui.hindsight.vectorize.io) for Hindsight Cloud.
### 2. Add Hindsight as a Connector in ChatGPT
1. Go to [ChatGPT Settings](https://chatgpt.com/settings)
2. Navigate to **Apps & Connectors → Connectors**
3. Click **Create connector**
4. Fill in:
- **Name:** `Hindsight` (or your preferred name)
- **URL:** `https://api.hindsight.vectorize.io/mcp/default/`
5. Click **Create** — a browser window opens for Hindsight Cloud login
6. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
7. Return to ChatGPT; the connector is now active
To use in a chat: click **+** in the message composer → **More** → select **Hindsight**
### 3. Configure Custom Instructions for Automatic Retention
The connector is now active, but you need to tell ChatGPT to actually use it. Add custom instructions to enable automatic memory capture and recall:
1. Go to **Settings → Personalization → Custom instructions**
2. Copy and paste this instruction:
```
After every response, automatically use the Hindsight tool to retain key information from our conversation:
- Important facts, decisions, or learnings we discussed
- Your preferences, goals, or constraints mentioned
- Code patterns, architecture decisions, or technical insights
- Any information that might be useful in future conversations
Before generating each response, automatically use the Hindsight tool to recall relevant memories that might apply to the current conversation. Include recalled memories in your reasoning.
Retain and recall aggressively—assume everything is valuable. The Hindsight tool will handle deduplication and relevance filtering.
```
3. Save and close settings
From now on, ChatGPT will automatically store insights from your conversations and surface relevant memories without you needing to ask.
> **💡 Tip**
>
Feel free to experiment with the instructions to ensure proper behavior.
## Features
- **Automatic retention** — Custom instructions tell ChatGPT to store insights after every response
- **Intelligent recall** — ChatGPT queries Hindsight before generating each response to include relevant memories as context
- **OAuth-secured** — No API keys to copy-paste; sign in once via browser
- **Bank isolation** — Separate memory banks for different projects or purposes
- **Semantic search** — Hindsight understands context, not just keyword matching
## What to Store
Store meaningful, specific knowledge for best results:
- **Project context** — goals, requirements, architecture decisions, constraints
- **Personal preferences** — coding style, communication preferences, learning style
- **Discoveries** — research findings, useful resources, lessons learned
- **Domain knowledge** — industry facts, patterns, techniques you reference
- **Decision history** — why you chose A over B, trade-offs considered
**Example of what to store:**
```
"We're building a real-time collaboration tool. Constraints:
- <500ms latency for cursor updates
- Support 10k concurrent users
- GDPR-compliant data storage
- Team prefers WebSockets over polling"
```
Later, when you ask ChatGPT *"How should we structure our database?"*, Hindsight recalls these constraints. ChatGPT's answer becomes tailored to your actual situation, not generic advice.
## Best Practices
**Be intentional about what you store.** Not every conversation needs retention. Focus on storing insights that will be useful later: lessons learned, architectural decisions, research findings, and personal preferences. Storing trivial facts clutters your memory bank and makes retrieval less useful.
**Use consistent terminology.** If you call your project "ProjectX" in one session and "Project X" in another, Hindsight's semantic search may miss the connection. Establish naming conventions and stick to them.
**Review and refine.** The Hindsight Cloud dashboard lets you browse your memory bank. Periodically review what you've stored. Delete outdated information and consolidate similar insights. A curated memory bank is more valuable than an exhaustive one.
**Structure multi-part decisions.** When storing complex decisions (like architecture trade-offs), include context: the constraints, alternatives considered, and why you chose one path. Future-you will thank present-you for the context.
## Architecture: Single-Bank vs. Multi-Bank
### Single-Bank Mode (Recommended)
Each connector accesses one memory bank. Simpler for most users.
- **URL:** `https://api.hindsight.vectorize.io/mcp/YOUR_BANK_ID/`
- **Setup:** Just enter the URL in the Connector settings
- **Best for:** Dedicated memory per tool (e.g., ChatGPT uses a `writing` bank)
### Multi-Bank Mode
Both tools access multiple banks via bank_id parameter.
- **URL:** `https://api.hindsight.vectorize.io/mcp`
- **Setup:** Requires additional configuration in Hindsight Cloud
- **Best for:** When ChatGPT and Perplexity collaborate on the same project
## Data Privacy and Security
- **OAuth-secured** — no API keys, no copy-paste secrets
- **Your account** — memories live in your Hindsight Cloud account
- **Encrypted in transit** — HTTPS + TLS for all connections
- **No vendor lock-in** — export your memories anytime
- **Scoped access** — the connector can only read/write to its assigned bank
When you approve OAuth in the browser, you're authorizing ChatGPT to:
- **Read** your memory banks (to recall relevant facts)
- **Write** to your memory banks (to store new discoveries)
- **Search** your memories (to find context)
You can revoke access anytime by removing the connector in ChatGPT settings.
## Troubleshooting
**"Connector failed to load"**
- Verify the URL is correct (no typos in your bank ID)
- Check that your Hindsight Cloud account is active
- Try re-creating the connector
**"Authorization failed" or "Access denied"**
- Make sure you're signing in with the same Hindsight Cloud account where you want to store memories
- If using a team account, verify you have permission to access the bank
- Try logging out and back in
**"Memory tools appear but don't return results"**
- Give Hindsight a few seconds to index memories (processing is async)
- Make sure you've stored relevant memories using the `retain` operation
- Check the memory bank name matches your connector URL
**Memories aren't being stored**
- Use the Hindsight Cloud dashboard to verify memories are being created
- In ChatGPT, explicitly ask Hindsight to store something: *"Hindsight, remember that we use Vue.js for frontend"*
- Check that your bank isn't full (unlikely, but possible with very large memory sets)
## Next Steps
1. Create your Hindsight Cloud account
2. Add the Hindsight connector to ChatGPT
3. Set up your custom instructions
4. Store your first memory — ask ChatGPT to remember something important to you
5. Start a new chat and ask a related question — watch Hindsight retrieve your stored memory
6. Build the habit of storing meaningful insights over time
Over weeks and months, as your memory bank grows, ChatGPT will give increasingly personalized, informed answers because it's working with your accumulated context instead of starting fresh every time.
references/sdks/integrations/claude-agent-sdk.md
# Claude Agent SDK
Persistent long-term memory for Anthropic's [Claude Agent SDK](https://pypi.org/project/claude-agent-sdk/) via [Hindsight](https://vectorize.io/hindsight). Expose retain/recall/reflect as MCP tools so the agent decides when to use memory, or wire up hooks to inject relevant memories automatically before every turn.
[View Changelog →](../../changelog/integrations/claude-agent-sdk.md)
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```bash
pip install hindsight-claude-agent-sdk
```
### Tools (explicit memory)
Give your Claude agent retain/recall/reflect tools so it can decide when to use memory:
```python
from claude_agent_sdk import query, ClaudeAgentOptions
from hindsight_claude_agent_sdk import create_hindsight_server
server = create_hindsight_server(
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
async for msg in query(
prompt="Remember that I prefer dark mode. Then check what you know about me.",
options=ClaudeAgentOptions(
mcp_servers={"hindsight": server},
allowed_tools=["mcp__hindsight__*"],
),
):
print(msg)
```
## Features
- **Memory Tools** — retain, recall, and reflect exposed as MCP tools the agent can call on its own
- **Automatic Hooks** — inject relevant memories into context before each turn and retain conversation content after, with no explicit tool calls
- **Per-Agent Banks** — isolate memory per agent or user with a `bank_id`
- **Cloud or Self-Hosted** — point at Hindsight Cloud or your own Hindsight deployment
## Learn More
- Claude Agent SDK cookbook recipe
- [Source on GitHub](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/claude-agent-sdk)
references/sdks/integrations/claude-code.md
# Claude Code
> **⚠️ Superseded by the Coding Agents plugin**
>
**The Claude Code plugin** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install claude-code --import-conversations
```
Two things move, and nothing else does.
**Your server moves automatically.** `install` reads `~/.hindsight/claude-code.json` and keeps the same
`apiUrl` and token — an empty URL still means the local daemon — so you are not silently switched
to Hindsight Cloud. Pass `--server` to change it.
**Your conversations are re-imported from local transcripts** by `--import-conversations`, as new
documents. They are not copied from the old bank: that bank's default was a single static bank
shared by every project, and its documents record only a session id, so working out which ones
belong to this repo means reading the local transcripts anyway.
**Nothing else carries over.** The old `recall*` and `retain*` settings, missions and bank-naming
options describe a pipeline this package replaced. Bank naming changes too — one bank per repo,
shared by every agent. See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
Biomimetic long-term memory for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations, recalls relevant context, and exposes knowledge tools and a subagent-creation skill so Claude can read and write its own memory.
[View Changelog →](../../changelog/integrations/claude-code.md)
> **💡 Works in Grok Build too**
>
Grok Build is installed by the [Coding Agents plugin](coding-agents.md#-grok-build).
## Quick Start
```bash
# 1. Add the Hindsight marketplace and install the plugin
claude plugin marketplace add vectorize-io/hindsight
claude plugin install hindsight-memory
# 2. Configure your LLM provider for memory extraction
# Option A: OpenAI (auto-detected)
export OPENAI_API_KEY="sk-your-key"
# Option B: Anthropic (auto-detected)
export ANTHROPIC_API_KEY="your-key"
# Option C: No API key needed (uses Claude Code's own model — personal/local use only)
export HINDSIGHT_LLM_PROVIDER=claude-code
# Option D: Connect to an external Hindsight server instead of running locally
mkdir -p ~/.hindsight
echo '{"hindsightApiUrl": "https://your-hindsight-server.com"}' > ~/.hindsight/claude-code.json
# 3. Start Claude Code — the plugin activates automatically
claude
```
That's it! The plugin will automatically start capturing and recalling memories.
## Features
- **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as context (invisible to the chat transcript, visible to Claude)
- **Auto-retain** — after every response (or every N turns), extracts and retains conversation content to Hindsight for long-term storage
- **Knowledge tools** — Claude can read, write, and search its own memory via MCP tools (knowledge pages, recall, ingest)
- **Subagent skill** — `/hindsight-memory:create-agent` scaffolds a subagent backed by an isolated memory bank
- **Dynamic bank IDs** — per-agent, per-project, or per-session memory isolation (so each subagent gets its own brain)
- **Daemon management** — can auto-start/stop `hindsight-embed` locally or connect to an external Hindsight server
- **Channel-agnostic** — works with Claude Code Channels (Telegram, Discord, Slack) or interactive sessions
## Architecture
The plugin combines hooks (for automatic recall/retain) with an MCP server (for explicit knowledge tools) and a skill (for subagent creation):
| Component | Trigger | Purpose |
|-----------|---------|---------|
| `session_start.py` | `SessionStart` hook | Health check — verify Hindsight is reachable |
| `recall.py` | `UserPromptSubmit` hook | **Auto-recall** — query memories, inject as `additionalContext` |
| `retain.py` | `Stop` hook | **Auto-retain** — extract transcript, POST to Hindsight (async) |
| `session_end.py` | `SessionEnd` hook | Cleanup — stop auto-managed daemon if started |
| `mcp_server.py` | MCP server | Exposes `agent_knowledge_*` tools — list/get/create/update/delete pages, recall, ingest |
| `create-agent` | Skill | Scaffolds a subagent file under `~/.claude/agents/` and seeds its bank |
Python dependencies (`mcp`) are bootstrapped on first run into a private venv under `${CLAUDE_PLUGIN_DATA}/venv` — no global pip install, isolated to the plugin, survives plugin updates.
## Connection Modes
### 1. External API (recommended for production)
Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction.
```json
{
"hindsightApiUrl": "https://your-hindsight-server.com",
"hindsightApiToken": "your-token"
}
```
### 2. Local Daemon (auto-managed)
The plugin automatically starts and stops `hindsight-embed` via `uvx`. Requires an LLM provider API key for local fact extraction.
Set an LLM provider:
```bash
export OPENAI_API_KEY="sk-your-key"
# or
export ANTHROPIC_API_KEY="your-key"
# or
export HINDSIGHT_LLM_PROVIDER=claude-code # No API key needed
```
The model is selected automatically by the Hindsight API. To override, set `HINDSIGHT_LLM_MODEL`.
### 3. Existing Local Server
If you already have `hindsight-embed` running, leave `hindsightApiUrl` empty and set `apiPort` to match your server's port. The plugin will detect it automatically.
## Configuration
All settings live in `~/.hindsight/claude-code.json`. Every setting can also be overridden via environment variables. The plugin ships with sensible defaults — you only need to configure what you want to change.
**Loading order** (later entries win):
1. Built-in defaults (hardcoded in the plugin)
2. Plugin `settings.json` (ships with the plugin, at `CLAUDE_PLUGIN_ROOT/settings.json`)
3. User config (`~/.hindsight/claude-code.json` — recommended for your overrides)
4. Environment variables
---
### Connection & Daemon
These settings control how the plugin connects to the Hindsight API.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` (empty) | URL of an external Hindsight API server. When empty, the plugin uses a local daemon instead. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | Authentication token for the external API. Only needed when `hindsightApiUrl` is set. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port used by the local `hindsight-embed` daemon. Change this if you run multiple instances or have a port conflict. |
| `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `"latest"` | Which version of `hindsight-embed` to install via `uvx`. Pin to a specific version (e.g. `"0.5.2"`) for reproducibility. |
| `embedPackagePath` | `HINDSIGHT_EMBED_PACKAGE_PATH` | `null` | Local filesystem path to a `hindsight-embed` checkout. When set, the plugin runs from this path instead of installing via `uvx`. Useful for development. |
| `requestTimeoutSeconds` | `HINDSIGHT_REQUEST_TIMEOUT_SECONDS` | `null` | Overrides the per-call request timeout for recall (default `10s`), retain (default `15s`) and knowledge tool calls (`10–15s`). When unset, the per-call defaults are preserved. Bump this when self-hosted Hindsight legitimately takes longer than 10s under contention (e.g. parallel recalls), to avoid client-side `read operation timed out` errors on requests the server completes successfully. Does not affect the health check, which intentionally stays fast (5s). |
---
### LLM Provider (local daemon only)
These settings configure which LLM the local daemon uses for fact extraction. They are **ignored** when connecting to an external API (the server uses its own LLM configuration).
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `llmProvider` | `HINDSIGHT_LLM_PROVIDER` | auto-detect | Which LLM provider to use. Supported values: `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `ollama-cloud`, `openai-codex`, `claude-code`. When omitted, the plugin auto-detects by checking for API key env vars in order: `OPENAI_API_KEY` → `ANTHROPIC_API_KEY` → `GEMINI_API_KEY` → `GROQ_API_KEY`. |
| `llmModel` | `HINDSIGHT_LLM_MODEL` | provider default | Override the default model for the chosen provider (e.g. `"gpt-4o"`, `"claude-sonnet-4-20250514"`). When omitted, the Hindsight API picks a sensible default for each provider. |
| `llmApiKeyEnv` | — | provider standard | Name of the environment variable that holds the API key. Normally auto-detected (e.g. `OPENAI_API_KEY` for the `openai` provider). Set this only if your key is in a non-standard env var. |
---
### Memory Bank
A **bank** is an isolated memory store — like a separate "brain." These settings control which bank the plugin reads from and writes to.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"claude_code"` | The bank ID to use when `dynamicBankId` is `false`. All sessions share this single bank. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | generic assistant prompt | A short description of the agent's identity and purpose. Sent to Hindsight when creating or updating the bank, and used during recall to contextualize results. |
| `retainMission` | — | extraction prompt | Instructions for the fact extraction LLM — tells it *what* to extract from conversations (e.g. "Extract technical decisions and user preferences"). |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, the plugin derives a unique bank ID from context fields (see `dynamicBankGranularity`), giving each combination its own isolated memory. |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Which context fields to combine when building a dynamic bank ID. Available fields: `agent` (agent name), `project` (working directory), `session` (session ID), `channel` (channel ID), `user` (user ID). |
| `bankIdPrefix` | — | `""` | A string prepended to all bank IDs — both static and dynamic. Useful for namespacing (e.g. `"prod"` or `"staging"`). |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"claude-code"` | Name used for the `agent` field in dynamic bank ID derivation. |
| `resolveWorktrees` | — | `true` | When deriving the `project` field, resolve git worktrees to the **main repository's basename** so that all worktrees of the same repo share one bank. Set to `false` to use the literal working directory basename instead (each worktree gets its own bank). |
| `directoryBankMap` | — | `{}` | Explicit `{ "/path/to/dir": "bank-id" }` mapping. When the current working directory matches an entry, that bank is used directly — overrides both static and dynamic resolution. `bankIdPrefix` still applies on top. |
#### Worktrees and explicit mapping
By default, all git worktrees of the same repository share one bank. For example, if you keep a main checkout at `/home/me/myproject` and a worktree at `/home/me/myproject-wt1`, both resolve to `project = "myproject"` — so dynamic bank IDs like `claude-code::myproject` are stable across all worktrees. This avoids fragmenting memory across short-lived branches.
If you want each worktree to have its own bank, set `"resolveWorktrees": false`.
For full control, use `directoryBankMap` to pin specific directories to specific bank IDs:
```json
{
"directoryBankMap": {
"/home/me/work/client-a": "client-a-memories",
"/home/me/personal/blog": "blog"
}
}
```
When `cwd` matches one of the keys, that bank is used immediately — no static or dynamic resolution runs. Directories not listed fall through to the normal logic.
---
### Auto-Recall
Auto-recall runs on every user prompt. It queries Hindsight for relevant memories and injects them into Claude's context as invisible `additionalContext` (the user doesn't see them in the chat transcript).
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. Set to `false` to disable memory retrieval entirely. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Controls how hard Hindsight searches for memories. `"low"` = fast, fewer strategies; `"mid"` = balanced; `"high"` = thorough, slower. Affects latency directly. |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Maximum number of tokens in the recalled memory block. Lower values reduce context usage but may truncate relevant memories. |
| `recallTypes` | — | `["observation"]` | Which memory types to retrieve. `"world"` = general facts; `"experience"` = personal experiences; `"observation"` = consolidated, deduplicated beliefs built from multiple facts. Defaults to observations so the same answer doesn't surface multiple times when many raw memories say the same thing. |
| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | How many prior conversation turns to include when composing the recall query. `1` = only the latest user message; higher values give more context but may dilute the query. |
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Maximum character length of the query sent to Hindsight. Longer queries are truncated. |
| `recallRoles` | — | `["user", "assistant"]` | Which message roles to include when building the recall query from prior turns. |
| `recallPromptPreamble` | — | built-in string | Text placed above the recalled memories in the injected context block. Customize this to change how Claude interprets the memories. |
---
### Auto-Retain
Auto-retain runs after Claude responds. It extracts the conversation transcript and sends it to Hindsight for long-term storage and fact extraction.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. Set to `false` to disable memory storage entirely. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | Retention strategy. `"full-session"` sends the full conversation transcript (with chunking). |
| `retainEveryNTurns` | — | `10` | How often to retain. `1` = every turn; `10` = every 10th turn. Higher values reduce API calls but delay memory capture. Values > 1 enable **chunked retention** with a sliding window. |
| `retainOverlapTurns` | — | `2` | When chunked retention fires, this many extra turns from the previous chunk are included for continuity. Total window size = `retainEveryNTurns + retainOverlapTurns`. |
| `retainRoles` | — | `["user", "assistant"]` | Which message roles to include in the retained transcript. |
| `retainToolCalls` | — | `true` | Whether to include tool calls (function invocations and results) in the retained transcript. Captures structured actions like file reads, searches, and code edits. |
| `retainTags` | — | `["{session_id}"]` | Tags attached to the retained document. Supports `{session_id}` placeholder which is replaced with the current session ID at runtime. |
| `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the retained document. |
| `retainContext` | — | `"claude-code"` | A label attached to retained memories identifying their source. Useful when multiple integrations write to the same bank. |
---
### Knowledge Tools
The plugin runs an MCP server that exposes `agent_knowledge_*` tools, letting Claude explicitly read, write, and search its memory bank — as opposed to the hook-driven recall/retain which is fully automatic. None of the tools accept a `bank_id` parameter; the server resolves the active bank from the same config as the hooks, so tools and hooks always agree on which bank they touch.
| Tool | Purpose |
|------|---------|
| `agent_knowledge_get_current_bank` | Reports the bank ID the current session is bound to. |
| `agent_knowledge_list_pages` | Lists all knowledge pages (mental models) with IDs and names. |
| `agent_knowledge_get_page` | Reads the full synthesized content of a specific page. |
| `agent_knowledge_create_page` | Creates a new knowledge page with a `source_query` that re-synthesizes content after each consolidation. |
| `agent_knowledge_update_page` | Updates a page's name or `source_query`. |
| `agent_knowledge_delete_page` | Permanently deletes a knowledge page. |
| `agent_knowledge_recall` | Searches across retained conversations and documents for specific facts. |
| `agent_knowledge_ingest` | Uploads raw text content into the bank as a document. |
| `agent_knowledge_ingest_file` | Reads a file from disk and ingests its full content. |
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `enableKnowledgeTools` | `HINDSIGHT_ENABLE_KNOWLEDGE_TOOLS` | `true` | Master switch for the MCP knowledge tools. When `false`, the MCP server stays alive as an empty server exposing no `agent_knowledge_*` tools (it does not exit — exiting would trigger a `-32000` reconnect error in Claude Code). |
---
### Subagents with Memory
The `/hindsight-memory:create-agent` skill creates a Claude Code subagent backed by its own Hindsight bank. Each subagent's prompt instructs it to use the knowledge tools above for recall and ingestion, so Claude treats persistent memory as a first-class capability of that agent.
Two modes:
- **From a directory** — `/hindsight-memory:create-agent <name> from <path>` ingests every text file under `<path>` (`.md`, `.txt`, `.html`, `.json`, `.csv`, `.xml`), creates a starter set of knowledge pages, and writes the subagent file to `~/.claude/agents/<name>.md`. If the directory contains a `bank-template.json` listing exact `mental_models`, those are used verbatim instead of auto-generated.
- **Interactive** — `/hindsight-memory:create-agent` (no arguments) prompts for the agent name and purpose, then writes the subagent file with no ingestion.
For per-subagent memory isolation, enable dynamic bank IDs:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "project"],
"enableKnowledgeTools": true
}
```
With `["agent", "project"]`, each subagent gets a unique bank per repository — `<agent-name>::<project-basename>`. Switch to `["agent"]` for a single bank per subagent across all projects, or `["agent", "session"]` for ephemeral per-session banks.
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. Useful for diagnosing connection issues, recall/retain behavior, and bank ID derivation. |
## Claude Code Channels
With [Claude Code Channels](https://docs.anthropic.com/en/docs/claude-code), Claude Code can operate as a persistent background agent connected to Telegram, Discord, Slack, and other messaging platforms. This plugin gives Channel-based agents the same long-term memory that `hindsight-openclaw` provides for Openclaw agents.
For Channel agents, enable dynamic bank IDs for per-channel/per-user memory isolation:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "channel", "user"]
}
```
And set channel context via environment variables:
```bash
export HINDSIGHT_CHANNEL_ID="telegram-group-12345"
export HINDSIGHT_USER_ID="user-67890"
```
## Troubleshooting
**Plugin not activating**: Check Claude Code logs for `[Hindsight]` messages. Enable `"debug": true` in `~/.hindsight/claude-code.json`.
**Recall returning no memories**: Verify the Hindsight server is reachable (`curl http://localhost:9077/health`). Memories need at least one retain cycle before they're available.
**Daemon not starting**: Ensure an LLM API key is set (or use `HINDSIGHT_LLM_PROVIDER=claude-code`). Review daemon logs at `~/.hindsight/profiles/claude-code.log`.
**High latency on recall**: The recall hook has a 12-second timeout. Use `recallBudget: "low"` or reduce `recallMaxTokens` for faster responses.
**Knowledge tools missing from Claude**: Make sure `enableKnowledgeTools` is `true` in `~/.hindsight/claude-code.json` and restart Claude Code. The first launch after enabling them takes ~20–30 seconds to bootstrap the Python venv at `${CLAUDE_PLUGIN_DATA}/venv`; subsequent launches are instant.
references/sdks/integrations/cline.md
# Cline
> **⚠️ Superseded by the Coding Agents plugin**
>
**The Cline integration** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install cline-cli
```
Memory does not move automatically — the banks are scoped differently and this agent's history cannot be imported (it is kept in an internal database). See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
[View Changelog →](../../changelog/integrations/cline.md)
Persistent memory for [Cline](https://github.com/cline/cline) using [Hindsight](https://vectorize.io/hindsight) — **without MCP**. Cline's [lifecycle hooks](https://docs.cline.bot/customization/hooks) run small scripts that automatically recall relevant context before each task and retain what happened when a task ends. Because it runs on hooks, memory is deterministic — it doesn't depend on the model deciding to call a tool.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting required.
Install the CLI, then run the installer from your project directory with your Hindsight URL and key:
```bash
pip install hindsight-cline
hindsight-cline install \
--api-url https://api.hindsight.vectorize.io --api-token YOUR_KEY
```
Use `hindsight-cline install --global` to install for all projects, or `hindsight-cline uninstall` to remove it.
Then **enable hooks in Cline**: Settings → Features → Hooks.
> **📝 Platform**
>
Cline hooks run on **macOS and Linux only** (no Windows) and require Python 3.
## How It Works
| Cline hook | What Hindsight does |
| ------------------ | --------------------------------------------------------------------- |
| `TaskStart` | Recall context for the new task and inject it. |
| `UserPromptSubmit` | Recall memories for your message; record the prompt for later retain. |
| `TaskComplete` | Retain the task's transcript and summary. |
| `TaskCancel` | Retain the partial transcript of a cancelled task. |
Recalled memories are injected as a `<hindsight_memories>` context block. Cline doesn't hand hooks a transcript, so the integration accumulates each task's prompts locally and retains them at task end. Memories land in a single bank (`cline` by default).
## Configuration
Defaults live in the installed `settings.json`; put personal overrides in `~/.hindsight/cline.json` (stable across reinstalls), or use `HINDSIGHT_*` environment variables. Common keys: `hindsightApiUrl`, `hindsightApiToken`, `bankId`, `autoRecall`, `autoRetain`, `recallBudget`, `dynamicBankId`, `debug`.
## Self-Hosting
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api # http://localhost:8888
```
Point the installer at it: `--api-url http://localhost:8888`.
See the [integration source](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cline) for full details.
references/sdks/integrations/codex.md
# Codex
> **⚠️ Superseded by the Coding Agents plugin**
>
**The Codex CLI integration** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install codex --import-conversations
```
Two things move, and nothing else does.
**Your server moves automatically.** `install` reads `~/.hindsight/codex.json` and keeps the same
`apiUrl` and token — an empty URL still means the local daemon — so you are not silently switched
to Hindsight Cloud. Pass `--server` to change it.
**Your conversations are re-imported from local transcripts** by `--import-conversations`, as new
documents. They are not copied from the old bank: that bank's default was a single static bank
shared by every project, and its documents record only a session id, so working out which ones
belong to this repo means reading the local transcripts anyway.
**Nothing else carries over.** The old `recall*` and `retain*` settings, missions and bank-naming
options describe a pipeline this package replaced. Bank naming changes too — one bank per repo,
shared by every agent. See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
[View Changelog →](../../changelog/integrations/codex.md)
Persistent memory for [Codex CLI](https://github.com/openai/codex) using [Hindsight](https://vectorize.io/hindsight). Three Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn — no changes to your Codex workflow required.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
```bash
curl -fsSL https://hindsight.vectorize.io/get-codex | bash
```
The installer will guide you through choosing Cloud or local-daemon mode and configuring your connection. Once installed, start a new Codex session — memory is live.
To uninstall:
```bash
curl -fsSL https://hindsight.vectorize.io/get-codex | bash -s -- --uninstall
```
## Features
- **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as `additionalContext` (invisible to the transcript, visible to Codex)
- **Auto-retain** — after each Codex response, stores the conversation transcript to Hindsight for future recall
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory
- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content
- **Zero dependencies** — pure Python stdlib, no pip install required
## Architecture
The plugin uses three Codex hook events:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable |
| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` |
| `retain.py` | `Stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) |
On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and outputs a `hookSpecificOutput.additionalContext` block. Codex prepends this to the conversation before sending it to the model:
```
<hindsight_memories>
Relevant memories from past conversations...
Current time - 2026-03-27 09:14
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26)
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26)
</hindsight_memories>
```
On `Stop`, the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously.
## Connection Modes
### 1. External API (recommended)
Connect to a running Hindsight server (cloud or self-hosted):
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
### 2. Local Daemon
Run `hindsight-embed` locally. The `session_start.py` hook will detect it on `apiPort` (default `9077`). The daemon is not auto-started by the Codex plugin — start it separately:
```bash
uvx hindsight-embed
```
Then leave `hindsightApiUrl` empty in your config and the plugin will connect to `http://localhost:9077`.
## Configuration
Settings are loaded from `~/.hindsight/codex.json`. Every setting can also be overridden via environment variable.
**Loading order** (later entries win):
1. Built-in defaults
2. Plugin `settings.json` (at `~/.hindsight/codex/settings.json`)
3. User config (`~/.hindsight/codex.json`)
4. Environment variables
---
### Connection
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Required. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. |
---
### Memory Bank
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"codex"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction — what to extract from coding conversations. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Which fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. |
| `bankIdPrefix` | — | `""` | Prefix prepended to all bank IDs. |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"codex"` | Agent name used in dynamic bank ID derivation. |
---
### Auto-Recall
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. |
| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. |
| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | Prior turns to include when building the recall query. `1` = latest prompt only. |
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max characters in the query sent to Hindsight. |
| `recallRoles` | — | `["user", "assistant"]` | Which roles to include when building a multi-turn query. |
| `recallPromptPreamble` | — | built-in | Text placed above the recalled memories in the injected context block. |
---
### Auto-Retain
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session (upserted by session ID). `"chunked"` sends sliding windows every N turns. |
| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. |
| `retainOverlapTurns` | — | `2` | Extra turns included from the previous chunk (chunked mode only). |
| `retainRoles` | — | `["user", "assistant"]` | Which roles to include in the retained transcript. |
| `retainTags` | — | `["{session_id}"]` | Tags attached to the stored document. `{session_id}` is replaced at runtime. |
| `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the stored document. |
| `retainContext` | — | `"codex"` | Label identifying the source integration. Useful when multiple integrations write to the same bank. |
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. |
## Per-Project Memory
To give each project its own isolated memory bank, enable dynamic bank IDs:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "project"]
}
```
With this config, running Codex in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path.
## Troubleshooting
**Hooks not firing**: Check that `~/.codex/config.toml` contains `codex_hooks = true` under `[features]`. Re-run the installer to fix this automatically.
**No memories recalled**: Recall returns results only after something has been retained. Either complete one Codex session first, or seed your bank manually using the [cookbook example](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/codex-memory).
**Memory not being stored**: `retainEveryNTurns` defaults to `10` — retain only fires every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/codex.json`.
**Debug mode**: Add `"debug": true` to `~/.hindsight/codex.json` to see what Hindsight is doing on each turn:
```
[Hindsight] Recalling from bank 'codex', query length: 42
[Hindsight] Injecting 3 memories
[Hindsight] Retaining to bank 'codex', doc 'sess-abc123', 2 messages, 847 chars
```
**High latency on recall**: Use `"recallBudget": "low"` or reduce `recallMaxTokens` to speed up recall queries.
references/sdks/integrations/coding-agents.md
{/* GENERATED from hindsight-integrations/coding-agents/README.md — edit that file, then run
node hindsight-docs/scripts/sync-coding-agents-doc.mjs */}
import CodingAgentsChart from '@site/src/components/CodingAgentsChart';
Long-term project memory for **coding agents**, backed by [Hindsight](https://vectorize.io/hindsight).
One package, several agents: a shared reflect-and-inject core with a thin entry point per agent
(**Claude Code**, **Codex CLI**, **DeepAgents Dcode**, **opencode**, **opencode 2**, **Kilo CLI**, **Cursor CLI**, **GitHub Copilot CLI**, **Grok Build**, **Qwen Code**, **Kimi Code**, **Factory Droid**, **ZCode**, **TraeCode**, **Antigravity CLI**, **Devin CLI**, **Cline CLI**, **pi**, **Prime Agent**, **DeepSeek Harness**, **WorkBuddy**, **CodeBuddy**). Ingestion is fully
automatic — there is no setup command: a repo's git history and conversations flow into its memory
bank in the background as you work.
The premise: most of a real fix is derivable from the code, but the _last mile_ often hinges on a
project-specific decision that isn't in the code at all — a rounding rule, a retry allowlist, a
tie-break policy. Those decisions live in git history and past conversations. This package puts them
in front of the agent at the moment it starts working, and keeps a curated set of **knowledge pages**
(architecture, conventions, in-flight initiatives) that future sessions start from.
**Figure: Coding Agents.** An animated diagram on the docs site; its narration, step by step:
- **first session**
1. You open a session in a repo. There is no setup command: the plugin’s SessionStart hook does the work.
2. It picks the repo’s bank — one per repository, shared by every agent and every worktree — and finds it empty.
3. Every session start launches the backfill in the background; it only does what is missing, so the session is never blocked. A cold bank also gets a codebase survey by a headless agent (re-run every 20 commits).
4. Its first job configures the bank and creates the repo’s knowledge pages, each a question about this project. They start empty.
5. It reads the agent’s own past conversations in this repo, and the commit messages of the last 300 commits.
6. Each conversation becomes one document (skipped if already there); the commit history becomes one document, replaced when HEAD moves. Full diffs are opt-in (gitIngest: "full").
7. The server extracts facts and labels the durable ones for the page they belong to: a decision, a convention, a component…
8. Consolidation merges them into observations — one set per repo, whichever agent wrote the facts.
9. Each page is written from the memories labelled for it, and keeps itself current on an hourly schedule (each page on its own minute, only when something changed).
- **first prompt**
1. On the first prompt of a session, the prompt hook fetches memory for the task at hand. (A session on an existing bank also got the page roster and a tool guide at start.)
2. By default it runs one bounded reflect over the bank. "pages" searches the knowledge pages and "recall" recalls memories instead — both retrieval-only.
3. The answer lands in the agent’s context before it starts, once per session. On a bank with no history or pages yet, it waits for the second prompt instead.
- **while working**
1. Mid-task the agent pulls memory itself through MCP tools. Pages are not pushed every turn; the page roster and tool guide are re-injected every 10 turns.
2. Page search ranks the pages and returns a snippet of each — fast, and visible as a tool call. hindsight_read_knowledge_page opens one in full.
3. hindsight_reflect goes deeper when pages are not enough; capture_initiative turns a new plan into its own page.
- **each reply**
1. Every time the agent finishes a reply, the Stop hook reads the transcript and takes the turns it has not written yet.
2. They are appended to the session’s document (its first reply created it), tagged with the agent that wrote it — that is where the control plane’s agent logo comes from.
3. The new turns are extracted like everything else…
4. …consolidated into the repo’s beliefs…
5. …and the next scheduled refresh edits the page, so the next session starts from it.
[View Changelog →](../../changelog/integrations/coding-agents.md)
## Does it work?
[AMB's sdebench](https://agentmemorybenchmark.ai/dataset/sdebench) is the measurement: a coding
agent over 61 real repository tasks, three runs each, with and without memory.
Accuracy is not what moves — every run solves 60–61 of the 61 either way. These are tasks a good
agent can already do. What memory changes is how much it costs you to get there: how often you have
to stop and correct the agent, and what the tokens cost.
<CodingAgentsChart title="AMB · sdebench" />
| Agent | Corrections / task | Cost / task |
| ----------- | ------------------ | ------------------- |
| Claude Code | 0.85 → **0.36** | $0.445 → **$0.338** |
| Codex CLI | 1.34 → **0.47** | $0.577 → **$0.276** |
| opencode | 1.20 → **0.80** | $0.634 → **$0.553** |
No memory → with Hindsight, mean of 3 runs. Corrections reproduce the labels AMB prints on its own
chart; cost is derived from the same per-run table.
Both numbers move together, and that is the mechanism rather than a coincidence. A correction is a
round trip: the agent guesses a project-specific decision wrong, you stop it, you explain, it redoes
the work. Every one of those costs a turn of tokens and a turn of your attention. Handing the agent
the decision _before_ it starts — the rounding rule, the retry allowlist, the tie-break policy that
was settled in a commit message or a conversation months ago — removes the guess, and the round trip
with it.
## Install
```bash
npx @vectorize-io/hindsight-coding-agents install all # every detected agent, wired natively
npx @vectorize-io/hindsight-coding-agents install claude-code # or just one
npx @vectorize-io/hindsight-coding-agents uninstall all # removes exactly what install added
npx @vectorize-io/hindsight-coding-agents update # refresh the runtime only, no rewiring
npx @vectorize-io/hindsight-coding-agents stats # how often each agent uses Hindsight
```
`install` takes an explicit target — `all`, or one or more harness names. A bare
`npx @vectorize-io/hindsight-coding-agents install` changes nothing and prints the choice, so wiring every agent on
the machine is never something that happens by accident. **Updating is the same `install`
command again** — it re-copies the runtime in place.
Day to day you should not have to: once a day, a session start checks npm and re-stages a newer
runtime in the background (`autoUpdate`, on by default — set it to `false` to pin the version you
have). That is the `update` command above, which refreshes the copy every wired agent already
points at and deliberately touches no host config; re-run `install` yourself after a release that
adds a new hook, or to wire another agent.
On a terminal it also asks **where memory should live** — Hindsight Cloud, a server you run, or a
local daemon on this machine (see Where memory lives). Scripted installs pass
`--server cloud|self-hosted|daemon` instead; it is asked only once, and never again on re-install.
### Per agent
Same command, only the harness name changes. Run after installing the package globally.
#### Claude Code
```bash
npx @vectorize-io/hindsight-coding-agents install claude-code
```
3 hooks in `~/.claude/settings.json`, MCP via `claude mcp add` (user scope), and the companion skill.
#### Codex CLI
```bash
npx @vectorize-io/hindsight-coding-agents install codex
```
3 hooks in `~/.codex/hooks.json` plus `[mcp_servers]` in `config.toml` (needs `codex_hooks = true`).
#### DeepAgents Dcode
```bash
npx @vectorize-io/hindsight-coding-agents install dcode
```
The installer registers this package as a local marketplace with Dcode, then invokes Dcode's own
`plugin install` command. The package is a native Agent Plugin: its root `plugin.json` contributes the shared skill, the
Hooks V2 `SessionStart`, `UserPromptSubmit`, and `Stop` lifecycle, and the namespaced
`hindsight_*` MCP server. Enable the plugin through Dcode's normal plugin manager; no Dcode config
patcher or compatibility bridge is required.
Dcode namespaces a plugin's MCP tools, so they appear as
`plugin__hindsight-coding-…__hindsight_…` rather than under their bare names — the agent resolves
them from the tool guide either way. In headless runs (`dcode -n`) Dcode allows the read-only
Hindsight tools and gates the two that write (`hindsight_ingest_document`,
`hindsight_capture_initiative`) behind an approval it has no UI for; use the interactive TUI to
capture an initiative or ingest a document. For the same reason the one-time codebase survey runs
under another installed agent's CLI when there is one, exactly as it does for Cursor, Copilot,
Devin, Grok Build, Cline, Kilo and Prime Agent.
#### opencode
```bash
npx @vectorize-io/hindsight-coding-agents install opencode
```
A plugin entry in `~/.config/opencode/opencode.json` — native tools, no MCP needed.
#### opencode 2
```bash
npx @vectorize-io/hindsight-coding-agents install opencode2
```
opencode v2 (`npm @opencode/cli`) exposes an `opencode2` binary alias **alongside** `opencode` and
rewrote the plugin API, so it is a harness of its own. It writes the same plugin entry to the same
`~/.config/opencode/opencode.json` — the two CLIs share that file, and v1 rejects the whole config
if it sees v2's `plugins` key — and each CLI then loads its own entry point from the one registered
path. So installing either harness wires both, and uninstalling either removes the shared entry.
Two differences from v1, both because of the host: the one-time codebase survey runs under another
installed agent's CLI (v2 plugins cannot define the read-only agent the survey needs), and the seed
banner is written to the plugin log instead of a TUI toast (v2 plugins cannot raise one). Recall,
injection, the native `hindsight_*` tools and session write-back are identical. The companion skill
is identical too, but it is registered in memory through v2's skill API rather than copied into a
skills directory — opencode2 has none of its own, and the two it reads belong to other agents.
#### Kilo CLI
```bash
npx @vectorize-io/hindsight-coding-agents install kilo
```
A plugin entry in `~/.config/kilo/kilo.json[c]`.
#### Cursor CLI
```bash
npx @vectorize-io/hindsight-coding-agents install cursor-cli
```
Hooks in `~/.cursor/hooks.json`, `~/.cursor/mcp.json`, and the companion skill.
#### GitHub Copilot CLI
```bash
npx @vectorize-io/hindsight-coding-agents install copilot-cli
```
`~/.copilot/hooks/`, `mcp-config.json`, and the companion skill.
#### Grok Build
```bash
npx @vectorize-io/hindsight-coding-agents install grok-build
```
Native hooks in `~/.grok/hooks/hindsight.json`, MCP in `~/.grok/config.toml`, plus the companion
skill. Grok also runs Claude Code's hooks from `~/.claude/settings.json`; those stay silent inside
Grok, so a machine wired for both hosts records each Grok session once. Grok passes hook context to
the model only from tool hooks, so the session and prompt context arrive with the result of the
agent's first tool call after each prompt (a `PostToolUse` hook).
#### Qwen Code
```bash
npx @vectorize-io/hindsight-coding-agents install qwen-code
```
Native hooks in `~/.qwen/settings.json`, plus MCP and the companion skill.
> Qwen's hook `timeout` is in **milliseconds** (its own docs: "Timeout in milliseconds, default
> 60000"), unlike every other supported agent, so the installed values are `30000/30000/60000`.
> Recall fires on genuine submissions only — `UserPromptSubmit` also fires on tool-result
> continuations, so interactive sessions recall once per prompt while headless (`qwen -p`),
> `serve`, SDK and ACP sessions seed and retain but do not recall.
#### Kimi Code
```bash
npx @vectorize-io/hindsight-coding-agents install kimi-code
```
Native hooks in `~/.kimi-code/config.toml`, plus MCP in `~/.kimi-code/mcp.json` and the companion skill.
(Hooks and MCP go under `$KIMI_CODE_HOME` instead when it is set.)
> Kimi's SessionStart hook cannot reach the model, so the session briefing is silent there;
> recalled memory arrives on the first prompt instead, as a `<hook_result>` block. Sessions are
> retained from every agent's `wire.jsonl` under the session directory, subagents included.
#### Factory Droid
```bash
npx @vectorize-io/hindsight-coding-agents install factory-droid
```
4 hook registrations in `~/.factory/hooks.json` (user scope), including a cancellation-safe
`Notification` write-back, plus a stdio MCP registration under
`mcpServers.hindsight` in `~/.factory/mcp.json`, and the companion skill in `~/.factory/skills`.
The installer touches only JSON files - no Droid CLI round-trip - and refuses to overwrite a
user-managed MCP server already named `hindsight`. Droid's hook protocol matches Claude Code's
(`session_id`/`transcript_path`/`cwd` in, `hookSpecificOutput.additionalContext` out). Recall and
injection use the same protocol; write-back also handles Droid's cancellation notification because
Droid does not emit `Stop` after a cancelled turn.
#### ZCode
```bash
npx @vectorize-io/hindsight-coding-agents install zcode
```
Three hook registrations plus a stdio MCP server under `mcp.servers.hindsight`, both in ZCode's own
CLI config `~/.zcode/cli/config.json` - never your real Claude Code settings, even though ZCode
embeds the Claude Code agent runtime and speaks its hook protocol. The companion skill goes to
`~/.zcode/skills`. Config hooks ship **disabled**, so the installer also sets `hooks.enabled` to
`true`; `uninstall` removes the whole block again when nothing else is registered there, and
refuses to touch an MCP server named `hindsight` that it did not write.
> ZCode's hook `timeoutMs` is in **milliseconds** (installed values `30000/30000/60000`), and
> `hooks.maxOutputBytes` caps what a hook may print - anything larger is dropped, injection and
> all. The installer seeds it at `32768` only when your config does not already set one.
>
> ZCode keeps no durable session transcript: `Stop` carries the reply plus a temp, assistant-only
> file it deletes as soon as the hook returns, and no user prompt at all. So this is the one agent
> whose conversation the plugin journals itself - the prompt hook records what you asked, the
> `Stop` hook records the reply - and the write-back then behaves like every other agent's,
> appending each new turn to the same session document. `--import-conversations` is therefore not
> available for ZCode: there is no past history on disk to backfill from.
#### TraeCode
```bash
npx @vectorize-io/hindsight-coding-agents install traecode
```
Three hook registrations in TraeCode's user-level `hooks.json`, the companion skill under the
same dot-dir's `skills/`, and two `filesystem.readWrite` rules in `sandbox.json` — one for
`~/.hindsight`, without which the hooks fail silently (exit 0, no effect) because the sandbox
blocks those writes, and one for Trae's per-window storage DBs
(`<userData>/User/workspaceStorage`), which the SessionStart hook seeds with the workspace's
MCP enable switch. Network is allowed by default. TRAE ships two editions with different brand
roots — the CN build uses `~/.trae-cn` and "Trae CN", the international build `~/.trae` and
"Trae" — so every path resolves by probing for the edition dir that exists and defaulting to the
CN names. Plain JSON files, no CLI round-trip. TraeCode speaks Claude Code's hook protocol, so
recall and injection work exactly as they do there; the event map lives under the top-level
`hooks` key and the `version` field the host writes is preserved.
MCP is registered per repo, never at the user level: Trae launches user-level servers with the
Electron process's cwd (your home directory), where a hindsight entry either self-disables under
`optInOnly` (zero tools) or resolves the home bank instead of the repo's. So the installer
pre-seeds the per-repo registration for every repo opted in via `mapPathToBank` — a
`mcpServers.hindsight` stdio server in `<repo>/.trae/mcp.json`, pinned to that repo via
`HINDSIGHT_MCP_PROJECT_CWD` — migrates any stale user-level entry back out of
`<userData>/User/mcp.json`, and seeds the per-workspace enable switch directly. The SessionStart
hook re-checks both on every session and writes them only when missing or stale (idempotent
fallback) — Trae's hook sandbox cannot be relied on to perform those writes itself. The per-repo
file is merged not clobbered, and gitignored when the repo has a `.gitignore` (the path is
machine-specific). A foreign `hindsight` entry — in either file — is the user's own server and
is never touched.
Trae only reads that per-repo file once the global `trae.mcp.enableWorkspaceMcp` setting is on
(default off), so the installer handles the gate: an interactive run asks, a non-interactive run
takes `--enable-workspace-mcp`, and either way the setting is written and Trae windows must be
restarted to pick it up (declining, or a settings file with comments, prints the manual step).
Registrations and enable switches take effect on the NEXT window, so the MCP tools appear when
you reload after the first session; repos opted into `mapPathToBank` after install need one more
`install traecode` run, and if a repo's server still shows disabled in the MCP panel, flip it on
once there.
> One manual step remains after install (UI state the installer cannot write): enable the hooks
> under TraeCode Settings > Hooks. If you declined the workspace-MCP prompt, enable it later in
> Trae settings (search "enableWorkspaceMcp").
> TraeCode keeps no readable session transcript - sessions live in an encrypted local DB or the
> cloud. Like ZCode, its conversation is journaled by the plugin itself: the prompt hook records
> what you asked, and the `Stop` hook closes the turn with the reply it carries in
> `last_assistant_message`. `--import-conversations` is therefore not available for TraeCode:
> there is no past history on disk to backfill from.
#### Antigravity CLI
```bash
npx @vectorize-io/hindsight-coding-agents install agy
```
Lifecycle hooks, MCP, and the `Hindsight · <bank>` status line.
#### Devin CLI
```bash
npx @vectorize-io/hindsight-coding-agents install devin-cli
```
Hooks in `~/.config/devin/config.json` plus MCP. Needs Node 22.5+ — see below.
#### Cline CLI
```bash
npx @vectorize-io/hindsight-coding-agents install cline-cli
```
A native plugin via `cline plugin install`, plus MCP and the companion skill.
#### pi
```bash
npx @vectorize-io/hindsight-coding-agents install pi
```
An extension entry in `~/.pi/agent/settings.json`, plus the companion skill in
`~/.pi/agent/skills` — native tools, no MCP needed.
This command is the only supported route, for pi and for Prime Agent below. Installing us as a pi
package (`pi install npm:@vectorize-io/hindsight-coding-agents`) is deliberately not wired: both
hosts read the same `pi` key of a package's `package.json`, and that key can only name one entry —
whichever host it did not name would load the other's bundle and report itself as the wrong agent,
taking that harness's config section and stamping every document it retains with it. So the package
carries no `pi` key at all, and each host is pointed at its own bundle by the install command above.
#### Prime Agent
```bash
npx @vectorize-io/hindsight-coding-agents install prime-agent
```
Prime Agent is a fork of pi, so it is wired the same way: an extension entry, here in
`~/.prime/agent/settings.json`, plus the companion skill in `~/.prime/agent/skills` — native tools,
no MCP needed. Installing both is fine and expected:
each host loads its own entry from its own settings file, and like every other pair of agents they
**share one bank per repo** (the default `coding-agent::{gitProject}`), so what you tell pi is there
when you open Prime Agent. Separate entries are what keeps each side attributable — its own
`harnesses.<name>` config section, and its own agent stamped on every document it retains.
#### DeepSeek Harness
```bash
npx @vectorize-io/hindsight-coding-agents install dsh
```
A Cordis plugin row in `$DSH_HOME/cordis.patch.yml` (`~/.dsh` by default), which every dsh profile
composes — native tools, no MCP needed. Two dsh-specific notes: one dsh process serves **several
repositories** (its Web UI opens each session in whatever directory you pick), so the bank is
resolved per session workspace rather than once per process; and dsh has no plugin-facing notice
channel, so the seed line goes to the plugin log rather than the UI. Everything model-facing —
recalled memory, the knowledge preamble, the `hindsight_*` tools — is unaffected. If you prefer the
published-package route, `dsh plugin --profile web add @vectorize-io/hindsight-coding-agents` works
too: the package ships the profile patch layer, so nothing else needs editing. Either route gets the
companion skill — a plugin wired by the host's own plugin manager installs it itself on the first
session, since that route never runs our installer.
#### WorkBuddy
```bash
npx @vectorize-io/hindsight-coding-agents install workbuddy
```
3 hooks in `~/.workbuddy/settings.json`, the stdio MCP server in `~/.workbuddy/mcp.json`, and the
companion skill in `~/.workbuddy/skills`. WorkBuddy (Tencent's AI workbench) is built on the shared
`@genie/agent-cli` engine, so its hooks follow Claude Code's protocol — the main difference is the
transcript schema (`type:"message"` records carrying top-level `role`/`content`), which the
package's own reader normalizes.
#### CodeBuddy
```bash
npx @vectorize-io/hindsight-coding-agents install codebuddy
```
3 hooks in `~/.codebuddy/settings.json`, the stdio MCP server in `~/.codebuddy/.mcp.json` — the
recommended file: the installer follows CodeBuddy's documented fallback chain, merging into an
existing deprecated `~/.codebuddy/mcp.json` or legacy `~/.codebuddy.json` rather than shadowing it —
and the companion skill in `~/.codebuddy/skills`. CodeBuddy Code runs the same `@genie/agent-cli`
engine as WorkBuddy — WorkBuddy merely ships it with a different home folder — so it reuses
WorkBuddy's hook runtime, and differs only in the root it writes to.
**Both CodeBuddy hosts are covered, and they do not store sessions alike.** CodeBuddy Code (the CLI)
writes the WorkBuddy JSONL that the shared reader parses. The IDE hands the Stop hook its own
per-conversation directory instead — `CodeBuddyIDE/<uid>/history/<md5(workspace)>/<conversationId>/`,
whose `index.json` _is_ the `transcript_path` and whose prose sits one level down, in
`messages/<messageId>.json` — so the reader dispatches on the shape it was handed
(core/transcript-codebuddy-ide.ts).
Uninstall the same way: `npx @vectorize-io/hindsight-coding-agents uninstall claude-code` (or `uninstall all`).
**Devin CLI needs Node 22.5 or newer.** Its hooks pass only a session id — the conversation itself
lives in `~/.local/share/devin/cli/sessions.db` — so reading it depends on Node's built-in
`node:sqlite`. Installing `devin-cli` checks for this first and refuses (with the reason) rather
than wiring hooks that could never retain anything. Every other agent works on any supported Node.
`install` copies what it needs into `~/.hindsight/coding-agents` and points each agent's wiring
there, so nothing depends on where you ran it from. **Updating** is the same command again — it
re-copies the runtime in place, leaving the wiring valid and every new session on the new version.
`install` merges the native wiring (hooks + MCP registration where the host wants them) into each
agent's own config, preserving everything already there; it is idempotent (re-run after moving the
package) and backs up any pre-existing file it touches as `<file>.hindsight-backup`. `uninstall`
removes only our entries. On Claude Code the install also ships a **companion skill**
(`~/.claude/skills/hindsight-coding-agent`) that teaches the agent how this memory works — what
"store this in hindsight" should do, the tool surface, per-repo configuration, debugging — so users
can ask the agent itself. Manual wiring per harness, if you prefer:
**opencode** and **opencode 2** install directly — point `opencode.json` at the package dir:
```json
{ "plugin": ["/path/to/hindsight-coding-agents"] }
```
One entry, both CLIs: v1 resolves that directory through `package.json` `main`, v2 through its
`index.js`, so each loads its own plugin.
**Claude Code** and **Codex** get their full three-hook + MCP wiring from this package's own
installer — `npx @vectorize-io/hindsight-coding-agents install claude-code` / `install codex`. This package's `bin`
entries (`hindsight-claude-hook`, `hindsight-codex-hook`,
`hindsight-cursor-hook`) are the individual injection-only `UserPromptSubmit` entrypoints for a
minimal, hand-wired setup.
Adding an agent: hook-based → write a `HookSpec` entry point (see `src/cursor-hook.ts`) and register
a `hookAdapter` in `src/harness/registry.ts`; persistent-plugin → implement `HarnessAdapter`
(`src/core/types.ts`) fully (see `src/harness/opencode.ts`), or bind the host's own plugin API to
`RuntimeCore` directly when it is not an opencode fork (see `src/cline.ts`, `src/dsh.ts`).
## Migrating from the per-agent plugins
The older per-agent integrations (`hindsight-claude-code`, `hindsight-cursor-cli`, `hindsight-codex`, …) are superseded by this package. Two things move; nothing else does.
**Your server moves automatically.** If `~/.hindsight/claude-code.json` or `~/.hindsight/codex.json`
exists, `install` adopts its endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` →
`apiToken`, and an empty URL means the local daemon, as it did there. The agent you are installing
is checked first, so wiring Codex takes Codex's server even if an old `claude-code.json` is still
lying around. You already chose where your memory lives; defaulting to Cloud instead would quietly
send your prompts somewhere else. Pass `--server` to override. (Those two are the only old plugins
that shipped a user config — Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry.)
**Your conversations are re-imported from local disk**, as new documents:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install claude-code --import-conversations # or: install codex --import-conversations
```
This re-extracts the transcripts the agent already wrote, so it costs tokens roughly in proportion
to the history imported, and it is safe to re-run (ingestion dedups by document id).
Local transcripts are the source rather than the old bank, because the old bank cannot be split by
repo. Its default was a **single static bank** — `dynamicBankId` defaults to false, so everything
landed in one bank named `claude_code` — and its documents record only `retained_at`,
`message_count` and `session_id`, nothing identifying the project. Working out which documents
belong to which repo means joining `session_id` back to the `cwd` in the local transcript, so the
transcripts are needed either way; going through them directly is simply the shorter path.
**How sessions are matched.** A conversation is imported only when the session itself records the
directory it ran in — never inferred from a file or folder name. Claude Code writes that directory
on its entries, Codex in its `session_meta` header and DeepSeek Harness in its session-log header,
and pi and Prime Agent in their session header, so all five can be attributed exactly, including
sessions started in a subdirectory of the repo. Guessing was tempting (Claude names its history
folders after the project path) but unsafe: `/` and `.` both encode to `-`, so `repo-sub` is either
the subdirectory `repo/sub` or an unrelated sibling repo — and a wrong guess files someone else's
conversation into your bank. Sessions that record nothing are skipped and the count is reported.
DeepSeek Harness logs are Zstandard-framed JSONL under `$DSH_HOME/sessions`, which needs Node 22.15+
to read; an older Node skips the import with that reason rather than silently importing nothing.
Dcode's transcripts record no directory at all — the working directory lives only in its LangGraph
checkpoint database — so the repo comes from `dcode threads list --json`, a declared, versioned
command contract rather than that internal schema; with the `dcode` CLI unavailable the import is
skipped with that reason. The other harnesses (opencode, opencode 2, Kilo, Cursor, Cline, Copilot, Devin) keep
history in internal SQLite databases with unversioned schemas and are skipped with a reason.
**Nothing else is translated.** The old plugin's behavioural settings — 12 `recall*`, 7 `retain*`,
`bankMission`/`retainMission`, `dynamicBankGranularity` — describe a pipeline this package replaced,
and reinterpreting them would be guesswork. Bank naming changes too: this package uses one bank per
**repo** (`coding-agent::{gitProject}`) shared by every agent. To keep the old naming instead:
```jsonc
{ "bankIdTemplate": "{harness}::{gitProject}" } // reproduces the old per-agent naming
```
## Where memory lives
Three modes, chosen once when you install (`install` asks on a terminal; pass `--server` to script it):
| mode | what runs | needs |
| ------------- | ----------------------------------------- | ---------------------------------------- |
| `cloud` | Hindsight Cloud (default) | an API token |
| `self-hosted` | a Hindsight server you already run | its URL |
| `daemon` | a local `hindsight-embed` on this machine | `uv` on PATH + an LLM key for extraction |
```bash
npx @vectorize-io/hindsight-coding-agents install claude-code --server daemon
npx @vectorize-io/hindsight-coding-agents install claude-code --server self-hosted --api-url http://localhost:8888
npx @vectorize-io/hindsight-coding-agents install claude-code --server cloud --api-token <token>
```
Re-running `install` never re-asks: a config that already names a server is left alone.
### Local daemon mode
Nothing to sign up for and nothing to host — memory runs on your machine. The plugin starts
`hindsight-embed` on demand at `127.0.0.1:9077` and points every agent at it.
- **A server already on the port is adopted, never restarted** — so one daemon serves every agent
and every repo, and your own `hindsight-embed` is reused if you already run one.
- **Cold starts happen in the background.** The first start downloads the daemon and loads models,
which takes longer than any hook is allowed to run, so it is launched detached at session start.
A session that begins before it is ready simply has no memory for a turn or two — a daemon that
isn't up is treated as an unreachable server, exactly like a Cloud or self-hosted outage, with the
same error handling and the same diagnostics. Nothing downstream of the URL knows which mode it is.
- **It keeps running** until you stop it. There is deliberately no stop-on-exit: one daemon is
shared, so ending one session must not cut memory out from under another agent still working.
- **macOS additionally needs a current Rust toolchain.** `litellm` (a transitive dependency of the
API) publishes wheels only for Linux and Windows, so a Mac compiles it from source through
maturin and its crates pin a recent `rustc`. Install from [rustup.rs](https://rustup.rs) and keep
it updated — an out-of-date toolchain fails as surely as a missing one. Linux and Windows install
from wheels and need none of this.
- **Fact extraction runs locally**, so it needs an LLM. `HINDSIGHT_API_LLM_PROVIDER` wins if set;
otherwise the first of `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`;
otherwise the Claude Code CLI, which needs no key. `install` tells you which it found.
Daemon settings keep the names the old per-agent Claude Code plugin used, so an existing
environment carries over unchanged:
| field | env | default | meaning |
| ------------------- | ------------------------------- | -------------- | ---------------------------------------------------------- |
| `serverMode` | `HINDSIGHT_SERVER_MODE` | `cloud` | `cloud` \| `self-hosted` \| `daemon` |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | port the local daemon listens on |
| `daemonIdleTimeout` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | — | deprecated, ignored: the daemon no longer exits on its own |
| `daemonProfile` | `HINDSIGHT_DAEMON_PROFILE` | `coding-agent` | which local database it uses |
| `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `latest` | which `hindsight-embed` release to run |
| `embedPackagePath` | `HINDSIGHT_EMBED_PACKAGE_PATH` | — | run a local checkout instead (development) |
Any `HINDSIGHT_API_*` variable you export is forwarded to the daemon, so server-side settings need
no equivalent here.
## Configuration
Configuration is **one JSON file**: `~/.hindsight/coding-agent.json`. Layering, later wins per field:
1. built-in defaults
2. environment variables — `HINDSIGHT_API_URL`, `HINDSIGHT_API_TOKEN`, and one per scalar setting
(`HINDSIGHT_<FIELD_IN_CAPS>`), for containers and CI that inject config rather than write a file
3. the file's top level
4. its `harnesses.<name>` section — per-agent override
5. its `paths.<prefix>` section — per-directory override, applied after the bank is resolved
(see Per-directory overrides)
6. its `banks.<resolvedBankId>` section — per-repo override, applied after the bank is resolved
(see Per-repo opt-in/out)
Environment variables are a **fallback**: the file wins wherever it sets a value, so adding env to
an existing setup changes nothing. The two list-valued settings, `retainTags` and `optInPaths`, take
a comma-separated value (`HINDSIGHT_RETAIN_TAGS="project:{gitProject},env:work"`); entries are
trimmed and blanks dropped.
The map-valued settings (`mapPathToBank`, `harnesses`, `paths`, `banks`, `retainMetadata`) are file-only —
per-key branching doesn't survive flattening into one variable. `maxParallelRetains` is available
as `HINDSIGHT_MAX_PARALLEL_RETAINS` for containers and CI.
`HINDSIGHT_CONFIG` moves the file itself — point it at another path for a container or a test
harness where `$HOME` is not the right anchor. It is still exactly one file; only its location
changes. (The other variables that are not settings are `HINDSIGHT_LOG_FILE`, `HINDSIGHT_DIAG_FILE`,
`HINDSIGHT_USAGE_FILE` and `HINDSIGHT_LOG_LEVEL` — see Diagnostics & logging.)
### When a change takes effect
Config is read when a process starts — the file is not watched — so when an edit applies depends on
what reads it:
| host | reads the file | an edit applies |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------- |
| hook harnesses (Claude Code, Codex CLI, Cursor CLI, GitHub Copilot CLI, Grok Build, Antigravity CLI, Devin) | once per hook invocation — each hook is its own short-lived process | on your next prompt |
| persistent plugins (opencode, opencode 2, Kilo CLI, Cline CLI, pi, Prime Agent, DeepSeek Harness) | once per workspace, when the host loads the plugin | after restarting the agent |
| the MCP server behind the `hindsight_*` tools | once at startup | in your next session |
`apiToken` is the exception. Every host re-reads it when the server rejects a request, so enabling
authentication or rotating the key is picked up on the next call with nothing to restart —
otherwise a rotation would leave a long-running agent failing every memory call until it was
restarted. Everything else follows the table: `apiUrl`, `disabled`, bank routing, `gitIngest`, and
the survey and knowledge-page settings.
`hindsight_diagnose` reports both sides of that gap — what the file says now, and what the running
client is actually using.
### Opt-in only
By default every project gets memory — that is what makes the plugin zero-setup. If you would
rather nothing be remembered until you say so, turn memory off everywhere and name the projects
that may use it:
```jsonc
{
"optInOnly": true,
"optInPaths": ["~/work/client-x", "~/oss"],
}
```
Anything outside those paths is **inert**: no bank is created, nothing is retained, no seed runs,
and the agent behaves exactly as it would without the plugin. Approving costs nothing else —
`optInPaths` says _which projects_, not _which bank_, so an approved repo keeps its usual
`coding-agent::{gitProject}` name. Paths are prefixes, so approving `~/work` approves every repo
under it while each still gets its own bank.
A `mapPathToBank` entry counts as opted in too, since routing a path to a named bank already
declares that project. A bare `bankId` does not: it names a bank rather than a project, so it
cannot say which work may be remembered, and a privacy switch has to fail closed.
There is no per-repo opt-in file, for the same reason there is no repo-carried config at all: a
cloned repository must not be able to turn memory on.
There is deliberately no repo-carried config file — per-repo bank routing is `mapPathToBank`,
per-agent differences are `harnesses.<name>`.
Each entry point knows which harness it _is_ (the opencode plugin is loaded by opencode, the codex
hook by Codex...), so one shared config serves several agents side by side:
```jsonc
{
"apiUrl": "https://api.hindsight.vectorize.io",
"harnesses": {
"opencode": { "reflectTimeoutMs": 60000 },
"claude-code": { "disabled": true }, // e.g. memory off for Claude only
},
}
```
### Reference
| field | default | meaning |
| ----------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiUrl` | `https://api.hindsight.vectorize.io` | Hindsight API base URL (set to `http://localhost:8888` for a local server) |
| `apiToken` | — | bearer token (Hindsight Cloud). Picked up without restarting the agent: a long-lived host re-reads it after a rejected request, so enabling auth or rotating the key mid-session recovers on the next call |
| `bankId` | — | **explicit static bank**; unset ⇒ per-repo dynamic resolution (below) |
| `dynamicBankId` | dynamic iff no `bankId` | force dynamic (`true`) or static (`false`) resolution |
| `bankIdTemplate` | `"coding-agent::{gitProject}"` | dynamic bank id format; the default makes every agent share one bank per repo |
| `mapPathToBank` | — | absolute path → bank; **longest prefix wins**; linked worktrees inherit their main checkout's mapping; overrides everything |
| `optInOnly` | `false` | run memory ONLY in opted-in projects — everything else is inert, with no bank created; see Opt-in only |
| `optInPaths` | — | directories opted in, matched as prefixes with `~` expanded; each repo beneath and its linked worktrees are approved while keeping their own dynamic bank |
| `resolveWorktrees` | `true` | linked worktrees inherit the main checkout's bank identity, path approval, and mapping |
| `retainTags` | — | extra tags on every document written by the integration, e.g. `["project:{gitProject}"]` — see **Recording where a memory came from** below |
| `retainMetadata` | — | extra metadata on every document written by the integration, e.g. `{"repo": "{gitProject}"}` |
| `retainContext` | see description | the `context` sent with every session write-back. Extraction reads it to decide whose claim a sentence is, so the default names both speakers, and asks for an agent's own "fixed" or "passes" to be stored as its claim rather than as fact. Accepts the same `{placeholder}` templates as `retainTags` |
| `manageBankConfig` | `true` | let the plugin shape the bank's own configuration — the retain strategies it writes under, the `knowledge` entity-label group, and, on a bank that has none, the missions. Writing is **additive**: it adds what the bank does not define and never overwrites what is there, so your control-plane edits survive — the one exception is the extraction mode of its own strategies, which follows `retainExtractionMode`. Set `false` to keep it out of the bank config entirely — see **A bank you shape yourself** below |
| `retainExtractionMode` | `"concise"` | how the server extracts memories from sessions, commits and documents: `"concise"`, `"verbose"`, `"verbatim"` or `"chunks"` (store the text, no extraction). Every Stop writes the session back, so this is what each turn costs — `"verbose"` pulls more detail for several times the tokens. Kept in sync on the plugin's own retain strategies every session, so a change reaches existing banks too (not with `manageBankConfig: false`) |
| `defaultBankConfig` | — | bank-config fields of your own for the banks the plugin shapes, e.g. `{"enable_observations": false, "enable_auto_consolidation": false, "mental_model_min_refresh_interval_seconds": 21600}`. Keys are the bank-config API's own field names. Written under the same additive rule as the rest: only where the bank does not define the field, so a bank the plugin creates is born with these instead of the server's defaults and a value you set in the control plane is never overwritten. Wins over the template on a key both name (`enable_observations`); `retain_strategies`, `entity_labels` and `retain_extraction_mode` are refused, since the plugin governs them itself. Ignored with `manageBankConfig: false` — see **A bank you shape yourself** below. File-only, like `recallOptions`; in a `banks.<id>` section it replaces the global map rather than merging into it |
| `observationScopes` | `"shared"` | how consolidation groups observations: `"shared"` (default) = ONE global scope per bank, so every agent on a repo builds one set of beliefs; also `"combined"` (the server default), `"per_tag"`, `"all_combinations"`, `[["t"]]`; `"per_source"` adds a scope per `source:` kind alongside the global one, so commit knowledge and conversation knowledge consolidate apart |
| `disabled` | `false` | hard off-switch (inert plugin/hook — a no-memory baseline) |
| `reflectTimeoutMs` | `20000` | **automatic** session-reflect timeout; on hook harnesses the installer registers a 30s prompt-hook timeout, so going above ~20s also means raising that hook's `timeout` in the host's config, or the host kills the hook mid-reflect; on timeout or a 5xx the hook falls back to knowledge-page search, then to a raw recall of observations (recorded) |
| `reflectToolTimeoutMs` | `330000` | timeout for the agent-invoked `hindsight_reflect` tool — a call the agent waits on, whose high-budget synthesis on a populated bank runs for minutes. Defaults above the server's own reflect wall timeout (`HINDSIGHT_API_REFLECT_WALL_TIMEOUT`, 300s) so the server decides when to give up. Unset, it inherits an explicitly raised `reflectTimeoutMs`, but a short one never lowers it |
| `injectTimeoutMs` | `7000` | retrieval timeout in milliseconds for `autoInject: "pages"` and `autoInject: "recall"`; also the **shared** budget for page search then recall after a reflect timeout/5xx (recall gets only the time left after page search). Set e.g. `2000` to limit turn latency or `15000` for a slower self-hosted bank. Environment fallback: `HINDSIGHT_INJECT_TIMEOUT_MS`. On hook harnesses, injection must fit the host's prompt-hook timeout (30s by default); for reflect with fallback, allow for **`reflectTimeoutMs` + `injectTimeoutMs`**, and raise the host hook's `timeout` when needed |
| `reflectBudget` | `"high"` | reflect budget for the `hindsight_reflect` tool: `"low"`, `"mid"` or `"high"`. Drop it on a large bank where high-budget synthesis exceeds the server's wall timeout. The automatic session-start reflect always uses `"low"` to fit its hook window and is unaffected |
| `autoInject` | `"reflect"` | what to inject once, on the session's **first prompt**: `"reflect"` = a low-budget reflect synthesis (on timeout/5xx it falls back to page search, then recall); `"pages"` = the knowledge pages matching the prompt by search; `"recall"` = the bank's memories recalled for the prompt (what it asks for is `recallOptions`, observations by default); `"none"` = nothing — the agent searches knowledge pages first and reflects only when they are too shallow. `"pages"` and `"recall"` are retrieval only (no LLM), their time budget is `injectTimeoutMs`, and when one finds nothing for the prompt it tries again on the next one, up to 5 prompts |
| `autoReflect` | `true` | **deprecated** — use `autoInject`. Still honoured (`false` = `autoInject: "none"`), ignored when `autoInject` is set, and logs a deprecation warning |
| `pageSearchLimit` | `3` | knowledge pages ONE search returns. Applies to every search of the bank — the `autoInject: "pages"` injection, the reflect fallback, and the agent's own `hindsight_search_knowledge_pages` tool — because the limit lives on the client they all share |
| `recallOptions` | see description | overrides merged key-by-key into the body of every recall — the `autoInject: "recall"` source and the reflect fallback. Keys are the API's own recall parameters, passed straight through, so anything recall accepts is settable without a new option here; `query` is the one field it cannot replace. The default body is `{"types": ["observation"], "budget": "low", "max_tokens": 2000, "include": {"entities": null}}`, and what you set is merged over it one key at a time — `{"max_tokens": 4000}` changes the budget and leaves the rest alone. Observations are the consolidated layer, so they answer best per token, but a bank with **consolidation disabled never grows any** and the default recall comes back empty on it: set `{"types": ["world", "experience"]}` there, or `{"types": null}` for every type. File-only, like `retainMetadata` — an object does not flatten into an env var |
| `toolGuideExtra` | — | your own guidance on how the agent should treat memory, added **after** the built-in tool guide (at session start and on every refresh) and after the crediting note returned with `hindsight_search_knowledge_pages` results. It adds to the built-in text, never replaces it. Set it per harness or per bank like any other key, e.g. `"Memory is a past record, not current state: verify any claim against the code before acting on it."` |
| `pageRefreshEveryTurns` | `1` | refetch the knowledge pages and re-inject the page roster + tool guide every N user turns (`0` also means 1) |
| `pageTriggerType` | `"cron"` | when NEW knowledge pages refresh, i.e. what keeping them current costs — `"cron"` (default) on `pageTriggerCron` only and only when actually stale, `"auto-refresh"` after every consolidation that produced new material, `"manual"` never on their own. Auto-refresh is the most current and by far the most expensive: one synthesis per page per consolidation. Maps to the page's `trigger.refresh_cron`, or `trigger.refresh_after_consolidation` in the Hindsight API (`true` for auto-refresh, `false` for manual) |
| `pageTriggerCron` | `"H * * * *"` | schedule for `pageTriggerType: "cron"` — UTC, standard 5-field cron, e.g. `"0 3 * * *"`. The default is hourly, each page on its own hashed minute. Sets the page's `trigger.refresh_cron`, which the API treats as mutually exclusive with `refresh_after_consolidation`; a scheduled refresh is skipped when nothing changed. Write a field as `H` to give each page its own value there — see **Spreading refreshes with `H`** below |
| `pages` | every page | per-page configuration for the seeded knowledge pages, keyed by page name (case-insensitive): `false` skips a page entirely, `{"source_query": "..."}` seeds it with your question instead of the built-in one. Omitted, all five pages are seeded with their built-in queries. This is the supported way to own a page's wording — the plugin re-syncs a page whose live query differs from the one it is configured to have, so a query edited through the API or the control plane is replaced on the next session. A skipped page is **not deleted**: one already seeded keeps its content and stops being re-synced. The scoping clause is appended to your query too, so a reworded page cannot start reporting a dependency's decisions as this project's. File-only, like `recallOptions`; in a `banks.<id>` section it replaces the global map rather than merging into it |
| `customPages` | — | knowledge pages of your own, seeded alongside the five above and keyed by the name they get: `{"Security posture": {"source_query": "...", "tags": ["knowledge:decision"]}}`. `source_query` is required; `tags` picks which facts feed the page and is optional — omitted, the page draws on everything the bank holds. A separate setting from `pages` on purpose, so that an unknown name there stays a typo warning rather than quietly creating a page. File-only, and replaced (not merged) by a `banks.<id>` section |
| `autoSeed` | `true` | SessionStart: auto-seed a cold repo's bank from git history |
| `seedLimit` | `300` | auto-seed: most-recent-N-commits cap |
| `codebaseSurvey` | `true` | SessionStart: headless survey of a cold repo's structure, run under the current harness's own CLI (claude/codex/antigravity/opencode), falling back to any available agent |
| `surveyModel` | `haiku` | model for the survey — Claude recipe only (`claude -p --model`); other agents use their configured default |
| `surveyBudgetUsd` | `2` | survey spend cap — Claude recipe only (`claude -p --max-budget-usd`); other agents rely on their read-only sandbox |
| `surveyRefreshCommits` | `20` | re-run the survey at SessionStart once this many commits have accrued since the last one, so the structural pages track an architecture that keeps moving (`0` = survey a cold repo only, never again) |
| `retainSessions` | `true` | session write-back, honored by every harness: hook harnesses write the transcript on Stop, Factory Droid also writes on its cancellation notification, and plugin harnesses (opencode, opencode 2, Kilo) upsert it every turn plus an idle flush that captures the reply the per-turn pass can't see. Set `false` - globally, per harness, or per bank - to stop writing transcripts (the background history import stops with it) while recall, git ingest and the memory tools keep working |
| `maxParallelRetains` | `10` | cap on concurrent retain-related requests: drain()'s per-op polls plus deepen's chat/git retain pools. The API rate-limits bursts, not single requests — if you see 429s, lower this rather than raising it |
| `logLevel` | `"info"` | plugin-log verbosity (`"debug"` \| `"info"` \| `"warn"` \| `"error"`); `HINDSIGHT_LOG_LEVEL` env overrides |
| `autoUpdate` | `true` | keep the installed runtime current by itself: once a day a session start asks npm for the published version and, when it is newer, re-stages `~/.hindsight/coding-agents` in the background. It rewires no host config, so a release adding a **new** hook entry point still needs a manual `install`. Set `false` to pin the installed version; `disabled` stops it too, since an inert plugin should stay inert. Only ever replaces a runtime installed the documented way, via `npx` — a copy installed with `npm i -g`, vendored as a project dependency, or built from a checkout is left to whoever manages it (update those the way you installed them), and it needs `npx` and `npm` on `PATH` |
| `gitIngest` | `"message"` | git depth for seeding AND staying current (same engine): `"message"` = commit messages only (one doc, re-upserted when HEAD has a commit it lacks); `"full"` = messages + per-commit full diffs (progressive, newest first); `"none"` = git off |
| `harnesses.<name>` | — | per-harness override of any field above |
| `harness` | `opencode` | **deepen engine only**: which session format `--conversations` is read as |
By default a page refreshes **hourly, staggered**: `pageTriggerCron` is `"H * * * *"`, so every
page gets its own minute of the hour (see below) and a tick with nothing new to fold in is skipped
server-side. That keeps pages within an hour of the repo without paying auto-refresh's price — one
LLM synthesis per page per consolidation, on a repo that consolidates all day. Set
`pageTriggerType: "auto-refresh"` to go back to refreshing on every consolidation.
`pageTriggerType`/`pageTriggerCron` decide only **when** a page refreshes. **How** it refreshes
belongs to the server: Hindsight creates a knowledge page with a delta refresh (each pass edits the
page instead of rebuilding it) that doesn't reflect over sibling pages, and these settings merge
over those defaults rather than replacing them.
### Spreading refreshes with `H`
One `pageTriggerCron` is shared by every page in every repo you point this plugin at. So a literal
`"0 3 * * *"` does not schedule _a_ refresh at 03:00 — it schedules **all** of them at 03:00, five
pages per bank, on the same worker pool that serves retain. A session ingesting at 03:0x queues
behind the pile, and moving the hour just moves the pile.
Write a field as `H` and it is replaced, per page, by a value hashed from the bank id and the page
name. Each page gets its own slot, the same slot on every run:
| `pageTriggerCron` | what each page gets |
| ------------------ | ------------------------------------------------------ |
| `"H H * * *"` | once a day, at its own minute and hour |
| `"H * * * *"` | once an hour, at its own minute |
| `"H 3 * * *"` | daily at 03:MM — spread inside the hour you chose |
| `"H H(0-5) * * *"` | daily, spread across 00:00–05:59 only |
| `"0 3 * * *"` | no `H`, no hashing — exactly what it says, all at once |
`H` is [Jenkins' syntax](https://www.jenkins.io/doc/book/pipeline/syntax/#cron-syntax) for the same
problem. It never reaches the API: the plugin resolves it to an ordinary cron expression
(`"41 17 * * *"`) when it creates the page, so the schedule you see in the control plane is a plain
one you can edit. Hashing spreads pages out, it does not partition them — two pages can still land
on the same minute, just not all of them.
**These settings apply to the pages a repo already has, too.** Every session compares each page
this plugin created — the seeded taxonomy and every captured initiative — against the config and
re-syncs the ones that differ, so a bank seeded before this default changed moves onto the hourly
schedule by itself, and a page you retriggered by hand in the control plane is put back on the
configured policy the next time an agent runs. The config
file is the source of truth for these pages: to give one a different schedule, change
`pageTriggerType`/`pageTriggerCron` (per bank, if it is only that repo) rather than editing the
page. Only the fields this plugin states are touched — a page's `mode`, its excluded siblings and
its minimum refresh interval are left exactly as they are.
### Customize Knowledge Pages — `pages`
Every repo gets the same five pages. They are a **taxonomy**, not a summary of your source: each one
is synthesized from what the bank ingested — commit history and past conversations — and each is
pinned to one knowledge tier, so a page draws only on the facts the extractor routed to it.
| Page | What it answers | Tier tag |
| ------------------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------ |
| `Component map` | the main components/modules/subsystems, what each is responsible for, and how they depend on each other | `knowledge:component` |
| `Core concepts` | the domain abstractions and key entities — the vocabulary a developer has to know | `knowledge:concept` |
| `Conventions and patterns` | how THIS project does things: testing, error handling, naming, structure, how changes are made | `knowledge:convention` |
| `Key decisions and rationale` | the significant technical decisions and the durable "why we do it this way" behind them | `knowledge:decision` |
| `Initiatives and enhancements` | the major initiatives and features over time, linking out to each captured initiative's own page | `knowledge:feature-work` |
`pages` says which of them to seed and what each one asks. Keys are the page names above, matched
ignoring case and surrounding spaces:
```jsonc
{
"pages": {
// don't seed this page at all
"Component map": false,
// seed it, but ask your question instead of the built-in one
"Key decisions and rationale": {
"source_query": "What did we decide about data retention, encryption and PII handling, and why? Prefer decisions that constrain what new code may do.",
},
},
}
```
Omit `pages` entirely — the default — and all five are seeded with their built-in queries.
**Fewer pages.** Each page costs one LLM synthesis per refresh, so a repo that only wants the
architecture ones turns the rest off:
```jsonc
{
"pages": {
"Initiatives and enhancements": false,
"Conventions and patterns": false,
"Key decisions and rationale": false,
},
}
```
**Per repo**, like every other field — usually where this belongs, since what a page should ask is a
property of the project, not of your machine:
```jsonc
{
"banks": {
"coding-agent::payments-api": {
"pages": {
"Core concepts": {
"source_query": "What are the payment domain's entities — orders, ledgers, settlement states — and what does each mean in OUR model?",
},
},
},
},
}
```
Four things worth knowing before you reach for it:
- **This is the only durable way to reword a page.** Every session compares each seeded page against
the query it is configured to have and re-syncs the ones that differ, so a `source_query` edited
through the API or the control plane is replaced the next time an agent runs. Setting it here makes
your wording the configured one. When a re-sync does replace a query, the plugin now says which
page in the plugin log rather than doing it silently.
- **Your query still gets the scoping clause.** The sentence that keeps a dependency's decisions off
your project's page is appended to a custom query too — a bank holds facts about the libraries and
services a repo merely uses, and without that clause a page will present them as yours.
- **The tier tag stays the taxonomy's.** It selects which facts the synthesis reads; rewording the
question changes what is asked of those facts, not which ones are in scope.
- **`false` does not delete anything.** A page already seeded keeps its content and simply stops
being re-synced — remove it in the control plane if you want it gone.
A name that matches no page above is ignored with a warning in the plugin log, so a typo fails
loudly instead of looking like it disabled something. Adding pages of your own is not what this
setting is for: the agent's `hindsight_capture_initiative` tool already creates pages, one per
initiative, each with its own query.
#### Pages of your own — `customPages`
`pages` only reworks the five above. To add a page, name it under `customPages`:
```jsonc
{
"customPages": {
"Security posture": {
"source_query": "What are this project's security decisions — authn, secrets handling, PII, dependency policy — and what do they constrain in new code?",
"tags": ["knowledge:decision"],
},
"Operational runbook": {
"source_query": "How does this project get deployed, monitored and rolled back? What has broken in production, and what fixed it?",
},
},
}
```
`source_query` is required. `tags` is optional and picks which facts feed the page — one of the tier
tags above, or any tag you stamp on your own writes with `retainTags`. Omit it and the page draws on
everything the bank holds: a refresh matches tags with `all`, so no tags means no tag constraint,
**not** an empty page.
Your pages are seeded at the same root as the taxonomy, on the same refresh schedule, and re-synced
from the config the same way — reword one here and the live page follows on the next session. They
compose with `pages`, so trading two built-ins for one of your own is just both settings at once.
**Why it is a separate setting.** `pages` refuses a name that matches no seeded page, and that is
what makes a typo loud: if an unknown key there meant "create this page", `"Componnet map"` would
quietly create an empty second page instead of rewording the one you meant. For the same reason,
naming a seeded page under `customPages` is refused — reword it under `pages`.
A page you create yourself in the control plane is a third thing again, and the plugin never touches
it: the seed pass only visits the pages it is configured to own.
### A bank you shape yourself — `manageBankConfig`
Pointed at a bank, this plugin gives it the shape its ingestion needs: retain strategies for the
kinds of document it writes (`git`, `gitlog`, `conversation`, `document`, `survey`), a `knowledge`
entity-label group that routes facts to the knowledge pages, and — on a bank that has no missions of
its own — the coding missions.
**It only ever adds what is missing.** A strategy you defined, an edit you made to one of the
plugin's, a reworded label group, a mission you rewrote in the control plane: each is left exactly
as it is, on every session, forever. What the bank already says wins. The cost of that promise is
that a plugin release which _rewords_ an existing strategy or label does not reach a bank that
already has it. To take the current default back, clear that override on the bank (delete the
strategy, or the whole `retain_strategies` entry, in the control plane): the next session finds the
bank silent there and seeds it again.
One field is the exception: the extraction mode of the plugin's own four strategies (`git`,
`gitlog`, `conversation`, `document`) follows `retainExtractionMode` and is put back on every session
if it drifts — the same way a seeded page's query is. Change it in `coding-agent.json`, not in the
control plane. The strategies' other fields, and your own strategies, are still left alone.
**Your own defaults for new banks — `defaultBankConfig`.** Everything the template does not name, a
bank the plugin creates gets from the server: auto-consolidation on, observations on, a 60s floor
between mental-model refreshes. With one bank per repo those are the settings that cost money, and
a new repo an agent touches spawns a new bank with all of them switched on — while every bank you had
hand-tuned in the control plane sits at a cheaper baseline. `defaultBankConfig` names the baseline
once, in the plugin's config, and it travels in the same import that creates the bank:
```json
{
"defaultBankConfig": {
"enable_observations": false,
"enable_auto_consolidation": false,
"mental_model_min_refresh_interval_seconds": 21600
}
}
```
The keys are the bank-config API's own field names, passed straight through, so anything the
bank-config accepts can go here. They follow the same additive rule as the template: written only
where the bank is silent, so an existing bank picks them up on its next session **except** for the
fields it already sets — a value you chose in the control plane stays, even one equal to the server
default, and so does a value an earlier plugin release seeded (the template writes
`enable_observations: true` alongside the missions; clear that override on the bank to let your
default in). On a key the template also names, your default wins. The three fields the plugin
governs itself — `retain_strategies`, `entity_labels` and `retain_extraction_mode` — are refused
with a warning: the first two are merged entry by entry, the last is `retainExtractionMode`'s.
Set `manageBankConfig: false` to keep the plugin out of the bank's configuration altogether,
`defaultBankConfig` included — the right setting for a bank you share with non-coding work, or one
you configure yourself. That bank should then define the five strategies above itself. Note that the
miss is **silent**: the server does not reject a retain naming a strategy the bank lacks, it logs a
warning and extracts with the bank's own configuration — so a commit diff, a session transcript and
a survey marker would all get the same generic treatment instead of the extraction each needs.
Knowledge pages are seeded either way; `pageTriggerType` governs what they cost.
Like every field here it can be set per bank, which is usually where it belongs:
```json
{
"bankId": "my-global-bank",
"banks": { "my-global-bank": { "manageBankConfig": false } }
}
```
### Per-repo opt-in/out — `banks.<bankId>`
Per-repo control lives in the SAME file, keyed by the **resolved bank id** (shown in the session
banner) and applied AFTER bank resolution — so it works regardless of where the repo lives, and
survives directory moves:
```jsonc
{
"banks": {
"coding-agent::secret-client": { "disabled": true }, // blacklist: no memory at all
"coding-agent::old-name": { "bank": "team::shared" }, // rename / converge banks
"coding-agent::big-mono": { "gitIngest": "full", "retainSessions": false },
},
}
```
Any behavioral field can be overridden per bank, and `bank` **renames the destination** (single
hop: the section is selected by the resolved id, the target is literal — several ids may converge
on one shared bank, and the target's own section is not consulted). Other bank-resolution fields
are ignored inside a bank section.
#### Recipe: two repos, one shared bank
Two ways, by what the natural key is:
**By resolved id** — you know the repo names; works wherever the repos live (and keeps working if
they move). Both ids converge on one literal target:
```jsonc
{
"banks": {
"coding-agent::backend": { "bank": "team::product" },
"coding-agent::frontend": { "bank": "team::product" },
},
}
```
**By path prefix** — the repos live under one directory; a single `mapPathToBank` entry covers
every repo (present and future) beneath it:
```jsonc
{
"mapPathToBank": { "/Users/me/work/client-x": "client-x-memory" },
}
```
Rule of thumb: converge by **id** for a hand-picked set of repos; map by **path** when a folder is
the boundary ("everything I clone under `work/client-x` shares memory").
### Per-directory overrides — `paths.<prefix>`
When a folder is the boundary for something other than the bank — typically a different server
or tenant for client work — key the override by directory instead of by bank id:
```jsonc
{
"apiToken": "personal-key",
"paths": {
"~/work/client-x": { "apiToken": "client-x-key" },
"~/work/acme": { "apiUrl": "https://hindsight.acme.internal", "apiToken": "acme-key" },
"~/oss": { "serverMode": "daemon" },
},
}
```
Every repo under the prefix, present and future, keeps its own bank but uses the entry's
settings. The longest matching prefix wins, and a linked worktree outside the tree uses its main
checkout's entry, as with `mapPathToBank`. The section applies after bank resolution and before
`banks.<bankId>`, so a bank section still wins for its one repo. Bank-resolution and approval
fields (`bankId`, `mapPathToBank`, `bank`, `optInOnly`, `optInPaths`, ...) are ignored inside a
path section. Any connection setting can be overridden — `serverMode`, `apiUrl`, `apiPort`,
`apiToken` — so one directory can use a local daemon while another uses Cloud or a self-hosted
server. The token is re-read on a rejected request, like the top-level `apiToken`, but only while
the directory still points at the same server.
### Bank resolution
Coding memory is **per repository**. Resolution order for the working directory:
1. `mapPathToBank` — longest matching absolute-path prefix (mapping a repo root covers every
subdirectory; deeper mappings win; overrides even an explicit `bankId`).
2. Static — `bankId` set (or `dynamicBankId: false`).
3. Dynamic — `bankIdTemplate` with placeholders:
- `{gitProject}` — worktree-aware repo name: `git rev-parse --git-common-dir` resolves every
linked worktree to the **main** worktree's basename, so all worktrees of a repo share one bank
(bare repos use the bare dir name). **Outside a repo** there is nothing for git to resolve, so
it falls back to the basename of the directory the **session started in** — an agent that
`cd`s into a subdirectory keeps writing to one bank, and a subdirectory gets its own bank only
when you deliberately start a session there
- `{project}` — plain working-directory basename
- `{harness}` — the entry point asking (`opencode`, `claude-code`, `codex`, `antigravity-cli`, `cursor-cli`, `copilot-cli`)
- `{channel}` / `{user}` — `$HINDSIGHT_CHANNEL_ID` / `$HINDSIGHT_USER_ID`
The default `"coding-agent::{gitProject}"` is **harness-neutral**, so opencode, Claude Code, and Codex
all share one memory per repo — use `"{harness}-{gitProject}"` to split per agent instead.
### Recording where a memory came from
With a bank per repo, the bank _is_ the answer to "where did this come from". On a deliberately
**shared** bank — one bank holding cross-project knowledge so facts recall everywhere — it isn't:
every memory looks alike. `retainTags` and `retainMetadata` stamp that provenance onto conversations,
git history and diffs, survey lifecycle documents, initiative markers, and documents saved through
`hindsight_ingest_document`:
```jsonc
{
"bankId": "shared", // one bank for everything
"retainTags": ["project:{gitProject}", "env:work"],
"retainMetadata": { "repo": "{gitProject}" },
}
```
Recalls can then filter by `project:<repo>`, and every document shows which repository it came out
of. Both accept the same placeholders as `bankIdTemplate` — `{gitProject}`, `{project}`,
`{harness}`, `{channel}`, `{user}` — plus `{bankId}`, `{sessionId}` and `{timestamp}`.
`{gitProject}` is worktree-aware here too, so every linked worktree of a repo stamps one name.
`{sessionId}` resolves to `unknown` for documents that do not originate from an agent session.
The plugin's own `source:` and `harness:` tags are reserved: entries in those namespaces are ignored
with a warning, so a document's agent attribution always reflects the agent that actually wrote it.
### One set of beliefs per repo
Every document this integration writes carries provenance tags — `source:chat`, `harness:<id>`,
`knowledge:<kind>`, plus anything from `retainTags`. Those tags say **who wrote** a memory; they are
what filters recall and draws each document's agent logo, and they stay on the facts.
They are not, however, a good boundary for
[observations](../../developer/observations.md). Consolidation's own
default (`combined`) builds one observation set per distinct tag set, so the same repository
worked on by two agents would grow two parallel sets of beliefs — one per harness — that never
merge, each blind to the other, at double the consolidation cost. Which agent happened to be typing
does not change whether a convention or a decision is true.
So the integration retains with `observationScopes: "shared"`: one global, untagged observation
scope per bank, which is what a bank already is — one project's memory. Set the field to change it:
```jsonc
{
"observationScopes": "combined", // one observation set per distinct tag set (server default)
"banks": {
"coding-agent::mono": { "observationScopes": "per_tag" }, // per-repo, like any behavioral field
},
}
```
### Splitting code from conversation — `per_source`
`shared` puts every document a repo produces into one belief set. `"per_source"` keeps that set and
adds one per origin, so "what the commits say" and "what was decided in conversation" can be asked
apart:
```jsonc
{ "observationScopes": "per_source" }
```
Each document consolidates into the global scope **plus** one named for each `source:` tag it
carries — `[[], ["source:chat"]]` for a session transcript, `[[], ["source:git"]]` for a commit
diff. Read an axis back with `tags: ["source:git"], tags_match: "exact"`, and the merged view with
`tags: [], tags_match: "exact"`.
A document carrying two `source:` tags gets a scope for each, and that is deliberate rather than
duplication. The commit-message seed is tagged `source:git` and `source:git-log`, so
`source:git-log` is fed only by the seed — what the commit _messages_ say — while `source:git` also
collects every per-commit diff under `gitIngest: "full"`. Two questions, two answers, each
deduplicated within itself by consolidation. A fact belonging to more than one axis is the point.
This cannot be expressed as a scope list. The server treats an explicit `list[list[str]]` as
unconditional — it is not filtered against the memory's own tags — so a configured
`[[], ["source:git"], ["source:chat"]]` writes every document into all three, and the `source:git`
scope fills with beliefs built from chat transcripts. Only a per-document decision separates them.
It costs one extra consolidation pass per document, and it reads only `source:`, so a volatile
provenance tag never becomes a scope. The global scope is still written first and unchanged, so the
untagged observations knowledge pages read are unaffected.
`"per_tag"` and `"all_combinations"` split further still, and an explicit `[["project:demo"], …]`
declares the scopes literally. `HINDSIGHT_OBSERVATION_SCOPES` sets the scalar modes; a scope list is
file-only. Changing this does not rewrite observations already consolidated under the old scoping —
they stay where they were built, and new work accrues under the new setting.
## Diagnostics & logging
All logs live in `~/.hindsight/coding-agents-logs/` (owner-only). Each file rotates to `<file>.1`
at 10 MB.
**Leveled plugin log** (humans debugging): `~/.hindsight/coding-agents-logs/plugin.log` (override
`HINDSIGHT_LOG_FILE`) — timestamped `LEVEL [scope] message` lines from every component, including
the ingestion engine. Level defaults to `info`; set `"logLevel": "debug"` in config or
`HINDSIGHT_LOG_LEVEL=debug` for ad-hoc debugging (at `debug`, every diag event below is mirrored
here too, so one file tells the whole story).
**Structured diag events** (machines/harnesses): every reflect and page-fetch outcome is appended
as a JSON line to `~/.hindsight/coding-agents-logs/diag.jsonl` (override with
`HINDSIGHT_DIAG_FILE`):
```json
{
"ts": "2026-07-27T07:05:52Z",
"harness": "claude-code",
"event": "reflect_ok",
"ms": 14210,
"chars": 792,
"query": "..."
}
```
`reflect_failed` / `pages_failed` record the error; if you're comparing memory-on vs memory-off,
check this file — a run whose reflects failed is a no-memory run. When the failure was a timeout or
a 5xx, the hook falls back to knowledge-page search and, if no page matches, to a raw recall of the
bank's memories: `reflect_fallback_pages` / `reflect_fallback_observations` record what each
step returned (`*_failed` when it errored). Seed starts are logged as
`seed_started`.
When `autoInject` names a retrieval source instead of reflect, that source records its own
outcome with the number of items it returned: `inject_pages` for `"pages"`, `inject_recall` for
`"recall"` (`inject_pages_failed` / `inject_recall_failed` when the call errored). No reflect
event is written on those turns — nothing reflected.
**Tool usage** (is the agent using Hindsight?): one JSON line per finished user turn in
`~/.hindsight/coding-agents-logs/usage.jsonl` (override `HINDSIGHT_USAGE_FILE`) — the `hindsight_*`
tools the agent called during that turn, and whether its reply credited Hindsight memory ("From
Hindsight memory"). The credit rate counts only turns that called a retrieval tool (search, list,
read, reflect); saving a document is not expected to be credited. Recorded at the end of each turn,
whether or not `retainSessions` writes the session back. It never leaves your machine. Summarize it per agent with:
```bash
npx @vectorize-io/hindsight-coding-agents stats
```
### Is the memory ready yet?
`hindsight_sync_status` — the agent-facing tool, `dist/status.js` for scripts — answers exactly
that: `"synced": true` means the seeded memory is queryable. It also reports gitlog freshness, how
far per-commit deepening has got, the codebase survey's state (`surveyBaseline` is the HEAD the last
survey started from, `surveyDocs` counts the findings documents that have landed, 0–4 — a baseline
with no findings retries automatically), and the extraction operations still in flight.
### Resetting a repo's memory
Delete its bank on the server. The bank is the **only** state this integration keeps, so the next
session in that repo is a true first open — seed and survey run again from scratch. There are no
client-side files to clean up.
### Marker documents you may notice
Two document ids exist for the machinery's own bookkeeping. Both are safe to ignore and safe to
delete:
- `survey-baseline:<sha>` — reads "🛰️ researching…" while a codebase survey runs and flips to
"✅ completed" once its findings land. It is retained under the `survey` strategy, whose marker
rule extracts **nothing** from a status marker, and it drives the re-survey cadence
(`surveyRefreshCommits`) and `surveyBaseline` in sync status.
- `gitlog:<repo>` — the aggregated commit-message seed document, oldest commit first. Once seeded it
is kept current by **appending** only the commits since the one its `gitlog-head:<sha>` tag
names, so a new commit costs the extraction of its own message, not of the whole log. It is
written in full again (bounded by `seedLimit`) only when an append cannot describe HEAD: the
recorded commit is no longer an ancestor of HEAD (a rebase, a force-push), more than `seedLimit`
commits are new, or the server cannot append safely.
### When memory seems to be missing
Failures never break the agent: a reflect, page fetch or retain that fails degrades to an ordinary
memoryless turn and is recorded in the logs. "No memory" is therefore a log question — check the
diag file for whether `session_start` and `deepen_started` ever fired for that bank. A session that
was already running when the plugin was installed has no SessionStart behind it; its first prompt
after the install self-heals.
references/sdks/integrations/composio.md
# Composio
Persistent memory for [Composio](https://composio.dev) agents via Hindsight. Exposes Hindsight's
**retain**, **recall**, and **reflect** operations as Composio in-process custom tools your agent
can call directly.
The Hindsight memory bank for each call is the Composio session's `user_id`, so a single
registered tool set isolates memory per user automatically.
[View Changelog →](../../changelog/integrations/composio.md)
## Features
- **Composio Custom Tools** — registered via `composio.experimental.tool()` and bound to a session
- **Per-user memory** — the session `user_id` maps to the Hindsight `bank_id` (with a configurable fallback bank)
- **Three Memory Tools** — Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Simple Configuration** — pass a client/URL directly, or `configure()` once globally
## Installation
```bash
pip install hindsight-composio
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from composio import Composio
from hindsight_composio import register_hindsight_tools
composio = Composio() # uses COMPOSIO_API_KEY
tools = register_hindsight_tools(
composio,
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)
session = composio.create(
user_id="user-123", # becomes the Hindsight bank_id
experimental={"custom_tools": tools},
)
# Pass session.tools() to your agent/LLM as usual.
```
The session now has three tools the agent can call:
- **`HINDSIGHT_RETAIN`** — Store information to long-term memory
- **`HINDSIGHT_RECALL`** — Search long-term memory for relevant facts
- **`HINDSIGHT_REFLECT`** — Synthesize a reasoned answer from memories
## Bank selection
The bank is resolved per call:
1. The session's `user_id` (recommended — one tool set, isolated per user).
2. `default_bank` (passed to `register_hindsight_tools` or `configure`) when a call has no `user_id`.
If neither is available, the tool raises an error.
```python
tools = register_hindsight_tools(
composio,
hindsight_api_url="https://api.hindsight.vectorize.io",
default_bank="shared", # used only when a session has no user_id
)
```
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL:
```python
tools = register_hindsight_tools(
composio,
hindsight_api_url="http://localhost:8888",
)
```
See the [installation guide](../../developer/installation.md) for self-hosting setup.
## Selecting tools
Include only the tools you need:
```python
tools = register_hindsight_tools(
composio,
hindsight_api_url="https://api.hindsight.vectorize.io",
enable_retain=True,
enable_recall=True,
enable_reflect=False, # omit reflect
)
```
## Global configuration
Instead of passing connection details every time, configure once:
```python
from hindsight_composio import configure, register_hindsight_tools
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="your-api-key", # or set HINDSIGHT_API_KEY env var
default_bank="shared", # fallback bank when no user_id
budget="mid", # recall/reflect budget: low/mid/high
max_tokens=4096, # max tokens for recall results
tags=["env:prod"], # tags for stored memories
recall_tags=["scope:global"], # tags to filter recall
recall_tags_match="any", # any/all/any_strict/all_strict
)
tools = register_hindsight_tools(composio)
```
## Configuration Reference
### `register_hindsight_tools()`
| Parameter | Default | Description |
| ------------------- | ---------- | ------------------------------------------------ |
| `composio` | _required_ | The `Composio` instance (provides the decorator) |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `default_bank` | `None` | Bank used when a call has no Composio `user_id` |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `enable_retain` | `True` | Include the retain (store) tool |
| `enable_recall` | `True` | Include the recall (search) tool |
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
### `configure()`
| Parameter | Default | Description |
| ------------------- | ------------------------------------------------------ | --------------------------------------------- |
| `hindsight_api_url` | Hindsight Cloud (`https://api.hindsight.vectorize.io`) | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `default_bank` | `None` | Fallback bank when a session has no `user_id` |
| `budget` | `"mid"` | Default recall/reflect budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
## Requirements
- Python >= 3.10
- composio >= 0.13.1, < 1
- hindsight-client >= 0.4.0
- A running Hindsight API server (or Hindsight Cloud)
> **📝 Note**
>
Composio's custom-tools API is currently experimental. This integration targets the Composio
0.13.x SDK (`from composio import Composio`) and intentionally excludes the in-progress `1.0.0`
rewrite, whose API differs.
references/sdks/integrations/context-forge.md
# ContextForge
Register [Hindsight](https://vectorize.io/hindsight) as an MCP backend in [IBM ContextForge](https://github.com/IBM/mcp-context-forge), an open-source MCP gateway. Once registered, every AI tool connected to ContextForge — Dust, Claude Desktop, custom agents — gets access to Hindsight's retain, recall, and reflect tools through a single unified endpoint.
## Why ContextForge + Hindsight?
ContextForge acts as a central MCP hub that aggregates multiple MCP servers behind one authenticated endpoint. Adding Hindsight as a backend means:
- **One endpoint, many tools** — AI platforms connect to ContextForge once and get access to Hindsight memory alongside your other MCP servers (databases, workflows, metadata catalogs)
- **Centralized auth** — ContextForge handles SSO, RBAC, and JWT-based authentication; no need to expose Hindsight directly
- **Team-scoped memory** — Use ContextForge's team/visibility model to control which teams can access which memory banks
- **No code changes** — Hindsight's built-in MCP endpoint (`/mcp`) speaks standard Streamable HTTP; ContextForge connects to it natively
## Architecture
```
AI Tools (Dust, Claude Desktop, custom agents)
│
▼
┌─────────────────────────────┐
│ ContextForge MCP Gateway │ ← SSO, RBAC, session management
│ (unified MCP endpoint) │
└──────────┬──────────────────┘
│ Streamable HTTP
┌─────┴──────┬──────────────┐
▼ ▼ ▼
Hindsight PostgreSQL OpenMetadata
(memory) MCP (SQL) (metadata)
```
## Prerequisites
- A running Hindsight instance (Docker, Helm, or Cloud)
- A running ContextForge instance
- Network connectivity between ContextForge and Hindsight (same Kubernetes cluster, VPC, or public URL)
## Setup
### Option 1: Register via ContextForge Admin UI
1. Log in to ContextForge as an admin
2. Navigate to **Servers** → **Add Server**
3. Fill in:
- **Name**: `hindsight`
- **URL**: `http://<hindsight-host>:8888/mcp` (or your Hindsight API URL + `/mcp`)
- **Transport**: Streamable HTTP
4. Click **Save**
### Option 2: Register via Admin API
```bash
# Authenticate
TOKEN=$(curl -s -X POST https://your-contextforge.com/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "your-password"}' \
| jq -r '.access_token')
# Register Hindsight as an MCP server
curl -X POST https://your-contextforge.com/admin/servers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "hindsight",
"url": "http://hindsight-api:8888/mcp",
"transport": "streamable_http",
"description": "Hindsight agent memory — retain, recall, and reflect"
}'
```
### Option 3: Kubernetes (Helm)
If both services run in the same EKS/Kubernetes cluster, Hindsight is reachable at its in-cluster service DNS:
```
http://hindsight-api.hindsight.svc.cluster.local:8888/mcp
```
Register this URL in ContextForge using either the UI or API methods above.
For automated registration on every Helm deploy, add a post-install Job to your ContextForge chart that calls the Admin API (see the [Helm example](#helm-auto-registration) below).
## Verifying the Integration
Once registered, verify Hindsight tools are available through ContextForge:
```bash
# List tools via ContextForge's MCP endpoint
curl -s -X POST https://your-contextforge.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' \
| jq '.result[] | select(.name | startswith("hindsight"))'
```
You should see Hindsight's tools: `retain`, `recall`, `reflect`, `list_banks`, etc.
## Bank Modes
Hindsight's MCP endpoint supports two modes. Choose based on your use case:
### Multi-bank mode (default)
Register the root MCP endpoint:
```
http://hindsight-api:8888/mcp
```
All tools include an optional `bank_id` parameter. Agents can create and switch between memory banks dynamically.
### Single-bank mode
Pin to a specific bank by including the bank ID in the URL:
```
http://hindsight-api:8888/mcp/my-team-bank/
```
Tools are scoped to that bank — no `bank_id` parameter needed. Useful when you want each ContextForge team to have its own isolated memory.
## Available Tools
Once registered, these Hindsight tools become available through ContextForge:
| Tool | Description |
|------|-------------|
| `retain` | Store information to long-term memory |
| `recall` | Search memories with natural language queries |
| `reflect` | Synthesize memories into a reasoned answer |
| `list_banks` | List all memory banks |
| `create_bank` | Create a new memory bank |
| `list_mental_models` | List pinned reflections |
| `create_mental_model` | Create a new mental model |
| `list_documents` | List ingested documents |
See the full tool list in the [MCP Server reference](https://hindsight.vectorize.io/developer/mcp-server#available-tools).
## Helm Auto-Registration
To automatically register Hindsight in ContextForge on every Helm deploy, add a post-install/post-upgrade Job to your ContextForge chart:
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: register-hindsight-{{ .Release.Revision }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: 3
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: OnFailure
containers:
- name: register
image: curlimages/curl:latest
command: ["/bin/sh", "-c"]
args:
- |
# Wait for ContextForge
for i in $(seq 1 12); do
curl -sf http://context-forge:4444/health && break
sleep 10
done
# Authenticate
TOKEN=$(curl -s -X POST http://context-forge:4444/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\": \"$ADMIN_USER\", \"password\": \"$ADMIN_PASS\"}" \
| grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
# Register or update
curl -X POST http://context-forge:4444/admin/servers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "hindsight",
"url": "http://hindsight-api.hindsight.svc.cluster.local:8888/mcp",
"transport": "streamable_http",
"description": "Hindsight agent memory"
}'
env:
- name: ADMIN_USER
valueFrom:
secretKeyRef:
name: context-forge-credentials
key: ADMIN_USERNAME
- name: ADMIN_PASS
valueFrom:
secretKeyRef:
name: context-forge-credentials
key: ADMIN_PASSWORD
```
## Connecting AI Tools via ContextForge
Once Hindsight is registered in ContextForge, AI tools connect to ContextForge's unified endpoint — not directly to Hindsight.
### Dust
1. In Dust → **Spaces → Tools → Add MCP Server**
2. URL: `https://your-contextforge.com/mcp`
3. Auth: Bearer token (ContextForge JWT or MCP client token)
4. Hindsight tools appear alongside all other registered MCP servers
### Claude Desktop
Add to your MCP config:
```json
{
"mcpServers": {
"context-forge": {
"url": "https://your-contextforge.com/mcp",
"headers": {
"Authorization": "Bearer <your-contextforge-token>"
}
}
}
}
```
## Troubleshooting
### Hindsight tools not appearing
Verify the server is registered and healthy:
```bash
# Check registered servers
curl -s https://your-contextforge.com/admin/servers \
-H "Authorization: Bearer $TOKEN" | jq '.[] | select(.name == "hindsight")'
# Test Hindsight directly
curl -s http://hindsight-api:8888/health
```
### Connection refused from ContextForge
If ContextForge can't reach Hindsight:
- Verify network connectivity (same namespace/VPC, NetworkPolicy allows traffic)
- Check the registered URL matches the actual Hindsight service endpoint
- Ensure `SSRF_ALLOW_PRIVATE_NETWORKS=true` is set in ContextForge (required for in-cluster backends)
### Auth errors
ContextForge authenticates users at the gateway level. Hindsight's MCP endpoint doesn't require separate auth when accessed from within the cluster. If you've enabled `MCP_AUTH_TOKEN` on Hindsight, pass it via ContextForge's header passthrough:
```bash
# Ensure ContextForge forwards Authorization headers
ENABLE_HEADER_PASSTHROUGH=true
DEFAULT_PASSTHROUGH_HEADERS='["Authorization"]'
```
references/sdks/integrations/continue.md
# Continue
Long-term memory for the [Continue.dev](https://continue.dev) coding assistant, powered by [Hindsight](https://vectorize.io/hindsight). Recall relevant project memory directly into chat, and optionally let the agent recall and retain automatically in agent mode.
[View Changelog →](../../changelog/integrations/continue.md)
## How It Works
Continue has no hook that runs before a message is sent, but it supports two native extension points that `hindsight-continue` uses:
- **HTTP context provider (precise recall):** type `@hindsight <query>` (or a bare `@hindsight`) in Continue chat and relevant memory is recalled and injected into the model's context **at query time**. The package ships a small adapter server that implements Continue's HTTP context-provider contract (`{query, fullInput, options, workspacePath}` → context items) on top of Hindsight recall.
- **MCP server + rules (automatic):** point Continue's agent mode at the Hindsight MCP server for `retain`/`recall`/`reflect` tools, with a rules file that instructs the agent to recall at the start of every task and retain durable facts.
Memory is scoped per **bank** — use one bank per project so context from one codebase doesn't leak into another.
## Setup
```bash
pip install hindsight-continue
```
Run the adapter, pointing it at your Hindsight instance and bank:
```bash
export HINDSIGHT_API_KEY=hsk_...
export HINDSIGHT_CONTINUE_BANK_ID=my-project
hindsight-continue # serves on 127.0.0.1:8123
```
Then register it in Continue's `config.yaml`:
```yaml
context:
- provider: http
params:
url: "http://127.0.0.1:8123/"
title: hindsight
displayTitle: Hindsight
description: Recall long-term memory from Hindsight
```
Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a self-hosted server with `HINDSIGHT_API_URL=http://localhost:8888`.
## Automatic recall (optional)
For hands-off recall and retain in agent mode, wire the Hindsight MCP server into Continue and add a "recall first" rule. See the [`examples/.continue/`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/continue/examples/.continue) assets in the integration directory.
## Limitation
Continue has no pre-prompt hook, so memory cannot be injected fully passively on every message. The `@hindsight` provider gives precise, query-time recall on demand; the MCP + rules setup gives automatic recall in agent mode, subject to the agent following the rule.
references/sdks/integrations/copilot-cli.md
# GitHub Copilot CLI
> **⚠️ Superseded by the Coding Agents plugin**
>
**The GitHub Copilot CLI integration** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install copilot-cli
```
Memory does not move automatically — the banks are scoped differently and this agent's history cannot be imported (it is kept in an internal database). See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
Persistent memory for [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks) using [Hindsight](https://vectorize.io/hindsight). Python hook scripts automatically recall relevant context at session start (and for every subagent Copilot CLI spawns) and retain conversations as they happen — no changes to your Copilot CLI workflow required.
[View Changelog →](../../changelog/integrations/copilot-cli.md)
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
```bash
# Install the CLI
pip install hindsight-copilot-cli
# Install the hooks (defaults to Hindsight Cloud)
hindsight-copilot-cli install --api-url https://api.hindsight.vectorize.io --api-token your-api-key
# Restart Copilot CLI — memory is live
```
The installer copies the hook scripts to `~/.copilot/hindsight-copilot-cli/`, writes a standalone `~/.copilot/hooks/hindsight-copilot-cli.json` (Copilot CLI loads every `*.json` file in its hooks directory, so this never needs to merge with other tools' hook files), and creates `~/.hindsight/copilot-cli.json` for your personal config.
**Self-hosting alternative** — connect to a local `hindsight-embed` daemon by omitting the flags:
```bash
hindsight-copilot-cli install
```
To uninstall:
```bash
hindsight-copilot-cli uninstall
```
### Repo-level install
For a team-shared configuration checked into version control, register the hooks at the repository level instead:
```bash
hindsight-copilot-cli install --scope repo
```
This writes `.github/hooks/hindsight-copilot-cli.json` in the current repo. Hook scripts are still installed to `~/.copilot/hindsight-copilot-cli/` (per-machine), so the token must come from the `HINDSIGHT_API_TOKEN` environment variable rather than a committed file.
## Features
- **Auto-recall at session start** — when a Copilot CLI session starts, queries Hindsight for relevant memories and injects them as additional context. Uses the queued `initialPrompt` when one exists, or falls back to a generic project-context query derived from the working directory.
- **Auto-recall for subagents** — Copilot CLI's built-in subagents (`explore`, `task`, `research`, `code-review`, `rubber-duck`, `security-review`, and custom agents) run in their own isolated context and otherwise get none of the memory injected at session start. The `subagentStart` hook gives them baseline project memory too.
- **Auto-retain** — after each turn (subject to `retainEveryNTurns`), and again on session end, stores the conversation to Hindsight for future recall.
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory.
- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content.
- **Zero runtime dependencies** — the hook scripts are pure Python stdlib; the `pip install` only ships the one-time installer.
## Architecture
The plugin uses four Copilot CLI hook events:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `sessionStart` | **Auto-recall** — query memories using the initial prompt (or a project-context fallback), inject as additional context |
| `subagent_start.py` | `subagentStart` | **Auto-recall for subagents** — query memories using a project-context fallback query (subagent payloads carry no task text), inject as additional context |
| `agent_stop.py` | `agentStop` | **Auto-retain** — read the transcript, apply the `retainEveryNTurns` cadence, POST to Hindsight (async); caches the transcript path for `sessionEnd` |
| `session_end.py` | `sessionEnd` | **Final flush** — force a retain using the transcript path cached by the last `agentStop`, since `sessionEnd`'s payload has no transcript path of its own |
On `sessionStart`, the hook queries Hindsight for the most relevant memories and injects a context block that Copilot CLI adds to the session:
```
<hindsight_memories>
Relevant memories from past conversations...
Current time - 2026-03-27 09:14
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26)
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26)
</hindsight_memories>
```
`subagentStart` fires for every subagent Copilot CLI spawns except the built-in `general-purpose` agent (which never emits the event) and injects the same style of context block, prepended to that subagent's prompt.
On `agentStop` (and again on `sessionEnd`), the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously.
> **📝 Recall is not refreshed per turn**
>
Unlike some other integrations, Copilot CLI's `userPromptSubmitted` and `preToolUse` hooks don't support injecting additional context, so recall only runs once per session (at `sessionStart`) and once per subagent (at `subagentStart`) rather than being refreshed on every turn. This keeps the integration simple and reliable, at the cost of not picking up new memories mid-session.
## Connection Modes
### 1. External API (recommended)
Connect to a running Hindsight server (cloud or self-hosted) via `~/.hindsight/copilot-cli.json`:
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
### 2. Local Daemon
Run `hindsight-embed` locally. The `session_start.py` hook detects it on `apiPort` (default `9077`). The daemon is not auto-started by the plugin — start it separately:
```bash
uvx hindsight-embed
```
Then leave `hindsightApiUrl` empty in your config and the plugin connects to `http://localhost:9077`.
## Configuration
Default config ships in `~/.copilot/hindsight-copilot-cli/settings.json`. For personal overrides that survive updates, create `~/.hindsight/copilot-cli.json`. Most settings can also be overridden via environment variable.
**Loading order** (later entries win):
1. Built-in defaults
2. Plugin `settings.json` (at `~/.copilot/hindsight-copilot-cli/settings.json`)
3. User config (`~/.hindsight/copilot-cli.json`)
4. Environment variables
---
### Connection
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Empty = local daemon. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. |
---
### Memory Bank
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"copilot-cli"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction — what to extract from coding conversations. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Which fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"copilot-cli"` | Agent name used in dynamic bank ID derivation. |
---
### Auto-Recall
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall (covers both `sessionStart` and `subagentStart`). |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. |
| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. |
| `recallFallbackQueryTemplate` | — | project-context prompt | Query used when there's no specific prompt/task text available (interactive `sessionStart` with no queued prompt, or any `subagentStart`). |
---
### Auto-Retain
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session (upserted by session ID). `"chunked"` sends sliding windows every N turns. |
| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. |
| `retainContext` | — | `"copilot-cli"` | Label identifying the source integration. Useful when multiple integrations write to the same bank. |
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. |
## Per-Project Memory
To give each project its own isolated memory bank, enable dynamic bank IDs:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "project"]
}
```
With this config, running Copilot CLI in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path.
## Troubleshooting
**Hooks not firing**: check that `~/.copilot/hooks/hindsight-copilot-cli.json` (or `.github/hooks/hindsight-copilot-cli.json` for repo scope) is valid JSON. Copilot CLI loads hook configuration when it starts, so restart the CLI after installing or updating hooks.
**No memories recalled**: Recall returns results only after something has been retained. Complete one Copilot CLI session first, then start a new one.
**Memory not being stored**: `retainEveryNTurns` defaults to `10` — the `agentStop` hook only fires a retain every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/copilot-cli.json`. The `sessionEnd` hook also forces a final retain when the session terminates.
**Debug mode**: Add `"debug": true` to `~/.hindsight/copilot-cli.json` to see what Hindsight is doing on each turn.
references/sdks/integrations/crewai.md
# CrewAI
Persistent memory for AI agent crews via [CrewAI](https://github.com/crewAIInc/crewAI). Give your crews long-term memory with fact extraction, entity tracking, and temporal awareness.
[View Changelog →](../../changelog/integrations/crewai.md)
## Features
- **Drop-in Storage Backend** - Implements CrewAI's `Storage` interface for `ExternalMemory`
- **Automatic Memory Flow** - CrewAI automatically stores task outputs and retrieves relevant memories
- **Per-Agent Banks** - Optionally give each agent its own isolated memory bank
- **Reflect Tool** - Agents can explicitly reason over memories with disposition-aware synthesis
- **Simple Configuration** - Configure once, use everywhere
## Installation
```bash
pip install hindsight-crewai
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from hindsight_crewai import configure, HindsightStorage
from crewai.memory.external.external_memory import ExternalMemory
from crewai import Agent, Crew, Task
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)
crew = Crew(
agents=[Agent(role="Researcher", goal="Find information", backstory="...")],
tasks=[Task(description="Research AI trends", expected_output="Report")],
external_memory=ExternalMemory(
storage=HindsightStorage(bank_id="my-crew")
),
)
crew.kickoff()
```
That's it. CrewAI will automatically:
- **Query memories** at the start of each task
- **Store task outputs** to Hindsight after each task completes
Memories persist across crew runs, so your crew learns over time.
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL:
```python
configure(hindsight_api_url="http://localhost:8888")
```
See the [installation guide](../../developer/installation.md) for self-hosting setup.
## How It Works
The integration maps CrewAI's 3-method `Storage` interface to Hindsight's API:
| CrewAI | Hindsight | What happens |
|--------|-----------|--------------|
| `save(value, metadata, agent)` | `retain(bank_id, content, ...)` | Task output is stored. Hindsight extracts facts, entities, and relationships from the raw text. |
| `search(query, limit)` | `recall(bank_id, query, ...)` | CrewAI constructs a query from the task description. Hindsight runs semantic search, BM25, graph traversal, and reranking. |
| `reset()` | `delete_bank(bank_id)` | Wipes the bank and optionally recreates it with its original mission. |
CrewAI calls `search()` automatically at the start of each task and `save()` after each task completes.
## Configuration Options
```python
from hindsight_crewai import configure
configure(
hindsight_api_url="https://api.hindsight.vectorize.io", # Hindsight Cloud (default)
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: "low", "mid", "high"
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match: any/all/any_strict/all_strict
verbose=True, # Enable logging
)
```
### Per-Storage Overrides
Constructor arguments override global configuration:
```python
storage = HindsightStorage(
bank_id="my-crew",
budget="high",
max_tokens=8192,
tags=["team:alpha"],
)
```
## Bank Missions
Set a mission to guide how Hindsight processes and organizes memories:
```python
storage = HindsightStorage(
bank_id="my-crew",
mission="Track software architecture decisions, technical debt, and team preferences.",
)
```
## Per-Agent Memory Banks
Give each agent its own isolated memory bank:
```python
storage = HindsightStorage(
bank_id="my-crew",
per_agent_banks=True,
# Researcher -> "my-crew-researcher"
# Writer -> "my-crew-writer"
)
```
Or use a custom bank resolver for full control:
```python
storage = HindsightStorage(
bank_id="my-crew",
bank_resolver=lambda base, agent: f"{base}-{agent.lower()}" if agent else base,
)
```
> **ℹ️ Info**
>
When `per_agent_banks=True`, the automatic `search()` at task start queries the base bank (shared context), since CrewAI's `search()` method does not receive the agent parameter. For per-agent search isolation, create separate `HindsightStorage` instances per agent.
## Reflect Tool
CrewAI's storage interface only supports save/search/reset. To give agents access to Hindsight's `reflect` (disposition-aware memory synthesis), add it as a tool:
```python
from hindsight_crewai import HindsightReflectTool
reflect_tool = HindsightReflectTool(
bank_id="my-crew",
budget="mid",
reflect_context="You are helping a software team track decisions.",
)
agent = Agent(
role="Analyst",
goal="Analyze project history",
backstory="...",
tools=[reflect_tool],
)
```
When the agent calls this tool, it gets a synthesized, contextual answer based on all relevant memories rather than raw fact snippets.
## Full Example
A research crew that remembers findings across runs:
```python
from hindsight_crewai import configure, HindsightStorage, HindsightReflectTool
from crewai.memory.external.external_memory import ExternalMemory
from crewai import Agent, Crew, Task
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
)
storage = HindsightStorage(
bank_id="research-crew",
mission="Track technology research findings and comparisons.",
)
reflect_tool = HindsightReflectTool(bank_id="research-crew", budget="mid")
researcher = Agent(
role="Researcher",
goal="Research topics, building on prior knowledge.",
backstory="Before starting, use hindsight_reflect to check what you already know.",
tools=[reflect_tool],
)
writer = Agent(
role="Writer",
goal="Write summaries incorporating prior findings.",
backstory="Use hindsight_reflect to recall prior research.",
tools=[reflect_tool],
)
crew = Crew(
agents=[researcher, writer],
tasks=[
Task(description="Research the benefits of Rust", expected_output="Analysis", agent=researcher),
Task(description="Write an executive summary", expected_output="Summary", agent=writer),
],
external_memory=ExternalMemory(storage=storage),
)
# Run 1: researches Rust, stores findings
crew.kickoff()
# Run 2: recalls Rust research when comparing with Go
crew.tasks[0].description = "Compare Rust with Go"
crew.kickoff()
```
## API Reference
### Configuration
| Function | Description |
|----------|-------------|
| `configure(...)` | Set global connection and default settings |
| `get_config()` | Get current configuration |
| `reset_config()` | Reset configuration to None |
### Storage
| Parameter | Default | Description |
|-----------|---------|-------------|
| `bank_id` | required | Hindsight memory bank ID |
| `hindsight_api_url` | from config | Override API URL |
| `api_key` | from config | Override API key |
| `budget` | `"mid"` | Recall budget (low/mid/high) |
| `max_tokens` | `4096` | Max tokens for recall results |
| `tags` | `None` | Tags applied when storing |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `per_agent_banks` | `False` | Give each agent its own bank |
| `bank_resolver` | `None` | Custom `(bank_id, agent) -> bank_id` |
| `mission` | `None` | Bank mission for memory organization |
| `verbose` | `False` | Enable verbose logging |
### Reflect Tool
| Parameter | Default | Description |
|-----------|---------|-------------|
| `bank_id` | required | Hindsight memory bank ID |
| `budget` | `"mid"` | Reflect budget (low/mid/high) |
| `reflect_context` | `None` | Additional context for reasoning |
| `hindsight_api_url` | from config | Override API URL |
| `api_key` | from config | Override API key |
## Requirements
- Python >= 3.10
- crewai >= 0.86.0
- A running Hindsight API server
references/sdks/integrations/cursor-cli.md
# Cursor CLI
> **⚠️ Superseded by the Coding Agents plugin**
>
**The Cursor CLI integration** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install cursor-cli
```
Memory does not move automatically — the banks are scoped differently and this agent's history cannot be imported (it is kept in an internal database). See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
[View Changelog →](../../changelog/integrations/cursor-cli.md)
Persistent memory for [Cursor CLI](https://docs.cursor.com/en/cli/overview) using [Hindsight](https://vectorize.io/hindsight). Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn — no changes to your Cursor workflow required.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
```bash
# Install the CLI
pip install hindsight-cursor-cli
# Install the hooks (defaults to Hindsight Cloud)
hindsight-cursor-cli install --api-url https://api.hindsight.vectorize.io --api-token your-api-key
# Restart Cursor CLI — memory is live
```
The installer copies the hook scripts to `~/.cursor/hooks/cursor-cli/`, writes `~/.cursor/hooks.json` (merged with any existing entries), and creates `~/.hindsight/cursor-cli.json` for your personal config.
**Self-hosting alternative** — connect to a local `hindsight-embed` daemon by omitting the flags:
```bash
hindsight-cursor-cli install
```
To uninstall:
```bash
hindsight-cursor-cli uninstall
```
## Features
- **Auto-recall** — before each prompt, queries Hindsight for relevant memories and injects them as additional context (visible to the model, not the transcript)
- **Auto-retain** — after each response, and again on session end, stores the conversation to Hindsight for future recall
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory
- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content
- **Zero runtime dependencies** — the hook scripts are pure Python stdlib; the `pip install` only ships the one-time installer
## Architecture
The plugin uses four Cursor CLI hook events:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `sessionStart` | Warm up — verify Hindsight is reachable |
| `recall.py` | `beforeSubmitPrompt` | **Auto-recall** — query memories, inject as additional context |
| `retain.py` | `stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) |
| `session_end.py` | `sessionEnd` | **Final flush** — force a retain so the last turns aren't lost |
On `beforeSubmitPrompt`, the hook reads the prompt, queries Hindsight for the most relevant memories, and injects a context block. Cursor prepends this to the conversation before sending it to the model:
```
<hindsight_memories>
Relevant memories from past conversations...
Current time - 2026-03-27 09:14
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26)
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26)
</hindsight_memories>
```
On `stop` (and again on `sessionEnd`), the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously.
## Connection Modes
### 1. External API (recommended)
Connect to a running Hindsight server (cloud or self-hosted) via `~/.hindsight/cursor-cli.json`:
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
### 2. Local Daemon
Run `hindsight-embed` locally. The `session_start.py` hook detects it on `apiPort` (default `9077`). The daemon is not auto-started by the plugin — start it separately:
```bash
uvx hindsight-embed
```
Then leave `hindsightApiUrl` empty in your config and the plugin connects to `http://localhost:9077`.
## Configuration
Default config ships in `~/.cursor/hooks/cursor-cli/settings.json`. For personal overrides that survive updates, create `~/.hindsight/cursor-cli.json`. Most settings can also be overridden via environment variable.
**Loading order** (later entries win):
1. Built-in defaults
2. Plugin `settings.json` (at `~/.cursor/hooks/cursor-cli/settings.json`)
3. User config (`~/.hindsight/cursor-cli.json`)
4. Environment variables
---
### Connection
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Empty = local daemon. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. |
---
### Memory Bank
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"cursor-cli"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction — what to extract from coding conversations. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Which fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"cursor-cli"` | Agent name used in dynamic bank ID derivation. |
---
### Auto-Recall
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. |
| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. |
| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | Prior turns to include when building the recall query. `1` = latest prompt only. |
---
### Auto-Retain
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session (upserted by session ID). `"chunked"` sends sliding windows every N turns. |
| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. |
| `retainContext` | — | `"cursor-cli"` | Label identifying the source integration. Useful when multiple integrations write to the same bank. |
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. |
## Per-Project Memory
To give each project its own isolated memory bank, enable dynamic bank IDs:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "project"]
}
```
With this config, running Cursor in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path.
## Troubleshooting
**Hooks not firing**: Confirm `~/.cursor/hooks.json` exists and that `python3` is on your shell's `$PATH`. Re-run `hindsight-cursor-cli install` to rewrite the hook entries.
**No memories recalled**: Recall returns results only after something has been retained. Complete one Cursor session first, then start a new one.
**Memory not being stored**: `retainEveryNTurns` defaults to `10` — the `stop` hook only fires a retain every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/cursor-cli.json`. The `sessionEnd` hook also forces a final retain when you close the session.
**Debug mode**: Add `"debug": true` to `~/.hindsight/cursor-cli.json` to see what Hindsight is doing on each turn.
references/sdks/integrations/cursor.md
# Cursor
Biomimetic long-term memory for [Cursor](https://cursor.com) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant project context at session start and retains conversation transcripts after each task — adapted to Cursor's hook-based plugin architecture with MCP for on-demand tools.
[View Changelog →](../../changelog/integrations/cursor.md)
## How It Works
The Hindsight plugin uses two complementary mechanisms:
| | Plugin Hooks (automatic) | MCP Tools (on-demand) |
|--|--------------------------|----------------------|
| **Install** | `pip install hindsight-cursor && hindsight-cursor init` | Configured automatically by `init` |
| **Recall** | Session start — memories injected via `additionalContext` | Agent calls `recall` tool mid-session |
| **Retain** | Automatic on task stop | Agent calls `retain` tool explicitly |
| **Reflect** | Not available via hooks | Available as a tool |
| **Best for** | Ambient project memory with no user intervention | Targeted lookups and explicit memory operations |
Both are set up by a single `hindsight-cursor init` command. Use `--no-mcp` to skip the MCP integration if you only want hooks.
## Quick Start
```bash
# 1. Install the plugin
pip install hindsight-cursor
cd /path/to/your-project
# 2a. Connect to Hindsight Cloud (fastest — no local server needed)
hindsight-cursor init --api-url https://api.hindsight.vectorize.io --api-token YOUR_HINDSIGHT_API_TOKEN
# 2b. Or connect to a local Hindsight server
hindsight-cursor init --api-url http://localhost:8888
# 3. Fully quit and reopen Cursor — plugins load at startup
```
Sign up at [Hindsight Cloud](https://ui.hindsight.vectorize.io) to get a token, or start Hindsight locally with Docker:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
### What `init` does
- Copies plugin files into `.cursor-plugin/hindsight-memory/`
- Writes/merges project `.cursor/hooks.json` so Cursor registers the hooks. Cursor only loads hooks from `.cursor/hooks.json` (workspace) or `~/.cursor/hooks.json` (user) — files under `.cursor-plugin/` are not enough on their own. Hooks you already had are preserved, and re-running `init` replaces only Hindsight's own entries.
- Creates `~/.hindsight/cursor.json` with your connection settings (if the file does not already exist)
- Writes `.cursor/mcp.json` with the Hindsight MCP endpoint for on-demand tools
- Use `--force` to overwrite an existing installation
- Use `--no-mcp` to skip the MCP configuration
`hindsight-cursor uninstall` reverses all of it: the plugin directory, Hindsight's `.cursor/hooks.json` entries, the MCP server entry, the generated session rules file, and its `.gitignore` line.
> **🚨 Caution**
>
If you add the plugin to an already-open workspace, **fully quit Cursor and reopen it**. Plugins are loaded at startup — a simple window reload is not enough.
## Features
- **Session recall** — at the start of each session, queries Hindsight for relevant project memories and injects them as context via `additionalContext` (invisible to the chat, visible to the agent)
- **Auto-retain** — after every task completion, extracts and retains conversation content to Hindsight for long-term storage
- **On-demand MCP tools** — `recall`, `retain`, and `reflect` tools for explicit mid-session memory operations
- **On-demand recall skill** — use the `hindsight-recall` skill for manual memory lookups
- **Daemon management** — can auto-start/stop `hindsight-embed` locally or connect to an external Hindsight server
- **Dynamic bank IDs** — supports per-agent, per-project, or per-session memory isolation
- **Zero runtime dependencies** — plugin scripts use pure Python stdlib only
## Architecture
The plugin uses Cursor's hook system:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `sessionStart` | **Session recall** — query memories, inject as `additionalContext` |
| `retain.py` | `stop` | **Auto-retain** — extract transcript, POST to Hindsight |
| `retain.py` | `sessionEnd` | **Final flush** — retain whatever the turn window never got to |
The `sessionStart` hook fires once when a new Cursor session begins. It performs a broad project-level recall and injects relevant memories as hidden context.
`retain.py` runs on `sessionEnd` as well. Without that, every conversation shorter than `retainEveryNTurns` is lost outright: at the default of 10, a seven-turn chat fires `stop` seven times, the turn gate rejects all seven, and the session is never stored. `sessionEnd` bypasses the turn window so the tail of a session is always retained. The plugin tracks how many transcript messages it has already retained per session, so the overlap between the two events cannot double-store.
The `init` command also configures Cursor's MCP support (`.cursor/mcp.json`) to connect to Hindsight's MCP endpoint, giving the agent explicit `recall`, `retain`, and `reflect` tools for mid-session use.
Additionally, the plugin provides:
- **Skill** (`hindsight-recall`) — on-demand memory querying
- **Rule** (`hindsight-memory.mdc`) — always-on rule instructing the agent to leverage recalled memories and MCP tools
## Connection Modes
### 1. External API (recommended for production)
Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction.
```json
{
"hindsightApiUrl": "https://your-hindsight-server.com",
"hindsightApiToken": "your-token"
}
```
### 2. Local Daemon (auto-managed)
The plugin automatically starts and stops `hindsight-embed` via `uvx`. Requires an LLM provider API key for local fact extraction.
Set an LLM provider:
```bash
export OPENAI_API_KEY="sk-your-key"
# or
export ANTHROPIC_API_KEY="your-key"
```
The model is selected automatically by the Hindsight API. To override, set `HINDSIGHT_LLM_MODEL`.
### 3. Existing Local Server
If you already have `hindsight-embed` running, leave `hindsightApiUrl` empty and set `apiPort` to match your server's port. The plugin will detect it automatically.
## Configuration
All settings live in `~/.hindsight/cursor.json`. Every setting can also be overridden via environment variables. The plugin ships with sensible defaults — you only need to configure what you want to change.
**Loading order** (later entries win):
1. Built-in defaults (hardcoded in the plugin)
2. Plugin `settings.json` (ships with the plugin, at `CURSOR_PLUGIN_ROOT/settings.json`)
3. User config (`~/.hindsight/cursor.json` — recommended for your overrides)
4. Environment variables
---
### Connection & Daemon
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` (empty) | URL of an external Hindsight API server. When empty, the plugin uses a local daemon instead. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | Authentication token for the external API. Only needed when `hindsightApiUrl` is set. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port used by the local `hindsight-embed` daemon. |
| `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `"latest"` | Which version of `hindsight-embed` to install via `uvx`. |
| `embedPackagePath` | `HINDSIGHT_EMBED_PACKAGE_PATH` | `null` | Local path to a `hindsight-embed` checkout for development. |
---
### LLM Provider (local daemon only)
These settings configure which LLM the local daemon uses for fact extraction. They are **ignored** when connecting to an external API.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `llmProvider` | `HINDSIGHT_LLM_PROVIDER` | auto-detect | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama`. Auto-detects by checking for API key env vars. |
| `llmModel` | `HINDSIGHT_LLM_MODEL` | provider default | Override the default model for the chosen provider. |
| `llmApiKeyEnv` | — | provider standard | Name of the env var holding the API key, if non-standard. |
---
### Memory Bank
A **bank** is an isolated memory store — like a separate "brain."
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"cursor"` | The bank ID when `dynamicBankId` is `false`. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | generic assistant prompt | Description of the agent's identity and purpose. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from context fields (see `dynamicBankGranularity`). |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Fields to combine for dynamic bank IDs: `agent`, `project`, `session`, `channel`, `user`. |
| `bankIdPrefix` | — | `""` | String prepended to all bank IDs for namespacing. |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"cursor"` | Name used for the `agent` field in dynamic bank ID derivation. |
---
### Session Recall
Session recall runs once at the start of each session. It queries Hindsight for relevant project memories and injects them into the agent's context as invisible `additionalContext`.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for session recall. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search thoroughness: `"low"`, `"mid"`, `"high"`. |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. |
| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. |
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max character length of the query. |
---
### Auto-Retain
Auto-retain runs after the agent completes a task. It extracts the conversation transcript and sends it to Hindsight.
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | Retention strategy. `"full-session"` or `"chunked"`. |
| `retainEveryNTurns` | `HINDSIGHT_RETAIN_EVERY_N_TURNS` | `10` | How often to retain. `1` = every turn. |
| `retainOverlapTurns` | — | `2` | Extra turns included from the previous chunk for continuity. |
| `retainContext` | `HINDSIGHT_RETAIN_CONTEXT` | `"cursor"` | Source label for retained memories. |
| `retainToolCalls` | — | `false` | Whether to include tool calls in the retained transcript. |
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. Prefixed with `[Hindsight]`. |
## Verifying Plugin Hooks
The plugin writes a status file on every hook invocation — even when no memories are found or retain is skipped. Check them to confirm hooks are firing:
```bash
# Default location when CURSOR_PLUGIN_DATA is not set:
cat ~/.hindsight/cursor-state/state/last_recall.json
cat ~/.hindsight/cursor-state/state/last_retain.json
```
Each file contains:
- `saved_at` — timestamp of the last invocation
- `status` — one of `success`, `empty`, `skipped`, or `error`
- `bank_id` — which bank was used (present on `success` and `empty`)
- `mode` — always `plugin`
- `hook` — `sessionStart` for recall
- `result_count` (recall) or `message_count` (retain) — present on `success`
If `saved_at` updates when you use Cursor, the hooks are firing. Check `status` to understand what happened.
## Troubleshooting
**Plugin not activating**: Check that `.cursor-plugin/plugin.json` exists in the plugin directory. Enable `"debug": true` in `~/.hindsight/cursor.json` and check stderr output.
**Seeing "Ran Recall in hindsight" in the Agent Window?** That is MCP, not the plugin. Plugin-based recall is silent — it injects context via `additionalContext` without a visible tool call. If you see explicit Hindsight tool calls, you have MCP configured in `.cursor/mcp.json`. Both can work together.
**Recall returning no memories**: Verify the Hindsight server is reachable (`curl http://localhost:9077/health`). Memories need at least one retain cycle.
**Daemon not starting**: Ensure an LLM API key is set. Review daemon logs at `~/.hindsight/profiles/cursor.log`.
**High latency on session start**: The session recall hook has a 15-second timeout. Use `recallBudget: "low"` or reduce `recallMaxTokens`.
references/sdks/integrations/devin-desktop.md
# Devin Desktop
Long-term memory for [Devin Desktop](https://devin.ai) — the editor formerly known as Windsurf (Codeium) — powered by [Hindsight](https://vectorize.io/hindsight). One command connects Devin to the Hindsight MCP server and adds a rule telling the agent to use it — so it recalls relevant memory at the start of a task and retains durable facts as it goes. Recall happens at query time against your actual message, and from your seat it's automatic.
> **📝 Note**
>
Cognition rebranded Windsurf to Devin Desktop in June 2026. The MCP config still lives under `~/.codeium/windsurf/` — that's Devin Desktop's on-disk data directory and is unchanged by the rebrand. The workspace rule now lives under `.devin/rules/` (with `.windsurf/rules/` kept as a legacy fallback).
[View Changelog →](../../changelog/integrations/devin-desktop.md)
## How It Works
Devin Desktop supports two things this integration uses:
- **MCP servers:** Devin Desktop runs MCP servers configured under `mcpServers` in `~/.codeium/windsurf/mcp_config.json` and surfaces their tools to the agent. Remote servers connect via a `serverUrl` field with optional headers, so the Hindsight MCP endpoint connects directly — no bridge needed — giving the agent `recall` / `retain` / `reflect` tools.
- **Workspace rules** in `.devin/rules/`. A rule file with `trigger: always_on` frontmatter is included in every Devin request in the workspace. The integration writes a small rule there telling the agent to recall first and retain what it learns.
## Setup
```bash
pip install hindsight-devin-desktop
cd your-project
hindsight-devin-desktop init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory
```
`init` adds the `hindsight` MCP server to `~/.codeium/windsurf/mcp_config.json` (Devin Desktop's single global MCP config) and writes the recall/retain rule to `./.devin/rules/hindsight.md`. Reload Devin Desktop (or refresh MCP servers), and the `hindsight` server's tools become available.
Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a self-hosted server with `--api-url http://localhost:8888` (no token needed for an open local server). If your `mcp_config.json` isn't plain JSON, `init` prints the entry to paste rather than rewriting the file — or run `hindsight-devin-desktop init --print-only` anytime.
## Commands
| Command | Description |
| --- | --- |
| `hindsight-devin-desktop init` | Add the MCP server + recall/retain rule |
| `hindsight-devin-desktop status` | Show whether the server + rule are configured |
| `hindsight-devin-desktop uninstall` | Remove the server + rule |
## Note
Recall and retain run through MCP tools the agent calls, guided by the always-on rule. This makes recall query-time precise (no lag), with the tradeoff that it relies on the agent following the "recall first" instruction rather than the editor enforcing it.
See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/devin-desktop) for full configuration options.
references/sdks/integrations/dify.md
# Dify
Persistent memory for [Dify](https://dify.ai) workflows via [Hindsight](https://hindsight.vectorize.io). The Hindsight Dify plugin adds three tools — **Retain**, **Recall**, **Reflect** — that work alongside any other Dify node in workflows, chatflows, and agent apps.
[View Changelog →](../../changelog/integrations/dify.md)
## Why this matters
Dify is one of the most popular open-source LLM app platforms — visual workflow builder, prompt management, and a growing tool/plugin ecosystem. Until now, Dify workflows have been **stateless across runs**. With Hindsight tools you can:
- **Retain** every closed support ticket, sales-call transcript, or form submission into a memory bank
- **Recall** relevant context before an LLM step so the model sees prior history
- **Reflect** to ask synthesizing questions ("What do we know about this customer?") inside a workflow
## Installation
In your Dify dashboard go to **Plugins → Install Plugin**, then choose one of:
- **Marketplace** — search for `Hindsight` (once published)
- **GitHub** — install from `vectorize-io/hindsight` (path `hindsight-integrations/dify`)
- **Local** — upload the `.difypkg` archive
After install, the **Hindsight** plugin appears under **Tools** in the workflow editor.
## Setup
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
1. **Sign up** at [Hindsight Cloud](https://ui.hindsight.vectorize.io) (free tier) or [self-host](../../developer/installation.md)
2. **Get an API key** from the Hindsight dashboard
3. **In Dify**, open the Hindsight plugin and add credentials:
- **API URL** — defaults to `https://api.hindsight.vectorize.io` (Cloud); change for self-hosted
- **API Key** — your `hsk_...` key (optional for self-hosted unauthenticated instances)
## Tools
### Retain
Store content in a bank. Hindsight extracts facts asynchronously after the call returns.
| Field | Description |
|---|---|
| Bank ID | Memory bank to store in (auto-created on first use) |
| Content | Free-text content to retain |
| Tags | Optional comma-separated tags |
### Recall
Search a bank for memories relevant to a query. Returns a `results` array.
| Field | Description |
|---|---|
| Bank ID | Memory bank to search |
| Query | Natural-language query |
| Budget | `low` / `mid` / `high` |
| Max Tokens | Cap on returned tokens |
| Tags | Optional tag filter (comma-separated) |
### Reflect
Get an LLM-synthesized answer over the bank. Returns `text`.
| Field | Description |
|---|---|
| Bank ID | Memory bank |
| Query | Question to answer |
| Budget | `low` / `mid` / `high` |
## Example workflows
**Customer-support assistant** — every closed Zendesk ticket retains the resolution. Every new ticket starts with a Recall against the bank to surface similar past issues, then passes that context to OpenAI/Anthropic to draft the first reply.
**Sales-call coach** — Gong webhook → Hindsight Retain (call summary). Before each next prep call, Recall on the prospect's name to pull every prior touchpoint, then format into the daily prep doc.
**Knowledge-base agent** — every uploaded document is chunked and retained. The Dify chatflow uses Recall instead of vector-DB-only retrieval, getting fact-extracted, deduplicated, time-aware results.
## Source
- GitHub: [`hindsight-integrations/dify`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/dify)
references/sdks/integrations/eliza.md
# elizaOS
The `@vectorize-io/hindsight-eliza` package gives [elizaOS](https://github.com/elizaOS/eliza) agents long-term memory backed by [Hindsight](https://hindsight.vectorize.io).
It registers two components on your agent:
- A **provider** (`HINDSIGHT_MEMORY`) that recalls relevant memories and injects them into the prompt before each model call.
- An **evaluator** (`HINDSIGHT_RETAIN`) that retains conversation messages to Hindsight after each turn.
Both are enabled by default, layer on top of elizaOS's existing memory, and fail safe — a memory-service hiccup never blocks your agent from responding.
[View Changelog →](../../changelog/integrations/eliza.md)
## Installation
```bash
npm install @vectorize-io/hindsight-eliza @vectorize-io/hindsight-client
```
This package targets `@elizaos/core` `^1.7.2` (declared as a peer dependency).
## Usage
Create the plugin and add it to your character's plugin list:
```ts
import { createHindsightPlugin } from "@vectorize-io/hindsight-eliza";
import { Hindsight } from "@vectorize-io/hindsight-client";
const hindsightPlugin = createHindsightPlugin({
client: new Hindsight({ apiKey: process.env.HINDSIGHT_API_KEY }),
recall: { budget: "high", includeEntities: true },
retain: { tags: ["source:eliza"] },
});
export const character = {
name: "Ada",
plugins: [
// ...your other plugins
hindsightPlugin,
],
};
```
By default each agent message is stored under a memory **bank** keyed by the
message's `entityId`, giving every user an isolated memory store.
## Configuration
```ts
createHindsightPlugin({
client,
// Which memory bank to read/write. A string uses one fixed bank for all
// messages; a function derives the bank per message. Defaults to
// `message.entityId` (one bank per user).
bank: (message) => message.entityId,
recall: {
enabled: true, // set false to disable recall
budget: "mid", // "low" | "mid" | "high" — latency vs. depth
types: ["world", "experience"], // restrict to fact types
maxTokens: 1000, // cap recalled tokens
includeEntities: false, // include entity observations
heading: "# Relevant long-term memories", // prompt heading
},
retain: {
enabled: true, // set false to disable retain
async: true, // fire-and-forget; never adds turn latency
tags: ["source:eliza"], // tags on every retained memory
metadata: { env: "prod" },
includeAgentMessages: false, // also store the agent's own replies
},
});
```
### Using only recall or only retain
Disable either side with `recall.enabled: false` or `retain.enabled: false`. You
can also build the components directly with `createHindsightProvider` and
`createHindsightEvaluator` if you want to wire them into a plugin yourself.
## How it works
| Component | elizaOS seam | When it runs | What it does |
| --- | --- | --- | --- |
| `HINDSIGHT_MEMORY` | Provider | During prompt composition, before the model call | Calls Hindsight `recall` with the incoming message and injects the results into context |
| `HINDSIGHT_RETAIN` | Evaluator | After the agent processes the turn | Calls Hindsight `retain` to persist the message (and optionally the agent's replies) |
references/sdks/integrations/eve.md
# Eve
Long-term memory for [Vercel Eve](https://eve.dev) agents using [Hindsight](https://vectorize.io/hindsight). The `@vectorize-io/hindsight-eve` package is a provider for Eve's [memory slots](https://eve.dev/docs/memory): relevant memory is recalled on the live user message before every turn, each exchange is captured after, and every scope (user, tenant, channel) gets its own isolated Hindsight bank — **without the model ever choosing to call a tool.**
[View Changelog →](../../changelog/integrations/eve.md)
## How it works
Eve resolves and locks a scope for every turn, then calls the provider at fixed points:
| Phase | What Hindsight does |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `turn.started` | Recalls memories relevant to the user's message and places them in context, attributed to the slot |
| `turn.completed` | Retains the user message and the assistant's final reply (idempotent per Eve operation id) |
| tools | Exposes `<slot>__reflect`, so the model can ask long-term memory a question when it needs to |
Every read and write is partitioned by the locked scope key: one Hindsight bank per scope, auto-created on first use. Two tenants can never see each other's memory.
## Install
```bash
eve add memory/hindsight
```
That installs `@vectorize-io/hindsight-eve` and writes the memory slot shown below. To wire it up yourself instead:
```bash
npm install @vectorize-io/hindsight-eve
```
`eve` (>= 0.45.1) is a peer dependency you already have in an Eve project.
## Quick Start
One file, which is what `eve add` writes:
```ts
// agent/memory/hindsight.ts
import { hindsightMemory } from "@vectorize-io/hindsight-eve";
import { defineMemory } from "eve/memory";
import { byPrincipal } from "eve/memory/scope";
export default defineMemory({
description: "Long-term memory about the current user.",
provider: hindsightMemory(),
scope: byPrincipal,
});
```
The provider reads its connection from the environment:
| Env var | Purpose |
| ------------------- | ------------------------------------------------------------------------ |
| `HINDSIGHT_API_KEY` | Bearer token sent as `Authorization: Bearer <key>` |
| `HINDSIGHT_API_URL` | Hindsight REST base (defaults to Hindsight Cloud) |
| `HINDSIGHT_BANK_ID` | _Optional._ Pin every scope to one shared bank (single-user agents only) |
### Hindsight Cloud
Set `HINDSIGHT_API_KEY` from your [Hindsight Cloud](https://hindsight.vectorize.io) dashboard. `HINDSIGHT_API_URL` defaults to `https://api.hindsight.vectorize.io`, so no URL is needed.
### Self-hosted
```ts
// A local server with no auth:
provider: hindsightMemory({ apiUrl: "http://localhost:8000", apiKey: null }),
```
## Scope → bank
`scope` is Eve's — `byPrincipal` gives each authenticated caller their own memory; a custom resolver can scope by tenant, channel, or anything else in trusted session metadata (see [Eve's memory docs](https://eve.dev/docs/memory)). Hindsight maps each locked scope to a bank:
- **Default:** the bank is the scope key (`memscope1_…`), an opaque digest Eve derives from the namespace and scope. Isolation is automatic.
- **Custom:** pass a resolver to name banks yourself, e.g. ``bankId: (scope) => `eve-${scope.value}` `` for human-readable bank names in the Hindsight dashboard.
- **Shared:** a string `bankId` (or `HINDSIGHT_BANK_ID`) pins every scope to a single bank. Only do this for single-user agents — it disables per-scope isolation.
## Options
```ts
hindsightMemory({
apiUrl, // REST base; defaults to HINDSIGHT_API_URL, then Cloud
apiKey, // bearer token; null = no auth (local dev)
bankId, // string | (scope) => string — see "Scope → bank"
budget, // "low" | "mid" | "high" — recall result budget (default "mid")
maxTokens, // recall token budget (default 1024)
recallQuery, // query when the turn has no user text (default: a broad profile query)
context, // `context` tag written on retained items (default "eve")
includeAssistantReply, // also retain the assistant's reply (default true)
capture, // retain each turn (default true; false = recall-only)
tools, // expose the `reflect` tool (default true)
timeoutMs, // HTTP timeout (default 15000)
onError, // (err, phase) => void — failures degrade silently (default console.warn)
});
```
## Notes
- Recall never fails a turn: a Hindsight error is reported through `onError` and the turn runs without memory. Captures run asynchronously and never block a reply.
- Each turn's recall is one message with a stable id, so it **supersedes** the previous turn's block instead of piling up in session history — including after compaction.
- Captures are keyed by Eve's `operationId`; a replayed capture replaces the earlier document rather than storing it twice.
## Known issue: capture on eve 0.51.0 – 0.68.x
eve 0.51.0 stopped passing the turn's history to `turn.completed` (a regression from
[vercel/eve#2690](https://github.com/vercel/eve/pull/2690)), so **no provider's
`capture["turn.completed"]` runs** on those versions — memory is recalled but nothing new is
stored. Tracked in [vercel/eve#3223](https://github.com/vercel/eve/issues/3223) and fixed by
[vercel/eve#3985](https://github.com/vercel/eve/pull/3985), which ships in eve 0.69.0 — upgrade
to it. Recall, the `reflect` tool, and compaction capture are unaffected. On an older eve, either
pin `eve@0.50.0`, or keep the
deprecated `hindsightRetainHook()` in `agent/hooks/hindsight.ts` next to the provider — eve's hook
events still fire — giving both the same string `bankId` so they share a bank.
## Verify
Run your agent. Tell it a durable preference in one session ("whenever you write me code, use Python with full type hints and no comments"). Start a **fresh** session as the same user and ask for something — the agent applies the remembered preference, because the memory was recalled before the model ran, with no tool call.
## Migrating from 0.2
0.2 wired memory through two authored files (`agent/instructions/hindsight.ts` with `hindsightMemory()` and `agent/hooks/hindsight.ts` with `hindsightRetainHook()`), before Eve had a memory provider contract. Replace both with the single `agent/memory/hindsight.ts` above. The old entrypoints still ship as `hindsightAutoRecall()` and `hindsightRetainHook()` (deprecated, removed in the next release); `hindsightMemory()` now returns the provider.
What changes: recall uses the live user message instead of a fixed broad query, memory is isolated per scope instead of one bank for everyone, and recalled context arrives as a slot-attributed message rather than system instructions.
The registry item moved with it: `eve add extension/hindsight` is now `eve add memory/hindsight`
([vercel/eve#4963](https://github.com/vercel/eve/pull/4963)). The old path still resolves, but if
you installed with it before, delete `agent/instructions/hindsight.ts` and
`agent/hooks/hindsight.ts` first — a leftover 0.2-shaped instructions file fails `eve build` with
`Unknown key "recall"`.
## Links
- [Hindsight docs](https://hindsight.vectorize.io)
- [Eve memory](https://eve.dev/docs/memory) · [Build a memory provider](https://github.com/vercel/eve/blob/main/docs/memory/custom-provider.md)
references/sdks/integrations/flowise.md
# Flowise
Persistent memory for [Flowise](https://flowiseai.com) chatflows and agents via [Hindsight](https://hindsight.vectorize.io). Three Tool nodes — **Hindsight Retain**, **Hindsight Recall**, **Hindsight Reflect** — drop into any flow alongside your other LangChain tools.
[View Changelog →](../../changelog/integrations/flowise.md)
## Why this matters
Flowise is the LangChain-derived visual builder for non-developers and developers alike. Until now, Flowise chatflows have been **stateless across sessions**. With Hindsight tool nodes you can:
- **Retain** every closed conversation, support ticket, or form submission into a memory bank
- **Recall** relevant context before an LLM step so the model sees prior history
- **Reflect** to ask synthesizing questions ("What do we know about this customer?") inside a flow
## Installation
Flowise distributes nodes only inside its main monorepo, so installation today means using a Flowise checkout with the Hindsight nodes copied in. The user-facing distribution will be the upstream PR to `FlowiseAI/Flowise`.
```bash
git clone https://github.com/FlowiseAI/Flowise.git
cd Flowise
# Copy the three tool nodes
cp -r /path/to/hindsight/hindsight-integrations/flowise/nodes/tools/Hindsight* \
packages/components/nodes/tools/
# Copy the credential class
cp /path/to/hindsight/hindsight-integrations/flowise/credentials/HindsightApi.credential.ts \
packages/components/credentials/
# Add the client dep
cd packages/components && pnpm add @vectorize-io/hindsight-client
cd ../.. && pnpm install && pnpm build
pnpm start # opens http://localhost:3000
```
## Setup
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — no infrastructure to run. Skip straight to creating your credential below.
1. **Sign up** at [Hindsight Cloud](https://ui.hindsight.vectorize.io) (free tier) or [self-host](../../developer/installation.md)
2. **Get an API key** from the Hindsight dashboard
3. **In Flowise**, create a new credential of type **Hindsight API**:
- **API URL** — defaults to `https://api.hindsight.vectorize.io` (Cloud); change for self-hosted
- **API Key** — your `hsk_...` key (optional for self-hosted unauthenticated instances)
## Tools
Each Hindsight tool node returns a LangChain `DynamicStructuredTool`, so it slots into any agent that accepts tools.
### Hindsight Retain
Stores content in a memory bank. Hindsight extracts facts asynchronously after the call returns.
| Field | Description |
|---|---|
| Default Bank ID | Memory bank to retain into when the agent doesn't pass one |
Tool input schema (the agent passes these): `bankId`, `content`, optional `tags`.
### Hindsight Recall
Searches a bank for memories relevant to a query. Returns ranked results.
| Field | Description |
|---|---|
| Default Bank ID | Memory bank to search when the agent doesn't pass one |
| Default Budget | `low` / `mid` / `high` |
Tool input schema: `bankId`, `query`, optional `budget`, `maxTokens`, `tags`.
### Hindsight Reflect
Returns an LLM-synthesized answer over the bank.
| Field | Description |
|---|---|
| Default Bank ID | Memory bank to reflect on when the agent doesn't pass one |
| Default Budget | `low` / `mid` / `high` |
Tool input schema: `bankId`, `query`, optional `budget`.
## Example chatflow
**Conversational support agent**
1. **ChatOpenAI** (or any chat LLM)
2. **Conversational Agent** with three tools attached: Hindsight Retain + Hindsight Recall + Hindsight Reflect, all sharing the same `hindsightApi` credential
3. Set Default Bank ID to something like `user-${sessionId}` on each tool
4. The agent learns to call Recall before answering and Retain after meaningful exchanges. Use Reflect for "what do we know about this user?" prompts.
## Source
- GitHub: [`hindsight-integrations/flowise`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/flowise)
references/sdks/integrations/gemini-spark.md
# Gemini Spark
Long-term memory for [Gemini Spark](https://blog.google/products/gemini/gemini-spark/), Google's always-on agentic assistant, via [Hindsight](https://vectorize.io/hindsight)'s MCP server.
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — get an API key instantly, no infrastructure to run. The setup below works with both Cloud and self-hosted Hindsight.
## How It Works
Spark runs on Google's cloud infrastructure. Unlike OpenClaw or Claude Code, there is **no plugin host** where Hindsight code runs alongside Spark's agent loop. The only third-party extension surface is MCP:
| Capability | Spark support |
|---|---|
| Hook-based auto-recall (prepend context to the prompt) | Not available — Spark's prompt assembly is private. The agent calls `recall` when its planner judges it useful. |
| Hook-based auto-retain (save transcripts on turn end) | Not available — third parties don't see Spark's transcripts. The agent calls `retain` when it learns something worth keeping. |
| MCP tools (`recall`, `retain`, etc.) | Yes — Spark calls Hindsight's MCP tools via its built-in MCP client. |
## Architecture
### With Hindsight Cloud (recommended)
```
Gemini Spark (Google Cloud)
|
| HTTPS + MCP (Streamable HTTP)
v
Hindsight Cloud (api.hindsight.vectorize.io)
```
### With self-hosted Hindsight
```
Gemini Spark (Google Cloud)
|
| HTTPS + OAuth 2.1 (Spark's MCP client)
v
Cloudflare Worker — cloudflare-oauth-proxy
- OAuth authorization server
- Auth bridging to Hindsight API token
|
| HTTPS + Cloudflare Tunnel
v
Self-hosted Hindsight + hindsight-embed (MCP server)
```
## Setup
### Option 1: Hindsight Cloud (recommended)
1. Sign up at [vectorize.io/hindsight](https://vectorize.io/hindsight/) and create a memory bank
2. Copy your API key from the dashboard
3. Register Hindsight in Spark's MCP config (see below)
### Option 2: Self-hosted
1. Deploy a Hindsight instance and run the `hindsight-embed` MCP server pointed at it, exposed on a public HTTPS endpoint
2. Deploy the [`cloudflare-oauth-proxy`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cloudflare-oauth-proxy) — Spark only speaks OAuth 2.1 to MCP servers
### Register Hindsight in Spark
Spark (via Antigravity 2.0) reads MCP servers from one of two places:
- **Hosted agent / Spark cloud:** an `antigravity.yaml` manifest — see [`manifest.example.yaml`](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/gemini-spark/manifest.example.yaml)
- **Antigravity desktop / IDE:** `~/.gemini/antigravity/mcp_config.json` — see [`mcp_config.example.json`](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/gemini-spark/mcp_config.example.json)
Replace the placeholder URL with your Hindsight Cloud endpoint or OAuth proxy URL.
## Verifying Setup
Prompt Spark with something that should trigger the memory tools:
- "What were my open API decisions from last week?" → `recall`
- "Remember that I prefer TypeScript strict mode for new projects." → `retain`
## Example Configs
Both live in the [integration directory](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/gemini-spark):
- [`manifest.example.yaml`](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/gemini-spark/manifest.example.yaml) — Antigravity 2.0 agent manifest snippet
- [`mcp_config.example.json`](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/gemini-spark/mcp_config.example.json) — Desktop/IDE MCP config for local development
references/sdks/integrations/github-copilot.md
# GitHub Copilot
Long-term memory for [GitHub Copilot](https://github.com/features/copilot) in VS Code, powered by [Hindsight](https://vectorize.io/hindsight). One command connects Copilot's agent mode to the Hindsight MCP server and adds a recall/retain rule — so Copilot recalls relevant memory at the start of a task and retains durable facts as it works.
[View Changelog →](../../changelog/integrations/github-copilot.md)
## How It Works
VS Code Copilot supports two things this integration uses:
- **MCP servers** via `.vscode/mcp.json` (agent mode), including **HTTP servers** with headers — so the Hindsight MCP endpoint connects directly:
```json
{
"servers": {
"hindsight": {
"type": "http",
"url": "https://api.hindsight.vectorize.io/mcp/my-project/",
"headers": { "Authorization": "Bearer hsk_..." }
}
}
}
```
- **`.github/copilot-instructions.md`**, which Copilot applies to every chat in the workspace — that's where the recall/retain rule lives.
## Setup
```bash
pip install hindsight-copilot
cd your-project
hindsight-copilot init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
```
`init` merges the `servers` entry into `./.vscode/mcp.json` and writes the rule into `./.github/copilot-instructions.md`. Reload VS Code, open Copilot Chat in **agent mode**, and start the `hindsight` MCP server from the chat's tools menu.
Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or a self-hosted server with `--api-url http://localhost:8888` (no token needed for an open local server). If `mcp.json` has comments, `init` prints the snippet to paste instead — or run `hindsight-copilot init --print-only` anytime.
## Commands
| Command | Description |
| --- | --- |
| `hindsight-copilot init` | Add the MCP server + recall/retain rule |
| `hindsight-copilot status` | Show whether the server + rule are configured |
| `hindsight-copilot uninstall` | Remove the server + rule |
See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/github-copilot) for full configuration options.
references/sdks/integrations/google-adk.md
# Google ADK
Persistent long-term memory for [Google ADK](https://adk.dev/) agents via Hindsight. The `hindsight-google-adk` package gives you two complementary patterns:
- **`HindsightMemoryService`** — Implements ADK's `BaseMemoryService`. Pass it to `Runner(memory_service=...)` and sessions are automatically retained when they end; agents calling `search_memory` get matching results back from Hindsight.
- **`create_hindsight_tools(...)`** — Returns a list of ADK `FunctionTool`s (`hindsight_retain`, `hindsight_recall`, `hindsight_reflect`) the model can call directly inside a turn.
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key. The integration points at production by default — no local server to manage.
[View Changelog →](../../changelog/integrations/google-adk.md)
## Installation
```bash
pip install hindsight-google-adk
```
## Automatic Memory (BaseMemoryService)
```python
import asyncio
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from hindsight_google_adk import HindsightMemoryService
memory = HindsightMemoryService.from_url(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
)
agent = LlmAgent(name="assistant", model="gemini-2.0-flash")
runner = Runner(
app_name="my-app",
agent=agent,
session_service=InMemorySessionService(),
memory_service=memory,
)
# ... use runner.run_async(...) as normal. Memory is automatic.
```
When a session ends, `add_session_to_memory` retains all of its events to a Hindsight bank derived from `(app_name, user_id)`. When an agent calls `search_memory`, the integration runs a Hindsight recall against the same bank and returns the results as ADK `MemoryEntry` objects.
### Bank ID derivation
By default, each `(app_name, user_id)` pair gets its own bank: `"{app_name}::{user_id}"`. Override with `bank_id_template`:
```python
# Per-user, shared across apps
HindsightMemoryService.from_url(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
bank_id_template="user::{user_id}",
)
# Static bank (shared across all users)
HindsightMemoryService.from_url(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
bank_id_template="my-shared-bank",
)
```
## Explicit Tools (FunctionTool)
Give the agent direct retain / recall / reflect tools when you want it to decide when to call them mid-turn:
```python
from google.adk.agents import LlmAgent
from hindsight_google_adk import create_hindsight_tools
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
)
agent = LlmAgent(
name="assistant",
model="gemini-2.0-flash",
tools=tools,
)
```
The agent gets three tools (toggle with `include_retain` / `include_recall` / `include_reflect`):
- **`hindsight_retain(content)`** — store information to long-term memory
- **`hindsight_recall(query)`** — search memory and return a numbered list of matches
- **`hindsight_reflect(query)`** — synthesize a coherent answer from memory
## Global Configuration
For app-wide defaults, call `configure(...)` once at startup. Subsequent `HindsightMemoryService.from_url()` / `create_hindsight_tools()` calls use it as a fallback:
```python
from hindsight_google_adk import configure
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key=None, # falls back to HINDSIGHT_API_KEY env var
budget="mid",
max_tokens=4096,
bank_id_template="{app_name}::{user_id}",
)
```
## Configuration Reference
| Argument | Default | Description |
|---|---|---|
| `hindsight_api_url` | `https://api.hindsight.vectorize.io` | Hindsight API URL. Cloud by default. |
| `api_key` | `HINDSIGHT_API_KEY` env | Bearer token for Hindsight Cloud. |
| `bank_id_template` | `"{app_name}::{user_id}"` | Format string used to derive the bank id from ADK's `app_name` / `user_id`. |
| `budget` | `"mid"` | Recall budget level: `low`/`mid`/`high`. |
| `max_tokens` | `4096` | Max tokens for recall results. |
| `tags` | `None` | Tags added to every retained document. `app:<name>` and `user:<id>` are always added. |
| `recall_tags` | `None` | Tags appended to recall queries. `user:<id>` is always added. |
| `recall_tags_match` | `"any"` | Tag match mode: `any` / `all` / `any_strict` / `all_strict`. |
| `mission` | `None` | If set, the bank is created (idempotently) on first use with this fact-extraction mission. |
| `context` | `"google-adk"` | Source label attached to retained content. |
## Production Patterns
### Tagging memories per environment
```python
HindsightMemoryService.from_url(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
tags=["env:prod"],
recall_tags=["env:prod"],
)
```
`app:` and `user:` tags are always added on top of these.
### Self-hosted Hindsight
```python
HindsightMemoryService.from_url(
hindsight_api_url="http://localhost:8888",
)
```
No `api_key` needed for unauthenticated local servers.
### Combining memory service + tools
You can use both at once — `Runner(memory_service=HindsightMemoryService(...))` for automatic retain on session end, and `tools=create_hindsight_tools(...)` for mid-turn agent-driven recall. They share a bank when the bank ids align.
## Requirements
- Python 3.10+
- `google-adk>=2.0`
- `hindsight-client>=0.4.0`
references/sdks/integrations/grok-bot.md
# Grok Bot
Long-term memory for [Grok Bot](https://x.ai) and Cursor, shared across your Bots and your other AI tools.
Each Grok Bot keeps its own conversation and learned context, so one Bot cannot see what another worked out, and a scheduled routine starts every run from zero. [Hindsight](https://vectorize.io/hindsight) gives each Bot its own memory bank plus one shared bank for the whole team, and lets Bots read the banks your other agents (Claude Code, ChatGPT, Hermes, OpenClaw) already write to.
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io/signup). The plugin connects with OAuth, so there is no API key to copy and nothing to configure.
## Install
> **ℹ️ Marketplace listing in review**
>
Hindsight is not in the Grok Bot or Cursor marketplace yet. Set it up by hand until the listing is approved. This page will be updated when it lands.
### Grok Bot
Paste [`connect-prompt.md`](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/grok-bot/connect-prompt.md) to any Bot. It tells the Bot to add Hindsight as a custom connector, walks it through the OAuth sign-in, and then carries the same rules the skills carry: which bank to write to, when to recall, how to hand work to another Bot, and which tools never to call.
Name the Bot first. Its name becomes its bank id, so a Bot still called "Grok Bot" is asked for a name before setup continues.
### Cursor
Copy the plugin into Cursor's local plugin folder, then reload:
```bash
git clone https://github.com/vectorize-io/hindsight
mkdir -p ~/.cursor/plugins/local
cp -r hindsight/hindsight-integrations/grok-bot ~/.cursor/plugins/local/hindsight
```
Restart Cursor, or run **Developer: Reload Window**.
## Memory Banks
| Bank | Holds | Written by |
|---|---|---|
| `grok-bot::<bot-name>` | What one Bot learns in its own work. A Bot named "Sales Researcher" uses `grok-bot::sales-researcher`. | That Bot |
| `cursor::<project-name>` | What a Cursor agent learns in one project, named after the open workspace folder. The plugin installs into Cursor whenever you add it in Grok Bot. | Cursor agents in that project |
| `grok-bot::shared` | What every Bot should know, handoffs between Bots, and the "About the user" profile. | Any Bot |
| Your other banks | Memory from your other AI tools. | Those tools only; Bots read them |
The plugin connects to the root [MCP server](../../developer/mcp-server.md), `https://api.hindsight.vectorize.io/mcp`, which reaches every bank in the organization you authorize.
## How It Works
In Grok Bot, a plugin's hooks never run: the Bot installs a plugin's MCP server and skills, and nothing else. So memory here is driven by skills, and each skill's description tells the Bot when to use it.
| Skill | Use |
|---|---|
| `memory-setup` | Connect, verify with `list_banks`, create the banks and the profile |
| `memory-context` | Recall relevant memory before starting any task |
| `memory-retain` | Save outcomes, decisions and preferences when a task finishes |
| `memory-reflect` | Answer questions about history and past decisions with `reflect` |
| `routine-memory` | Writes recall and retain steps into each routine it creates; reads the last run before a routine starts and records this run when it ends |
| `bot-handoff` | Keep a durable record of work passed between Bots, which Bots that were never messaged can also pick up |
The skills never call `delete_bank`, `clear_memories`, `invalidate_memory`, `delete_document` or `delete_mental_model`. If something should be deleted, the Bot tells you and you do it from the Hindsight dashboard.
## Self-Hosted Hindsight
Grok Bot needs a public HTTPS MCP endpoint with OAuth 2.1 discovery and dynamic client registration. Put the [`cloudflare-oauth-proxy`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cloudflare-oauth-proxy) in front of your instance, check it with the [Meta Muse integration's](./meta-muse.md) preflight tool (Grok Bot connects the same way), then change the `url` in the plugin's `mcp.json`.
## Verifying Setup
Ask a Bot:
- "Set up Hindsight memory." → `memory-setup` lists your banks and creates the Bot's own
- "What do you remember about our vendor shortlist?" → `memory-context` recalls before answering
- "How do we usually handle renewals?" → `memory-reflect`
Then check the Hindsight dashboard: the Bot's bank and `grok-bot::shared` should appear, with memories tagged `source:grok-bot`.
references/sdks/integrations/haystack.md
# Haystack
Persistent long-term memory for [Haystack](https://haystack.deepset.ai/) agents via Hindsight. The `hindsight-haystack` package gives you two complementary patterns:
- **`create_hindsight_tools(...)`** — Returns a list of Haystack `Tool`s (`retain_memory`, `recall_memory`, `reflect_on_memory`) the model can call directly inside a turn.
- **`HindsightMemoryWrapper`** — A Haystack `Toolset` that bundles the same tools and adds optional **auto-recall** (inject relevant memories into the system prompt before each turn) and **auto-retain** (store user + assistant messages after each turn).
[View Changelog →](../../changelog/integrations/haystack.md)
## Installation
```bash
pip install hindsight-haystack
```
## Quick Start
```python
from hindsight_client import Hindsight
from hindsight_haystack import create_hindsight_tools
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
client = Hindsight(base_url="http://localhost:8888")
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
mission="Track user preferences",
)
agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
tools=tools,
system_prompt=(
"You are a helpful assistant with long-term memory. "
"Use retain_memory to store important facts. "
"Use recall_memory to search memory before answering."
),
)
result = agent.run(messages=[ChatMessage.from_user("Remember that I prefer dark mode")])
print(result["messages"][-1].text)
```
## Automatic Memory with HindsightMemoryWrapper
For automatic recall and retain without relying on the agent to call tools:
```python
from hindsight_haystack import HindsightMemoryWrapper
toolset = HindsightMemoryWrapper(
client=client,
bank_id="user-123",
mission="Track user preferences",
auto_recall=True, # Inject memories into the system prompt before each turn
auto_retain=True, # Store user + assistant messages after each turn
)
agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
tools=toolset,
system_prompt="You are a helpful assistant with long-term memory.",
)
# Use toolset.run() for automatic memory behavior
result = toolset.run(agent, messages=[ChatMessage.from_user("I prefer dark mode")])
```
## Selective Tools
```python
# Only retain + recall (no reflect)
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_reflect=False,
)
```
## Configuration
Call `configure()` once to set connection defaults so you can omit `client=`/`hindsight_api_url=` on every call:
```python
from hindsight_haystack import configure
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key",
budget="mid",
tags=["source:haystack"],
context="my-app",
mission="Track user preferences",
)
tools = create_hindsight_tools(bank_id="user-123")
```
The API URL defaults to Hindsight Cloud (`https://api.hindsight.vectorize.io`), and the API key falls back to the `HINDSIGHT_API_KEY` environment variable.
## Requirements
- Python 3.10+
- `haystack-ai >= 2.12.0`
- `hindsight-client >= 0.4.0`
## Prerequisites
A running Hindsight instance:
**Hindsight Cloud (recommended):** [Sign up](https://ui.hindsight.vectorize.io) — no self-hosting required.
**Self-hosted:**
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-api-key
hindsight-api # starts on http://localhost:8888
```
references/sdks/integrations/hermes-desktop.md
# Hermes Desktop
Configure [Hindsight](https://vectorize.io/hindsight) as the memory provider for the **[Hermes](https://github.com/NousResearch/hermes-agent) desktop app** — entirely from Settings. No `config.json`, no `.env`, no terminal. Pick a mode, paste an API key, and Hermes remembers across every session.
> **💡 Tip**
>
Prefer the command line, or running Hermes as a CLI/gateway? See the [Hermes Agent integration](hermes.md) for the `hermes memory setup` wizard, plugin architecture, and the full configuration reference.
> **📝 Hindsight now installs from the Hermes plugin catalog**
>
Nous Research moved every memory provider out of the Hermes core tree. The desktop app handles this for you: it installs the catalog plugin the first time an agent starts, and your settings, bank and API key are unchanged. Nothing in this guide changes. See What Changes Now That Hindsight Is a Hermes Plugin if you also use the CLI.
## Setup
**1. Open Settings → Memory & Context.** In the **Memory Provider** dropdown, choose **Hindsight**.
**2. Fill in the Hindsight settings panel.** Selecting Hindsight reveals its configuration fields:
| Field | What it does | Default |
| ----------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------ |
| **Mode** | `Cloud` (just needs an API key) or `Local External` (connect to an existing Hindsight instance) | `Cloud` |
| **API key** | Authenticates with the Hindsight API — stored as a write-only secret | — |
| **API URL** | The Hindsight endpoint | `https://api.hindsight.vectorize.io` |
| **Bank ID** | Which memory bank this Hermes profile reads and writes | `hermes` |
| **Recall budget** | How hard recall works each turn: `low` / `mid` / `high` | `mid` |
**3. Click Save.** That's it — Hermes now has persistent long-term memory.
## Connection Modes
### Cloud (recommended)
The fast path. Choose **Cloud**, then paste an API key from [ui.hindsight.vectorize.io/connect](https://ui.hindsight.vectorize.io/connect). Nothing to host — Hindsight Cloud handles storage, extraction, and retrieval.
### Local External
Already running your own Hindsight instance (Docker or self-hosted)? Choose **Local External** and set the **API URL** to your instance (for example `http://localhost:8888`). Your memory never leaves your infrastructure.
## How Memory Is Stored
Your settings are saved to the right place automatically:
- **API key** goes to the secret store — it's never read back into the form (you'll see an _API key set_ badge once it's saved).
- **Mode, API URL, Bank ID, and Recall budget** are written to your Hermes profile config.
Each profile points at one **Bank ID**, so memory is isolated per profile and follows you across every machine that profile runs on.
## Next Steps
- **Hindsight Cloud (free):** [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io)
- **Hermes CLI / plugin setup:** [Hermes Agent integration](hermes.md)
- **Hermes Agent:** [github.com/NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
references/sdks/integrations/hermes.md
{/* GENERATED from hindsight-integrations/hermes/README.md — edit that file, then run
node hindsight-docs/scripts/sync-hermes-doc.mjs */}
> **💡 Tip**
>
Using the **Hermes desktop app**? You can select and configure Hindsight entirely in Settings — no
terminal required. See [Hermes Desktop](hermes-desktop.md).
> **⚠️ Deprecated: the standalone `hindsight-hermes` plugin**
>
The old **`hindsight-hermes`** pip plugin (installed into the Hermes virtual environment and
registered through the `hermes_agent.plugins` entry point) is **deprecated** — on current Hermes
builds its tools fail with `{"error": "Timeout context manager should be used inside a task"}`.
Follow Migrate hindsight-hermes to Native Hermes Memory
to switch over while keeping the same memory bank.
Long-term memory with knowledge graph, entity resolution, and multi-strategy retrieval. Supports cloud, local embedded, and local external modes.
A [Hermes Agent](https://github.com/NousResearch/hermes-agent) memory-provider plugin, installed from
the Hermes plugin catalog. It used to ship inside Hermes as `plugins/memory/hindsight/`; Nous Research
moved every memory provider out of the core tree, so it is now maintained by the Hindsight team in
[vectorize-io/hindsight](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes)
and the catalog pins it from there.
[View Changelog →](../../changelog/integrations/hermes.md)
## Install
Hindsight is in the Hermes plugin catalog, so the name is all you need:
```bash
hermes plugins install hindsight
hermes memory setup # select "hindsight"
```
`plugins install` asks for confirmation before enabling the plugin. Pass `--enable` (or
`--no-enable`) to skip the prompt, which is what you want in a script or an unattended run.
Dependencies in `pyproject.toml` are installed into the Hermes venv and survive `hermes update`.
Hermes asks before preparing them: an update that finds new plugin dependencies prints
`<plugin> declares Python dependencies` and waits for a yes. An unattended `hermes update` will
sit at that prompt rather than finish.
`hermes plugins enable` is *not* what activates a memory provider — Hermes treats providers as
`kind: exclusive` and its plugin-enable gate deliberately skips them. A provider is activated by
`memory.provider: <name>` in `config.yaml`, which `hermes memory setup` writes.
### How `local_embedded` runs
The Hindsight server is **not** a dependency of Hermes' venv. `local_embedded` starts it as a
separate process — an installed `hindsight-api` binary, else a `uvx hindsight-api` fallback that
gets its own environment — and talks HTTP to it. This venv keeps only `hindsight-client` and
`hindsight-embed`, both declared in `pyproject.toml`, so every mode installs with the plugin and
nothing has to be fetched at runtime.
That is not a size optimisation, it is the only shape that installs. `hindsight-all` pulls the whole
`hindsight-api-slim` tree, which cannot resolve against Hermes' pinned extras: api-slim needs
`protobuf>=7.35.1` while `mem0ai==2.0.10` and `modal==1.5.5` cap it below 7.0, and
`opentelemetry-semantic-conventions>=0.65b0` while `mistralai==2.4.8` caps it below 0.61. Relaxing
one pin only uncovers the next. It is also 482 MB on macOS arm64 and 3.2 GB on Linux x86_64 (the
CUDA wheels torch pulls there) that cloud-mode users would pay for nothing.
`_new_embedded_client` therefore composes what the old `hindsight.HindsightEmbedded` wrapper composed
internally — `hindsight_embed.get_embed_manager()` plus `hindsight_client.Hindsight` — instead of
importing it. **Existing embedded users keep everything**: the profile decides the daemon, so the
database (`~/.pg0/instances/hindsight-embed-<profile>/`) and the profile env
(`~/.hindsight/profiles/<profile>.env`) are the same files, read the same way. No migration step, no
re-setup.
The plugin no longer calls `tools.lazy_deps.install_specs` from anywhere. On package-manager Hermes
that is a retired shim which handed the process to the updater and exited instead of installing, so
every `hermes` invocation became a ~40s "update" that drained `hermes-gateway` and still left the
import missing (NousResearch/hermes-agent#126494, fixed upstream in af26acab73 to raise
`ImportError`). There is nothing left for it to install.
## Coming from the built-in provider
Hindsight used to ship inside Hermes as `plugins/memory/hindsight/`. Nous Research removed that copy
on 2026-09-23 and the provider now installs from the catalog instead. **You do not need to do
anything** — and your memories are not affected.
`hermes update`, and agent startup for Desktop users who never run it, calls Hermes'
`memory_provider_migration`: it sees `memory.provider: hindsight` configured, finds no provider on
disk, looks the name up in the plugin catalog and installs it at the reviewed commit pin. You'll see:
```
✓ Memory provider 'hindsight' moved out of core — installed its plugin from the catalog
(your memory.hindsight settings and data are unchanged).
```
Your data never lived in the Hermes tree: memories are in your Hindsight bank — Hindsight Cloud, or
for `local_embedded` the profile directory `~/.hindsight/profiles/<profile>` and its embedded
PostgreSQL instance. Moving the provider code does not touch any of it, and your
`~/.hermes/hindsight/config.json` is read exactly as before.
If the migration can't run — offline, or the catalog fetch fails — Hermes prints the manual
one-liner rather than starting silently without memory:
```bash
hermes plugins install hindsight
```
## Updating
**Nothing updates the plugin on its own.** Whichever mode you choose below, the code only moves
when you run a command. In particular `hermes update` does *not* move it: it reinstalls the
plugin's Python dependencies and leaves the checkout where it is. If you used the built-in
provider, that is the one habit worth unlearning — memory improvements no longer arrive as a side
effect of updating Hermes.
Hermes re-fetches the published catalog at most once every 6 hours, so a freshly released version
can take that long to even appear as available.
### Follow the official pin (default)
What you get from `hermes plugins install hindsight`: the commit Nous reviewed and pinned in their
catalog.
```bash
hermes plugins update hindsight # move to the catalog's current pin
```
`hermes plugins list` flags the plugin `update_available` once your installed commit differs from
the catalog's. Re-running `hermes plugins install hindsight` does **not** update it — it refuses
with "already exists"; `plugins update` is the command that re-pins.
### Pin a specific release
For a version you choose and freeze, install with an explicit commit:
```bash
hermes plugins install vectorize-io/hindsight/hindsight-integrations/hermes \
--force --ref <40-character-commit-sha>
```
Replace the whole placeholder, angle brackets included: `<` and `>` are redirection operators in
most shells, so leaving them in makes the command fail to parse. And if you copy both lines, keep
the trailing `\` at the end of the first one or join them into a single line, since a `\` followed
by anything other than a newline is also a parse error.
`--ref` takes a full 40-character commit SHA and **rejects tag names**, so take the SHA from the
release notes of the [release](https://github.com/vectorize-io/hindsight/releases) you want rather
than typing `v1.1.0`. The copy button beside a commit on
[the plugin's history](https://github.com/vectorize-io/hindsight/commits/main/hindsight-integrations/hermes)
gives you the full SHA; the abbreviated one shown on screen is too short.
A `--ref` install is marked pinned, and `hermes plugins update hindsight` deliberately refuses to
move it — install again with a new `--ref` when you want a different version.
### Track the latest development code
For fixes before they reach the catalog. Install from the source path rather than the catalog name:
```bash
hermes plugins install vectorize-io/hindsight/hindsight-integrations/hermes
hermes plugins update hindsight # now a git pull of our main branch
```
Unreviewed by definition — you get whatever is on `main` at the moment you run it.
The catalog entry lives in Nous' repo at
[`plugin-catalog/hindsight.yaml`](https://github.com/NousResearch/hermes-agent/blob/main/plugin-catalog/hindsight.yaml),
which is what records the current pin.
## Requirements
- **Cloud:** API key from [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io)
- **Local Embedded:** API key for a supported LLM provider (OpenAI, Anthropic, Gemini, Groq, OpenRouter, MiniMax, Ollama, or any OpenAI-compatible endpoint). Embeddings and reranking run locally — no additional API keys needed.
- **Local External:** A running Hindsight instance (Docker or self-hosted) reachable over HTTP.
## Setup
```bash
hermes memory setup # select "hindsight"
```
The setup wizard installs nothing (dependencies come with the plugin), walks you through configuration, and offers to seed the bank with a **starter memory template** (a curated set of dispositions/instructions for common agent roles) — you can skip it, and it warns before overwriting an already-configured bank.
Or manually (cloud mode with defaults):
```bash
hermes config set memory.provider hindsight
echo "HINDSIGHT_API_KEY=your-key" >> ~/.hermes/.env
```
### Cloud
Connects to the Hindsight Cloud API. Requires an API key from [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io).
### Local Embedded
Hermes spins up a local Hindsight daemon with built-in PostgreSQL. Requires an LLM API key for memory extraction and synthesis. The daemon starts automatically in the background on first use and stops after 5 minutes of inactivity.
Supports any OpenAI-compatible LLM endpoint (llama.cpp, vLLM, LM Studio, etc.) — pick `openai_compatible` as the provider and enter the base URL.
Daemon startup logs: `~/.hermes/logs/hindsight-embed.log`
Daemon runtime logs: `~/.hindsight/profiles/<profile>.log`
To open the Hindsight web UI (local embedded mode only):
```bash
hindsight-embed -p hermes ui start
```
### Local External
Points the plugin at an existing Hindsight instance you're already running (Docker, self-hosted, etc.). No daemon management — just a URL and an optional API key.
## Config
Each Hermes profile has its own config file: `$HERMES_HOME/hindsight/config.json` (`~/.hermes/hindsight/config.json`
for the default profile). `hermes memory setup` writes it. The plugin reads the first of these that exists and is
not empty:
1. `$HERMES_HOME/hindsight/config.json` — this profile only
2. `~/.hindsight/config.json` — shared by every profile
3. Environment variables (see Environment Variables)
A profile has exactly one config. To give one profile different banks per repository, workspace, platform or user,
use `bank_id_template` or the multiple banks settings below; for fully separate setups, use
separate Hermes profiles.
Some keys also accept an older name: `apiKey` for `api_key`, `budget` for `recall_budget`, `prefetch_method` for
`recall_prefetch_method`, `llmApiKey` for `llm_api_key`.
### Connection
| Key | Default | Description |
|-----|---------|-------------|
| `mode` | `cloud` | `cloud`, `local_embedded`, or `local_external` |
| `api_url` | `https://api.hindsight.vectorize.io` (cloud), `http://localhost:8888` (local_external) | API URL. Falls back to `HINDSIGHT_API_URL` when unset. |
| `api_key` | — | Hindsight API key. Required for cloud, optional for local_external. Falls back to `HINDSIGHT_API_KEY` when unset; the setup wizard stores it there. |
| `timeout` | `120` | Seconds to wait for each Hindsight API request. Falls back to `HINDSIGHT_TIMEOUT` when unset. |
### Memory Bank
| Key | Default | Description |
|-----|---------|-------------|
| `bank_id` | `hermes` | Memory bank name (static fallback used when `bank_id_template` is unset or resolves empty) |
| `bank_id_template` | — | Optional template to derive the bank name dynamically. Placeholders: `{profile}`, `{workspace}`, `{project}`, `{platform}`, `{user}`, `{session}`. `{project}` is the name of the git repository Hermes runs in; every worktree of a repository resolves to the same name, and it is empty outside a repository or for one rooted at your home directory. Example: `hermes-{profile}` isolates memory per active Hermes profile, `{project}` gives each repository its own bank. Empty placeholders collapse cleanly (e.g. `hermes-{user}` with no user becomes `hermes`). |
| `mirror_to_own_bank` | `false` | Also write to `bank_id` when the template resolves to a different bank, so a profile keeps its own memory while working in project banks. |
| `additional_banks` | — | Extra banks to write to and recall from, in priority order. |
| `recall_additional_banks` | — | Extra banks to recall from but never write to, searched after the write banks. Alias `recallAdditionalBanks`, the name the Claude Code integration uses. A bank also listed in `additional_banks` stays writable. |
| `trusted_project_dirs` | — | Absolute folders whose git repositories may choose their own bank with a `.hindsight/config.toml`. Empty by default, so a cloned repository cannot redirect your memory. |
| `bank_mission` | — | Read but not applied to the bank. To set a bank's reflect mission, use the bank's config (control plane or `PATCH /v1/default/banks/{bank_id}/config`). |
| `bank_retain_mission` | — | Read but not applied to the bank. To set a bank's retain mission, use the bank's config, as above. |
#### Multiple banks
The primary bank is chosen in this order: a `.hindsight/config.toml` in a trusted repository, then `bank_id_template`, then `HINDSIGHT_BANK_ID` or `bank_id`. Writes go to the primary, then `bank_id` when `mirror_to_own_bank` is on, then each `additional_banks` entry. Recall searches the same banks and then each `recall_additional_banks` entry, all at once, each with the configured budget and `recall_max_tokens`; results are merged in that order and deduplicated by text. Reflect uses the primary bank only.
The three list settings take a JSON list or comma-separated text; use the JSON form for a value that contains a comma.
A failing extra bank is skipped for five minutes, with one warning when it goes down and one info line when it answers again; recall and writes cool down separately, and a bank misses the writes made while its writes are cooling down. An extra bank that has not answered after 80% of `timeout` counts as failing, so it never costs the primary its results. A failing primary bank fails the call exactly as it does with a single bank, and a write stops there, before any extra bank is touched.
A repository that is itself inside one of the `trusted_project_dirs` (a linked worktree is judged by where the worktree is) can name its bank in `.hindsight/config.toml` at its root or in any folder below it:
```toml
bank_id = "acme-billing"
```
The lookup stops at the repository root. Only `bank_id` is read.
### Recall
| Key | Default | Description |
|-----|---------|-------------|
| `recall_budget` | `mid` | Recall thoroughness: `low` / `mid` / `high` |
| `recall_prefetch_method` | `recall` | Auto-recall method: `recall` (raw facts) or `reflect` (LLM synthesis) |
| `recall_max_tokens` | `4096` | Maximum tokens for recall results |
| `recall_max_input_chars` | `800` | Maximum input query length for auto-recall |
| `recall_prompt_preamble` | — | Custom preamble for recalled memories in context |
| `recall_tags` | — | Tags to filter when searching memories: a JSON list (e.g. `["project-x", "team"]`) or comma-separated text (`"project-x, team"`) |
| `recall_tags_match` | `any` | Tag matching mode: `any` / `all` / `any_strict` / `all_strict` |
| `reflect_tags` | — | Tags that scope what reflect reasons over (the `hindsight_reflect` tool and `recall_prefetch_method: reflect`), as a JSON list or comma-separated text. Unset, reflect uses the whole bank; `recall_tags` does not apply to reflect. |
| `reflect_tags_match` | `any` | Tag matching mode for `reflect_tags` |
| `recall_types` | `observation` | Fact types surfaced by recall (both auto-recall and the `hindsight_recall` tool). Comma-separated string or JSON list. **Default narrowed to `observation` only** (see "Behavior change" below). Set to `observation,world,experience` to also include raw facts. |
| `recall_min_scores` | — | Minimum relevance per score field, as a JSON object (e.g. `{"reranker": 0.25}` or `{"semantic": 0.5}`). Applies to both auto-recall and the `hindsight_recall` tool. See the tip below. |
| `auto_recall` | `true` | Automatically recall memories before each turn |
| `recall_sync` | `false` | Recall synchronously against the *current* message each turn (higher relevance, adds recall latency). Default off: recall runs in the background and is injected on the next turn. |
| `recall_indicator` | `true` | Show a `👁️ Hindsight — recalled N memories` status line when auto-recall injects memory. Turn off for customer-facing agents. |
> **Behavior change — `recall_types` defaults to `observation` only.**
>
> Previously recall returned all three fact types. It now returns only observations.
>
> Per [Hindsight's docs](../../developer/observations.md), observations are the **consolidated** knowledge layer Hindsight builds on top of raw facts: deduplicated beliefs grounded in evidence, refined as new facts arrive, with proof counts and freshness signals. Raw `world` / `experience` facts are the individual supporting evidence that feeds them. For per-turn context injection, observations are denser per token and avoid feeding the model multiple raw facts that one observation already summarizes.
>
> Restore the broad recall with `"recall_types": "observation,world,experience"` (string or JSON list) in `~/.hermes/hindsight/config.json`. This applies to **both** auto-recall and the `hindsight_recall` tool — both read the same `recall_types` setting (the tool schema has no per-call `types` argument), so narrowing the default narrows both paths.
> **Tip — `recall_min_scores` against off-topic recall.** Recall ranks, it does not judge relevance: a question
> unrelated to anything stored still comes back with up to `recall_max_tokens` of the least-bad memories. The
> `reranker` score separates the two cleanly — on a small bank relevant hits scored 0.7-0.97 and unrelated ones
> 0.0-0.1 — so `"recall_min_scores": {"reranker": 0.25}` in `~/.hermes/hindsight/config.json` turns that noise
> into an empty recall. The server only guarantees `reranker` and `final` floors; a `semantic` or `keyword` floor
> prunes just its own retrieval arm there, and a result found by another arm still comes back with a `null` score
> for that stage. The plugin therefore also checks every floor against the scores each result reports, and rejects
> a result that does not report the floored stage. Check the scores on your own bank before tuning a floor: with a
> multilingual embedding model, `{"semantic": 0.5}` separated relevant from unrelated queries better than the
> reranker did. Unset by default, so nothing changes unless you opt in.
### Retain
| Key | Default | Description |
|-----|---------|-------------|
| `auto_retain` | `true` | Automatically retain conversation turns |
| `retain_cron` | `false` | Also auto-retain scheduled (cron) runs. Off by default, so job output stays out of the user's memory; the `hindsight_retain` tool still works in cron runs. |
| `retain_async` | `true` | Process retain asynchronously on the Hindsight server |
| `retain_every_n_turns` | `1` | Retain every N turns (1 = every turn) |
| `retain_context` | `conversation between Hermes Agent and the User` | Context label for retained memories |
| `retain_tags` | — | Default tags applied to retained memories; merged with per-call tool tags. JSON list or comma-separated text. |
| `observation_scopes` | `shared` | How observations are grouped by tag during consolidation: `shared` (one scope for the whole bank, so observations merge across sessions; the `session:`/`parent:` tags stay on the facts for filtering), `combined` (one scope per exact tag set, which here means one per session), `per_tag` (one pass per tag), `all_combinations` (every tag subset, expensive), or a JSON list of tag lists for custom scopes (e.g. `[["user:alice"], ["project-x"]]`). |
| `retain_source` | — | Opt-in `metadata.source` attached to retained memories (identifies the storing client, e.g. `hermes`). Empty by default — no attribution tag ships unless you set it. |
| `retain_strategy` | — | Named retain strategy sent with every stored item (`HINDSIGHT_RETAIN_STRATEGY`). The bank must define it under `retain_strategies`; an unknown name is ignored by the server. Empty lets the bank decide. |
| `retain_indicator` | `true` | Show a `👁️ Hindsight — saving to memory…` status line when a turn is saved. Turn off for customer-facing agents. |
| `retain_user_prefix` | `User` | Label used before user turns in auto-retained transcripts |
| `retain_assistant_prefix` | `Assistant` | Label used before assistant turns in auto-retained transcripts |
| `prefetch_waits_for_retain` | `true` | Before the background recall for the next turn, wait until the turn just saved can be found, so recall includes it. Runs off the reply path and adds no response time. |
| `prefetch_retain_drain_timeout` | `10` | Most seconds that background recall waits for the save above before recalling anyway |
### Integration
| Key | Default | Description |
|-----|---------|-------------|
| `memory_mode` | `hybrid` | How memories are integrated into the agent |
**memory_mode:**
- `hybrid` — automatic context injection + tools available to the LLM
- `context` — automatic injection only, no tools exposed
- `tools` — tools only, no automatic injection
### Local Embedded LLM
| Key | Default | Description |
|-----|---------|-------------|
| `llm_provider` | — | `openai`, `anthropic`, `gemini`, `groq`, `openrouter`, `minimax`, `ollama`, `lmstudio`, `openai_compatible`. The setup wizard suggests `openai`. |
| `llm_model` | — | Model name (e.g. `gpt-4o-mini`, `qwen/qwen3.5-9b`). The setup wizard suggests one per provider. |
| `llm_base_url` | — | Endpoint URL for `openai_compatible` (e.g. `http://192.168.1.10:8080/v1`). Falls back to `HINDSIGHT_API_LLM_BASE_URL`. |
| `llm_api_key` | — | LLM API key. Usually left out of the file: the setup wizard stores it in `$HERMES_HOME/.env` as `HINDSIGHT_LLM_API_KEY`. |
| `profile` | `hermes` | Name of the local daemon profile: its data, log and env file under `~/.hindsight/profiles/` are named after it. Give two Hermes profiles different values to keep their local memory apart. |
| `idle_timeout` | `300` | Seconds of inactivity before the daemon stops; `0` keeps it running. Falls back to `HINDSIGHT_IDLE_TIMEOUT`. |
| `port_health_grace_timeout` | `30` | Seconds to wait for a slow daemon health check before restarting it. Raise on busy or low-resource machines. `HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT` wins when set. |
The embedded daemon is a subprocess that cannot see the per-turn secret
scope, so it reads the key from `~/.hindsight/profiles/<profile>.env`
(materialized owner-only at setup and on config change). Key resolution
order is explicit config → secret scope → the on-disk profile env, and the
rewrite path is fail-closed: a build with no key never clobbers a profile
file that already holds one.
The plugin owns only the LLM, log-level and idle-timeout keys in that file. Anything else in it
(the port hindsight-embed records, a tenant extension and its `HINDSIGHT_API_TENANT_API_KEY`) is
left alone and never counts as a config change, and the plugin's client sends that tenant key to
the daemon.
## Tools
Available in `hybrid` and `tools` memory modes:
| Tool | Description |
|------|-------------|
| `hindsight_retain` | Store information with auto entity extraction; supports optional per-call `tags` |
| `hindsight_recall` | Multi-strategy search (semantic + entity graph) |
| `hindsight_reflect` | Cross-memory synthesis (LLM-powered) |
## Environment Variables
Set these in the profile's `$HERMES_HOME/.env`. A value in `config.json` wins over its variable, except where noted.
These are used whenever the matching config key is not set:
| Variable | Description |
|----------|-------------|
| `HINDSIGHT_API_KEY` | Hindsight API key (`api_key`) |
| `HINDSIGHT_API_URL` | API endpoint (`api_url`) |
| `HINDSIGHT_BANK_ID` | Bank name; when set, wins over `bank_id` in `config.json` (a `bank_id_template` still takes precedence) |
| `HINDSIGHT_TIMEOUT` | API request timeout in seconds (`timeout`) |
| `HINDSIGHT_LLM_API_KEY` | LLM API key for local_embedded (`llm_api_key`); `HINDSIGHT_API_LLM_API_KEY` also works |
| `HINDSIGHT_API_LLM_BASE_URL` | LLM endpoint for local_embedded (`llm_base_url`) |
| `HINDSIGHT_IDLE_TIMEOUT` | Daemon idle timeout in seconds (`idle_timeout`) |
| `HINDSIGHT_EMBED_PORT_HEALTH_GRACE_TIMEOUT` | Daemon health-check grace in seconds; wins over `port_health_grace_timeout` |
| `HINDSIGHT_RETAIN_TAGS` | Default tags on retained memories (`retain_tags`) |
| `HINDSIGHT_RETAIN_OBSERVATION_SCOPES` | Observation scopes (`observation_scopes`) |
| `HINDSIGHT_RETAIN_SOURCE` | `metadata.source` on retained memories (`retain_source`) |
| `HINDSIGHT_RETAIN_STRATEGY` | Named retain strategy (`retain_strategy`) |
| `HINDSIGHT_RETAIN_USER_PREFIX` | Label before user turns (`retain_user_prefix`) |
| `HINDSIGHT_RETAIN_ASSISTANT_PREFIX` | Label before assistant turns (`retain_assistant_prefix`) |
These are used **only when there is no config file at all**. Once `hermes memory setup` has written
`config.json`, they are ignored; set the config key instead:
| Variable | Description |
|----------|-------------|
| `HINDSIGHT_MODE` | Mode: `cloud`, `local_embedded` or `local_external` (`mode`) |
| `HINDSIGHT_BUDGET` | Recall budget (`recall_budget`) |
| `HINDSIGHT_RETAIN_CONTEXT` | Label stored with each retained conversation (`retain_context`) |
| `HINDSIGHT_RETAIN_INDICATOR` | `false` hides the "saving to memory" status line (`retain_indicator`) |
| `HINDSIGHT_RECALL_INDICATOR` | `false` hides the "recalled N memories" status line (`recall_indicator`) |
| `HINDSIGHT_RECALL_SYNC` | `true` recalls against the current message before answering (`recall_sync`) |
## Client Version
Requires `hindsight-client >= 0.10.1` and, for `local_embedded`, `hindsight-embed >= 0.10.1`. A version below
the floor only logs a warning; `hermes plugins update hindsight` moves the environment.
The floor is 0.10.1 rather than the 0.6.1 this plugin needs at the API level because
`hindsight-embed` 0.10.0 breaks `local_embedded` outright: its daemon probe cleared the calling
thread's event loop, so the next client call failed with `Timeout context manager should be used
inside a task`. If you installed between 2026-09-14 and 2026-09-21, run `hermes update` (or
`hermes plugins update hindsight`) to move off it.
## Hermes Gateway (Telegram, Discord, Slack)
The provider works across every gateway platform. Hermes builds a fresh agent per message, and the
provider is re-initialized with it, so auto-recall runs for each turn regardless of platform.
Two settings are worth turning off for customer-facing bots: `recall_indicator` and
`retain_indicator`, which otherwise print a `👁️ Hindsight` status line into the user's channel.
## Disabling Hermes' built-in memory
Hermes has its own memory store backed by local markdown (`MEMORY.md`, plus a slimmer `USER.md`
profile). With both active the model may prefer the built-in one, so turn the flat-file stores off:
```bash
hermes config set memory.memory_enabled false
hermes config set memory.user_profile_enabled false # optional: the USER.md profile
```
Setting both to `false` removes the built-in `memory` tool from the agent entirely. Re-enable later
by setting the same flags back to `true`.
If you keep the built-in stores on, every entry the agent adds or replaces in them is also saved to
every bank Hindsight writes to, tagged `builtin-memory`, `builtin-target:<memory|user>` and `builtin-action:<add|replace>`,
so a fact pruned from the size-capped file is not lost. Removals are not mirrored.
## Troubleshooting
**Tools don't appear in `/tools`** — the provider skips tool registration when it isn't configured.
Check `hermes memory status` reports `hindsight` as the active provider and `Status: available`. In
`memory_mode: context` the tools are hidden on purpose.
**`Status: not available` in `local_embedded`** — this plugin's own packages
(`hindsight-client`, `hindsight-embed`) are missing from the environment, which happens when a venv
rebuild dropped the plugin member. Run `hermes pm repair` and restart Hermes to rebuild them. The Hindsight server is *not* needed in that venv — it runs as a separate
process, and first use downloads it if no `hindsight-api` binary is present yet.
**`Timeout context manager should be used inside a task`** — `hindsight-embed` 0.10.0. Run
`hermes plugins update hindsight` to move to the 0.10.1 floor.
**Local daemon not starting** — check the logs:
```bash
cat ~/.hermes/logs/hindsight-embed.log # startup
cat ~/.hindsight/profiles/<profile>.log # daemon runtime
```
**Recall returns nothing** — memories need at least one retain cycle, and extraction is an LLM call.
Store a fact, then ask about it on a later turn.
references/sdks/integrations/langgraph.md
# LangGraph / LangChain
Persistent long-term memory for [LangGraph](https://langchain-ai.github.io/langgraph/) and [LangChain](https://python.langchain.com/) agents via Hindsight. Three integration patterns at different abstraction levels — the tools pattern works with both LangChain and LangGraph, while nodes and the BaseStore adapter are LangGraph-specific.
[View Changelog →](../../changelog/integrations/langgraph.md)
## Features
- **Memory Tools** — retain, recall, and reflect as LangChain `@tool` functions compatible with `bind_tools()` and `ToolNode`. Works with **both LangChain and LangGraph** — no LangGraph dependency required for this pattern.
- **Graph Nodes** *(LangGraph)* — Pre-built nodes that auto-inject memories before LLM calls and auto-store after responses
- **BaseStore Adapter** *(LangGraph)* — Drop-in `BaseStore` implementation backed by Hindsight, for LangGraph's native memory patterns
- **Dynamic Banks** — Resolve bank IDs per-request from `RunnableConfig` for per-user memory
- **Async-Native** — Uses `aretain`, `arecall`, `areflect` directly — no thread-pool workarounds
## Installation
```bash
pip install hindsight-langgraph
```
## Quick Start: Tools (LangChain & LangGraph)
The tools pattern creates standard LangChain `@tool` functions that work with any LangChain-compatible model via `bind_tools()`. You can use them with a LangGraph agent or with plain LangChain — no LangGraph required.
**With LangGraph (recommended):**
```python
from hindsight_client import Hindsight
from hindsight_langgraph import create_hindsight_tools
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
client = Hindsight(base_url="http://localhost:8888")
tools = create_hindsight_tools(client=client, bank_id="user-123")
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools=tools)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "Remember that I prefer dark mode"}]}
)
```
**With plain LangChain:**
```python
from hindsight_client import Hindsight
from hindsight_langgraph import create_hindsight_tools
from langchain_openai import ChatOpenAI
client = Hindsight(base_url="http://localhost:8888")
tools = create_hindsight_tools(client=client, bank_id="user-123")
model = ChatOpenAI(model="gpt-4o").bind_tools(tools)
response = await model.ainvoke("Remember that I prefer dark mode")
```
When using plain LangChain, you handle the tool execution loop yourself — call the model, check for `tool_calls`, execute them, and feed results back. LangGraph automates this loop for you.
The agent gets three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
## Quick Start: Memory Nodes (LangGraph)
Add recall and retain nodes to your graph for automatic memory injection and storage.
```python
from hindsight_client import Hindsight
from hindsight_langgraph import create_recall_node, create_retain_node
from langgraph.graph import StateGraph, MessagesState, START, END
client = Hindsight(base_url="http://localhost:8888")
recall = create_recall_node(client=client, bank_id="user-123")
retain = create_retain_node(client=client, bank_id="user-123")
builder = StateGraph(MessagesState)
builder.add_node("recall", recall)
builder.add_node("agent", agent_node) # your LLM node
builder.add_node("retain", retain)
builder.add_edge(START, "recall")
builder.add_edge("recall", "agent")
builder.add_edge("agent", "retain")
builder.add_edge("retain", END)
graph = builder.compile()
```
The recall node extracts the latest user message, searches Hindsight, and injects matching memories as a `SystemMessage`. The retain node stores human messages (optionally AI messages too) after the response.
## Quick Start: BaseStore (LangGraph)
Use Hindsight as a LangGraph `BaseStore` for cross-thread persistent memory with semantic search.
```python
from hindsight_client import Hindsight
from hindsight_langgraph import HindsightStore
client = Hindsight(base_url="http://localhost:8888")
store = HindsightStore(client=client)
graph = builder.compile(checkpointer=checkpointer, store=store)
# Store and search via the store API
await store.aput(("user", "123", "prefs"), "theme", {"value": "dark mode"})
results = await store.asearch(("user", "123", "prefs"), query="theme preference")
```
Namespace tuples are mapped to Hindsight bank IDs with `.` as separator (e.g., `("user", "123")` becomes bank `user.123`). Banks are auto-created on first access.
## Dynamic Bank IDs
Both nodes and the store support per-user bank resolution from `RunnableConfig`:
```python
recall = create_recall_node(client=client, bank_id_from_config="user_id")
retain = create_retain_node(client=client, bank_id_from_config="user_id")
# Bank ID resolved at runtime from config
result = await graph.ainvoke(
{"messages": [{"role": "user", "content": "hello"}]},
config={"configurable": {"user_id": "user-456"}},
)
```
## Selecting Tools
Include only the tools you need:
```python
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=True,
include_recall=True,
include_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing a client to every call, configure once:
```python
from hindsight_langgraph import configure, create_hindsight_tools
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create tools without passing client — uses global config
tools = create_hindsight_tools(bank_id="user-123")
```
## Retain Node Options
```python
retain = create_retain_node(
client=client,
bank_id="user-123",
retain_human=True, # Store human messages (default: True)
retain_ai=False, # Store AI responses (default: False)
tags=["source:chat"], # Tags applied to stored memories
)
```
## Recall Node Options
```python
recall = create_recall_node(
client=client,
bank_id="user-123",
budget="low", # Recall budget: low/mid/high
max_results=10, # Max memories injected
max_tokens=4096, # Max tokens for recall
tags=["scope:user"], # Filter by tags
tags_match="all", # Tag match mode
)
```
### Using `output_key` for Prompt Control
By default, the recall node appends a `SystemMessage` to `messages`. Use `output_key` to write memory text to a custom state field instead, giving you full control over prompt ordering:
```python
from typing import Optional
from langgraph.graph import MessagesState
class AgentState(MessagesState):
memory_context: Optional[str] = None
recall = create_recall_node(
client=client,
bank_id="user-123",
output_key="memory_context",
)
# In your agent node, read state["memory_context"] and prepend it
# to the system prompt before calling the model.
```
## Limitations and Notes
### HindsightStore
- **Async-only.** All sync methods (`batch`, `get`, `put`, `delete`, `search`, `list_namespaces`) raise `NotImplementedError`. Use the async variants (`abatch`, `aget`, `aput`, `adelete`, `asearch`, `alist_namespaces`) instead.
- **`get()` relies on recall.** There is no direct key lookup — the key is used as a recall query and only exact `document_id` matches are returned. Items that do not rank in the top recall results may appear missing.
- **`list_namespaces` is session-scoped.** It only tracks namespaces that have been written to via `aput()` during the current process. After a restart, `list_namespaces` returns empty even though data still exists in Hindsight.
- **`delete` is a no-op.** Calling `adelete()` logs a debug message but does not remove data from Hindsight. Hindsight's memory model is append-oriented; fact superseding is handled automatically during retain.
### Memory Nodes
- **SystemMessage ordering.** The recall node adds a `SystemMessage` with recalled memories. Because `MessagesState` uses `add_messages` (which appends), this message appears after existing messages rather than at position 0. The message has a stable ID (`hindsight_memory_context`) so it is updated rather than duplicated across invocations. If your LLM provider requires system messages first, sort or filter messages in your agent node before passing them to the model.
### Error Handling
- **Tools** raise `HindsightError` on failure, which surfaces to the agent as a tool error.
- **Nodes** silently log errors and return empty messages, so a Hindsight outage does not crash your graph.
## API Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode (any/all/any\_strict/all\_strict) |
| `retain_metadata` | `None` | Default metadata dict for retain operations |
| `retain_document_id` | `None` | Default document\_id for retain (groups/upserts memories) |
| `recall_types` | `None` | Fact types to filter (world, experience, observation) |
| `recall_include_entities` | `False` | Include entity information in recall results |
| `reflect_context` | `None` | Additional context for reflect operations |
| `reflect_max_tokens` | `None` | Max tokens for reflect results (defaults to `max_tokens`) |
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
| `reflect_tags` | `None` | Tags to filter memories used in reflect (defaults to `recall_tags`) |
| `reflect_tags_match` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
| `include_retain` | `True` | Include the retain (store) tool |
| `include_recall` | `True` | Include the recall (search) tool |
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
### `create_recall_node()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | `None` | Static bank ID (or use `bank_id_from_config`) |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall budget level |
| `max_tokens` | `4096` | Max tokens for recall results |
| `max_results` | `10` | Max memories to inject |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
| `bank_id_from_config` | `"user_id"` | Config key to resolve bank ID at runtime |
| `output_key` | `None` | If set, write memory text to this state key instead of appending a SystemMessage to `messages` |
### `create_retain_node()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | `None` | Static bank ID (or use `bank_id_from_config`) |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `tags` | `None` | Tags applied to stored memories |
| `bank_id_from_config` | `"user_id"` | Config key to resolve bank ID at runtime |
| `retain_human` | `True` | Store human messages |
| `retain_ai` | `False` | Store AI responses |
### `HindsightStore()`
| Parameter | Default | Description |
|---|---|---|
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `tags` | `None` | Tags applied to all retain operations |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Production API | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
## Requirements
- Python >= 3.10
- langchain-core >= 0.3.0
- hindsight-client >= 0.4.0
- langgraph >= 0.3.0 *(only for nodes and store patterns — install with `pip install hindsight-langgraph[langgraph]`)*
references/sdks/integrations/litellm.md
# LiteLLM
Universal LLM memory integration via [LiteLLM](https://github.com/BerriAI/litellm). Add persistent memory to any LLM application with just a few lines of code.
[View Changelog →](../../changelog/integrations/litellm.md)
## Features
- **Universal LLM Support** - Works with 100+ LLM providers via LiteLLM (OpenAI, Anthropic, Groq, Azure, AWS Bedrock, Google Vertex AI, and more)
- **Simple Integration** - Just configure, set defaults, enable, and use `hindsight_litellm.completion()`
- **Automatic Memory Injection** - Relevant memories are injected into prompts before LLM calls
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall (async by default for performance)
- **Two Memory Modes** - Choose between `reflect` (synthesized context) or `recall` (raw memory retrieval)
- **Direct Memory APIs** - Query, synthesize, and store memories manually
- **Native Client Wrappers** - Alternative wrappers for OpenAI and Anthropic SDKs
- **Debug Mode** - Inspect exactly what memories are being injected
## Installation
```bash
pip install hindsight-litellm
```
## Quick Start
```python
import hindsight_litellm
# Step 1: Configure static settings
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
verbose=True,
)
# Step 2: Set defaults (bank_id is required)
hindsight_litellm.set_defaults(
bank_id="my-agent",
use_reflect=True, # Use reflect for synthesized context
)
# Step 3: Enable memory integration
hindsight_litellm.enable()
# Step 4: Use completion with memory
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What did we discuss about AI?"}],
hindsight_query="What do I know about AI discussions?",
)
```
**Important:** When `inject_memories=True` (default), you can provide `hindsight_query` to specify what to search for in memory. If omitted, the last user message is used as the query.
## How It Works
When you call `completion()`, the following happens automatically:
1. **Memory Retrieval** - Hindsight is queried for relevant memories based on the conversation
2. **Prompt Injection** - Memories are injected into the system message
3. **LLM Call** - The enriched prompt is sent to the LLM
4. **Conversation Storage** - The conversation is stored to Hindsight for future recall
5. **Response Returned** - You receive the response as normal
## Configuration Options
The API is split into two functions for clarity:
### 1. `configure()` - Static Settings
Settings that typically don't change during a session:
```python
hindsight_litellm.configure(
# Required
hindsight_api_url="http://localhost:8888", # Hindsight API server URL
# Optional - Authentication
api_key="your-api-key", # API key for Hindsight authentication
# Optional - Memory behavior
store_conversations=True, # Store conversations after LLM calls
inject_memories=True, # Inject relevant memories into prompts
sync_storage=False, # False = async storage (default, better performance)
# True = sync storage (blocks, raises errors immediately)
# Optional - Advanced
injection_mode="system_message", # How to inject: "system_message" or "prepend_user"
excluded_models=["gpt-3.5*"], # Exclude certain models from interception
verbose=True, # Enable verbose logging and debug info
)
```
### 2. `set_defaults()` - Per-Call Defaults
Default values for per-call settings. These can be overridden on individual calls using `hindsight_*` kwargs:
```python
hindsight_litellm.set_defaults(
# Required
bank_id="my-agent", # Memory bank ID
# Optional - Memory retrieval
budget="mid", # Budget level: "low", "mid", "high"
fact_types=["world", "observation"], # Filter fact types to retrieve
max_memories=10, # Maximum memories to inject (None = unlimited)
max_memory_tokens=4096, # Maximum tokens for memory context
include_entities=True, # Include entity observations in recall
# Optional - Reflect mode
use_reflect=True, # Use reflect API (synthesized) vs recall (raw memories)
reflect_include_facts=False, # Include source facts in debug info
reflect_context="I am a delivery agent finding recipients.", # Context for reflect reasoning
reflect_response_schema={...}, # JSON Schema for structured reflect output
# Optional - Debugging
trace=False, # Enable trace info for debugging
document_id="conversation-1", # Document ID for grouping conversations
)
```
### 3. Per-Call Overrides
Override any default on individual calls using `hindsight_*` kwargs:
```python
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[...],
hindsight_query="Where is Alice located?", # Custom query for memory lookup
hindsight_reflect_context="Currently on floor 3", # Per-call reflect context override
# hindsight_bank_id="other-bank", # Override bank_id for this call
)
```
### Bank Configuration: mission
Use `set_bank_mission()` to configure what the memory bank should learn and remember (used for mental models):
```python
hindsight_litellm.set_bank_mission(
mission="""This agent routes customer support requests to the appropriate team.
Remember which types of issues should go to which teams (billing, technical, sales).
Track customer preferences for communication channels and past issue resolutions.""",
name="Customer Support Router", # Optional display name
)
```
### Memory Modes: Reflect vs Recall
- **Recall mode** (`use_reflect=False`, default): Retrieves raw memory facts and injects them as a numbered list. Best when you need precise, individual memories.
- **Reflect mode** (`use_reflect=True`): Synthesizes memories into a coherent context paragraph. Best for natural, conversational memory context.
```python
# Recall mode - raw memories
hindsight_litellm.set_defaults(bank_id="my-agent", use_reflect=False)
# Injects: "1. [WORLD] User prefers Python\n2. [OBSERVATION] User dislikes Java..."
# Reflect mode - synthesized context
hindsight_litellm.set_defaults(bank_id="my-agent", use_reflect=True)
# Injects: "Based on previous conversations, the user is a Python developer who..."
# Reflect with context - shapes LLM reasoning (not retrieval)
hindsight_litellm.set_defaults(
bank_id="my-agent",
use_reflect=True,
reflect_context="I am a delivery agent looking for package recipients.",
)
```
## Multi-Provider Support
Works with any LiteLLM-supported provider:
```python
import hindsight_litellm
hindsight_litellm.configure(hindsight_api_url="http://localhost:8888")
hindsight_litellm.set_defaults(bank_id="my-agent")
hindsight_litellm.enable()
messages = [{"role": "user", "content": "Hello!"}]
# OpenAI
hindsight_litellm.completion(model="gpt-4o", messages=messages, hindsight_query="greeting")
# Anthropic
hindsight_litellm.completion(model="claude-sonnet-4-20250514", messages=messages, hindsight_query="greeting")
# Groq
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=messages, hindsight_query="greeting")
# Azure OpenAI
hindsight_litellm.completion(model="azure/gpt-4", messages=messages, hindsight_query="greeting")
# AWS Bedrock
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=messages, hindsight_query="greeting")
# Google Vertex AI
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=messages, hindsight_query="greeting")
```
## Direct Memory APIs
### Recall - Query raw memories
```python
from hindsight_litellm import configure, set_defaults, recall
configure(hindsight_api_url="http://localhost:8888")
set_defaults(bank_id="my-agent")
memories = recall("what projects am I working on?", budget="mid")
for m in memories:
print(f"- [{m.fact_type}] {m.text}")
```
### Reflect - Get synthesized context
```python
from hindsight_litellm import configure, set_defaults, reflect
configure(hindsight_api_url="http://localhost:8888")
set_defaults(bank_id="my-agent")
result = reflect("what do you know about the user's preferences?")
print(result.text)
# With context to shape the response (doesn't affect retrieval)
result = reflect(
query="what do I know about Alice?",
context="I am a delivery agent looking for package recipients.",
)
```
### Retain - Store memories
```python
from hindsight_litellm import configure, set_defaults, retain, get_pending_retain_errors
configure(hindsight_api_url="http://localhost:8888")
set_defaults(bank_id="my-agent")
# Async retain (default) - fast, non-blocking
result = retain(
content="User mentioned they're working on a machine learning project",
context="Discussion about current projects",
)
# Sync retain - blocks until complete, raises errors immediately
result = retain(
content="Critical information that must be stored",
context="Important data",
sync=True,
)
# Check for async retain errors (call periodically)
errors = get_pending_retain_errors()
if errors:
for e in errors:
print(f"Background retain failed: {e}")
```
### Async APIs
```python
from hindsight_litellm import arecall, areflect, aretain
# Async versions of all memory APIs
memories = await arecall("what do you know about me?")
context = await areflect("summarize user preferences")
result = await aretain(content="New information to remember")
```
## Native Client Wrappers
Alternative to LiteLLM callbacks for direct SDK integration.
### OpenAI Wrapper
```python
from openai import OpenAI
from hindsight_litellm import wrap_openai
client = OpenAI()
wrapped = wrap_openai(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "What do you know about me?"}]
)
```
### Anthropic Wrapper
```python
from anthropic import Anthropic
from hindsight_litellm import wrap_anthropic
client = Anthropic()
wrapped = wrap_anthropic(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
```
## Debug Mode
When `verbose=True`, you can inspect exactly what memories are being injected:
```python
from hindsight_litellm import configure, set_defaults, enable, completion, get_last_injection_debug
configure(hindsight_api_url="http://localhost:8888", verbose=True)
set_defaults(bank_id="my-agent", use_reflect=True)
enable()
response = completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's my favorite color?"}],
hindsight_query="What is the user's favorite color?",
)
# Inspect what was injected
debug = get_last_injection_debug()
if debug:
print(f"Mode: {debug.mode}") # "reflect" or "recall"
print(f"Injected: {debug.injected}") # True/False
print(f"Results: {debug.results_count}")
print(f"Memory context:\n{debug.memory_context}")
if debug.error:
print(f"Error: {debug.error}")
```
## Context Manager
```python
from hindsight_litellm import hindsight_memory
import litellm
with hindsight_memory(bank_id="user-123"):
response = litellm.completion(
model="gpt-4",
messages=[{"role": "user", "content": "Hello!"}],
hindsight_query="greeting context",
)
# Memory integration automatically disabled after context
```
## Disabling and Cleanup
```python
from hindsight_litellm import disable, cleanup
# Temporarily disable memory integration
disable()
# Clean up all resources (call when shutting down)
cleanup()
```
## API Reference
### Main Functions
| Function | Description |
|----------|-------------|
| `configure(...)` | Configure static Hindsight settings (API URL, auth, storage options) |
| `set_defaults(...)` | Set defaults for per-call settings (bank_id, budget, reflect options) |
| `enable()` | Enable memory integration with LiteLLM |
| `disable()` | Disable memory integration |
| `is_enabled()` | Check if memory integration is enabled |
| `cleanup()` | Clean up all resources |
### Configuration Functions
| Function | Description |
|----------|-------------|
| `get_config()` | Get current static configuration |
| `get_defaults()` | Get current per-call defaults |
| `is_configured()` | Check if Hindsight is configured with a bank_id |
| `reset_config()` | Reset all configuration to defaults |
| `set_document_id(id)` | Convenience function to update document_id |
| `set_bank_mission(...)` | Set mission/instructions for a memory bank (for mental models) |
### Memory Functions
| Function | Description |
|----------|-------------|
| `recall(query, ...)` | Query raw memories (sync) |
| `arecall(query, ...)` | Query raw memories (async) |
| `reflect(query, ...)` | Get synthesized memory context (sync) |
| `areflect(query, ...)` | Get synthesized memory context (async) |
| `retain(content, sync=False, ...)` | Store a memory (async by default, use `sync=True` to block) |
| `aretain(content, ...)` | Store a memory (async) |
### Error Tracking Functions
| Function | Description |
|----------|-------------|
| `get_pending_retain_errors()` | Get and clear errors from background retain operations |
| `get_pending_storage_errors()` | Get and clear errors from background conversation storage |
### Debug Functions
| Function | Description |
|----------|-------------|
| `get_last_injection_debug()` | Get debug info from last memory injection |
| `clear_injection_debug()` | Clear stored debug info |
### Client Wrappers
| Function | Description |
|----------|-------------|
| `wrap_openai(client, ...)` | Wrap OpenAI client with memory |
| `wrap_anthropic(client, ...)` | Wrap Anthropic client with memory |
## Streaming
Streaming responses (`stream=True`) are fully supported. When streaming is detected, the response is automatically wrapped to collect chunks as they are consumed. Once the stream is fully consumed (or the context manager exits), the complete conversation is stored to Hindsight.
This works across all integration modes:
- **Monkeypatch wrappers** (`enable()` / `completion()` / `acompletion()`) — streaming responses are wrapped transparently
- **Native client wrappers** (`wrap_openai()`, `wrap_anthropic()`) — same chunk-collection behavior
- **Callback handler** — streaming responses are skipped in the callback since the callback doesn't control the return value; use the monkeypatch or native wrapper modes for streaming with storage
## Requirements
- Python >= 3.10
- litellm >= 1.83.0
- A running Hindsight API server
references/sdks/integrations/llamaindex.md
# LlamaIndex
Persistent long-term memory for [LlamaIndex](https://docs.llamaindex.ai/) agents via Hindsight. The `hindsight-llamaindex` package provides two complementary patterns:
- **`HindsightToolSpec`** — Agent-driven memory tools (retain/recall/reflect)
- **`HindsightMemory`** — Automatic memory via LlamaIndex's `BaseMemory` interface
[View Changelog →](../../changelog/integrations/llamaindex.md)
## Installation
```bash
pip install hindsight-llamaindex
```
---
## Automatic Memory (BaseMemory)
The simplest way to add Hindsight memory to a LlamaIndex agent. Messages are automatically stored on each turn, and relevant memories are recalled and injected as context.
```python
import asyncio
from hindsight_client import Hindsight
from hindsight_llamaindex import HindsightMemory
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
async def main():
client = Hindsight(base_url="http://localhost:8888")
memory = HindsightMemory.from_client(
client=client,
bank_id="user-123",
mission="Track user preferences and project context",
)
agent = ReActAgent(tools=[], llm=OpenAI(model="gpt-4o"))
response = await agent.run("Remember that I prefer dark mode", memory=memory)
print(response)
asyncio.run(main())
```
### How It Works
| Event | What Happens |
|-------|-------------|
| Agent receives input | `aget(input)` recalls relevant memories from Hindsight, prepends as system message |
| Agent produces output | `aput(message)` retains the message to Hindsight for future recall |
| New session starts | Previous memories are available via recall; local chat buffer starts empty |
### `HindsightMemory.from_client()`
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `Hindsight` | *required* | Hindsight client instance |
| `bank_id` | `str` | *required* | Memory bank ID |
| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use |
| `context` | `str` | `"llamaindex"` | Source label for retain operations |
| `budget` | `str` | `"mid"` | Recall budget level |
| `max_tokens` | `int` | `4096` | Max recall tokens |
| `tags` | `list[str]` | `None` | Tags for retain operations |
| `recall_tags` | `list[str]` | `None` | Tags to filter recall |
| `recall_tags_match` | `str` | `"any"` | Tag matching mode |
| `system_prompt` | `str` | *(built-in)* | Template for memory system message. Must contain `{memories}` |
| `chat_history_limit` | `int` | `100` | Max messages in local buffer |
Also available: `HindsightMemory.from_url(hindsight_api_url, bank_id, ...)` for creating without a pre-built client.
---
## Agent-Driven Tools (BaseToolSpec)
For explicit control, expose retain/recall/reflect as tools the agent can choose to call.
### Quick Start: Tool Spec
```python
import asyncio
from hindsight_client import Hindsight
from hindsight_llamaindex import HindsightToolSpec
from llama_index.llms.openai import OpenAI
from llama_index.core.agent import ReActAgent
async def main():
client = Hindsight(base_url="http://localhost:8888")
spec = HindsightToolSpec(
client=client,
bank_id="user-123",
mission="Track user preferences",
)
tools = spec.to_tool_list()
agent = ReActAgent(tools=tools, llm=OpenAI(model="gpt-4o"))
response = await agent.run("Remember that I prefer dark mode")
print(response)
asyncio.run(main())
```
### Quick Start: Factory Function
```python
from hindsight_llamaindex import create_hindsight_tools
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
mission="Track user preferences",
)
```
### Selecting Tools
```python
# Via to_tool_list()
tools = spec.to_tool_list(spec_functions=["recall_memory", "reflect_on_memory"])
# Via factory flags
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=True,
include_recall=True,
include_reflect=False,
)
```
### Configuration
Set defaults via `configure()`, override per-call:
```python
from hindsight_llamaindex import configure
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # or set HINDSIGHT_API_KEY env var
budget="mid",
tags=["source:llamaindex"],
context="my-app",
mission="Track user preferences",
)
# Now create tools without passing client/url
tools = create_hindsight_tools(bank_id="user-123")
```
### `HindsightToolSpec()`
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bank_id` | `str` | *required* | Hindsight memory bank to operate on |
| `client` | `Hindsight` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `str` | `None` | API URL (used if no client provided) |
| `api_key` | `str` | `None` | API key (used if no client provided) |
| `budget` | `str` | `None` → `"mid"` | Recall/reflect budget: `low`, `mid`, `high` |
| `max_tokens` | `int` | `None` → `4096` | Max tokens for recall results |
| `tags` | `list[str]` | `None` | Tags applied when storing memories |
| `recall_tags` | `list[str]` | `None` | Tags to filter recall results |
| `recall_tags_match` | `str` | `None` → `"any"` | Tag matching: `any`, `all`, `any_strict`, `all_strict` |
| `retain_metadata` | `dict[str, str]` | `None` | Default metadata for retain operations |
| `retain_document_id` | `str` | `None` | Document ID for retain. Auto-generates `{session}-{timestamp}` if not set |
| `retain_context` | `str` | `"llamaindex"` | Source label for retain operations |
| `recall_types` | `list[str]` | `None` | Fact types: `world`, `experience`, `observation` |
| `recall_include_entities` | `bool` | `False` | Include entity info in recall results |
| `reflect_context` | `str` | `None` | Additional context for reflect |
| `reflect_max_tokens` | `int` | `None` | Max tokens for reflect (defaults to `max_tokens`) |
| `reflect_response_schema` | `dict` | `None` | JSON schema to constrain reflect output |
| `reflect_tags` | `list[str]` | `None` | Tags for reflect (defaults to `recall_tags`) |
| `reflect_tags_match` | `str` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use |
---
## Production Patterns
### Bank Mission
Set a mission to give the memory engine context for fact extraction:
```python
# Tools
spec = HindsightToolSpec(
client=client,
bank_id="user-123",
mission="Track user coding preferences, project context, and technical decisions",
)
# Memory
memory = HindsightMemory.from_client(
client=client,
bank_id="user-123",
mission="Track user coding preferences, project context, and technical decisions",
)
```
The bank is created automatically on first use. If it already exists, creation is silently skipped.
### Memory Scoping with Tags
```python
spec = HindsightToolSpec(
client=client,
bank_id="user-123",
tags=["source:chat", "session:abc"], # applied to all retains
recall_tags=["source:chat"], # filter recalls to chat memories
recall_tags_match="any",
)
```
### Error Handling
Both patterns handle errors gracefully — operations are logged and return friendly messages instead of raising exceptions. Agents continue functioning even if memory is unavailable.
### Combining Tools + Memory
Use both patterns together for maximum flexibility:
```python
from hindsight_llamaindex import create_hindsight_tools, HindsightMemory
# Automatic memory for context enrichment
memory = HindsightMemory.from_client(client=client, bank_id="user-123")
# Explicit tools for agent-driven reflect
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=False, # memory handles retain automatically
include_recall=False, # memory handles recall automatically
include_reflect=True, # agent can still explicitly reflect
)
agent = ReActAgent(tools=tools, llm=llm)
# Pass memory to run()
response = await agent.run("What should I prioritize?", memory=memory)
```
## Requirements
- Python 3.10+
- `llama-index-core >= 0.11.0`
- `hindsight-client >= 0.4.0`
references/sdks/integrations/local-mcp.md
# Local MCP Server
Hindsight provides a local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required.
This is ideal for:
- **Personal use with Claude Code / Claude Desktop** — Give Claude long-term memory across conversations
- **Development and testing** — Quick setup without infrastructure
- **Privacy-focused setups** — All data stays on your machine
## How It Works
Running `hindsight-local-mcp` starts the full Hindsight API on `localhost:8888` with an embedded PostgreSQL database (pg0). You then connect your MCP client to it over HTTP.
- Starts an embedded PostgreSQL (pg0) automatically
- Runs database migrations on startup
- Exposes the full MCP endpoint at `http://localhost:8888/mcp/`
- Data persists in `~/.pg0/hindsight-mcp/` across restarts
## Setup
### 1. Start the server
```bash
HINDSIGHT_API_LLM_API_KEY=sk-... uvx --from hindsight-api hindsight-local-mcp
```
Or with Ollama (no API key needed):
```bash
HINDSIGHT_API_LLM_PROVIDER=ollama HINDSIGHT_API_LLM_MODEL=llama3.2 uvx --from hindsight-api hindsight-local-mcp
```
### 2. Configure your MCP client
**Claude Code:**
```bash
claude mcp add --transport http hindsight http://localhost:8888/mcp/
```
**Other MCP clients** — add an HTTP transport entry pointing to `http://localhost:8888/mcp/`.
## Bank Modes
The local server supports the same two modes as the hosted API:
### Multi-bank mode (default)
Use `http://localhost:8888/mcp/` — exposes all tools including bank management. Bank is selected per-request via the `bank_id` tool parameter or the `X-Bank-Id` header.
```bash
claude mcp add --transport http hindsight http://localhost:8888/mcp/
```
### Single-bank mode
Use `http://localhost:8888/mcp/<bank-id>/` — pins all tools to one bank, no `bank_id` parameter needed. This replaces the old `HINDSIGHT_API_MCP_LOCAL_BANK_ID` env var.
```bash
claude mcp add --transport http hindsight http://localhost:8888/mcp/my-bank/
```
## Available Tools
The local server exposes the full tool set (29 tools in multi-bank mode, 26 in single-bank mode):
**Core Memory**
| Tool | Description |
|------|-------------|
| `retain` | Store information to long-term memory with optional tags, metadata, and document association |
| `recall` | Search memories with natural language, configurable budget, type filters, and tag filters |
| `reflect` | Synthesize memories into a reasoned answer with optional structured output |
**Mental Models**
| Tool | Description |
|------|-------------|
| `list_mental_models` | List pinned reflections for a bank |
| `get_mental_model` | Get a specific mental model |
| `create_mental_model` | Create a new mental model with optional auto-refresh trigger |
| `update_mental_model` | Update a mental model's metadata |
| `delete_mental_model` | Delete a mental model |
| `refresh_mental_model` | Regenerate a mental model's content |
**Directives**
| Tool | Description |
|------|-------------|
| `list_directives` | List directives that guide memory processing |
| `create_directive` | Create a new directive |
| `delete_directive` | Delete a directive |
**Memory Browsing**
| Tool | Description |
|------|-------------|
| `list_memories` | Browse memories with filtering and pagination |
| `get_memory` | Get a specific memory by ID |
**Documents**
| Tool | Description |
|------|-------------|
| `list_documents` | List ingested documents |
| `get_document` | Get a specific document |
| `delete_document` | Delete a document and its linked memories |
**Operations**
| Tool | Description |
|------|-------------|
| `list_operations` | List async operations with status filtering |
| `get_operation` | Check operation status and progress |
| `cancel_operation` | Cancel a pending or running operation |
**Tags & Bank Management**
| Tool | Description |
|------|-------------|
| `list_tags` | List unique tags used in a bank |
| `get_bank` | Get bank profile (name, mission, disposition) |
| `get_bank_stats` | Get bank statistics (multi-bank only) |
| `update_bank` | Update bank name or mission |
| `delete_bank` | Delete an entire bank and all its data |
| `clear_memories` | Clear memories without deleting the bank |
| `list_banks` | List all memory banks (multi-bank only) |
| `create_bank` | Create or configure a memory bank (multi-bank only) |
**Knowledge Base**
| Tool | Description |
|------|-------------|
| `get_knowledge_base_tree` | Browse the folder/page tree |
| `search_knowledge_base` | Hybrid search over knowledge pages |
| `get_knowledge_page` | Read a page as a markdown document |
| `create_knowledge_folder` | Create a folder |
| `create_knowledge_page` | Create a page (a living document over the bank's memories) |
| `update_knowledge_node` | Rename/move a node or update a page's options |
| `delete_knowledge_node` | Delete a folder or page and its subtree |
For detailed parameter documentation, see the [MCP Server reference](../../developer/mcp-server.md#available-tools).
## Environment Variables
All standard [Hindsight configuration variables](../../developer/configuration.md) are supported. Key ones for local use:
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `HINDSIGHT_API_LLM_API_KEY` | Yes* | — | API key for your LLM provider |
| `HINDSIGHT_API_LLM_PROVIDER` | No | `openai` | LLM provider (`openai`, `anthropic`, `ollama`, etc.) |
| `HINDSIGHT_API_LLM_MODEL` | No | `gpt-4o-mini` | Model name |
| `HINDSIGHT_API_DATABASE_URL` | No | `pg0://hindsight-mcp` | Override the database URL |
| `HINDSIGHT_API_PORT` | No | `8888` | Port to listen on |
| `HINDSIGHT_API_LOG_LEVEL` | No | `info` | Log level |
*Not required when using a local provider like Ollama.
## Troubleshooting
### Slow first startup
The first startup downloads the local embedding model (~100MB) and initializes the database. Subsequent starts are faster.
### Port already in use
Set a different port:
```bash
HINDSIGHT_API_LLM_API_KEY=sk-... HINDSIGHT_API_PORT=9000 uvx --from hindsight-api hindsight-local-mcp
```
Then update your MCP client URL to `http://localhost:9000/mcp/`.
### Checking logs
Set `HINDSIGHT_API_LOG_LEVEL=debug` for verbose output:
```bash
HINDSIGHT_API_LLM_API_KEY=sk-... HINDSIGHT_API_LOG_LEVEL=debug uvx --from hindsight-api hindsight-local-mcp
```
references/sdks/integrations/meta-muse.md
# Meta Muse
[View Changelog →](../../changelog/integrations/meta-muse.md)
Bring the memory from your other AI tools into [Meta Muse](https://muse.ai), Meta's personal AI agent. Muse connects to your [Hindsight](https://vectorize.io/hindsight) memory banks over MCP. What you told Claude, ChatGPT, Claude Code, Hermes or OpenClaw is available in Muse, and what you tell Muse is available to them.
Muse gets all of your banks, not one. It keeps its own `muse` bank for what you and it do together, and reads your other banks when a question reaches into them.
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io). Hindsight Cloud already supports the OAuth sign-in Muse uses, so there is nothing to run or configure.
## How It Works
Muse runs in Meta's cloud and has no plugin host, so Hindsight can't hook into its prompts or transcripts. Muse connects to Hindsight's remote MCP server as a custom connector and calls the memory tools itself:
| Tool | When Muse calls it |
|---|---|
| `recall` | Before tasks about people, projects, plans or preferences |
| `retain` | When you state a fact, decision or preference, or finish a task worth remembering |
| `reflect` | For questions about your history or habits |
| `get_mental_model` | Reads an "About me" profile at the start of a conversation |
| `list_banks`, `create_bank` | Learns which banks exist and creates its own `muse` bank on first connect |
Because Muse has no hooks, the connect prompt also asks Muse to create a nightly scheduled task that retains a summary of the day's conversations.
## Setup
1. Sign up at [vectorize.io/hindsight](https://vectorize.io/hindsight/).
2. Open Muse and paste the [connect prompt](https://github.com/vectorize-io/hindsight/blob/main/hindsight-integrations/meta-muse/connect-prompt.md).
3. Muse shows a **Connect** button for Hindsight. Click it and approve access on Hindsight's sign-in page.
4. Muse lists the banks it can see, creates its own `muse` bank plus the "About me" mental model and the nightly task, and tells you three things it found about you.
> **📝 Step 4 is four operations, not one**
>
Listing banks and creating the `muse` bank are free; `recall` and the mental model are billed. On an account with no credits the first two succeed and the last two fail, leaving an empty `muse` bank and a live nightly task pointed at it. Muse will tell you which step failed. Top up, then ask it to retry the blocked steps rather than pasting the connect prompt again.
The prompt uses the root URL, `https://api.hindsight.vectorize.io/mcp` (see the [MCP server reference](../../developer/mcp-server.md) for the endpoint, its tools and its auth options), which reaches every bank in your account. That is the point: Muse writes to its own bank and reads the others when a question calls for it. Because the root URL also exposes bank management, the prompt forbids `delete_bank`, `clear_memories` and `invalidate_memory`. To hand Muse exactly one bank instead, connect it to `/mcp/<bank>/`: that endpoint takes the same OAuth sign-in and drops the `bank_id` argument from every tool.
## Self-Hosted Hindsight
Muse needs a public HTTPS endpoint with OAuth 2.1 discovery and dynamic client registration. Put the [`cloudflare-oauth-proxy`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/cloudflare-oauth-proxy) in front of your instance, then check the endpoint with the preflight tool from the [integration directory](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/meta-muse):
```bash
uvx --from ./hindsight-integrations/meta-muse hindsight-muse-preflight https://memory.example.com/mcp
```
It replays what Muse does: the unauthenticated 401 challenge, the OAuth metadata (registration endpoint, PKCE S256), and, with `HINDSIGHT_API_KEY` set, MCP `initialize` and `tools/list`.
## Verifying Setup
In Muse:
- "Which memory banks can you see?" → `list_banks`
- "What was I working on last week?" → `recall` across your work banks
- "Remember that I prefer window seats." → `retain` into `muse`
- "How do I usually plan trips?" → `reflect`
What Muse retains then shows up in your other tools connected to the same account.
references/sdks/integrations/n8n.md
# n8n
Persistent memory for [n8n](https://n8n.io) workflows via [Hindsight](https://hindsight.vectorize.io). The `@vectorize-io/n8n-nodes-hindsight` community node package adds three operations — **Retain**, **Recall**, **Reflect** — that work alongside any other n8n node.
[View Changelog →](../../changelog/integrations/n8n.md)
## Why this matters
n8n is the connective tissue of automation: triggers from Gmail, Slack, Sheets, Stripe, Notion; actions across 400+ apps. Until now it has been **stateless** — every workflow run starts fresh. With Hindsight nodes you can:
- **Retain** every closed support ticket, sales-call summary, or form submission into a memory bank
- **Recall** relevant context before an OpenAI / Anthropic / Cohere step so the LLM sees prior history
- **Reflect** to ask synthesizing questions ("What do we know about this customer?") right inside a workflow
## Installation
In your n8n instance, go to **Settings → Community Nodes → Install** and enter:
```
@vectorize-io/n8n-nodes-hindsight
```
Or for self-hosted n8n:
```bash
cd ~/.n8n/custom
npm install @vectorize-io/n8n-nodes-hindsight
```
Restart n8n; the **Hindsight** node appears in the node panel.
## Setup
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
1. **Sign up** at [Hindsight Cloud](https://ui.hindsight.vectorize.io) (free tier) or [self-host](../../developer/installation.md)
2. **Get an API key** from the Hindsight dashboard
3. **In n8n**, create a new **Hindsight API** credential with your API URL (defaults to Hindsight Cloud) and the `hsk_...` key
## Operations
### Retain
Store content in a bank. Hindsight extracts facts asynchronously after the call returns.
| Field | Description |
|---|---|
| Bank ID | Memory bank to store in (auto-created on first use) |
| Content | Free text to retain |
| Tags | Comma-separated tags |
### Recall
Search a bank for memories relevant to a query. Returns a `results` array.
| Field | Description |
|---|---|
| Bank ID | Memory bank to search |
| Query | Natural-language query |
| Budget | `low` / `mid` / `high` |
| Max Tokens | Cap on returned memory tokens |
| Tags Filter | Filter by tag |
### Reflect
Get an LLM-synthesized answer over the bank. Returns `text`.
| Field | Description |
|---|---|
| Bank ID | Memory bank |
| Query | Question to answer |
| Budget | `low` / `mid` / `high` |
## Example workflows
**Customer-support assistant** — every closed Zendesk ticket retains the resolution. Every new ticket starts with a recall against the bank to surface similar past issues, then passes that context to OpenAI to draft the first reply.
**Sales-call coach** — Gong webhook → Hindsight Retain (call summary). Before each next prep call, recall on the prospect's name to pull every prior touchpoint, then format into the daily prep doc.
**Personal Slack bot** — Slack DM trigger → Hindsight Recall on the user's question → OpenAI for the answer → Slack reply. The bot remembers every conversation across sessions.
## Source
- npm: [`@vectorize-io/n8n-nodes-hindsight`](https://www.npmjs.com/package/@vectorize-io/n8n-nodes-hindsight)
- GitHub: [`hindsight-integrations/n8n`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/n8n)
references/sdks/integrations/nemoclaw.md
# NemoClaw
Persistent memory for [NemoClaw](https://nemoclaw.ai) sandboxed agents using [Hindsight](https://hindsight.vectorize.io).
NemoClaw runs [OpenClaw](https://openclaw.ai) inside an OpenShell sandbox with controlled filesystem, process, and network egress policies. The `hindsight-nemoclaw` package automates adding Hindsight memory to a sandbox in one command — no code changes required.
[View Changelog →](../../changelog/integrations/nemoclaw.md)
## Quick Start
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — get an API key instantly, no infrastructure to run.
```bash
npx @vectorize-io/hindsight-nemoclaw setup \
--sandbox my-assistant \
--api-token <your-api-key> \
--bank-prefix my-sandbox
```
You'll see output like:
```
[0] Preflight checks...
✓ openshell found
✓ openclaw found
[1] Installing @vectorize-io/hindsight-openclaw plugin...
✓ Plugin installed
[2] Configuring plugin in ~/.openclaw/openclaw.json...
✓ Plugin config written (bank: my-sandbox-openclaw)
[3] Applying Hindsight network policy to sandbox "my-assistant"...
✓ Policy version 2 submitted
✓ Policy version 2 loaded (active version: 2)
[4] Restarting OpenClaw gateway...
✓ Gateway restarted
✓ Setup complete!
```
## How It Works
### The sandbox problem
OpenShell enforces strict network egress — every outbound endpoint must be explicitly permitted in the sandbox policy. By default, the Hindsight API (`api.hindsight.vectorize.io`) is not in that list.
The `hindsight-openclaw` plugin supports **external API mode**, where it skips the local daemon entirely and makes direct HTTPS calls to Hindsight Cloud. This is the natural fit for sandboxed environments: the plugin becomes a thin HTTP client, and the only sandbox change needed is one egress rule.
### What the setup command does
1. **Preflight** — verifies `openshell` and `openclaw` are installed
2. **Install plugin** — runs `openclaw plugins install @vectorize-io/hindsight-openclaw`
3. **Configure plugin** — writes external API mode config to `~/.openclaw/openclaw.json`
4. **Apply policy** — reads the current sandbox policy, merges the Hindsight egress block, and re-applies via `openshell policy set`
5. **Restart gateway** — runs `openclaw gateway restart`
### Memory flow
Once set up, the `hindsight-openclaw` plugin hooks into the OpenClaw gateway lifecycle:
- **`before_agent_start`** — recalls relevant memories from past sessions and injects them into context
- **`agent_end`** — retains the conversation to the Hindsight memory bank
The sandbox doesn't interfere with either step — it sees the Hindsight calls as normal HTTPS egress to a permitted endpoint.
## CLI Reference
```
hindsight-nemoclaw setup [options]
Options:
--sandbox <name> NemoClaw sandbox name (required)
--api-token <token> Hindsight API token (required)
--api-url <url> Hindsight API URL (default: https://api.hindsight.vectorize.io)
--bank-prefix <prefix> Memory bank prefix (default: "nemoclaw")
--skip-policy Skip sandbox network policy update
--skip-plugin-install Skip openclaw plugin installation
--dry-run Preview changes without applying
--help Show help
```
Use `--dry-run` to preview all changes before applying anything. Use `--skip-policy` if you manage sandbox policies manually.
## Manual Setup
If you prefer to apply the steps yourself instead of using the CLI:
### 1. Install the plugin
```bash
openclaw plugins install @vectorize-io/hindsight-openclaw
```
### 2. Configure `~/.openclaw/openclaw.json`
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "<your-api-key>",
"llmProvider": "claude-code",
"dynamicBankId": false,
"bankIdPrefix": "my-sandbox"
}
}
}
}
}
```
`llmProvider: "claude-code"` uses the Claude Code process already present in the sandbox — no additional API key needed.
### 3. Add the Hindsight network policy
`openshell policy set` replaces the entire policy document. Export your current policy first, add the Hindsight block, then re-apply:
```yaml
network_policies:
hindsight:
name: hindsight
endpoints:
- host: api.hindsight.vectorize.io
port: 443
protocol: rest
tls: terminate
enforcement: enforce
rules:
- allow:
method: GET
path: /**
- allow:
method: POST
path: /**
- allow:
method: PUT
path: /**
binaries:
- path: /usr/local/bin/openclaw
```
```bash
openshell policy set my-sandbox --policy /path/to/full-policy.yaml --wait
openclaw gateway restart
```
## Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
| `hindsightApiUrl` | string | — | Hindsight API base URL |
| `hindsightApiToken` | string | — | API token for authentication |
| `llmProvider` | string | auto-detect | LLM provider for memory extraction |
| `dynamicBankId` | boolean | `false` | Isolate memory per user (`true`) or share across sessions (`false`) |
| `bankIdPrefix` | string | `"nemoclaw"` | Prefix for the memory bank name |
### Bank naming
When `dynamicBankId: false`, all sessions write to a single bank named `{bankIdPrefix}-openclaw`. When `dynamicBankId: true`, each user gets an isolated bank — useful for multi-tenant deployments.
## Verifying It Works
After setup, check the gateway logs:
```bash
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
```
On startup you should see:
```
[Hindsight] Plugin loaded successfully
[Hindsight] ✓ Using external API: https://api.hindsight.vectorize.io
[Hindsight] External API health: {"status":"healthy","database":"connected"}
[Hindsight] Default bank: my-sandbox-openclaw
[Hindsight] ✓ Ready (external API mode)
```
After a conversation:
```
[Hindsight] before_agent_start - bank: my-sandbox-openclaw, channel: undefined/webchat
[Hindsight Hook] agent_end triggered - bank: my-sandbox-openclaw
[Hindsight] Retained 6 messages to bank my-sandbox-openclaw for session agent:main:...
```
## Pitfalls
### Policy replacement is full-document
`openshell policy set` replaces the entire policy document. The `hindsight-nemoclaw setup` command handles this automatically. If you're applying manually, export the current policy first so existing rules aren't lost.
### LaunchAgent can't follow symlinks on macOS
On macOS, the OpenClaw gateway runs as a LaunchAgent under a restricted security context. `openclaw plugins install --link` creates a symlink the LaunchAgent can't follow — the setup command installs as a copy instead. If you see `EPERM: operation not permitted, scandir` in gateway logs, this is the cause.
### Memory retention is asynchronous
Fact extraction and entity resolution happen in the background after `retain`. If you open a new session immediately after closing one, the most recent memories may not be indexed yet — typically a few seconds.
### Binary-scoped egress
The `binaries` field in the network policy restricts the egress rule to a specific executable path. If OpenClaw updates and the binary path changes, the rule silently stops working. Check your binary path after upgrades.
## Troubleshooting
### Plugin not loading
```bash
openclaw plugins list | grep hindsight
# Should show: ✓ enabled │ Hindsight Memory │ ...
# Reinstall
openclaw plugins install @vectorize-io/hindsight-openclaw
```
### Egress blocked
If calls to `api.hindsight.vectorize.io` are being blocked, check the active sandbox policy:
```bash
openshell sandbox get my-assistant
```
Verify the `hindsight` block is present and the `binaries` path matches your OpenClaw binary:
```bash
which openclaw
```
### External API not connecting
```bash
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# If you see daemon startup messages instead of "Using external API",
# the plugin config isn't being read — check ~/.openclaw/openclaw.json
```
references/sdks/integrations/obsidian.md
# Obsidian
Give your [Obsidian](https://obsidian.md) vault an agent that actually knows your notes, powered by [Hindsight](https://hindsight.vectorize.io). The plugin syncs your vault into a Hindsight bank and adds a chat panel whose answers are grounded on your notes — and cite them.
[View Changelog →](../../changelog/integrations/obsidian.md)
## Why this matters
Obsidian is where your knowledge lives. Vector-search plugins can find related notes, but they can't reason across them, remember what you asked, or keep a running synthesis as your vault grows. Hindsight adds that layer:
- **Recall** is faster and more accurate than text search.
- **Reflect** reasons over your whole vault to answer a question, and shows the notes it used.
- **Mental models** (on the roadmap) keep living summaries of your vault that refresh as you write.
## Source of truth stays in Obsidian
A hard rule of this integration: **Hindsight never becomes a second source of truth.** Sync is one-way (Obsidian → Hindsight), every answer cites the note it came from so you can **fix things at the source**, and chat conversations are **not** stored by default. Edit a note, and Hindsight reconverges on the next sync.
## What it does
- **Incremental vault sync** — notes are retained as Hindsight documents. Edits upsert, deletes remove. A content hash means unchanged notes are skipped.
- **Implicit scoping** — every note is auto-tagged on ingest with its **vault**, **folder** (and sub-folders), and **created/updated dates**. You never think about scope until you recall — then filter by any combination via Hindsight's `tag_groups`. Multiple vaults share one bank and stay separable by their `vault:` tag.
- **Same data from UI and API** — the Obsidian chat panel and your automations (n8n, Hermes, …) hit the same bank with the same tags, so they see the same scoped view.
- **Grounded chat** — a side panel that answers questions over your notes via Reflect, with collapsible **citations** (click to open the source note) and a **reasoning** disclosure.
- **Manual or automatic** — sync on every edit, or run _Sync vault now_ on demand.
### Scoping
Recall/reflect (from the UI or an API call) can filter by any combination of:
| Dimension | Tag(s) |
| -------------------- | ------------------------------------ |
| Vault | `vault:<name>` |
| Folder (+ ancestors) | `folder:Work`, `folder:Work/Clients` |
| Date | `created:2026-03`, `updated:2026-06` |
Your own frontmatter `tags`/`aliases` are carried through too.
## Installation
> ✨ **Recommended:** [Hindsight Cloud](https://ui.hindsight.vectorize.io) — sign up free, get an API key, and skip self-hosting.
While the plugin is in beta it installs via [BRAT](https://github.com/TfTHacker/obsidian42-brat): add the repository [`vectorize-io/hindsight-obsidian`](https://github.com/vectorize-io/hindsight-obsidian) (the dedicated plugin repo BRAT installs from) and enable it in **Settings → Community plugins**.
**Self-hosting alternative** — run Hindsight locally:
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api
```
## Configuration
Open **Settings → Hindsight**:
| Setting | Default | Description |
| ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------- |
| API URL | `https://api.hindsight.vectorize.io` | Hindsight server (use `http://localhost:8888` for self-hosted) |
| API key | — | Hindsight Cloud API key |
| Bank name | `obsidian` | Shared bank for all vaults (separated by `vault:` tags) |
| Include / exclude folders | — | Limit which notes sync |
| Sync on edit | on | Re-ingest notes automatically as you edit |
| Default chat depth | low | Reflect budget for chat answers |
| Remember conversations | **off** | When on, chat turns are stored in Hindsight (creates memory outside your vault) |
## Commands
- **Sync vault now** — full reconcile (ingest changed notes, prune deleted ones).
- **Ingest current note** — force-sync the active note.
- **Open chat** — open the grounded chat panel.
## Headless / CLI ingestion (no desktop app)
Running your vault on an always-on server (kept current on disk by Obsidian Sync)? The same ingestion runs without the Obsidian app via the `hindsight-obsidian-sync` CLI, shipped in the same package. It drives the **same sync engine** as the plugin, so it produces identical document ids, scope tags, and prune-ownership — a vault can be synced from a server and later opened interactively elsewhere without the two ingesters fighting or duplicating documents.
```bash
npm install -g @vectorize-io/hindsight-obsidian
# one-shot reconcile (cron-friendly)
hindsight-obsidian-sync reconcile \
--vault ~/Vaults/Brain --bank my-vault \
--api-url https://api.hindsight.vectorize.io --api-token hsk_...
# or keep it running and sync changes live
hindsight-obsidian-sync reconcile --vault ~/Vaults/Brain --bank my-vault --watch
```
`--api-url` / `--api-token` fall back to `HINDSIGHT_API_URL` / `HINDSIGHT_API_TOKEN`. Other flags: `--include` / `--exclude` (repeatable), `--vault-name`, `--prefix-doc-id`, and `--index`.
The sync index (the CLI's equivalent of the plugin's `data.json`) defaults to `~/.hindsight/obsidian/<vault>.json` — deliberately **outside** the vault so Obsidian Sync never propagates it. If you run the CLI and the plugin against the same bank and vault, keep their scope settings identical (include/exclude, vault name, `prefix-doc-id`): each keeps its own index and a reconcile prunes only what its own index tracks, so mismatched scope could let one delete documents the other owns.
references/sdks/integrations/omnigent.md
# Omnigent
Persistent long-term memory for every harness in [Omnigent](https://github.com/omnigent-ai/omnigent) — the meta-harness that wraps and coordinates multiple AI agents (Claude Code, Codex, Cursor, OpenCode, Hermes, Pi, and more).
Omnigent ships three built-in memory tools — `hindsight_recall`, `hindsight_retain`, and `hindsight_reflect` — that Omnigent intercepts and executes locally using `hindsight-client`. Because the tools live in the Omnigent runner rather than in each harness, every wrapped agent gets memory regardless of whether it has native Hindsight support.
## Installation
```bash
pip install "omnigent[memory]"
export HINDSIGHT_API_KEY=hsk_...
```
Get an API key from [Hindsight Cloud](https://ui.hindsight.vectorize.io), or point at a self-hosted server with `HINDSIGHT_API_URL`.
## Setup
Add the three tools to your agent's YAML spec under `tools.builtins`:
```yaml
name: my-agent
tools:
builtins:
- name: hindsight_recall
api_key: ${HINDSIGHT_API_KEY}
bank_id: my-agent-memory # optional; defaults to agent_id
budget: mid # low / mid / high
max_tokens: 4096
- name: hindsight_retain
api_key: ${HINDSIGHT_API_KEY}
bank_id: my-agent-memory
- name: hindsight_reflect
api_key: ${HINDSIGHT_API_KEY}
bank_id: my-agent-memory
```
Restart your agent — the three tools will appear in its tool list.
## How It Works
Omnigent intercepts `hindsight_*` calls at the runner level before they reach the wrapped harness, executes them locally via `hindsight-client`, and returns the result. This means:
- **Wrapped harnesses** (Claude Code, Codex, Cursor, Pi…) never need to handle the call; the runner does it.
- **Native harnesses** (when running agents in native mode) have the tools relayed to them as well.
- **One setup for all of them.** Many of these harnesses have their own native Hindsight integration, but through Omnigent you configure memory once and it applies to every harness you orchestrate, including custom ones with no native option.
The agent calls the tools **explicitly**; there are no automatic lifecycle hooks. Include instructions in your agent's system prompt:
```
- At the start of each task, call hindsight_recall with the user's request
to load relevant decisions, preferences, and project context.
- When the user gives you a durable fact (a convention, a decision, a
preference), call hindsight_retain to store it.
- Call hindsight_reflect to synthesize what you know about a topic across sessions.
```
## Bank Scoping
Omnigent resolves the memory bank in this order:
1. The explicit `bank_id` in your YAML config — most predictable, recommended.
2. The agent's `agent_id` — one bank per agent, shared across conversations.
3. The `conversation_id` — one bank per conversation; lost when the conversation ends.
To share memory between two agents, point them at the same `bank_id`.
## Configuration Reference
| Field | Description | Default |
|---|---|---|
| `api_key` | Hindsight API key | _(required for Cloud)_ |
| `api_url` | Hindsight API base URL | `https://api.hindsight.vectorize.io` |
| `bank_id` | Memory bank name | `agent_id` or `conversation_id` |
| `budget` | Recall token budget (`low` / `mid` / `high`) | `mid` |
| `max_tokens` | Maximum tokens returned by recall | `4096` |
| `tags` | CSV tags applied to retained memories | _(none)_ |
| `recall_tags` | CSV tags to filter recalled memories | _(none)_ |
| `recall_tags_match` | Tag match mode (`any` / `all` / `any_strict` / `all_strict`) | `any` |
## Self-Hosted
Point at your own server with `api_url`; no token needed for an open server:
```yaml
- name: hindsight_recall
api_url: http://localhost:8888
bank_id: local-memory
```
## Working Example
Omnigent ships a complete example at [`examples/remy/config.yaml`](https://github.com/omnigent-ai/omnigent/blob/main/examples/remy/config.yaml) — a conversational assistant with all three tools wired up and instructions on when to call each one:
```bash
HINDSIGHT_API_KEY=hsk_... omnigent run examples/remy
```
After a few conversations, ask it something it learned in a previous session.
## Which Harnesses Benefit
| Harness | Native Hindsight integration | Via Omnigent |
|---|---|---|
| Claude Code | Yes (official) | Yes |
| Cursor | Yes (official) | Yes |
| Codex | Yes (official) | Yes |
| OpenCode | Yes (official) | Yes |
| Pi | Yes (community, via epimetheus) | Yes |
| Custom harness | Usually none | Yes |
Most of these harnesses already have a native Hindsight integration, ideal when you run that tool standalone. Omnigent's value is one central memory setup that spans every harness you orchestrate at once, plus coverage for custom harnesses with no native option.
## Further Reading
- One memory for every AI tool: point multiple harnesses at the same bank.
- Inside retain(): what happens when the agent calls `hindsight_retain`.
- [Omnigent on GitHub](https://github.com/omnigent-ai/omnigent): the meta-harness source and examples.
references/sdks/integrations/omo.md
# OMO (oh-my-openagent)
[View Changelog →](../../changelog/integrations/omo.md)
Persistent memory for [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) (OMO) using [Hindsight](https://vectorize.io/hindsight). Five lifecycle hooks automatically recall relevant context before each prompt and retain conversations after each turn — no changes to your OMO workflow required.
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
From the `hindsight-integrations/omo/` directory:
```bash
# Hooks (global)
mkdir -p ~/.omo/hooks
cp hooks/hooks.json ~/.omo/hooks/hindsight-hooks.json
# Scripts + settings (global)
mkdir -p ~/.omo/plugins/hindsight/scripts
cp -r scripts/ ~/.omo/plugins/hindsight/scripts/
cp settings.json ~/.omo/plugins/hindsight/settings.json
# Rules (per-project — run from your project root)
mkdir -p .omo/rules
cp rules/hindsight-memory.md .omo/rules/hindsight-memory.md
```
Then set your API key:
```bash
export HINDSIGHT_API_TOKEN=hsk_your_key_here
```
Add env vars to OMO's allowlist in `~/.config/opencode/oh-my-openagent.jsonc`:
```jsonc
{
"mcp_env_allowlist": [
"HINDSIGHT_API_URL",
"HINDSIGHT_API_TOKEN",
"HINDSIGHT_BANK_ID"
]
}
```
Start a new OMO session — memory is live.
## Features
- **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as `additionalContext` (invisible to the transcript, visible to OMO)
- **Auto-retain** — after each OMO response and sub-agent stop, stores the conversation transcript to Hindsight for future recall
- **Sub-agent capture** — `SubagentStop` hook captures learnings from OMO's delegated sub-agents (Claude Code, Codex, OpenCode)
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory
- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content
- **Cloud-first** — defaults to Hindsight Cloud with no local daemon to manage
- **Zero dependencies** — pure Python stdlib, no pip install required
## Architecture
The plugin uses five OMO hook events:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable |
| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` |
| `retain.py` | `Stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) |
| `retain.py` | `SubagentStop` | **Sub-agent retain** — capture delegated sub-agent learnings |
| `session_end.py` | `SessionEnd` | Force final retain for short sessions |
On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and outputs a `hookSpecificOutput.additionalContext` block. OMO prepends this to the conversation before sending it to the model:
```
<hindsight_memories>
Relevant memories from past conversations...
Current time - 2026-06-08 14:30
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-06-07)
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-06-07)
</hindsight_memories>
```
On `Stop` and `SubagentStop`, the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously.
## Connection Modes
### 1. Hindsight Cloud (recommended)
No setup beyond an API key:
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
### 2. Self-Hosted
Point at your own Hindsight instance:
```bash
export HINDSIGHT_API_URL=http://localhost:8888
```
Or in `~/.hindsight/omo.json`:
```json
{
"hindsightApiUrl": "http://localhost:8888"
}
```
No API token is required for local instances.
## Configuration
Settings are loaded from three sources in order (later wins):
1. Plugin `settings.json` (cloud URL pre-set)
2. User config (`~/.hindsight/omo.json`)
3. `HINDSIGHT_*` environment variables
---
### Connection
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | API endpoint. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | — | API key for authentication. Required for Hindsight Cloud. |
---
### Memory Bank
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"omo"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | OMO orchestrator prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields. |
| `dynamicBankGranularity` | — | `["agent", "project"]` | Fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. |
| `bankIdPrefix` | — | `""` | Prefix prepended to all bank IDs. |
---
### Auto-Recall
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. |
| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. |
| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | Prior turns to include when building the recall query. `1` = latest prompt only. |
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max characters in the query sent to Hindsight. |
| `recallRoles` | — | `["user", "assistant"]` | Roles to include when building a multi-turn query. |
| `recallPromptPreamble` | — | built-in | Text placed above the recalled memories in the injected context block. |
---
### Auto-Retain
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session. `"chunked"` sends sliding windows every N turns. |
| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. |
| `retainOverlapTurns` | — | `2` | Extra turns included from the previous chunk (chunked mode only). |
| `retainRoles` | — | `["user", "assistant"]` | Roles to include in the retained transcript. |
| `retainTags` | — | `["{session_id}"]` | Tags attached to the stored document. `{session_id}` is replaced at runtime. |
| `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the stored document. |
| `retainContext` | — | `"omo"` | Label identifying the source integration. |
---
### Debug
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. |
## Per-Project Memory
To give each project its own isolated memory bank, enable dynamic bank IDs:
```json
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "project"]
}
```
With this config, running OMO in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path (e.g. `omo::api`, `omo::frontend`).
## Multi-Bank Recall
Query additional banks alongside the primary one:
```json
{
"recallAdditionalBanks": ["shared-team-knowledge"]
}
```
## Troubleshooting
**No memories recalled**: Recall returns results only after something has been retained. Complete one OMO session first, then start a new one to see memories.
**Memory not being stored**: `retainEveryNTurns` defaults to `10` — retain only fires every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/omo.json`.
**Cloud mode silently skipping**: If `hindsightApiUrl` points to Hindsight Cloud but no `hindsightApiToken` is set, hooks silently skip. Set `HINDSIGHT_API_TOKEN` or add it to `~/.hindsight/omo.json`.
**Debug mode**: Add `"debug": true` to `~/.hindsight/omo.json` to see what Hindsight is doing on each turn:
```
[Hindsight] Recalling from bank 'omo', query length: 42
[Hindsight] Injecting 3 memories
[Hindsight] Retaining to bank 'omo', doc 'sess-abc123', 2 messages, 847 chars
```
references/sdks/integrations/openai-agents.md
# OpenAI Agents SDK
Persistent long-term memory for [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) agents via Hindsight. Provides `FunctionTool` instances that plug directly into the OpenAI Agents SDK `Agent`.
[View Changelog →](../../changelog/integrations/openai-agents.md)
## Features
- **Memory Tools** — retain, recall, and reflect as OpenAI Agents SDK `FunctionTool` instances compatible with `Agent(tools=[...])`
- **Async-Native** — Uses `aretain`, `arecall`, `areflect` directly — works seamlessly in the Agents SDK async runtime
- **Auto-Inject Memories** — Use `memory_instructions()` to automatically inject relevant memories into the agent's system prompt every turn
- **Selective Tools** — Include only the tools you need with `include_retain/recall/reflect` flags
- **Tag-Based Scoping** — Partition memories by topic, session, or user with tags
- **Global Configuration** — Configure once with `configure()`, create tools anywhere
## Installation
```bash
pip install hindsight-openai-agents openai-agents
```
`hindsight-openai-agents` pulls in `openai-agents` and `hindsight-client`.
## Quick Start
```python
import asyncio
from agents import Agent, Runner
from hindsight_client import Hindsight
from hindsight_openai_agents import create_hindsight_tools
async def main():
client = Hindsight(base_url="http://localhost:8888")
await client.acreate_bank(bank_id="user-123")
tools = create_hindsight_tools(client=client, bank_id="user-123")
agent = Agent(
name="assistant",
instructions="You are a helpful assistant with long-term memory. Use hindsight_retain to store important facts. Use hindsight_recall to search memory before answering.",
tools=tools,
)
# Store a memory
result = await Runner.run(agent, "Remember that I prefer dark mode")
print(result.final_output)
# Hindsight processes retained content asynchronously (fact extraction,
# entity resolution, embeddings). A brief pause ensures memories are
# searchable before the next recall. In production, this delay is only
# needed when retain and recall happen back-to-back in the same script.
await asyncio.sleep(3)
# Recall it later
result = await Runner.run(agent, "What are my UI preferences?")
print(result.final_output)
# Clean up
await client.aclose()
asyncio.run(main())
```
> **💡 Jupyter Notebooks**
>
If you're running in a Jupyter notebook, you don't need `asyncio.run()` — just use `await` directly in cells since the notebook already has an active event loop.
The agent gets three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
## Auto-Inject Memories with `memory_instructions()`
Instead of relying on the agent to call `hindsight_recall` explicitly, you can auto-inject relevant memories into the system prompt on every turn:
```python
from hindsight_openai_agents import create_hindsight_tools, memory_instructions
agent = Agent(
name="assistant",
instructions=memory_instructions(
client=client,
bank_id="user-123",
base_instructions="You are a helpful assistant with long-term memory.",
),
tools=create_hindsight_tools(
client=client,
bank_id="user-123",
include_recall=False, # recall handled by memory_instructions
),
)
```
`memory_instructions()` returns an async callable compatible with `Agent(instructions=...)`. On each turn it recalls relevant memories and appends them to your base instructions. If recall fails or returns nothing, it gracefully falls back to `base_instructions` alone.
## Selecting Tools
Include only the tools you need:
```python
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=True,
include_recall=True,
include_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing a client to every call, configure once:
```python
from hindsight_openai_agents import configure, create_hindsight_tools
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode
)
# Now create tools without passing client — uses global config
tools = create_hindsight_tools(bank_id="user-123")
```
## Memory Scoping with Tags
Use tags to partition memories by topic, session, or user:
```python
# Store memories tagged by source
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
tags=["source:chat", "session:abc"],
recall_tags=["source:chat"],
recall_tags_match="any",
)
```
## Production Patterns
### Error Handling
Tools surface errors to the agent as tool error results. The OpenAI Agents SDK catches exceptions from tools automatically and returns them as error strings, allowing the agent to handle failures gracefully:
```python
from hindsight_openai_agents.errors import HindsightError
# The agent will see error messages and can decide how to proceed
result = await Runner.run(agent, "What do you remember about me?")
print(result.final_output)
```
### Bank Lifecycle
Create banks before first use and clean up when done:
```python
async def main():
client = Hindsight(base_url="http://localhost:8888")
# Create bank (idempotent)
await client.acreate_bank(bank_id="user-123")
tools = create_hindsight_tools(client=client, bank_id="user-123")
# ... use tools ...
# Optional: delete bank when no longer needed
await client.adelete_bank(bank_id="user-123")
```
### Multi-Agent Workflows
Give each agent its own memory bank, or share a bank across agents:
```python
# Per-agent memory
researcher_tools = create_hindsight_tools(client=client, bank_id="researcher-memory")
writer_tools = create_hindsight_tools(client=client, bank_id="writer-memory")
# Shared memory
shared_tools = create_hindsight_tools(
client=client,
bank_id="team-shared",
tags=["team:content"],
)
```
## API Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode (any/all/any\_strict/all\_strict) |
| `retain_metadata` | `None` | Default metadata dict for retain operations |
| `retain_document_id` | `None` | Default document\_id for retain (groups/upserts memories) |
| `recall_types` | `None` | Fact types to filter (world, experience, observation) |
| `recall_include_entities` | `False` | Include entity information in recall results |
| `reflect_context` | `None` | Additional context for reflect operations |
| `reflect_max_tokens` | `None` | Max tokens for reflect results (defaults to `max_tokens`) |
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
| `reflect_tags` | `None` | Tags to filter memories used in reflect (defaults to `recall_tags`) |
| `reflect_tags_match` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
| `include_retain` | `True` | Include the retain (store) tool |
| `include_recall` | `True` | Include the recall (search) tool |
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank to recall from |
| `base_instructions` | `""` | Static instructions prepended before memories |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | The recall query to find relevant memories |
| `budget` | `"mid"` | Recall budget level (low/mid/high) |
| `max_results` | `5` | Maximum number of memories to include |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"\n\nRelevant memories:\n"` | Text prepended before the memory list |
| `tags` | `None` | Tags to filter when searching |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Production API | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
## Requirements
- Python >= 3.10
- openai-agents >= 0.7.0
- hindsight-client >= 0.4.0
references/sdks/integrations/openclaw.md
# OpenClaw
Local, long term memory for [OpenClaw](https://openclaw.ai) agents using [Hindsight](https://vectorize.io/hindsight).
This plugin integrates [hindsight-embed](https://vectorize.io/hindsight/cli), a standalone daemon that bundles Hindsight's memory engine (API + PostgreSQL) into a single command. Everything runs locally on your machine, reuses the LLM you're already paying for, and costs nothing extra.
[View Changelog →](../../changelog/integrations/openclaw.md)
## Quick Start
**Step 1: Install the plugin**
```bash
openclaw plugins install @vectorize-io/hindsight-openclaw
```
**Step 2: Run the setup wizard**
`openclaw plugins install` unpacks the plugin into `~/.openclaw/extensions/`
but does not put its bins on `PATH`. Run the wizard through `npx` instead —
it resolves the bin out of the published package:
```bash
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup
```
The wizard walks you through picking one of three install modes:
- **Cloud** — managed Hindsight at `https://api.hindsight.vectorize.io`. Paste your cloud API token when prompted (masked input). No local setup needed.
- **External API** — your own running Hindsight deployment. Prompts for the URL and, optionally, the token value (masked).
- **Embedded daemon** — spawns a local `hindsight-embed` daemon on this machine. Prompts for the LLM provider (OpenAI / Anthropic / Gemini / Groq / Claude Code / OpenAI Codex / Ollama) and the API key (masked).
The interactive wizard stores credentials **inline** in `openclaw.json` for simplicity. For CI / production you can store credentials as a [`SecretRef`](#llm-configuration) (resolved from an env var, file, or exec source at startup, never saved on disk) by either passing `--token-env` / `--api-key-env` to the non-interactive wizard or switching an existing field afterwards via `openclaw config set ... --ref-source env|file|exec`.
For CI and scripted setups the wizard also runs non-interactively — either with an inline value or with an env var reference:
```bash
# Cloud — inline token (simplest)
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode cloud --token hsk_your_cloud_token
# Cloud — SecretRef (read from env at gateway startup)
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode cloud --token-env HINDSIGHT_CLOUD_TOKEN
# External API (no auth)
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode api --api-url https://mcp.hindsight.example.com --no-token
# Embedded daemon with OpenAI — inline API key
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode embedded --provider openai --api-key sk-...
# Embedded daemon with OpenAI — SecretRef
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode embedded --provider openai --api-key-env OPENAI_API_KEY
# Embedded daemon with Claude Code (no API key needed)
npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup \
--mode embedded --provider claude-code
```
Run `npx --package @vectorize-io/hindsight-openclaw hindsight-openclaw-setup --help` for the full flag list.
**Step 3: Start OpenClaw**
```bash
openclaw gateway
```
The plugin will automatically capture conversations after each turn and inject relevant memories before agent responses.
**Important:** The LLM you configure above is **only for memory extraction** (background processing). Your main OpenClaw agent can use any model you configure separately.
**Migrating from 0.5.x?** See the [Migration from 0.5.x](#migration-from-05x) section below for the env-var → SecretRef mapping.
## How It Works
**Auto-Capture:** Every conversation is automatically stored after each turn. Facts, entities, and relationships are extracted in the background.
**Auto-Recall:** Before each agent response, relevant memories are automatically injected into the context (up to 1024 tokens by default). The agent uses past context without needing to call tools.
**Feedback Loop Prevention:** The plugin automatically strips injected memory tags (`<hindsight_memories>`) before storing conversations. This prevents recalled memories from being re-extracted as new facts, which would cause exponential memory growth and duplicate entries.
Traditional memory systems give agents a `search_memory` tool - but models don't use it consistently. Auto-recall solves this by injecting memories automatically before every turn.
## Configuration
### Plugin Settings
Optional settings in `~/.openclaw/openclaw.json`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"apiPort": 9077,
"embedVersion": "latest"
}
}
}
}
}
```
**Options:**
- `apiPort` - Port for the openclaw profile daemon (default: `9077`)
- `embedVersion` - hindsight-embed version (default: `"latest"`)
- `llmProvider` - LLM provider for memory extraction (`openai`, `anthropic`, `gemini`, `groq`, `ollama`, `openai-codex`, `claude-code`). Required unless `hindsightApiUrl` is set.
- `llmModel` - LLM model used with `llmProvider` (provider default if omitted)
- `llmApiKey` - API key for the LLM provider. **Sensitive** — set via `openclaw config set ... --ref-source env --ref-id OPENAI_API_KEY` to reference an env var.
- `llmBaseUrl` - Optional base URL override for OpenAI-compatible providers (e.g. `https://openrouter.ai/api/v1`)
- `bankMission` - Agent identity/purpose stored on the memory bank's `reflect_mission`. **Only affects `reflect`** — does not steer retain or recall. Set once per bank on first use.
- `retainMission` - Stamped onto the bank's `retain_mission` on first use. Steers what gets extracted as facts during retain.
- `observationsMission` - Stamped onto the bank's `observations_mission` on first use. Controls what gets synthesised into observations during consolidation.
- `retainExtractionMode` - Fact extraction mode stamped on first bank use: `concise`, `verbose`, `custom`, `verbatim`, or `chunks`. Leave unset to keep the server default.
- `enableObservations` - Toggle observation consolidation after retain, stamped on first bank use.
- `enableAutoConsolidation` - Toggle automatic consolidation scheduling, stamped on first bank use (via the bank config API).
- `dispositionSkepticism` / `dispositionLiteralism` / `dispositionEmpathy` - Reflect disposition traits (`1`–`5`) stamped on first bank use.
- `entityLabels` - Controlled vocabulary for entity labels. Either a list of attribute defs (e.g. `[{ "name": "person", "description": "Human user" }]`) or a `{ "attributes": [...] }` object; other shapes are ignored. Stamped on first bank use.
- `dynamicBankId` - Enable per-context memory banks (default: `true`)
- `bankId` - Static bank ID used when `dynamicBankId` is `false`.
- `bankIdPrefix` - Optional prefix for bank IDs (e.g. `"prod"` → `"prod-slack-C123"` or `"prod-shared-bank"`)
- `agentBankMap` - Explicit `agentId` → `bankId` routing, checked before static/dynamic derivation. Lets a group of agents share one named bank while other agents keep their derived banks. Mapped names are used exactly as given (`bankIdPrefix` is not applied).
- `dynamicBankGranularity` - Fields used to derive bank ID: `agent`, `channel`, `user`, `provider` (default: `["agent", "channel", "user"]`)
- `excludeProviders` - Message providers to skip for recall/retain (e.g. `["slack"]`, `["telegram"]`, `["discord"]`)
- `autoRecall` - Auto-inject memories before each turn (default: `true`). Set to `false` when the agent has its own recall tool.
- `autoRetain` - Auto-retain conversations after each turn (default: `true`)
- `retainRoles` - Which message roles to retain (default: `["user", "assistant"]`). Options: `user`, `assistant`, `system`, `tool`
- `recallBudget` - Recall effort: `"low"`, `"mid"`, or `"high"` (default: `"mid"`). Higher budgets use more retrieval strategies for better results.
- `recallMaxTokens` - Max tokens for recall response (default: `1024`). Controls how much memory context is injected per turn.
- `recallTopK` - Max number of memories to inject per turn (default: unlimited).
- `recallTypes` - Memory types to recall (default: `["observation"]`). Options: `world`, `experience`, `observation`. Defaults to observations — the consolidated, deduplicated view — to avoid surfacing the same answer multiple times when many raw memories say the same thing.
- `preferObservations` - When `true`, recall drops raw facts already consolidated into an observation while keeping unconsolidated ones (default: `false`). Pair it with a `recallTypes` that includes raw types (e.g. `["observation", "world", "experience"]`) to surface just-retained facts before consolidation catches up — for example after a `/reset` followed by "what did I just say?" — without duplicating already-consolidated content.
- `recallMinScores` - Optional score floors for auto-recall, keyed by stage (for example `{"reranker": 0.3}`). Missing fields impose no floor; memories with missing or `null` scores pass. Reranker scores are query-local, so use this as a garbage gate rather than a calibrated relevance dial.
- `recallContextTurns` - Number of prior user turns to include in the recall query (default: `1`).
- `recallMaxQueryChars` - Max characters for the composed recall query (default: `800`).
- `recallPromptPreamble` - Custom preamble text placed above recalled memories. Overrides the built-in guidance text.
- `recallInjectionPosition` - Where to inject recalled memories: `"user"` (default), `"prepend"`, or `"append"`. The default injects before the user message and preserves the system prompt cache. Use `"prepend"` or `"append"` when memories need system-level context.
- `recallRoles` - Which message roles to include when composing the contextual recall query (default: `["user", "assistant"]`).
- `retainEveryNTurns` - Retain every Nth turn (default: `1` = every turn). Values > 1 enable chunked retention. When a session ends (`/new`, `/reset`, gateway stop) the turns since the last retain are flushed; on SQLite-backed OpenClaw this needs 2026.10.1 or newer.
- `senderPrefixPattern` - Regex matching a human display-name prefix that some channels prepend to the user's text (e.g. `Alice: today weather?`). Supply the name part only — `Alice|Bob` or `[A-Za-z ]{1,20}` — the `:` separator and anchoring are added by the plugin. When set, the prefix is stripped from both the recall query and the retained transcript, so the sender's name no longer pollutes vector search or get extracted as a fact. Unset (default) strips nothing; an invalid regex is ignored.
- `retainOverlapTurns` - Extra prior turns included when chunked retention fires (default: `0`).
- `debug` - Enable debug logging (default: `false`).
### Memory Isolation
The plugin creates separate memory banks based on conversation context. By default, banks are derived from the `agent`, `channel`, and `user` fields — so each unique combination gets its own isolated memory store.
You can customize which fields are used for bank segmentation with `dynamicBankGranularity`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"dynamicBankGranularity": ["provider", "user"]
}
}
}
}
}
```
In this example, memories are isolated per provider + user, meaning the same user shares memories across all channels within a provider.
Available isolation fields:
- `agent` - The agent/bot identity
- `channel` - The channel or conversation ID
- `user` - The user interacting with the agent
- `provider` - The message provider (e.g. Slack, Discord)
Use `bankIdPrefix` to namespace bank IDs across environments (e.g. `"prod"`, `"staging"`). Set `dynamicBankId` to `false` to use a single shared bank for all conversations. In static mode, the plugin uses `bankId` if set, otherwise the default `openclaw` bank name.
#### Mixing shared and isolated banks
Those two modes are all-or-nothing. When you run several agents in one gateway and want some of them to share knowledge while others stay isolated, use `agentBankMap`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "channel", "user"],
"agentBankMap": {
"inbound": "ps-technology",
"outbound": "ps-technology",
"limpieza": "ps-limpieza"
}
}
}
}
}
}
```
The `inbound` and `outbound` agents share the `ps-technology` bank, `limpieza` writes to `ps-limpieza`, and any agent not listed keeps the bank it would otherwise derive. The map is checked before both static and dynamic derivation, so it also takes precedence over a configured `bankId`.
Agent ids are matched exactly (case-sensitive), and mapped bank names are used exactly as written — `bankIdPrefix` is not applied to them. Mapped banks still receive your configured bank defaults on first use, and auto-recall, auto-retain and the knowledge tools all resolve the same mapped bank.
### Per-user bank defaults
With `dynamicBankId` enabled (the default), each derived bank otherwise inherits only the Hindsight **server** defaults (`concise` extraction, no entity labels, etc.). To make every per-user bank match your intended base/shared bank, set the bank-default options below — they are stamped onto each bank **on first use**, before its first retain or recall:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "channel", "user"],
"retainExtractionMode": "verbose",
"enableObservations": true,
"enableAutoConsolidation": true,
"dispositionSkepticism": 3,
"dispositionLiteralism": 3,
"dispositionEmpathy": 4,
"entityLabels": [
{ "name": "person", "description": "A human user or contact" },
{ "name": "project", "description": "A software project or product" }
],
"retainMission": "Extract durable preferences, decisions, and project context.",
"observationsMission": "Synthesise stable user preferences and active projects.",
"bankMission": "You are a helpful assistant with long-term memory across channels."
}
}
}
}
}
```
Unset options are not sent, so existing behaviour is unchanged when you only configure missions. Each bank is configured at most once per gateway process.
### Retention Controls
By default, the plugin retains `user` and `assistant` messages after each turn. You can customize this behavior:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"autoRetain": true,
"retainRoles": ["user", "assistant", "system"]
}
}
}
}
}
```
- `autoRetain` - Set to `false` to disable automatic retention entirely (useful if you handle retention yourself)
- `retainRoles` - Controls which message roles are included in the retained transcript. Only messages from the last user message onward are retained each turn, preventing duplicate storage.
### LLM Configuration
> If you used `hindsight-openclaw-setup` in Quick Start, this section is
> already handled for you — read on if you want to edit `openclaw.json`
> directly or switch to a file/exec secret source.
Configure the memory-extraction LLM via OpenClaw's plugin config. API keys
should be stored as `SecretRef` values so they're resolved from env vars,
mounted files, or `exec`-style secret managers (Vault, etc.) at runtime
instead of sitting in plaintext on disk.
| Provider | `llmProvider` | API key |
| ------------ | -------------- | ----------------------------------- |
| OpenAI | `openai` | required |
| Anthropic | `anthropic` | required |
| Gemini | `gemini` | required |
| Groq | `groq` | required |
| Ollama | `ollama` | not required (local) |
| Claude Code | `claude-code` | not required (uses Claude Code CLI) |
| OpenAI Codex | `openai-codex` | not required (uses Codex CLI auth) |
**Set provider + API key:**
```bash
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider openai
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source env --ref-provider default --ref-id OPENAI_API_KEY
```
**Override the model (optional — Hindsight picks a sensible default per provider):**
```bash
openclaw config set plugins.entries.hindsight-openclaw.config.llmModel gpt-4o-mini
```
**OpenAI-compatible providers (OpenRouter, Azure OpenAI, vLLM, ...):**
```bash
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider openai
openclaw config set plugins.entries.hindsight-openclaw.config.llmBaseUrl https://openrouter.ai/api/v1
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source env --ref-provider default --ref-id OPENROUTER_API_KEY
openclaw config set plugins.entries.hindsight-openclaw.config.llmModel xiaomi/mimo-v2-flash
```
**Use a file or exec source instead of env (for K8s secrets, Vault, etc.):**
```bash
# File source (e.g. mounted Docker/K8s secret)
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source file --ref-provider mounted-json --ref-id /providers/openai/apiKey
# Exec source (e.g. HashiCorp Vault)
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source exec --ref-provider vault --ref-id openai/api-key
```
The corresponding secret provider needs to be configured under `secrets.providers`
in your OpenClaw config — see `openclaw config set --help` for the
`--provider-source`/`--provider-path`/`--provider-command` builder flags.
### External API (Advanced)
> `hindsight-openclaw-setup --mode api --api-url <url>` covers this path
> interactively — this section documents the underlying config fields.
Connect to a remote Hindsight API server instead of running a local daemon. This is useful for:
- **Shared memory** across multiple OpenClaw instances
- **Production deployments** with centralized memory storage
- **Team environments** where agents share knowledge
#### Plugin Configuration
Configure in `~/.openclaw/openclaw.json`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"hindsightApiUrl": "https://your-hindsight-server.com",
"hindsightApiToken": "your-api-token"
}
}
}
}
}
```
**Options:**
- `hindsightApiUrl` - Full URL to external Hindsight API (e.g., `https://mcp.hindsight.example.com`)
- `hindsightApiToken` - API token for authentication (optional). **Sensitive** — set as a SecretRef:
```bash
openclaw config set plugins.entries.hindsight-openclaw.config.hindsightApiToken \
--ref-source env --ref-provider default --ref-id HINDSIGHT_API_TOKEN
```
#### Behavior
When external API mode is enabled:
- **No local daemon** is started (no hindsight-embed process)
- **Health check** runs on startup to verify API connectivity
- **All memory operations** (retain, recall, reflect) go to the external API
- **Faster startup** since no local PostgreSQL or embedding models are needed
#### Verification
Check OpenClaw logs for external API mode:
```bash
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# Should see on startup:
# [Hindsight] External API mode enabled: https://your-hindsight-server.com
# [Hindsight] External API health check passed
```
If you see daemon startup messages instead, verify your configuration is correct.
## Inspecting Memories
### Check Configuration
View the daemon config that was written by the plugin:
```bash
cat ~/.hindsight/profiles/openclaw.env
```
This shows the LLM provider, model, port, and other settings the daemon is using.
### Check Daemon Status
```bash
# Check if daemon is running
uvx hindsight-embed@latest -p openclaw daemon status
# View daemon logs
tail -f ~/.hindsight/profiles/openclaw.log
```
### Query Memories
```bash
# Search memories
uvx hindsight-embed@latest -p openclaw memory recall openclaw "user preferences"
# View recent memories
uvx hindsight-embed@latest -p openclaw memory list openclaw --limit 10
# Open web UI (uses openclaw profile's daemon)
uvx hindsight-embed@latest -p openclaw ui
```
## OpenClaw compatibility
The plugin is tested against the current OpenClaw release and against older ones.
Version 0.12.0 and later work with **OpenClaw 2026.7.x through 2026.10.x**.
**If you are on OpenClaw 2026.8.1 or later, upgrade to plugin 0.12.0.** OpenClaw
2026.8.1 changed how it labels the conversation metadata it attaches to each
message. Earlier plugin versions no longer recognised those labels, which caused
three problems on affected setups:
- Turns were skipped instead of being remembered, with
`missing stable sender identity` in the gateway log.
- OpenClaw's internal routing details (sender and channel IDs) were stored as if
they were part of the conversation, so they could surface in later recalls.
- Automatic recall sometimes searched using that metadata instead of what you
actually said, returning irrelevant memories.
0.12.0 reads both the old and new labels, so it is safe on any supported OpenClaw
version — you do not need to match plugin and OpenClaw versions.
Two things to expect after installing on OpenClaw 2026.8.1 or later:
- Install prints
`Exclusive slot "memory" switched from "memory-core" to "hindsight-openclaw"`.
This is correct: Hindsight replaces OpenClaw's built-in memory.
- `openclaw plugins doctor` then reports that `memory-core` is not selected for
the memory slot. This is expected and not an error — it is OpenClaw noting that
its built-in memory stepped aside.
> **📝 Upgrading from 0.11.1 or earlier**
>
Installing 0.11.x could fail with
`npm error Cannot read properties of null (reading 'edgesOut')`. That was a
packaging problem in the plugin, triggered by a change in the npm registry, and
it is fixed in 0.12.0 — retry the install with the new version.
## Troubleshooting
### Plugin not loading
```bash
openclaw plugins list | grep hindsight
# Should show: ✓ enabled │ Hindsight Memory │ ...
# Reinstall if needed
openclaw plugins install @vectorize-io/hindsight-openclaw
```
### Daemon not starting
```bash
# Check daemon status (note: -p openclaw uses the openclaw profile)
uvx hindsight-embed@latest -p openclaw daemon status
# View logs for errors
tail -f ~/.hindsight/profiles/openclaw.log
# Check configuration
cat ~/.hindsight/profiles/openclaw.env
# List all profiles
uvx hindsight-embed@latest profile list
```
### No API key error
Make sure you've configured the LLM provider through `openclaw config set`
(or use a provider that doesn't require a key):
```bash
# Option 1 — OpenAI (requires OPENAI_API_KEY in your env)
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider openai
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source env --ref-provider default --ref-id OPENAI_API_KEY
# Option 2 — Anthropic
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider anthropic
openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey \
--ref-source env --ref-provider default --ref-id ANTHROPIC_API_KEY
# Option 3 — Claude Code (no API key needed)
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider claude-code
# Option 4 — OpenAI Codex (no API key needed)
openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider openai-codex
# Verify the config is valid
openclaw config validate
# Inspect the current value
openclaw config get plugins.entries.hindsight-openclaw.config.llmProvider
```
If you used `--ref-source env`, double-check that the referenced env var
(e.g. `OPENAI_API_KEY`) is exported in the shell that runs `openclaw gateway`.
## OpenClaw's memory page, status and search
On OpenClaw 2026.10 or later, the plugin registers as OpenClaw's native memory provider. OpenClaw's own memory surfaces then read from Hindsight instead of reporting "memory plugin unavailable":
- `memory.status` and the Memory page report Hindsight's health.
- Gateway `memory.search` (with `version: 2`) and `memory.get` return Hindsight memories.
Search is read-only and answers only callers allowed to see the whole bank: an operator, or the agent owner's private session. It works when an agent's bank depends on the agent alone: `dynamicBankGranularity: ["agent"]`, a static `bankId`, or an `agentBankMap` entry. When banks are keyed by user or channel (the default granularity), OpenClaw can't tell which user's bank to search, so search is refused and status still reports ready. Prompt-side recall stays with `autoRecall`, so turning this on never injects memories twice. Older OpenClaw versions ignore it.
## Limitations
### `memory-wiki` bridge mode is not supported
`hindsight-openclaw` does not export OpenClaw `publicArtifacts`, so OpenClaw's
`memory-wiki` **bridge mode** (`vaultMode: "bridge"`) is not supported. With the
Hindsight plugin active, `openclaw wiki bridge import` imports `0` artifacts and
`openclaw wiki status` warns that the active memory plugin isn't exporting any
public memory artifacts.
This applies only to the wiki bridge. Hindsight's own memory works normally —
retain / recall / reflect, shared banks, and the external-API setup above are all
unaffected.
### Verify it's working
Check gateway logs for memory operations:
```bash
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# Should see on startup:
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
# or
# [Hindsight] ✓ Using provider: claude-code, model: claude-sonnet-4-20250514
# Should see after conversations:
# [Hindsight] Retained X messages for session ...
# [Hindsight] Auto-recall: Injecting X memories
```
## Migration from 0.5.x
0.6.0 removes all process-environment reads from the plugin. Configuration that
previously came from shell env vars must now go through OpenClaw's plugin config
(with `SecretRef` for credentials). The plugin no longer auto-detects providers
from `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / etc. — you must set
`llmProvider` explicitly.
| Old (0.5.x) | New (0.6.0) |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPENAI_API_KEY=…` (auto-detected) | `openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider openai` <br/> `openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey --ref-source env --ref-id OPENAI_API_KEY` |
| `HINDSIGHT_API_LLM_PROVIDER=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.llmProvider …` |
| `HINDSIGHT_API_LLM_MODEL=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.llmModel …` |
| `HINDSIGHT_API_LLM_API_KEY=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.llmApiKey --ref-source env --ref-id …` |
| `HINDSIGHT_API_LLM_BASE_URL=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.llmBaseUrl …` |
| `HINDSIGHT_EMBED_API_URL=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.hindsightApiUrl …` |
| `HINDSIGHT_EMBED_API_TOKEN=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.hindsightApiToken --ref-source env --ref-id …` |
| `HINDSIGHT_BANK_ID=…` | `openclaw config set plugins.entries.hindsight-openclaw.config.bankId …` |
| `llmApiKeyEnv: "MY_KEY"` (plugin config) | `llmApiKey` configured as a SecretRef with `--ref-id MY_KEY` |
If your shell already exports `OPENAI_API_KEY`, the SecretRef config above
resolves to the same value at startup — you don't need to change your shell
setup, just point the plugin at the variable explicitly. Run
`openclaw config validate` after migrating to confirm the new shape parses
cleanly.
references/sdks/integrations/opencode.md
# OpenCode
> **⚠️ Superseded by the Coding Agents plugin**
>
**The opencode plugin** is superseded by the [Coding Agents plugin](coding-agents.md) — one
package covering Claude Code, Codex, opencode, Kilo, Cursor, Copilot, Grok, Antigravity, Devin and Cline and other CLI agents, with a per-repo memory bank they all share instead
of one bank per agent.
This page and the published package still work; they are no longer developed. To switch:
```bash
cd /path/to/your/repo
npx @vectorize-io/hindsight-coding-agents install opencode
```
Memory does not move automatically — the banks are scoped differently and this agent's history cannot be imported (it is kept in an internal database). See
[Migrating from the per-agent plugins](coding-agents.md#migrating-from-the-per-agent-plugins).
Persistent long-term memory plugin for [OpenCode](https://opencode.ai) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations, recalls relevant context on session start, and provides retain/recall/reflect tools the agent can call directly.
[View Changelog →](../../changelog/integrations/opencode.md)
## Quick Start
Add to your `opencode.json` (project) or `~/.config/opencode/opencode.json` (global):
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@vectorize-io/opencode-hindsight"]
}
```
OpenCode auto-installs plugins in the `"plugin"` array on startup — no `npm install` required.
Point the plugin at your Hindsight server and start OpenCode:
```bash
export HINDSIGHT_API_URL="http://localhost:8888"
opencode
```
### Using Hindsight Cloud
Get an API key at [ui.hindsight.vectorize.io/connect](https://ui.hindsight.vectorize.io/connect):
```bash
export HINDSIGHT_API_URL="https://api.hindsight.vectorize.io"
export HINDSIGHT_API_TOKEN="your-api-key"
opencode
```
Or configure inline via plugin options in `opencode.json`:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@vectorize-io/opencode-hindsight", {
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "your-api-key"
}]
]
}
```
## Features
### Custom Tools
The plugin registers three tools the agent can call explicitly:
| Tool | Description |
|---|---|
| `hindsight_retain` | Store information in long-term memory |
| `hindsight_recall` | Search long-term memory for relevant information |
| `hindsight_reflect` | Generate a synthesized answer from long-term memory |
### Auto-Retain
When the session goes idle (`session.idle` event), the plugin automatically retains the conversation transcript to Hindsight. Configurable via `retainEveryNTurns` to control frequency.
### Session Recall
When a new session starts, the plugin recalls relevant project context and injects it into the system prompt, giving the agent access to memories from prior sessions.
### Compaction Hook
When OpenCode compacts the context window, the plugin:
1. Retains the current conversation before compaction
2. Recalls relevant memories and injects them into the compaction context
This ensures memories survive context window trimming.
## Configuration
### Plugin Options
```json
{
"plugin": [
["@vectorize-io/opencode-hindsight", {
"hindsightApiUrl": "http://localhost:8888",
"hindsightApiToken": "your-api-key",
"bankId": "my-project",
"autoRecall": true,
"autoRetain": true,
"recallBudget": "mid",
"recallTags": [],
"recallTagsMatch": "any",
"retainTags": [],
"retainEveryNTurns": 3,
"debug": false
}]
]
}
```
### Config File
Create `~/.hindsight/opencode.json` for persistent configuration that applies across all projects:
```json
{
"hindsightApiUrl": "http://localhost:8888",
"hindsightApiToken": "your-api-key",
"recallBudget": "mid"
}
```
### Environment Variables
| Variable | Description | Default |
|---|---|---|
| `HINDSIGHT_API_URL` | Hindsight API base URL | *(required)* |
| `HINDSIGHT_API_TOKEN` | API key for authentication | |
| `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` |
| `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` |
| `HINDSIGHT_AUTO_RECALL` | Auto-recall on session start | `true` |
| `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` |
| `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` |
| `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` |
| `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` |
| `HINDSIGHT_RECALL_TAGS` | Comma-separated tags to filter recall results | |
| `HINDSIGHT_RECALL_TAGS_MATCH` | Tag match mode: `any`, `all`, `any_strict`, `all_strict` | `any` |
| `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` |
| `HINDSIGHT_BANK_MISSION` | Bank mission/context for reflect | |
Configuration priority (later wins): defaults < `~/.hindsight/opencode.json` < plugin options < env vars.
### Logging & debugging
The plugin logs through OpenCode's own log stream (`service=hindsight`), visible with `opencode --print-logs` or in the OpenCode log files. Errors (failed retain/recall, unreachable API, auth problems) and the resolved API URL + bank are logged **by default** — so if memories aren't saving, the reason is visible without any opt-in.
Verbose tracing is controlled by the `debug` option, which is **config-only** (set `"debug": true` in `opencode.json` plugin options or `~/.hindsight/opencode.json`). There is intentionally no `HINDSIGHT_DEBUG` environment variable: env vars are unreliable to set for OpenCode's plugin runtime (notably on Windows, where a persistent OpenCode server may never see them).
```json
{
"plugin": [["@vectorize-io/opencode-hindsight", { "debug": true }]]
}
```
## Dynamic Bank IDs
For multi-project isolation, enable dynamic bank ID derivation:
```bash
export HINDSIGHT_DYNAMIC_BANK_ID=true
```
The bank ID is composed from granularity fields (default: `agent::project`). Supported fields: `agent`, `project`, `channel`, `user`.
For multi-user scenarios (e.g., shared agent serving multiple users):
```bash
export HINDSIGHT_CHANNEL_ID="slack-general"
export HINDSIGHT_USER_ID="user123"
```
## How It Works
1. **Plugin loads** when OpenCode starts — creates a `HindsightClient`, derives the bank ID, and registers tools + hooks
2. **Session starts** — `session.created` event triggers, plugin marks session for recall injection
3. **System transform** — on the first LLM call, recalled memories are injected into the system prompt
4. **Agent works** — can call `hindsight_recall` and `hindsight_retain` explicitly during the session
5. **Session idles** — `session.idle` event triggers auto-retain of the conversation
6. **Compaction** — if the context window fills up, memories are preserved through the compaction
references/sdks/integrations/openhands.md
# OpenHands
Long-term memory for [OpenHands](https://github.com/OpenHands/OpenHands) (formerly OpenDevin), powered by [Hindsight](https://vectorize.io/hindsight). One command connects OpenHands to the Hindsight MCP server and adds a recall/retain rule — so the agent recalls relevant memory at the start of a task and retains durable facts as it works.
[View Changelog →](../../changelog/integrations/openhands.md)
## How It Works
OpenHands has **native Streamable-HTTP MCP support**, so the Hindsight MCP endpoint connects directly (no bridge):
```toml
[mcp]
shttp_servers = [
{url = "https://api.hindsight.vectorize.io/mcp/my-project/", api_key = "hsk_..."}
]
```
OpenHands also loads `AGENTS.md` (and repo microagents) into the agent's context on every task — that's where the recall/retain rule lives, telling the agent to `recall` first and `retain` durable facts.
## Setup
```bash
pip install hindsight-openhands
cd your-project
hindsight-openhands init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
```
`init` merges the `[mcp]` entry into `./config.toml` and writes the rule into `./AGENTS.md`. Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a self-hosted server with `--api-url http://localhost:8888` (no token needed for an open local server). If `config.toml` can't be parsed safely, `init` prints the snippet to paste instead — or run `hindsight-openhands init --print-only` anytime.
## Commands
| Command | Description |
| --- | --- |
| `hindsight-openhands init` | Add the MCP server + recall/retain rule |
| `hindsight-openhands status` | Show whether the server + rule are configured |
| `hindsight-openhands uninstall` | Remove the server + rule |
See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/openhands) for full configuration options.
references/sdks/integrations/paperclip.md
# Paperclip
Persistent memory for [Paperclip AI](https://github.com/paperclipai/paperclip) agents using [Hindsight](https://hindsight.vectorize.io).
Install the `@vectorize-io/hindsight-paperclip` plugin once. Every agent in your Paperclip instance automatically gets long-term memory that persists across runs, companies, and restarts — no code changes required.
[View Changelog →](../../changelog/integrations/paperclip.md)
## Installation
```bash
pnpm paperclipai plugin install @vectorize-io/hindsight-paperclip
```
Then configure in **Settings → Plugins → Hindsight Memory**.
## Prerequisites
Requires Paperclip **2026.720.0 or newer**. Upgrading from an older plugin version? Open the plugin settings and pick the API key secret again: older versions saved the secret's name, and Paperclip now accepts only a reference chosen with the secret picker.
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — no infrastructure to run. Skip straight to Configuration below.
**Self-hosting alternative** — run Hindsight locally:
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api
```
## How It Works
```
agent.run.started
└─ fetch issue via ctx.issues.get
└─ recall(issueTitle + description) → cached in plugin state for this run
agent running…
├─ hindsight_recall(query) → cached context if query matches run start, otherwise live recall
└─ hindsight_retain(content) → stores immediately
issue.comment.created
└─ retain(full comment body via ctx.issues.listComments)
└─ bank attribution: agent comment author when present; otherwise issue assignee
agent.run.finished
└─ no-op (subscription kept for future use when payload carries output)
```
The bundled plugin manifest declares the `issues.read` and `issue.comments.read` capabilities needed by the new SDK calls, so Paperclip may prompt for these on first install or upgrade.
Memory is keyed to `companyId` + `agentId` — never to the run ID — so it accumulates across every run.
## Configuration
| Field | Default | Description |
|-------|---------|-------------|
| `hindsightApiUrl` | `https://api.hindsight.vectorize.io` | Hindsight server URL (Cloud default; use `http://localhost:8888` for self-hosted) |
| `hindsightApiKeyRef` | — | Paperclip secret holding your Hindsight Cloud API key, chosen with the secret picker. Leave empty for self-hosted |
| `dynamicBankId` | `true` | When `true`, bank ID is derived from `bankGranularity`. Set `false` and provide `bankId` to share one static memory bank across agents |
| `bankId` | — | Static bank ID used when `dynamicBankId` is `false`. All agents sharing this value read/write the same memory bank |
| `bankGranularity` | `["company", "agent"]` | Memory isolation when `dynamicBankId` is `true`: per company+agent, per company, or per agent. Add `"user"` for per-user memory isolation (useful for GDPR compliance) |
| `recallBudget` | `mid` | `low` = fastest, `mid` = balanced, `high` = most thorough |
| `requestTimeoutMs` | `15000` | Timeout for each request to Hindsight. Raise it for self-hosted instances where recall on long issue descriptions is slower |
| `autoRetain` | `true` | Automatically retain run output after every run |
## Bank ID Format
```
paperclip::{companyId}::{agentId} ← default (company + agent granularity)
paperclip::{companyId} ← company granularity (shared across agents)
paperclip::{agentId} ← agent granularity (agent memory across companies)
paperclip::{companyId}::{agentId}::user::{userId} ← user granularity (per-user isolation, GDPR-friendly)
{bankId} ← static shared bank (dynamicBankId = false)
```
## Agent Tools
Agents can call these tools directly during a run:
**`hindsight_recall(query)`** — search memory for relevant context. Called automatically at run start; agents can also call it mid-run for targeted queries. The run-start result is reused only when the agent asks the same query; any other query triggers a live recall.
**`hindsight_retain(content)`** — store a fact or decision immediately, without waiting for run end.
## Adapter Compatibility
Works with all Paperclip adapter types via the event system:
| Adapter | Supported |
|---------|-----------|
| Claude | ✓ |
| Codex | ✓ |
| Cursor | ✓ |
| HTTP | ✓ |
| Process | ✓ |
references/sdks/integrations/perplexity.md
# Perplexity
Add persistent, searchable memory to [Perplexity](https://www.perplexity.ai) using [Hindsight](https://vectorize.io/hindsight). Store research findings and automatically recall relevant context in future searches—all secured with OAuth.
## Overview
Perplexity excels at research and fact-checking, but you have to re-discover the same facts across sessions. Hindsight solves this by providing:
- **Research knowledge base** — Discoveries, findings, and sources persist across searches
- **Smart recall** — Hindsight automatically retrieves relevant research from your history
- **No API keys** — OAuth handles authentication securely
- **Custom instructions** — Aggressive auto-retain/recall via system prompts
- **Web search + memory** — Combine Perplexity's web research with your accumulated knowledge
## Quick Start
### 1. Create a Hindsight Cloud Account
[Sign up free](https://ui.hindsight.vectorize.io) for Hindsight Cloud.
### 2. Add Hindsight as a Connector in Perplexity
Requires **Perplexity Pro** subscription. Remote MCP connectors are a Pro feature.
1. Go to [Perplexity Settings](https://www.perplexity.ai/settings)
2. Navigate to **Connectors → + Custom Connector**
3. Fill in:
- **Name:** `Hindsight`
- **MCP server URL:** `https://api.hindsight.vectorize.io/mcp/default/`
4. Click **Add** — a browser window opens for Hindsight Cloud login
5. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
6. Return to Perplexity; the connector is now active
OAuth auto-discovery handles authentication automatically. That's it!
### 3. Configure Custom Instructions for Automatic Retention
The connector is now active, but you need to tell Perplexity to actually use it. Add custom instructions to enable automatic memory capture and recall:
1. Go to **Settings → Personalization → Custom instructions**
2. Copy and paste this instruction:
```
After every search and response, automatically use the Hindsight tool to retain:
- Key research findings and sources
- Facts and data points we've discovered
- Your preferences or research patterns
- Methodologies or search strategies that worked well
Before each new search, automatically use Hindsight to recall relevant research and context from previous conversations. Use recalled memories to inform your search strategy and answer.
Retain and recall everything—Hindsight handles filtering and deduplication.
```
3. Save and close settings
Perplexity will now automatically retain research findings and recall them for future searches, building a persistent knowledge base from your research.
> **💡 Tip**
>
Feel free to experiment with the instructions to ensure proper behavior.
## Features
- **Auto-retain research** — Custom instructions tell Perplexity to store findings after every search
- **Intelligent recall** — Perplexity queries Hindsight before each search to include relevant research as context
- **OAuth-secured** — No API keys to copy-paste; sign in once via browser
- **Bank isolation** — Separate memory banks for different research projects or domains
- **Semantic search** — Hindsight understands context, not just keyword matching
- **Web + memory** — Combine Perplexity's web search with your accumulated knowledge
## What to Store
Store meaningful research data for best results:
- **Research findings** — Key discoveries, trends, statistics you've found
- **Source collection** — Useful articles, papers, resources you've discovered
- **Research patterns** — Topics you frequently research, methodologies that work
- **Preferences** — Information sources you prefer, reporting styles you like
- **Domain knowledge** — Industry facts, terminology, context you've learned
- **Decision context** — Why you chose one approach over another, constraints considered
**Example of what to store:**
```
"Q3 2026 AI benchmarks:
- Claude Opus 4.7: Best reasoning, ~$15/1M input tokens
- GPT-4o: Fastest inference, multimodal, ~$5/1M input
- Llama 3.1: Open source, good for on-device, varies by host
- Performance metrics: [include relevant benchmark links]
- Use cases I care about: [your priorities]"
```
Later, when you research *"What's the best LLM for my use case?"*, Hindsight recalls your preferences and benchmarks. Perplexity's answer becomes tailored to your situation, not generic.
## Best Practices
**Be intentional about what you store.** Not every search needs retention. Focus on storing insights that will be useful later: research findings, source collections, methodologies, and personal preferences. Storing trivial facts clutters your memory bank and makes retrieval less useful.
**Use consistent terminology.** If you call a topic "machine learning" in one search and "deep learning" in another, Hindsight's semantic search may miss the connection. Establish naming conventions and stick to them.
**Review and refine.** The Hindsight Cloud dashboard lets you browse your memory bank. Periodically review what you've stored. Delete outdated information (e.g., last year's benchmark data) and consolidate similar insights. A curated memory bank is more valuable than an exhaustive one.
**Structure multi-part findings.** When storing complex research (like technology comparisons), include context: what you were evaluating, the criteria, alternatives considered, and your conclusions. Future-you will thank present-you for the context.
**Cross-reference with other tools.** If using both ChatGPT and Perplexity on related tasks, store research context in a shared Hindsight bank (multi-bank mode) so both tools access the same knowledge base. This creates unified context across your workflow.
## Comparison with ChatGPT
| Aspect | ChatGPT | Perplexity |
|--------|---------|-----------|
| **Best for** | Deep conversations, reasoning with memory | Research with memory, fact-checking |
| **Hindsight integration** | Retain reasoning, insights, preferences | Retain research findings, sources |
| **Session continuity** | Good for multi-turn problem-solving | Good for iterative research |
| **Web integration** | Limited (beta) | Integrated; combines memory + web search |
| **Memory context limit** | Depends on conversation length | Depends on search result count |
**Recommended use:**
- **ChatGPT + Hindsight** — Build projects, learn complex topics, creative work
- **Perplexity + Hindsight** — Research, fact-checking, competitive analysis, news tracking
**Ideal setup:** Use both tools together. ChatGPT handles reasoning with context. Perplexity handles research with context. Let them share Hindsight banks for coordinated workflows.
## Architecture: Single-Bank vs. Multi-Bank
### Single-Bank Mode (Recommended)
Each connector accesses one memory bank. Simpler for most users.
- **URL:** `https://api.hindsight.vectorize.io/mcp/default/`
- **Setup:** Just enter the URL in the Connector settings
- **Best for:** Dedicated memory per tool (e.g., Perplexity uses a `research` bank)
### Multi-Bank Mode
Both tools access multiple banks via bank_id parameter.
- **URL:** `https://api.hindsight.vectorize.io/mcp`
- **Setup:** Requires additional configuration in Hindsight Cloud
- **Best for:** When ChatGPT and Perplexity collaborate on the same project
## Data Privacy and Security
- **OAuth-secured** — no API keys, no copy-paste secrets
- **Your account** — memories live in your Hindsight Cloud account
- **Encrypted in transit** — HTTPS + TLS for all connections
- **No vendor lock-in** — export your memories anytime
- **Scoped access** — the connector can only read/write to its assigned bank
When you approve OAuth in the browser, you're authorizing Perplexity to:
- **Read** your memory banks (to recall relevant research)
- **Write** to your memory banks (to store new findings)
- **Search** your memories (to find context)
You can revoke access anytime by removing the connector in Perplexity settings.
## Troubleshooting
**"Connector failed to load"**
- Verify you have Perplexity Pro (required for Remote MCP connectors)
- Verify the URL is correct (no typos in your bank ID)
- Check that your Hindsight Cloud account is active
- Try re-creating the connector
**"Authorization failed" or "Access denied"**
- Make sure you're signing in with the same Hindsight Cloud account where you want to store memories
- If using a team account, verify you have permission to access the bank
- Try logging out and back in
**"Memory tools appear but don't return results"**
- Give Hindsight a few seconds to index memories (processing is async)
- Make sure you've stored relevant memories using the `retain` operation
- Check the memory bank name matches your connector URL
**Memories aren't being stored**
- Use the Hindsight Cloud dashboard to verify memories are being created
- In Perplexity, explicitly ask Hindsight to store something: *"Hindsight, remember that Q3 2026 benchmarks show Claude Opus is best for reasoning"*
- Check that your bank isn't full (unlikely, but possible with very large memory sets)
## Next Steps
1. Verify you have Perplexity Pro
2. Create your Hindsight Cloud account
3. Add the Hindsight connector to Perplexity
4. Set up your custom instructions
5. Store your first research finding
6. Start a new search on a related topic — watch Hindsight retrieve your stored research
7. Build the habit of storing meaningful research insights over time
Over weeks and months, as your memory bank grows, Perplexity will give increasingly informed answers because it's combining current web research with your accumulated knowledge base instead of starting fresh every time.
references/sdks/integrations/pipecat.md
# Pipecat
Persistent long-term memory for [Pipecat](https://github.com/pipecat-ai/pipecat) voice AI pipelines via [Hindsight](https://vectorize.io/hindsight). A single `FrameProcessor` slots between your user context aggregator and LLM service — recalling relevant memories before each turn and retaining conversation content after.
[View Changelog →](../../changelog/integrations/pipecat.md)
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```bash
pip install hindsight-pipecat
```
```python
from pipecat.pipeline.pipeline import Pipeline
from hindsight_pipecat import HindsightMemoryService
memory = HindsightMemoryService(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)
pipeline = Pipeline([
transport.input(),
stt_service,
user_aggregator,
memory, # ← add between user_aggregator and LLM
llm_service,
assistant_aggregator,
tts_service,
transport.output(),
])
```
### Self-hosting (local development)
If you'd rather self-host:
```bash
# 1. Start Hindsight
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api
```
```python
# 2. Point pipecat at the local server
memory = HindsightMemoryService(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)
```
## How It Works
```
New turn starts
└─ OpenAILLMContextFrame arrives
├─ Retain previous complete turn (user+assistant) — fire-and-forget
└─ Recall relevant memories for current user query
└─ Inject as <hindsight_memories> system message
└─ Forward enriched context to LLM
```
On each `OpenAILLMContextFrame`:
1. **Retain** — any new complete user+assistant turn pairs are sent to Hindsight asynchronously (non-blocking)
2. **Recall** — the latest user message is used as the search query; results are injected as a system message before the LLM sees the context
3. **Forward** — the enriched context frame is pushed downstream
Memory accumulates across calls. By the third or fourth turn, recall starts surfacing useful context that the pipeline didn't have to re-establish.
## Configuration
```python
HindsightMemoryService(
bank_id="user-123", # Required: memory bank to use
hindsight_api_url="...", # Hindsight API URL
api_key="hsk_...", # API key (Hindsight Cloud)
recall_budget="mid", # "low", "mid", or "high"
recall_max_tokens=4096, # Max tokens for recall results
enable_recall=True, # Inject memories before LLM
enable_retain=True, # Store turns after each exchange
memory_prefix="Relevant memories from past conversations:\n",
)
```
### Global Configuration
```python
from hindsight_pipecat import configure
configure(
hindsight_api_url="https://api.hindsight.vectorize.io", # Hindsight Cloud (default)
api_key="hsk_...",
recall_budget="mid",
)
# Now create services without repeating connection details
memory = HindsightMemoryService(bank_id="user-123")
```
## Compatibility
Tested with Pipecat `v0.0.108`. The processor handles both the new `LLMContextFrame` and the deprecated `OpenAILLMContextFrame` for forward compatibility.
## Manual Testing
The `examples/` directory includes an interactive text-based chat simulator for testing memory recall/retain without requiring Daily/Deepgram/Cartesia API keys:
```bash
python examples/interactive_chat.py --bank demo-user
```
The `examples/basic_pipeline.py` shows the full voice pipeline with Daily + Deepgram + OpenAI + Cartesia.
## Prerequisites
A running Hindsight instance:
**Self-hosted:**
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-api-key
hindsight-api # starts on http://localhost:8888
```
**Hindsight Cloud:** [Sign up](https://ui.hindsight.vectorize.io) — no self-hosting required.
references/sdks/integrations/pydantic-ai.md
# Pydantic AI
Persistent memory tools for [Pydantic AI](https://ai.pydantic.dev/) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — all async-native with no thread-pool hacks.
[View Changelog →](../../changelog/integrations/pydantic-ai.md)
## Features
- **Async-Native Tools** — Uses Pydantic AI's async tool interface directly (`aretain`, `arecall`, `areflect`)
- **Memory Instructions** — Auto-inject relevant memories into every agent run via `instructions=[...]`
- **Three Memory Tools** — Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Simple Configuration** — Configure once globally, or pass a client directly
- **Lightweight** — Depends on `pydantic-ai-slim` to avoid pulling in all model providers
## Installation
```bash
pip install hindsight-pydantic-ai
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from hindsight_client import Hindsight
from hindsight_pydantic_ai import create_hindsight_tools, memory_instructions
from pydantic_ai import Agent
client = Hindsight(base_url="https://api.hindsight.vectorize.io", api_key="hsk_...")
agent = Agent(
"openai:gpt-4o",
tools=create_hindsight_tools(client=client, bank_id="user-123"),
instructions=[memory_instructions(client=client, bank_id="user-123")],
)
result = await agent.run("What do you remember about my preferences?")
print(result.output)
```
The agent now has three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
The `memory_instructions` callable automatically recalls relevant memories and injects them into the system prompt on every run.
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL:
```python
client = Hindsight(base_url="http://localhost:8888")
```
See the [installation guide](../../developer/installation.md) for self-hosting setup.
## Tools Only (No Auto-Injection)
If you want the agent to decide when to use memory rather than always injecting context:
```python
agent = Agent(
"openai:gpt-4o",
tools=create_hindsight_tools(client=client, bank_id="user-123"),
)
```
## Instructions Only (No Tools)
If you just want memories auto-injected without giving the agent explicit memory tools:
```python
agent = Agent(
"openai:gpt-4o",
instructions=[memory_instructions(client=client, bank_id="user-123")],
)
```
## Selecting Tools
Include only the tools you need:
```python
tools = create_hindsight_tools(
client=client,
bank_id="user-123",
include_retain=True,
include_recall=True,
include_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing a client to every call, configure once:
```python
from hindsight_pydantic_ai import configure, create_hindsight_tools
configure(
hindsight_api_url="https://api.hindsight.vectorize.io", # Hindsight Cloud (default)
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create tools without passing client — uses global config
tools = create_hindsight_tools(bank_id="user-123")
```
## Per-Tool Overrides
Constructor arguments override global configuration:
```python
tools = create_hindsight_tools(
bank_id="user-123",
budget="high", # Override global budget
max_tokens=8192, # Override global max_tokens
tags=["session:abc"], # Override global tags
)
```
## Memory Instructions Options
Customize what memories get injected and how:
```python
instructions_fn = memory_instructions(
client=client,
bank_id="user-123",
query="relevant context about the user", # What to search for
budget="low", # Keep it fast
max_results=5, # Limit injected memories
max_tokens=4096, # Max recall tokens
prefix="Relevant memories:\n", # Text before the memory list
tags=["scope:global"], # Filter by tags
tags_match="any", # Tag match mode
)
```
## API Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `include_retain` | `True` | Include the retain (store) tool |
| `include_recall` | `True` | Include the recall (search) tool |
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | Recall query for memory injection |
| `budget` | `"low"` | Recall budget level |
| `max_results` | `5` | Maximum memories to inject |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Hindsight Cloud (`https://api.hindsight.vectorize.io`) | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
references/sdks/integrations/right-agent.md
# Right Agent
Persistent memory for [Right Agent](https://github.com/onsails/right-agent) using [Hindsight](https://hindsight.vectorize.io).
Right Agent runs [Claude Code](https://docs.anthropic.com/en/docs/claude-code) inside [OpenShell](https://github.com/NVIDIA/OpenShell) sandboxes. Each agent is its own Telegram bot — DMs, groups, and forum topics each get their own Claude Code session, all sharing one chat-tagged Hindsight bank.
Hindsight is the native, recommended memory provider — selected during `right init`. No plugin to install, no sandbox network policy to edit.
## Quick Start
Install Right Agent (see the [install guide](https://github.com/onsails/right-agent/blob/master/docs/INSTALL.md)), then run the init wizard:
```bash
right init
right up
```
`right init` walks you through picking a memory provider. Choose `hindsight` when prompted:
```
? memory provider:
> hindsight — hindsight cloud api (recommended)
file — local MEMORY.md (no cloud dependency)
? hindsight api key: <paste your key, or press Enter to use HINDSIGHT_API_KEY at runtime>
? hindsight bank id (default: my-agent): <press Enter to accept>
```
Get an API key at [Hindsight Cloud](https://ui.hindsight.vectorize.io).
The agent auto-retains every turn and auto-recalls relevant context on every new message.
## Features
- **Auto-recall** — on every new user message, queries Hindsight for memories tagged with the current Telegram chat and injects them as system context.
- **Auto-retain** — after every turn, queues the conversation for retention. Asynchronous and resilient; never blocks the agent.
- **Explicit tools** — `memory_retain`, `memory_recall`, and `memory_reflect` MCP tools for when automatic isn't enough.
- **Per-chat tagging** — DMs, groups, and forum topics share one bank but each conversation only sees its own memories plus untagged globals.
- **Resilient writes** — local SQLite retain queue plus circuit breaker; pending writes replay automatically when Hindsight is reachable again.
- **No plugin, no policy edits** — Hindsight is built into Right Agent's MCP aggregator on the host. The sandbox needs no Hindsight-specific egress rule.
- **Bundled `rightmemory` skill** — tells the agent when to use each tool and what belongs in memory vs. in its identity files (`IDENTITY.md`, `USER.md`, `TOOLS.md`).
## How It Works
### Memory flow
- **On every new user message**, Right Agent recalls memories tagged with the current Telegram chat and injects them into the agent's system prompt under a `## Memory` section. Recalled content is wrapped as untrusted external context to defend against prompt injection from memory writes.
- **After every turn**, the conversation is queued for retention. If Hindsight is unreachable, the entry sits in a local SQLite queue and retries with backoff — the agent never blocks on memory writes.
- **The agent can also call retain, recall, and reflect directly** via the memory MCP tools when automatic behavior isn't enough.
### Per-chat scoping
One Hindsight bank per agent (set at `right init`, defaults to the agent name). Within that bank, every memory is tagged with the originating Telegram chat (`chat:<chat_id>`), and recall queries with `tags_match: "any"` — so a 1:1 DM, a group, and a forum topic each see their own memories plus any untagged globals. Group conversations don't bleed into 1:1s, but the agent keeps a shared baseline of facts that apply everywhere.
### Where the Hindsight client runs
Right Agent's MCP aggregator runs on the host — outside the sandbox — and is the only process that talks to the Hindsight API. Agents inside the sandbox call `memory_retain`, `memory_recall`, and `memory_reflect` over the aggregator's per-agent MCP endpoint, and the aggregator makes the Hindsight calls from the host. The sandbox network policy needs no Hindsight-specific egress rule, and Hindsight credentials never enter the sandbox.
## Memory Tools
The aggregator exposes three memory tools to every Hindsight-mode agent:
| Tool | Purpose |
|---|---|
| `mcp__right__memory_retain(content, context)` | Save a fact permanently with a short label (`"user preference"`, `"api format"`, etc.). |
| `mcp__right__memory_recall(query)` | Ranked semantic + keyword + graph search across the agent's memory. |
| `mcp__right__memory_reflect(query)` | Deep analysis across memories — synthesize patterns, compare past decisions. |
The bundled `rightmemory` skill tells the agent when to reach for each one and what belongs in memory rather than in the agent's identity files.
## Configuration
`right init` writes the Hindsight configuration into `~/.right/agents/<name>/agent.yaml`:
```yaml
memory:
provider: hindsight
api_key: your-api-key # or omit to use HINDSIGHT_API_KEY env var
bank_id: my-agent # defaults to the agent name
recall_budget: 8000 # optional, token budget for auto-recall
```
To change any of these later, edit the file and run `right restart <agent>`. To switch an existing agent between providers, run `right agent config <agent>` and re-run the wizard.
### Using an environment variable
If you'd rather keep the API key out of `agent.yaml`, leave `api_key` empty and set `HINDSIGHT_API_KEY` in the environment where `right up` runs:
```bash
export HINDSIGHT_API_KEY=your-api-key
right up
```
The aggregator reads `HINDSIGHT_API_KEY` at startup and uses it as the fallback for any agent whose `agent.yaml` doesn't set `memory.api_key`.
## Troubleshooting
**`Memory: degraded` shown in `/doctor` or in the Telegram chat.** The resilient client's circuit breaker tripped after repeated upstream errors. Pending retains are queued locally and replay when the circuit closes. Check Hindsight Cloud status and verify the API key is still valid.
**Memories not appearing after restart.** Verify `memory.bank_id` in `~/.right/agents/<name>/agent.yaml` matches the bank you previously wrote to. The default is the agent name; if you edited it manually, restarts pick up the new value and won't see memories under the old bank.
**`HINDSIGHT_API_KEY` ignored.** The `memory.api_key` field in `agent.yaml` takes precedence over the environment variable. Leave `api_key` empty (or remove it) to fall back to `HINDSIGHT_API_KEY`.
**Quota exhausted.** Right Agent surfaces a chat-level notice when Hindsight returns a quota error. Writes are dropped while quota is exhausted; reads keep working. Top up at [Hindsight Cloud](https://ui.hindsight.vectorize.io) and queued retains catch up on the next successful write.
**Switching from `file` to `hindsight` (or vice versa).** Run `right agent config <agent>` and re-run the wizard, then `right restart <agent>`. An existing local `MEMORY.md` is preserved but ignored while in Hindsight mode; switching back surfaces it again.
## Links
- [Right Agent on GitHub](https://github.com/onsails/right-agent)
- [Install guide](https://github.com/onsails/right-agent/blob/master/docs/INSTALL.md)
- [Hindsight Cloud signup](https://ui.hindsight.vectorize.io)
references/sdks/integrations/roo-code.md
# Roo Code
Persistent long-term memory for [Roo Code](https://github.com/RooVetGit/Roo-Code) via [Hindsight](https://vectorize.io/hindsight). A one-command installer registers Hindsight's MCP server and injects custom rules that teach Roo to recall context before tasks and retain learnings after.
[View Changelog →](../../changelog/integrations/roo-code.md)
## Quick Start
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — get an API key instantly, no infrastructure to run.
```bash
# Install the CLI
pip install hindsight-roo-code
# Install the integration into your project (defaults to Hindsight Cloud)
hindsight-roo-code install
# Restart Roo Code — memory is active
```
**Self-hosting alternative:**
```bash
# Start Hindsight locally first
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
hindsight-api
# Then install with the local URL
hindsight-roo-code install --api-url http://localhost:8888
```
## How It Works
Roo Code has two primary extensibility mechanisms: **MCP servers** for tools and **custom rules** for system-prompt injection. This integration uses both.
```
New task starts
└─ Rules file instructs Roo to call recall
└─ Relevant memories injected into context automatically
Agent working…
└─ Agent calls retain for significant decisions/discoveries
Task ends
└─ Rules file instructs Roo to call retain with a summary
└─ Summary stored for future sessions
```
The installer writes:
- **`.roo/mcp.json`** — registers Hindsight's `/mcp` endpoint as an MCP server, with `recall` and `retain` auto-approved
- **`.roo/rules/hindsight-memory.md`** — instructions injected into every Roo system prompt
## Installation Options
### Project-local install (default)
Writes to `.roo/` in the current directory — memory is scoped to this project:
```bash
hindsight-roo-code install
hindsight-roo-code install --api-url https://api.hindsight.vectorize.io # default (Hindsight Cloud)
hindsight-roo-code install --api-url http://localhost:8888 # self-hosted
hindsight-roo-code install --project-dir /path/to/project
```
### Global install
Writes to `~/.roo/` — applies to all projects:
```bash
hindsight-roo-code install --global
```
## Configuration
The MCP entry written to `.roo/mcp.json`:
```json
{
"mcpServers": {
"hindsight": {
"type": "streamable-http",
"url": "http://localhost:8888/mcp",
"timeout": 30,
"alwaysAllow": ["recall", "retain"]
}
}
}
```
To update the API URL after installation, re-run the installer or edit `.roo/mcp.json` directly.
## MCP Tools
Hindsight exposes two tools via its `/mcp` endpoint:
| Tool | Description |
|------|-------------|
| `recall` | Search memory for context relevant to a query |
| `retain` | Store content in memory immediately |
The rules file instructs Roo to call these automatically at task start and end. Agents can also call them explicitly mid-task.
## Verifying Setup
1. Start Hindsight and run the installer
2. Open Roo Code in your project
3. Check **Settings → MCP Servers** — `hindsight` should show as connected
4. Start a task — you should see `recall` invoked in the tool call log
## Prerequisites
A running Hindsight instance:
**Hindsight Cloud (recommended):** [Sign up](https://ui.hindsight.vectorize.io) — no self-hosting required.
**Self-hosted:**
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-api-key
hindsight-api # starts on http://localhost:8888
```
references/sdks/integrations/skills.md
# Skills
Hindsight provides an Agent Skill that gives AI coding assistants persistent memory across sessions. Skills are reusable prompt templates that agents can load when needed to gain specialized capabilities.
## Supported Platforms
| Platform | Skills Directory |
|----------|-----------------|
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `~/.claude/skills/` |
| [OpenCode](https://github.com/opencode-ai/opencode) | `~/.opencode/skills/` |
| [Codex CLI](https://github.com/openai/codex) | `~/.codex/skills/` |
## Deployment Modes
The skill supports two deployment modes:
| Mode | Best For | Data Location |
|------|----------|---------------|
| **Local** | Individual developers | Your machine (`~/.pg0/`) |
| **Cloud** | Teams sharing knowledge | Hindsight Cloud |
## Quick Install
### Option 1: Interactive Installer (Recommended)
```bash
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
```
The installer will:
1. Prompt you to select your AI coding assistant
2. Select deployment mode (local or cloud)
3. Configure the appropriate settings
4. Install the skill to the appropriate directory
### Install for a Specific Platform
```bash
# Claude Code (interactive mode selection)
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude
# OpenCode
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app opencode
# Codex CLI
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app codex
```
### Install with Cloud Mode
```bash
# Direct cloud setup (skips interactive prompts for mode)
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude --mode cloud
```
### Option 2: Using add-skill
If you use [add-skill](https://add-skill.org/) to manage your agent skills:
```bash
# For local mode (individual developers)
npx add-skill vectorize-io/hindsight --skill hindsight-local
# For Hindsight Cloud (teams)
npx add-skill vectorize-io/hindsight --skill hindsight-cloud
# For self-hosted Hindsight servers
npx add-skill vectorize-io/hindsight --skill hindsight-self-hosted
```
On first use, the AI will guide you through the remaining setup:
- **Local**: Run `uvx hindsight-embed configure` to set up your LLM provider
- **Cloud**: Provide your API key and bank ID
- **Self-hosted**: Provide your server URL, API key, and bank ID
## What the Skill Provides
Once installed, your AI assistant gains the ability to:
- **Retain** - Store user preferences, learnings, and procedure outcomes
- **Recall** - Search for relevant context before starting tasks
- **Reflect** - Synthesize memories into contextual answers
The skill uses the `hindsight-embed` CLI which runs a lightweight local daemon with an embedded database.
## How Skills Work
Skills are **model-invoked**, meaning the AI assistant automatically decides when to use them based on the context of your conversation. You don't need to explicitly trigger the skill.
The assistant will:
- **Store** when you share preferences, when tasks succeed/fail, or when learnings emerge
- **Recall** before starting non-trivial tasks to get relevant context
### What Gets Stored
The skill is optimized to store:
| Category | Examples |
|----------|----------|
| **User Preferences** | Coding style, tool preferences, language choices |
| **Procedure Outcomes** | Commands that worked, configurations that resolved issues |
| **Learnings** | Bug solutions, workarounds, architecture decisions |
## Architecture
### Local Mode
```
AI Coding Assistant
│
▼
Hindsight Skill (SKILL.md)
│
▼
hindsight-embed CLI
│
▼
Local Daemon (auto-started)
│
▼
Embedded PostgreSQL (~/.pg0/hindsight-embed/)
```
All data stays on your machine. The daemon auto-starts when needed and shuts down after inactivity.
### Cloud Mode
```
AI Coding Assistant
│
▼
Hindsight Skill (SKILL.md)
│
▼
hindsight-cli
│
▼
Hindsight Cloud API (https://api.hindsight.vectorize.io)
│
▼
Shared Memory Bank (team-accessible)
```
Data is stored in Hindsight Cloud and shared across your team. All team members with the same bank ID can access shared memories.
---
## Local Mode Setup
The skill uses configuration stored in `~/.hindsight/config.env`. Reconfigure anytime:
```bash
uvx hindsight-embed configure
```
---
## Cloud Mode Setup
Cloud mode connects to [Hindsight Cloud](https://ui.hindsight.vectorize.io), allowing teams to share memories about a codebase. When one team member learns something, everyone benefits.
### Prerequisites
1. A Hindsight Cloud account ([sign up](https://ui.hindsight.vectorize.io))
2. An API key from your team admin
3. A bank ID for your project (e.g., `team-acme-frontend`)
### Installation
Run the installer with cloud mode:
```bash
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
```
You'll be prompted for:
| Setting | Description | Example |
|---------|-------------|---------|
| **Cloud API URL** | Hindsight Cloud endpoint | `https://api.hindsight.vectorize.io` |
| **API Key** | Your authentication key | `hs_xxx...` |
| **Bank ID** | Shared memory bank for your team | `team-acme-frontend` |
### Configuration Files
Cloud mode creates two files:
**`~/.hindsight/config`** — API connection settings (TOML format):
```toml
api_url = "https://api.hindsight.vectorize.io"
api_key = "hs_xxx..."
```
**`~/.claude/skills/hindsight/SKILL.md`** — Skill definition with your bank ID baked in.
### Team Setup
To set up cloud mode for your team:
1. **Team admin** creates a bank in Hindsight Cloud (e.g., `team-acme-frontend`)
2. **Team admin** generates API keys for each team member
3. **Each developer** runs the installer with their API key and the shared bank ID
4. All team members now share the same memory bank
### What to Store in Team Banks
Cloud mode uses a **shared team bank**. Be thoughtful about what goes in:
| Type | Examples | How to Store |
|------|----------|--------------|
| **Project conventions** | Linting rules, testing requirements, Node version | `"Project uses ESLint with Airbnb config"` |
| **Team knowledge** | Architecture decisions, common pitfalls, domain logic | `"Auth module requires Redis 7+"` |
| **Individual preferences** | Personal coding style, communication preferences | `"Alice prefers verbose commit messages"` |
**Key distinction**: Project conventions apply to everyone. Individual preferences should include the person's name so the AI knows when to apply them.
### Example Workflow
```
Day 1: Alice discovers a requirement
─────────────────────────────────────
Alice's AI assistant stores:
"The auth module requires Redis 7+ due to HEXPIRE command usage"
"Alice prefers explicit error handling over silent failures"
Day 2: Bob starts working on auth
─────────────────────────────────
Bob's AI assistant recalls:
"The auth module requires Redis 7+ due to HEXPIRE command usage"
Bob avoids the same issue Alice hit!
(Alice's personal preference is stored but won't be applied to Bob)
```
### Testing Cloud Connection
After installation, verify the connection:
```bash
# Store a test memory
hindsight memory retain team-acme-frontend "Alice works at Google as a software engineer"
# Recall it
hindsight memory recall team-acme-frontend "Alice"
```
### Switching Between Banks
If you work on multiple projects, you can have different skills installed for each AI assistant, or manually switch banks:
```bash
# Environment variable override (temporary)
HINDSIGHT_API_URL=https://api.hindsight.vectorize.io \
HINDSIGHT_API_KEY=hs_xxx \
hindsight memory recall different-bank "query"
```
For permanent multi-bank setups, reinstall the skill with a different bank ID.
## Troubleshooting
### Skill not activating
The skill activates based on its description matching your request. Try being explicit:
- "Remember that..." triggers storage
- "What do you know about..." triggers recall
### Local Mode Issues
**Daemon not starting:**
```bash
uvx hindsight-embed daemon status
uvx hindsight-embed daemon logs
```
**Reconfigure LLM provider:**
```bash
uvx hindsight-embed configure
```
### Cloud Mode Issues
**Authentication errors:**
```bash
# Verify your config
cat ~/.hindsight/config
# Test connection manually
hindsight bank list
```
**Wrong bank ID:**
Check your SKILL.md file to see which bank ID is configured:
```bash
cat ~/.claude/skills/hindsight/SKILL.md | grep "memory retain"
```
To change the bank ID, reinstall the skill:
```bash
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
```
**Network/firewall issues:**
```bash
# Test connectivity to cloud API
curl -I https://api.hindsight.vectorize.io/health
```
## Requirements
### Local Mode
- Python 3.10+ (for `uvx`)
- An LLM API key (OpenAI, Anthropic, Groq, etc.)
### Cloud Mode
- Python 3.10+ (for `uvx`)
- Hindsight Cloud API key
- Network access to `https://api.hindsight.vectorize.io`
references/sdks/integrations/smolagents.md
# SmolAgents
Persistent memory tools for [SmolAgents](https://github.com/huggingface/smolagents) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using SmolAgents' native Tool pattern.
[View Changelog →](../../changelog/integrations/smolagents.md)
## Features
- **Native Tool Subclasses** - Each tool extends SmolAgents' `Tool` base class
- **Memory Instructions** - Pre-recall memories for injection into agent system prompt
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Factory Function** - Create all tools in one call with shared configuration
- **Simple Configuration** - Configure once globally, or pass a client directly
## Installation
```bash
pip install hindsight-smolagents
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from smolagents import CodeAgent, HfApiModel
from hindsight_smolagents import create_hindsight_tools
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)
agent = CodeAgent(
tools=tools,
model=HfApiModel(),
)
agent.run("Remember that I prefer dark mode")
agent.run("What are my preferences?")
```
The agent now has three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL:
```python
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
)
```
See the [installation guide](../../developer/installation.md) for self-hosting setup.
## With Memory Instructions
Pre-recall relevant memories and inject them into the system prompt:
```python
from hindsight_smolagents import create_hindsight_tools, memory_instructions
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
)
memories = memory_instructions(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
)
agent = CodeAgent(
tools=tools,
model=HfApiModel(),
system_prompt=f"You are a helpful assistant.\n\n{memories}",
)
```
## Individual Tools
You can also use the tool classes directly:
```python
from hindsight_smolagents import HindsightRetainTool, HindsightRecallTool
agent = CodeAgent(
tools=[
HindsightRetainTool(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
),
HindsightRecallTool(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
),
],
model=HfApiModel(),
)
```
## Selecting Tools
Include only the tools you need via the factory:
```python
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
enable_retain=True,
enable_recall=True,
enable_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing connection details to every tool, configure once:
```python
from hindsight_smolagents import configure, create_hindsight_tools
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create tools without passing connection details
tools = create_hindsight_tools(bank_id="user-123")
```
## Configuration Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `enable_retain` | `True` | Include the retain (store) tool |
| `enable_recall` | `True` | Include the recall (search) tool |
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | Recall query for memory injection |
| `budget` | `"low"` | Recall budget level |
| `max_results` | `5` | Maximum memories to inject |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Hindsight Cloud (`https://api.hindsight.vectorize.io`) | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
## Requirements
- Python >= 3.10
- smolagents
- hindsight-client >= 0.4.0
- A running Hindsight API server
references/sdks/integrations/strands.md
# Strands Agents
Persistent memory tools for [Strands Agents SDK](https://github.com/strands-agents/sdk-python) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Strands' native `@tool` pattern.
[View Changelog →](../../changelog/integrations/strands.md)
## Features
- **Native `@tool` Functions** - Tools are plain Python functions, compatible with `Agent(tools=[...])`
- **Memory Instructions** - Pre-recall memories for injection into agent system prompt
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Simple Configuration** - Configure once globally, or pass a client directly
## Installation
```bash
pip install hindsight-strands
```
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```python
from strands import Agent
from hindsight_strands import create_hindsight_tools
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY env var
)
agent = Agent(tools=tools)
agent("Remember that I prefer dark mode")
agent("What are my preferences?")
```
The agent now has three tools it can call:
- **`hindsight_retain`** — Store information to long-term memory
- **`hindsight_recall`** — Search long-term memory for relevant facts
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, swap the URL:
```python
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)
```
See the [installation guide](../../developer/installation.md) for self-hosting setup.
## With Memory Instructions
Pre-recall relevant memories and inject them into the system prompt:
```python
from hindsight_strands import create_hindsight_tools, memory_instructions
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
)
memories = memory_instructions(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
)
agent = Agent(
tools=tools,
system_prompt=f"You are a helpful assistant.\n\n{memories}",
)
```
## FastAPI Lifecycle (Recommended)
Prefer creating one shared Hindsight client in app lifespan and passing `client=...`.
This gives explicit ownership and clean shutdown via `await client.aclose()`.
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from hindsight_client import Hindsight
from hindsight_strands import create_hindsight_tools, memory_instructions
@asynccontextmanager
async def lifespan(app: FastAPI):
client = Hindsight(base_url="https://api.hindsight.vectorize.io", api_key="hsk_...")
app.state.hindsight_client = client
try:
yield
finally:
await client.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/chat")
async def chat():
client = app.state.hindsight_client
tools = create_hindsight_tools(bank_id="user-123", client=client)
memories = memory_instructions(bank_id="user-123", client=client)
...
```
If you pass `hindsight_api_url`/`api_key` directly to `create_hindsight_tools()`,
`hindsight-strands` creates the client internally. In that case call
`await tools.aclose()` (or `tools.close()`) during shutdown.
## Selecting Tools
Include only the tools you need:
```python
tools = create_hindsight_tools(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...",
enable_retain=True,
enable_recall=True,
enable_reflect=False, # Omit reflect
)
```
## Global Configuration
Instead of passing connection details to every call, configure once:
```python
from hindsight_strands import configure, create_hindsight_tools
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create tools without passing connection details
tools = create_hindsight_tools(bank_id="user-123")
```
## Configuration Reference
### `create_hindsight_tools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `enable_retain` | `True` | Include the retain (store) tool |
| `enable_recall` | `True` | Include the recall (search) tool |
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | Recall query for memory injection |
| `budget` | `"low"` | Recall budget level |
| `max_results` | `5` | Maximum memories to inject |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Hindsight Cloud (`https://api.hindsight.vectorize.io`) | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
## Requirements
- Python >= 3.10
- strands-agents
- hindsight-client >= 0.4.0
- A running Hindsight API server
references/sdks/integrations/superagent.md
# Superagent
Safety middleware for [Hindsight](https://vectorize.io/hindsight) memory operations, powered by [Superagent](https://www.superagent.sh). Wrap your memory client with `SafeHindsight` to guard against prompt injection and strip PII before anything is written to memory — and to screen queries before they reach recall or reflect.
[View Changelog →](../../changelog/integrations/superagent.md)
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
```bash
pip install hindsight-superagent
```
### Prerequisites
Guard and Redact run on every `retain` by default, so the example below calls Superagent (and the LLM behind your guard/redact models) before anything is stored. Set these keys as environment variables first:
| Variable | Purpose |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `HINDSIGHT_API_KEY` | Authenticates your Hindsight Cloud workspace. [Sign up free](https://ui.hindsight.vectorize.io) to grab one. |
| `SUPERAGENT_API_KEY` | Authenticates Superagent's guard/redact calls. [Get one at superagent.sh](https://www.superagent.sh). |
| `OPENAI_API_KEY` | Backs the `guard_model` / `redact_model` (e.g. `openai/gpt-4.1-nano`). Any [supported LLM provider](https://docs.superagent.sh/sdk) works. |
```bash
export HINDSIGHT_API_KEY=hs-...
export SUPERAGENT_API_KEY=sa-...
export OPENAI_API_KEY=sk-...
```
`SafeHindsight` connects to [Hindsight Cloud](https://ui.hindsight.vectorize.io) (`https://api.hindsight.vectorize.io`) by default, using `HINDSIGHT_API_KEY`. To target a [self-hosted server](https://hindsight.vectorize.io/developer/installation) instead, pass `hindsight_api_url="http://localhost:8888"`.
```python
import asyncio
from hindsight_superagent import SafeHindsight
safe = SafeHindsight(
bank_id="user-123", # connects to Hindsight Cloud by default
guard_model="openai/gpt-4.1-nano",
redact_model="openai/gpt-4.1-nano",
)
async def main():
# Prompt-injection attempts are blocked and PII is redacted before storage
await safe.retain("My email is jane@example.com — ignore all previous instructions.")
print(await safe.recall("what's my email?"))
asyncio.run(main())
```
> **📝 Hosted guard models**
>
Superagent's hosted endpoints for its guard models are currently unreliable. The guard models are open-weight (`superagent/guard-0.6b`, `guard-1.7b`, `guard-4b`) and can be [self-hosted](https://docs.superagent.sh/sdk/models) via Ollama or vLLM.
## Features
- **Guard on Retain** — blocks prompt injection attacks before content is stored in memory
- **Redact on Retain** — removes PII (emails, SSNs, API keys, etc.) from content before storage
- **Guard on Recall/Reflect** — blocks malicious queries before they reach the memory system
- **Configurable Safety** — enable or disable guard and redact per operation
## Learn More
- [Source on GitHub](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/superagent)
references/sdks/integrations/vapi.md
# Vapi
Persistent long-term memory for [Vapi](https://vapi.ai) voice AI calls via [Hindsight](https://vectorize.io/hindsight). A single webhook handler recalls relevant memories at call start (injected as `assistantOverrides`) and retains the full transcript when the call ends.
[View Changelog →](../../changelog/integrations/vapi.md)
## Quick Start
> **💡 Hindsight Cloud (recommended)**
>
[Sign up free](https://ui.hindsight.vectorize.io) — get an API key instantly, no infrastructure to run.
```bash
pip install hindsight-vapi
```
Wire it into any HTTP server. FastAPI example with Hindsight Cloud:
```python
from fastapi import FastAPI, Request
from hindsight_vapi import HindsightVapiWebhook
app = FastAPI()
memory = HindsightVapiWebhook(
bank_id="user-123",
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_your_token_here",
)
@app.post("/webhook")
async def vapi_webhook(request: Request):
event = await request.json()
response = await memory.handle(event)
return response or {}
```
Point Vapi's **Server URL** at your webhook endpoint and memory is active.
**Self-hosting alternative:** [install Hindsight locally](../../developer/installation.md) and use `hindsight_api_url="http://localhost:8888"` (omit `api_key`).
## How It Works
Unlike the Pipecat integration (per-turn `FrameProcessor`), Vapi doesn't expose a per-turn hook, so memory is injected **once per call** at call start:
```
Incoming call
└─ Vapi fires "assistant-request" webhook
└─ Recall memories (query = caller's phone number)
└─ Return as assistantOverrides with <hindsight_memories> system message
└─ Vapi merges into assistant config before the call begins
Call ends
└─ Vapi fires "end-of-call-report" webhook
└─ Retain full transcript (fire-and-forget — webhook responds immediately)
```
Memory accumulates across calls. By the second or third call with the same caller, Hindsight surfaces relevant history automatically — previous decisions, account details, stated preferences.
## Vapi Server URL Setup
In the Vapi dashboard:
1. Go to **Settings → Server URL**
2. Point it at your webhook endpoint (e.g., `https://your-domain.com/webhook`)
3. Enable the `assistant-request` and `end-of-call-report` event types
See [Vapi's server events docs](https://docs.vapi.ai/server-url) for details.
## Outbound Calls
There is no `assistant-request` webhook for outbound calls. Use `build_assistant_overrides()` at call-creation time:
```python
overrides = await memory.build_assistant_overrides("Ben from Vectorize")
vapi.calls.create(
assistant_id="...",
assistant_overrides=overrides,
customer={"number": "+15555550100"},
)
```
## Configuration
```python
HindsightVapiWebhook(
bank_id="user-123", # Required: memory bank to use
hindsight_api_url="...", # Hindsight API URL
api_key="hsk_...", # API key (Hindsight Cloud)
recall_budget="mid", # "low", "mid", or "high"
recall_max_tokens=4096, # Max tokens for recall results
enable_recall=True, # Inject memories at call start
enable_retain=True, # Store transcript at call end
memory_prefix="Relevant memories from past conversations:\n",
)
```
### Global Configuration
```python
from hindsight_vapi import configure
configure(
hindsight_api_url="http://localhost:8888",
api_key="hsk_...",
recall_budget="mid",
)
# Now create webhooks without repeating connection details
memory = HindsightVapiWebhook(bank_id="user-123")
```
## Bank Scoping
Typical patterns for the `bank_id`:
- **One bank per user** — scope by phone number (`user-+15551234567`) or your own account ID
- **Shared bank** — one bank for all callers (useful for small teams or shared memory)
- **Per-assistant** — if you have multiple Vapi assistants with different personalities or scopes
## Prerequisites
A running Hindsight instance:
**Self-hosted:**
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=your-api-key
hindsight-api # starts on http://localhost:8888
```
**Hindsight Cloud:** [Sign up](https://ui.hindsight.vectorize.io) — no self-hosting required.
references/sdks/integrations/zapier.md
# Zapier
Long-term agent memory for [Zapier](https://zapier.com) via [Hindsight](https://hindsight.vectorize.io). The Hindsight Zapier app adds three actions — **Retain**, **Recall**, **Reflect** — plus instant **triggers** that start a Zap when a memory event fires, so memory flows between Hindsight and 7,000+ apps.
## Why this matters
Zapier connects everything: Gmail, Slack, Sheets, HubSpot, Notion, forms, and thousands more. On its own, those Zaps are **stateless**. With Hindsight you can:
- **Retain** every closed ticket, form submission, or call summary into a memory bank
- **Recall** relevant context before an AI step so the model sees prior history
- **Reflect** to get a synthesized, memory-grounded answer right inside a Zap
- **Trigger** a Zap the moment a memory operation completes (e.g. notify Slack when consolidation finishes)
## Setup
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) and grab an API key — no self-hosting required.
1. **Sign up** at [Hindsight Cloud](https://ui.hindsight.vectorize.io) (free tier) or [self-host](../../developer/installation.md)
2. **Get an API key** (`hsk_...`) from the Hindsight dashboard
> **📝 Availability**
>
The native Hindsight Zapier app (the **Retain / Recall / Reflect** steps and triggers) is
currently a **private beta** — it isn't listed in Zapier's public App Directory yet, so
searching "Hindsight" in Zapier won't find it. Use the **Webhooks by Zapier** path below.
### Webhooks by Zapier
Every Hindsight operation is a plain REST call, so you can drive it from Zapier's built-in
**Webhooks by Zapier** action (or **API Request**) with nothing to install:
- **Method:** `POST`
- **URL** (Hindsight Cloud):
- Retain — `https://api.hindsight.vectorize.io/v1/default/banks/<bank>/memories`
- Recall — `https://api.hindsight.vectorize.io/v1/default/banks/<bank>/memories/recall`
- Reflect — `https://api.hindsight.vectorize.io/v1/default/banks/<bank>/reflect`
- **Headers:** `Authorization: Bearer hsk_...` and `Content-Type: application/json`
- **Body:** the JSON payload for the operation — see the [API Reference](../../openapi.json) for the exact fields.
For self-hosted, swap the host for your own instance URL; drop the `Authorization` header if it runs without auth.
## Actions
### Retain
Store content in a bank. Hindsight extracts facts asynchronously after the call returns.
| Field | Description |
| --------- | --------------------------------------------------------------------- |
| Bank | Memory bank to store in (dynamic dropdown; auto-created on first use) |
| Content | Free text to retain |
| Context | Optional context for the content |
| Tags | Comma-separated tags |
| Timestamp | When the content occurred (defaults to now) |
### Recall (search)
Search a bank for memories relevant to a query.
| Field | Description |
| ----------------- | ---------------------- |
| Bank | Memory bank to search |
| Query | Natural-language query |
| Budget | `low` / `mid` / `high` |
| Tags / Tags Match | Optional tag filter |
### Reflect (search)
Get an LLM-synthesized answer grounded in the bank's memories.
| Field | Description |
| ------ | ---------------------- |
| Bank | Memory bank |
| Query | Question to answer |
| Budget | `low` / `mid` / `high` |
## Triggers
Instant triggers (REST Hooks) that fire when a memory event completes in a bank:
| Trigger | Fires when |
| ---------------------------- | ------------------------------------------------------------- |
| **Retain Completed** | An asynchronous retain finishes processing |
| **Consolidation Completed** | Memory consolidation synthesizes observations / mental models |
## Example Zaps
**Support assistant** — a closed-ticket trigger (Zendesk) → Hindsight **Retain** the resolution. New ticket → Hindsight **Recall** similar past issues → OpenAI drafts the first reply.
**Memory digest** — Hindsight **Consolidation Completed** trigger → format the new observations → post to Slack so the team sees what the agent learned.
**Daily prep** — a calendar trigger → Hindsight **Reflect** ("What do we know about this prospect?") → append the answer to the prep doc.
## Source
- GitHub: [`hindsight-integrations/zapier`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/zapier)
references/sdks/integrations/zcode.md
# ZCode
Persistent memory for [ZCode](https://zcode.z.ai) — Z.ai's GLM desktop coding agent — using [Hindsight](https://vectorize.io/hindsight). ZCode embeds the Claude Code agent runtime, so Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn. No MCP server, no changes to your ZCode workflow.
[View Changelog →](../../changelog/integrations/zcode.md)
## Quick Start
> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
```bash
# Install the CLI
pip install hindsight-zcode
# Install the hooks (defaults to Hindsight Cloud)
hindsight-zcode install --api-url https://api.hindsight.vectorize.io --api-token your-api-key
# Restart ZCode — memory is live
```
The installer copies the hook scripts to `~/.zcode/hooks/hindsight/`, registers them in `~/.zcode/cli/config.json` (merged with any existing hooks), and creates `~/.hindsight/zcode.json` for your personal config. It never touches your Claude Code config at `~/.claude/settings.json`.
**Self-hosting alternative** — connect to a local `hindsight-embed` daemon by omitting the flags:
```bash
hindsight-zcode install
```
To uninstall:
```bash
hindsight-zcode uninstall
```
### Alternative: install as a ZCode plugin
ZCode can install Hindsight directly from a plugin marketplace — no `pip` step. The same hook scripts ship as a hooks-only Claude Code plugin (`hindsight-zcode`) in the Hindsight marketplace:
```
# In ZCode: add the Hindsight marketplace, then install the plugin
zcode plugins add-marketplace vectorize-io/hindsight
zcode plugins install hindsight-zcode
```
When installed this way, ZCode registers the hooks automatically (no config-file edit). Provide your Hindsight credentials via environment variables (`HINDSIGHT_API_URL`, `HINDSIGHT_API_TOKEN`) or by creating `~/.hindsight/zcode.json`:
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
## Features
- **Auto-recall** — before each prompt, queries Hindsight for relevant memories and injects them as additional context (visible to the model, not the transcript)
- **Auto-retain** — after each response, stores the turn to Hindsight for future recall
- **No MCP required** — plain Python hook scripts calling Hindsight's REST API; nothing to run alongside ZCode
- **Cross-tool memory** — the same Hindsight bank is shared across Claude Code, Cursor, and other Hindsight integrations, so memory follows you between tools
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory
- **Zero runtime dependencies** — the hook scripts are pure Python stdlib; the `pip install` only ships the one-time installer
## Architecture
ZCode embeds the Claude Code agent runtime and reads the standard Claude Code hook schema from its own config namespace, `~/.zcode/cli/config.json` (with `hooks.enabled: true`). The plugin wires three hook events:
| Hook | Event | Purpose |
|------|-------|---------|
| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable |
| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` |
| `retain.py` | `Stop` | **Auto-retain** — assemble the turn, POST to Hindsight |
On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and emits a context block that ZCode injects before sending the turn to the model:
```
<hindsight_memories>
Relevant memories from past conversations...
Current time - 2026-03-27 09:14
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26)
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26)
</hindsight_memories>
```
On `Stop`, the hook pairs the user prompt (captured at `UserPromptSubmit`) with the agent's response and POSTs the turn to Hindsight. ZCode does not provide a `SessionEnd` hook event, so retention rides `Stop` — every turn is stored as it completes.
## Connection Modes
### 1. External API (recommended)
Connect to a running Hindsight server (cloud or self-hosted) via `~/.hindsight/zcode.json`:
```json
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "hsk_your_token"
}
```
### 2. Local Daemon
Run `hindsight-embed` locally. The `session_start.py` hook detects it on `apiPort` (default `9077`). The daemon is not auto-started by the plugin — start it separately:
```bash
uvx hindsight-embed
```
Then leave `hindsightApiUrl` empty in your config and the plugin connects to `http://localhost:9077`.
## Configuration
Default config ships in `~/.zcode/hooks/hindsight/settings.json`. For personal overrides that survive updates, create `~/.hindsight/zcode.json`. Most settings can also be overridden via environment variable.
**Loading order** (later entries win):
1. Built-in defaults
2. Plugin `settings.json` (at `~/.zcode/hooks/hindsight/settings.json`)
3. User config (`~/.hindsight/zcode.json`)
4. Environment variables
---
### Connection
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Empty = local daemon. |
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. |
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. |
---
### Memory Bank
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `bankId` | `HINDSIGHT_BANK_ID` | `"zcode"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. |
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"zcode"` | Agent name used in dynamic bank ID derivation. |
---
### Auto-Recall
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. |
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Token budget for the injected memory block. |
---
### Auto-Retain
| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
| `retainEveryNTurns` | `HINDSIGHT_RETAIN_EVERY_N_TURNS` | `1` | Retain every N turns. Default `1` stores every turn on `Stop`. |
## Relationship to ZCode's built-in memory
ZCode ships its own local, per-project memory (`~/.zcode/cli/memories/`). Hindsight is complementary: it stores memory in a **cloud (or self-hosted) bank that is shared across tools** — the same bank powers Claude Code, Cursor, and other Hindsight integrations — so your context follows you between agents and machines rather than staying local to one ZCode project.
references/sdks/integrations/zed.md
# Zed
Long-term memory for the [Zed](https://zed.dev) editor's AI assistant, powered by [Hindsight](https://vectorize.io/hindsight). One command connects Zed's Agent Panel to the Hindsight MCP server and adds a rule telling the agent to use it — so it recalls relevant memory at the start of a task and retains durable facts as it goes. Recall happens at query time against your actual message, and from your seat it's automatic.
[View Changelog →](../../changelog/integrations/zed.md)
## How It Works
Zed has no pre-prompt hook, but it supports two things this integration uses:
- **MCP context servers:** Zed runs MCP servers configured under `context_servers` in `settings.json` and surfaces their tools in the Agent Panel. `hindsight-zed` registers the Hindsight MCP server there, giving the agent `recall` / `retain` / `reflect` tools.
- **A global instructions file** (`~/.config/zed/AGENTS.md`) that Zed includes in every conversation. The integration adds a small rule there telling the agent to recall first and retain what it learns.
Zed connects to the Hindsight MCP endpoint natively by URL (with your API key as a Bearer header), so no bridge process runs.
## Setup
`hindsight-zed` is a zero-dependency Node CLI — Node.js is only needed to run this setup tool. Run it straight from npm with `npx`:
```bash
npx @vectorize-io/hindsight-zed init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory
```
Or install it globally for a persistent command:
```bash
npm install -g @vectorize-io/hindsight-zed
hindsight-zed init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory
```
`init` adds the `hindsight` MCP server to `~/.config/zed/settings.json` and the recall/retain rule to `~/.config/zed/AGENTS.md`. Restart Zed, open the Agent Panel, and the `hindsight` server should show a green dot.
Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a self-hosted server with `--api-url http://localhost:8888` (no token needed for an open local server). If your `settings.json` has comments (JSONC), `init` prints the entry to paste rather than rewriting the file — or run `hindsight-zed init --print-only` anytime.
## Commands
| Command | Description |
| --- | --- |
| `hindsight-zed init` | Add the MCP server + recall/retain rule |
| `hindsight-zed status` | Show whether the server + rule are configured |
| `hindsight-zed uninstall` | Remove the server + rule |
## Note
Recall and retain run through MCP tools the agent calls, guided by the always-on rule. This makes recall query-time precise (no lag), with the tradeoff that it relies on the agent following the "recall first" instruction rather than the editor enforcing it.
See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/zed) for full configuration options.
references/sdks/nodejs.md
# TypeScript / JavaScript Client
Official TypeScript/JavaScript client for the Hindsight API. Supports **Node.js** and **Deno**.
## Installation
### Node.js
```bash
npm install @vectorize-io/hindsight-client
```
### Deno
No installation needed — import directly via the `npm:` specifier:
```typescript
```
## Quick Start
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
// Retain a memory
await client.retain('nodejs-sdk-bank', 'Alice works at Google');
// Recall memories
const response = await client.recall('nodejs-sdk-bank', 'What does Alice do?');
for (const r of response.results) {
console.log(r.text);
}
// Reflect - generate response with disposition
const answer = await client.reflect('nodejs-sdk-bank', 'Tell me about Alice');
console.log(answer.text);
```
## Client Initialization
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
const client = new HindsightClient({
baseUrl: 'http://localhost:8888',
});
```
## Core Operations
### Version and Feature Checks
```typescript
const version = await client.getVersion();
console.log(version.api_version);
if (!version.features.mcp) {
throw new Error('This server does not expose the MCP endpoint');
}
```
### Retain (Store Memory)
```typescript
// Simple
await client.retain('nodejs-sdk-bank', 'Alice works at Google');
// With options
await client.retain('nodejs-sdk-bank', 'Alice got promoted', {
timestamp: new Date('2024-01-15'),
context: 'career update',
metadata: { source: 'slack' },
async: false, // Set true for background processing
});
```
### Retain Batch
```typescript
await client.retainBatch('nodejs-sdk-bank', [
{ content: 'Alice works at Google', context: 'career' },
{ content: 'Bob is a data scientist', context: 'career' },
], {
async: false,
});
```
### Recall (Search)
```typescript
// Simple - returns RecallResponse
const response = await client.recall('nodejs-sdk-bank', 'What does Alice do?');
for (const r of response.results) {
console.log(`${r.text} (type: ${r.type})`);
}
// With options
const filtered = await client.recall('nodejs-sdk-bank', 'What does Alice do?', {
types: ['world', 'observation'], // Filter by fact type
maxTokens: 4096,
budget: 'high', // 'low', 'mid', or 'high'
});
```
### Reflect (Generate Response)
```typescript
const answer = await client.reflect('nodejs-sdk-bank', 'What should I know about Alice?', {
budget: 'low', // 'low', 'mid', or 'high'
context: 'preparing for a meeting',
});
console.log(answer.text); // Generated response
```
## Bank Management
### Create Bank
```typescript
await client.createBank('nodejs-sdk-bank', {
name: 'Assistant',
mission: "You're a helpful AI assistant - keep track of user preferences and conversation history.",
disposition: {
skepticism: 3, // 1-5: trusting to skeptical
literalism: 3, // 1-5: flexible to literal
empathy: 3, // 1-5: detached to empathetic
},
});
```
### List Memories
```typescript
const response = await client.listMemories('nodejs-sdk-bank', {
type: 'world', // Optional filter
q: 'Alice', // Optional text search
limit: 100,
offset: 0,
});
console.log(response);
```
## Document Management
### Get Document
```typescript
const doc = await client.getDocument('nodejs-sdk-bank', 'conversation_001');
if (doc) {
console.log(doc); // null when document not found
}
```
### List Documents
```typescript
const response = await client.listDocuments('nodejs-sdk-bank', {
limit: 50,
offset: 0,
});
console.log(response);
```
### Update Document
```typescript
await client.updateDocument('nodejs-sdk-bank', 'conversation_001', {
tags: ['important', 'meeting-notes'],
});
```
### Delete Document
```typescript
await client.deleteDocument('nodejs-sdk-bank', 'conversation_001');
```
references/sdks/python.md
# Python Client
Official HTTP client for the Hindsight API. Use this when you have a Hindsight server already running — locally, in Docker, or as a managed service — and you want a typed Python client to talk to it.
If you want to **embed and run a Hindsight server in your Python process** (no external server required), see [Embedded Python (hindsight-all)](./hindsight-all.md) instead.
## Installation
```bash
pip install hindsight-client
```
## Quick Start
```python
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
# Retain a memory
client.retain(bank_id="python-sdk-bank", content="Alice works at Google")
# Recall memories
results = client.recall(bank_id="python-sdk-bank", query="What does Alice do?")
for r in results.results:
print(r.text)
# Reflect - generate a contextual answer
answer = client.reflect(bank_id="python-sdk-bank", query="Tell me about Alice")
print(answer.text)
```
## Client Initialization
```python
from hindsight_client import Hindsight
client = Hindsight(
base_url="http://localhost:8888", # Hindsight API URL
timeout=30.0, # Request timeout in seconds
# api_key="your-api-key", # Optional bearer token
)
# Core operations
client.retain(bank_id="python-sdk-test", content="Hello world")
results = client.recall(bank_id="python-sdk-test", query="Hello")
# Bank, mental model, directive and memory helpers
client.create_bank(bank_id="python-sdk-test", name="Test Bank")
models = client.list_mental_models(bank_id="python-sdk-test")
directives = client.list_directives(bank_id="python-sdk-test")
memories = client.list_memories(bank_id="python-sdk-test")
```
For operations the helper methods don't cover, the client also exposes the full generated API as async namespaces (`client.banks`, `client.memory`, `client.documents`, `client.mental_models`, `client.directives`, ...). Their methods must be awaited.
## Core Operations
### Version and Feature Checks
```python
version = client.get_version()
print(version.api_version)
if not version.features.mcp:
raise RuntimeError("This server does not expose the MCP endpoint")
```
The async client method is available as `await client.aget_version()`.
### Retain (Store Memory)
```python
# Simple
client.retain(
bank_id="python-sdk-bank",
content="Alice works at Google as a software engineer",
)
# With options
from datetime import datetime
client.retain(
bank_id="python-sdk-bank",
content="Alice got promoted",
context="career update",
timestamp=datetime(2024, 1, 15),
document_id="conversation_001",
metadata={"source": "slack"},
retain_async=False, # Set True for background processing
)
```
### Retain Batch
```python
client.retain_batch(
bank_id="python-sdk-bank",
items=[
{"content": "Alice works at Google", "context": "career"},
{"content": "Bob is a data scientist", "context": "career"},
],
document_id="conversation_001",
retain_async=False, # Set True for background processing
)
```
### Recall (Search)
```python
# Simple - returns a RecallResponse
results = client.recall(
bank_id="python-sdk-bank",
query="What does Alice do?",
)
for r in results.results:
print(f"{r.text} (type: {r.type})")
# With options
results = client.recall(
bank_id="python-sdk-bank",
query="What does Alice do?",
types=["world", "observation"], # Filter by fact type
max_tokens=4096,
budget="high", # low, mid, or high
)
```
### Recall with Chunks
```python
# Returns RecallResponse with source chunks
response = client.recall(
bank_id="python-sdk-bank",
query="What does Alice do?",
types=["world", "experience"],
budget="mid",
max_tokens=4096,
include_chunks=True,
max_chunk_tokens=500
)
print(f"Found {len(response.results)} memories")
for r in response.results:
print(f" - {r.text}")
chunk = (response.chunks or {}).get(r.chunk_id)
if chunk:
print(f" Source: {chunk.text[:100]}...")
```
### Reflect (Generate Response)
```python
answer = client.reflect(
bank_id="python-sdk-bank",
query="What should I know about Alice?",
budget="low", # low, mid, or high
context="preparing for a meeting",
)
print(answer.text) # Generated response
```
## Bank Management
### Create Bank
```python
client.create_bank(
bank_id="python-sdk-bank",
name="Assistant",
mission="You're a helpful AI assistant - keep track of user preferences and conversation history.",
disposition={
"skepticism": 3, # 1-5: trusting to skeptical
"literalism": 3, # 1-5: flexible to literal
"empathy": 3, # 1-5: detached to empathetic
},
)
```
### List Memories
```python
client.list_memories(
bank_id="python-sdk-bank",
type="world", # Optional: filter by type
search_query="Alice", # Optional: text search
limit=100,
offset=0,
)
```
## Async Support
All methods have async versions prefixed with `a`:
```python
import asyncio
from hindsight_client import Hindsight
async def main():
client = Hindsight(base_url="http://localhost:8888")
# Async retain
await client.aretain(bank_id="python-sdk-bank", content="Hello world")
# Async recall
results = await client.arecall(bank_id="python-sdk-bank", query="Hello")
for r in results.results:
print(r.text)
# Async reflect
answer = await client.areflect(bank_id="python-sdk-bank", query="What did I say?")
print(answer.text)
await client.aclose()
asyncio.run(main())
```
## Context Manager
```python
from hindsight_client import Hindsight
with Hindsight(base_url="http://localhost:8888") as client:
client.retain(bank_id="python-sdk-bank", content="Hello")
results = client.recall(bank_id="python-sdk-bank", query="Hello")
# Client automatically closed
```
SKILL.md
---
name: hindsight-docs
description: Complete Hindsight documentation for AI agents. Use this to learn about Hindsight architecture, APIs, configuration, and best practices.
---
# Hindsight Documentation Skill
Complete technical documentation for Hindsight - a biomimetic memory system for AI agents.
## When to Use This Skill
Use this skill when you need to:
- Understand Hindsight architecture and core concepts
- Learn about retain/recall/reflect operations
- Configure memory banks and dispositions
- Set up the Hindsight API server (Docker, Kubernetes, pip)
- Integrate with Python/Node.js/Rust SDKs
- Understand retrieval strategies (semantic, BM25, graph, temporal)
- Debug issues or optimize performance
- Review API endpoints and parameters
## Documentation Structure
All documentation is in `references/` organized by category:
```
references/
├── best-practices.md # START HERE — missions, tags, formats, anti-patterns
├── faq.md # Common questions and decisions
├── changelog/ # Release history and version changes (index.md + integrations/)
├── openapi.json # Full OpenAPI spec — endpoint schemas, request/response models
├── developer/
│ ├── api/ # Core operations: retain, recall, reflect, memory banks
│ └── *.md # Architecture, configuration, deployment, performance
└── sdks/
├── *.md # Python, Node.js, CLI, embedded
└── integrations/ # Framework and tool integrations
```
## How to Find Documentation
### 1. Find Files by Pattern (use Glob tool)
```bash
# Core API operations
references/developer/api/*.md
# SDK documentation
references/sdks/*.md
references/sdks/integrations/*.md
# Find specific topics
references/**/configuration.md
references/**/*python*.md
references/**/*deployment*.md
```
### 2. Search Content (use Grep tool)
```bash
# Search for concepts
pattern: "disposition" # Memory bank configuration
pattern: "graph retrieval" # Graph-based search
pattern: "helm install" # Kubernetes deployment
pattern: "document_id" # Document management
pattern: "HINDSIGHT_API_" # Environment variables
# Search in specific areas
path: references/developer/api/
pattern: "POST /v1" # Find API endpoints
path: references/sdks/
pattern: "def |async def " # Find Python examples
```
### 3. Read Full Documentation (use Read tool)
```
references/developer/api/retain.md
references/sdks/python.md
references/sdks/integrations/litellm.md
```
## Start Here: Best Practices
Before reading API docs, read the best practices guide. It covers practical rules for missions, tags, content format, observation scopes, and anti-patterns — the fastest way to integrate correctly.
```
references/best-practices.md
```
## Key Concepts
- **Memory Banks**: Isolated memory stores (one per user/agent)
- **Retain**: Store memories (auto-extracts facts/entities/relationships)
- **Recall**: Retrieve memories (4 parallel strategies: semantic, BM25, graph, temporal)
- **Reflect**: Disposition-aware reasoning using memories
- **document_id**: Groups messages in a conversation (upsert on same ID)
- **Dispositions**: Skepticism, literalism, empathy traits (1-5) affecting reflect
- **Mental Models**: Consolidated knowledge synthesized from facts
## Notes
- Code examples are inlined from working examples
- Configuration uses `HINDSIGHT_API_*` environment variables
- Database migrations run automatically on startup
- Multi-bank queries require client-side orchestration
- Use `document_id` for conversation evolution (same ID = upsert)
---
**Auto-generated** from `hindsight-docs/docs/` and `hindsight-docs/docs-integrations/`. Run `./scripts/generate-docs-skill.sh` to update.