diff --git a/hindsight-api-slim/tests/test_codex_tool_choice.py b/hindsight-api-slim/tests/test_codex_tool_choice.py
index e35ced8d5d..408d7f1328 100644
--- a/hindsight-api-slim/tests/test_codex_tool_choice.py
+++ b/hindsight-api-slim/tests/test_codex_tool_choice.py
@@ -1,35 +1,22 @@
-import asyncio
-import sys
-import types
-import unittest
-from unittest.mock import AsyncMock, MagicMock, patch
-from pathlib import Path
-
+"""
+Regression tests for Codex provider tool_choice normalization.
-ROOT = Path(__file__).resolve().parents[1]
-PACKAGE_ROOT = ROOT / "hindsight_api"
-if str(ROOT) not in sys.path:
- sys.path.insert(0, str(ROOT))
+The reflect agent forces tool selection via named tool_choice dicts on early iterations:
+ {"type": "function", "function": {"name": "recall"}}
+The Codex Responses API expects the function name at the top level instead:
+ {"type": "function", "name": "recall"}
-def ensure_package(name: str, path: Path) -> None:
- module = sys.modules.get(name)
- if module is None:
- module = types.ModuleType(name)
- module.__path__ = [str(path)]
- sys.modules[name] = module
+Without normalization, Codex rejects the request with:
+ 400 Unknown parameter: 'tool_choice.function'
+"""
+from unittest.mock import AsyncMock, MagicMock, patch
-ensure_package("hindsight_api", PACKAGE_ROOT)
-ensure_package("hindsight_api.engine", PACKAGE_ROOT / "engine")
-ensure_package("hindsight_api.engine.providers", PACKAGE_ROOT / "engine" / "providers")
-fake_metrics = types.ModuleType("hindsight_api.metrics")
-fake_metrics.get_metrics_collector = lambda: types.SimpleNamespace(record_llm_call=lambda **kwargs: None)
-sys.modules["hindsight_api.metrics"] = fake_metrics
+import pytest
from hindsight_api.engine.providers.codex_llm import CodexLLM
-
TOOLS = [
{
"type": "function",
@@ -56,52 +43,46 @@ def build_llm() -> CodexLLM:
)
-class CodexToolChoiceTests(unittest.TestCase):
- def test_codex_normalizes_legacy_named_tool_choice_shape(self) -> None:
- async def scenario() -> dict:
- llm = build_llm()
- response = MagicMock()
- response.status_code = 200
- response.raise_for_status.return_value = None
- with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
- mock_post.return_value = response
- with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
- mock_parse.return_value = (None, [])
- await llm.call_with_tools(
- messages=[{"role": "user", "content": "recall the memory"}],
- tools=TOOLS,
- tool_choice={"type": "function", "function": {"name": "recall"}},
- max_retries=0,
- )
- return mock_post.call_args.kwargs["json"]
-
- sent_payload = asyncio.run(scenario())
- self.assertEqual(sent_payload["tool_choice"], {"type": "function", "name": "recall"})
-
- def test_codex_forced_tool_choice_still_yields_tool_calls(self) -> None:
- async def scenario() -> tuple[object, dict]:
- llm = build_llm()
- response = MagicMock()
- response.status_code = 200
- response.raise_for_status.return_value = None
- tool_call = {"id": "call-1", "name": "recall", "arguments": {"query": "memory"}}
- with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
- mock_post.return_value = response
- with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
- mock_parse.return_value = (None, [tool_call])
- result = await llm.call_with_tools(
- messages=[{"role": "user", "content": "recall the memory"}],
- tools=TOOLS,
- tool_choice={"type": "function", "function": {"name": "recall"}},
- max_retries=0,
- )
- return result, mock_post.call_args.kwargs["json"]
-
- result, sent_payload = asyncio.run(scenario())
- self.assertEqual(len(result.tool_calls), 1)
- self.assertEqual(result.tool_calls[0].name, "recall")
- self.assertEqual(sent_payload["tool_choice"], {"type": "function", "name": "recall"})
-
-
-if __name__ == "__main__":
- unittest.main()
+@pytest.mark.asyncio
+async def test_codex_normalizes_legacy_named_tool_choice_shape():
+ llm = build_llm()
+ response = MagicMock()
+ response.status_code = 200
+ response.raise_for_status.return_value = None
+ with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
+ mock_post.return_value = response
+ with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
+ mock_parse.return_value = (None, [])
+ await llm.call_with_tools(
+ messages=[{"role": "user", "content": "recall the memory"}],
+ tools=TOOLS,
+ tool_choice={"type": "function", "function": {"name": "recall"}},
+ max_retries=0,
+ )
+ sent_payload = mock_post.call_args.kwargs["json"]
+
+ assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"}
+
+
+@pytest.mark.asyncio
+async def test_codex_forced_tool_choice_still_yields_tool_calls():
+ llm = build_llm()
+ response = MagicMock()
+ response.status_code = 200
+ response.raise_for_status.return_value = None
+ tool_call = {"id": "call-1", "name": "recall", "arguments": {"query": "memory"}}
+ with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
+ mock_post.return_value = response
+ with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
+ mock_parse.return_value = (None, [tool_call])
+ result = await llm.call_with_tools(
+ messages=[{"role": "user", "content": "recall the memory"}],
+ tools=TOOLS,
+ tool_choice={"type": "function", "function": {"name": "recall"}},
+ max_retries=0,
+ )
+ sent_payload = mock_post.call_args.kwargs["json"]
+
+ assert len(result.tool_calls) == 1
+ assert result.tool_calls[0].name == "recall"
+ assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"}
diff --git a/hindsight-api-slim/tests/test_hierarchical_config.py b/hindsight-api-slim/tests/test_hierarchical_config.py
index 203ea6ab85..2bad365264 100644
--- a/hindsight-api-slim/tests/test_hierarchical_config.py
+++ b/hindsight-api-slim/tests/test_hierarchical_config.py
@@ -88,8 +88,16 @@ async def test_hierarchical_fields_categorization():
assert "entities_allow_free_form" in configurable
assert "entity_labels" in configurable
+ # Verify other configurable fields
+ assert "retain_default_strategy" in configurable
+ assert "retain_strategies" in configurable
+ assert "max_observations_per_scope" in configurable
+ assert "reflect_source_facts_max_tokens" in configurable
+ assert "llm_gemini_safety_settings" in configurable
+ assert "mcp_enabled_tools" in configurable
+
# Verify count is correct
- assert len(configurable) == 20
+ assert len(configurable) == 21
# Verify credential fields (NEVER exposed)
assert "llm_api_key" in credentials
diff --git a/hindsight-docs/sidebars.ts b/hindsight-docs/sidebars.ts
index 2423c8240a..aad36fa994 100644
--- a/hindsight-docs/sidebars.ts
+++ b/hindsight-docs/sidebars.ts
@@ -262,6 +262,12 @@ const sidebars: SidebarsConfig = {
label: 'AG2',
customProps: { icon: '/img/icons/ag2.svg' },
},
+ {
+ type: 'doc',
+ id: 'sdks/integrations/llamaindex',
+ label: 'LlamaIndex',
+ customProps: { icon: '/img/icons/llamaindex.png' },
+ },
{
type: 'doc',
id: 'sdks/integrations/skills',
diff --git a/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md
new file mode 100644
index 0000000000..edb3b69794
--- /dev/null
+++ b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md
@@ -0,0 +1,189 @@
+---
+sidebar_position: 6
+---
+
+# OpenAI Codex CLI
+
+Persistent memory for [OpenAI 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
+
+```bash
+# 1. Clone the Hindsight repo and install the plugin
+git clone https://github.com/vectorize-io/hindsight.git
+cd hindsight/hindsight-integrations/codex
+./install.sh
+
+# 2. Configure your Hindsight connection
+cat > ~/.hindsight/codex.json << 'EOF'
+{
+ "hindsightApiUrl": "https://api.hindsight.vectorize.io",
+ "hindsightApiToken": "hsk_your_token_here",
+ "bankId": "codex"
+}
+EOF
+
+# 3. Start Codex — memory is live
+codex
+```
+
+For a local Hindsight instance, set `hindsightApiUrl` to `http://localhost:9077` and omit `hindsightApiToken`.
+
+## 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:
+
+```
+
+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)
+
+```
+
+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 `install.sh` to write 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.
diff --git a/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md
new file mode 100644
index 0000000000..4a3823715c
--- /dev/null
+++ b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md
@@ -0,0 +1,256 @@
+---
+sidebar_position: 8
+---
+
+# LlamaIndex
+
+Persistent long-term memory for [LlamaIndex](https://docs.llamaindex.ai/) agents via Hindsight. Two packages are available:
+
+- **`llama-index-tools-hindsight`** — Agent-driven memory tools (retain/recall/reflect)
+- **`llama-index-memory-hindsight`** — Automatic memory via LlamaIndex's `BaseMemory` interface
+
+Both follow the LlamaIndex namespace package convention (`from llama_index.tools.hindsight import ...`).
+
+## Installation
+
+```bash
+# Tools only (agent-driven)
+pip install llama-index-tools-hindsight
+
+# Memory only (automatic)
+pip install llama-index-memory-hindsight
+
+# Both
+pip install llama-index-tools-hindsight llama-index-memory-hindsight
+```
+
+---
+
+## 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 llama_index.memory.hindsight 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=tools, llm=OpenAI(model="gpt-4o"), memory=memory)
+ response = await agent.run("Remember that I prefer dark mode")
+ print(response)
+
+asyncio.run(main())
+```
+
+### How It Works
+
+| Event | What Happens |
+|-------|-------------|
+| Agent receives input | `get(input)` recalls relevant memories from Hindsight, prepends as system message |
+| Agent produces output | `put(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 llama_index.tools.hindsight 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 llama_index.tools.hindsight 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 llama_index.tools.hindsight 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`, `opinion`, `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 packages 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 packages together for maximum flexibility:
+
+```python
+from llama_index.tools.hindsight import create_hindsight_tools
+from llama_index.memory.hindsight import 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, memory=memory)
+```
+
+## Requirements
+
+- Python 3.10+
+- `llama-index-core >= 0.11.0`
+- `hindsight-client >= 0.4.0`
diff --git a/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json b/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json
index 256f8fab10..6a3a009325 100644
--- a/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json
+++ b/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json
@@ -331,6 +331,22 @@
"icon": "/img/icons/ag2.svg"
}
},
+ {
+ "type": "doc",
+ "id": "sdks/integrations/llamaindex",
+ "label": "LlamaIndex",
+ "customProps": {
+ "icon": "/img/icons/llamaindex.png"
+ }
+ },
+ {
+ "type": "doc",
+ "id": "sdks/integrations/codex",
+ "label": "OpenAI Codex CLI",
+ "customProps": {
+ "icon": "/img/icons/openai.png"
+ }
+ },
{
"type": "doc",
"id": "sdks/integrations/skills",
diff --git a/skills/hindsight-docs/references/developer/configuration.md b/skills/hindsight-docs/references/developer/configuration.md
index c0702ebbf9..0f3c2e9755 100644
--- a/skills/hindsight-docs/references/developer/configuration.md
+++ b/skills/hindsight-docs/references/developer/configuration.md
@@ -878,6 +878,7 @@ Observations are consolidated knowledge synthesized from facts.
| `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. | `-1` |
| `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` |
#### Customizing observations: when to use what
@@ -1154,7 +1155,7 @@ Configuration fields are categorized for security:
1. **Configurable Fields** - Safe behavioral settings that can be customized per-bank:
- Retention: `retain_chunk_size`, `retain_extraction_mode`, `retain_mission`, `retain_custom_instructions`
- - Observations: `enable_observations`, `observations_mission`
+ - Observations: `enable_observations`, `observations_mission`, `max_observations_per_scope`
- MCP access control: `mcp_enabled_tools`
2. **Credential Fields** - NEVER exposed or configurable via API:
diff --git a/skills/hindsight-docs/references/sdks/integrations/codex.md b/skills/hindsight-docs/references/sdks/integrations/codex.md
new file mode 100644
index 0000000000..edb3b69794
--- /dev/null
+++ b/skills/hindsight-docs/references/sdks/integrations/codex.md
@@ -0,0 +1,189 @@
+---
+sidebar_position: 6
+---
+
+# OpenAI Codex CLI
+
+Persistent memory for [OpenAI 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
+
+```bash
+# 1. Clone the Hindsight repo and install the plugin
+git clone https://github.com/vectorize-io/hindsight.git
+cd hindsight/hindsight-integrations/codex
+./install.sh
+
+# 2. Configure your Hindsight connection
+cat > ~/.hindsight/codex.json << 'EOF'
+{
+ "hindsightApiUrl": "https://api.hindsight.vectorize.io",
+ "hindsightApiToken": "hsk_your_token_here",
+ "bankId": "codex"
+}
+EOF
+
+# 3. Start Codex — memory is live
+codex
+```
+
+For a local Hindsight instance, set `hindsightApiUrl` to `http://localhost:9077` and omit `hindsightApiToken`.
+
+## 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:
+
+```
+
+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)
+
+```
+
+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 `install.sh` to write 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.
diff --git a/skills/hindsight-docs/references/sdks/integrations/llamaindex.md b/skills/hindsight-docs/references/sdks/integrations/llamaindex.md
new file mode 100644
index 0000000000..4a3823715c
--- /dev/null
+++ b/skills/hindsight-docs/references/sdks/integrations/llamaindex.md
@@ -0,0 +1,256 @@
+---
+sidebar_position: 8
+---
+
+# LlamaIndex
+
+Persistent long-term memory for [LlamaIndex](https://docs.llamaindex.ai/) agents via Hindsight. Two packages are available:
+
+- **`llama-index-tools-hindsight`** — Agent-driven memory tools (retain/recall/reflect)
+- **`llama-index-memory-hindsight`** — Automatic memory via LlamaIndex's `BaseMemory` interface
+
+Both follow the LlamaIndex namespace package convention (`from llama_index.tools.hindsight import ...`).
+
+## Installation
+
+```bash
+# Tools only (agent-driven)
+pip install llama-index-tools-hindsight
+
+# Memory only (automatic)
+pip install llama-index-memory-hindsight
+
+# Both
+pip install llama-index-tools-hindsight llama-index-memory-hindsight
+```
+
+---
+
+## 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 llama_index.memory.hindsight 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=tools, llm=OpenAI(model="gpt-4o"), memory=memory)
+ response = await agent.run("Remember that I prefer dark mode")
+ print(response)
+
+asyncio.run(main())
+```
+
+### How It Works
+
+| Event | What Happens |
+|-------|-------------|
+| Agent receives input | `get(input)` recalls relevant memories from Hindsight, prepends as system message |
+| Agent produces output | `put(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 llama_index.tools.hindsight 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 llama_index.tools.hindsight 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 llama_index.tools.hindsight 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`, `opinion`, `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 packages 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 packages together for maximum flexibility:
+
+```python
+from llama_index.tools.hindsight import create_hindsight_tools
+from llama_index.memory.hindsight import 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, memory=memory)
+```
+
+## Requirements
+
+- Python 3.10+
+- `llama-index-core >= 0.11.0`
+- `hindsight-client >= 0.4.0`