diff --git a/hindsight-api-slim/hindsight_api/engine/reflect/delta_ops.py b/hindsight-api-slim/hindsight_api/engine/reflect/delta_ops.py index 4c864912b..2d505139a 100644 --- a/hindsight-api-slim/hindsight_api/engine/reflect/delta_ops.py +++ b/hindsight-api-slim/hindsight_api/engine/reflect/delta_ops.py @@ -73,20 +73,32 @@ class InsertBlockOp(_OpBase): class ReplaceBlockOp(_OpBase): - """Replace the block at ``index`` of an existing section.""" + """Replace the block at ``index`` of an existing section. + + When ``expected_block`` is set the operation is only applied if the block + currently at ``index`` serialises to the same JSON — protecting against + LLM-computed index drift where a wrong-but-in-range index would otherwise + silently overwrite an unrelated block (#3273). + """ op: Literal["replace_block"] = "replace_block" section_id: str index: int = Field(ge=0) block: Block + expected_block: Block | None = None class RemoveBlockOp(_OpBase): - """Remove the block at ``index`` of an existing section.""" + """Remove the block at ``index`` of an existing section. + + When ``expected_block`` is set the operation is only applied if the block + currently at ``index`` serialises to the same JSON (#3273). + """ op: Literal["remove_block"] = "remove_block" section_id: str index: int = Field(ge=0) + expected_block: Block | None = None class AddSectionOp(_OpBase): @@ -299,6 +311,11 @@ def _op_summary(op: Operation) -> dict[str, Any]: } +def _blocks_equal(a: Block, b: Block) -> bool: + """Return True when two blocks serialise to identical JSON.""" + return a.model_dump_json() == b.model_dump_json() + + def apply_operations( doc: StructuredDocument, operations: list[Operation], @@ -355,6 +372,14 @@ def skip(op: Operation, reason: str) -> None: f"index out of range: {op.index} >= {len(section.blocks)}", ) continue + # Content-based validation: when expected_block is provided, + # refuse to replace a block that doesn't match (#3273). + if op.expected_block is not None and not _blocks_equal(section.blocks[op.index], op.expected_block): + skip( + op, + f"expected_block does not match current block at index {op.index} — index drift detected", + ) + continue section.blocks[op.index] = op.block applied.append(_op_summary(op)) continue @@ -370,6 +395,13 @@ def skip(op: Operation, reason: str) -> None: f"index out of range: {op.index} >= {len(section.blocks)}", ) continue + # Content-based validation (#3273) + if op.expected_block is not None and not _blocks_equal(section.blocks[op.index], op.expected_block): + skip( + op, + f"expected_block does not match current block at index {op.index} — index drift detected", + ) + continue section.blocks.pop(op.index) applied.append(_op_summary(op)) continue diff --git a/skills/hindsight-docs/references/sdks/integrations/agent-plugin.md b/skills/hindsight-docs/references/sdks/integrations/agent-plugin.md new file mode 100644 index 000000000..c47aa3256 --- /dev/null +++ b/skills/hindsight-docs/references/sdks/integrations/agent-plugin.md @@ -0,0 +1,85 @@ + +# 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**. + +## Quick Start + +> **💡 Recommended: Hindsight Cloud** +> +[Sign up free](https://ui.hindsight.vectorize.io/signup) 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; defaults to "default" + ``` + +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` | `default` | 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`. +**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. + +## 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 native, hook-based integration built for your specific tool, such as [Claude Code](claude-code.md) or [Codex](codex.md). Both share 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). + +**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/signup)