@handsomecheese/opencode-hindsight
v0.2.17
Published
Hindsight memory plugin for OpenCode - Give your AI coding agent persistent long-term memory
Maintainers
Readme
@handsomecheese/opencode-hindsight
Hindsight memory plugin for OpenCode. It gives coding agents long-term memory across sessions while keeping the legacy single-bank path and the newer server-authorized policy path explicit.
Features
- Explicit tools:
hindsight_retain,hindsight_recall,hindsight_reflect - Legacy mode for the current single-bank or dynamic-bank workflow
- Server mode for policy-aware OpenCode calls backed by Hindsight Server
- Auto-recall, auto-retain, and compaction hooks, with fail closed behavior in server mode when no reliable snapshot exists
Quick Start
1. Enable the plugin
Add this to opencode.json in your project, or to ~/.config/opencode/opencode.json globally:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@handsomecheese/opencode-hindsight"]
}OpenCode installs plugins listed here when it starts. No separate npm install step is needed for normal use.
2. Pick a setup path
Most users only need to list the plugin in opencode.json, then set a few values with environment variables, inline plugin options, or ~/.hindsight/opencode.json.
Minimal legacy setup
export HINDSIGHT_API_URL="http://localhost:8888"
export HINDSIGHT_POLICY_MODE="legacy"
export HINDSIGHT_BANK_ID="my-project"This starts the current single-bank path. Every explicit tool call and automatic hook uses the same legacy bank.
Server policy mode setup
export HINDSIGHT_API_URL="http://localhost:8888"
export HINDSIGHT_POLICY_MODE="server"
export HINDSIGHT_POLICY_CANONICAL_AGENT_ID="opencode:workspace-1:builder"In policyMode="server", Hindsight Server resolves OpenCode policy through /v1/opencode/* endpoints. The plugin does not authorize by picking a local fallback bank. Automatic hooks also fail closed when there is no reliable policy snapshot for the current session. The desired policy authored in Dashboard AgentAccess is the server-mode source of truth.
Using Hindsight Cloud
Get an API key at ui.hindsight.vectorize.io/connect, then either export environment variables:
export HINDSIGHT_API_URL="https://api.hindsight.vectorize.io"
export HINDSIGHT_API_TOKEN="your-api-key"
export HINDSIGHT_POLICY_MODE="server"
export HINDSIGHT_POLICY_CANONICAL_AGENT_ID="opencode:workspace-1:builder"Or configure inline in opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "your-api-key",
"policyMode": "server"
}
]
]
}Configuration
OpenCode users normally configure this plugin in opencode.json. You can also keep shared defaults in ~/.hindsight/opencode.json, then override them per project or per shell.
Plugin Array Forms
Minimal plugin list:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@handsomecheese/opencode-hindsight"]
}Inline tuple with options:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "http://localhost:8888",
"policyMode": "legacy",
"bankId": "my-project"
}
]
]
}OpenCode installs every plugin listed in the plugin array on startup.
Inline Plugin Options
Pass options directly in opencode.json:
{
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "http://localhost:8888",
"policyMode": "legacy",
"bankId": "my-project",
"dynamicBankId": false,
"autoRecall": true,
"autoRetain": true,
"retainMode": "full-session",
"recallBudget": "mid",
"recallMaxTokens": 1024,
"debug": false
}
]
]
}Common inline option names:
| Option | Meaning | Default |
| --- | --- | --- |
| hindsightApiUrl | Hindsight API base URL | null |
| hindsightApiToken | API token for authenticated servers or cloud | null |
| requestTimeoutSeconds | Bounds outbound Hindsight requests in seconds. 0 keeps the current unbounded request behavior. This is a plugin runtime guard only, not a persisted AgentAccess or server policy setting | 0 |
| policyMode | legacy or server | legacy |
| policyApiPrefix | Server policy endpoint prefix | /v1/opencode |
| policyTenantId | Optional tenant override in server mode | null |
| policyRuntimeId | Optional runtime override in server mode | null |
| policyWorkspaceId | Optional workspace override in server mode | null |
| policyCanonicalAgentId | Agent identity used for server policy registration | null |
| bankId | Static bank ID in legacy mode | null, plugin falls back to opencode |
| bankIdPrefix | Prefix added before static or dynamic bank IDs | "" |
| directoryBankMap | Legacy directory prefix to bank ID map. Longest matching prefix wins before static or dynamic bank selection | {} |
| dynamicBankId | Derive legacy bank IDs from context | false |
| dynamicBankGranularity | Dynamic bank ID fields | ["agent", "project"] |
| resolveWorktrees | Resolve linked git worktrees to the main worktree root before directory mapping or path-aware dynamic fields | false |
| agentName | Agent naming hint used by legacy bank derivation | opencode |
| autoRecall | Enable automatic recall hook | true |
| recallAdditionalBanks | Extra bank IDs appended to recall reads. Empty keeps the current primary bank or server-selected read set | [] |
| autoRetain | Enable automatic retain hook | true |
| retainMode | full-session or last-turn | full-session |
| retainEveryNTurns | Retain cadence for automatic retention | 3 |
| retainOverlapTurns | Overlap when retainMode is last-turn | 2 |
| retainContext | Retain context label | opencode |
| retainTags | Tags attached to retained memories | [] |
| retainRoles | Role allowlist for retained transcript candidate messages. Empty keeps all roles | [] |
| sessionEndRetain | Opt in to a final retain pass when OpenCode reports idle status | false |
| recallBudget | low, mid, or high | mid |
| recallMaxTokens | Max tokens used for recall results | 1024 |
| recallContextTurns | Recent turns used to shape recall query | 1 |
| recallMaxQueryChars | Max recall query length | 800 |
| recallTags | Optional recall tag filter | [] |
| recallTagsMatch | any, all, any_strict, all_strict | any |
| recallRoles | Role allowlist for prior-context lines used to compose recall queries. Empty keeps all roles | [] |
| bankMission | Legacy bank mission text | "" |
| retainMission | Optional retain mission override | null |
| debug | Enable plugin debug logging | false |
All of the options above are opt in, except the existing defaults that already power the current plugin behavior. Leaving the new ported options unset preserves the current OpenCode behavior.
Compact example with all five ported capability groups enabled together:
{
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "http://localhost:8888",
"policyMode": "legacy",
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "gitProject"],
"recallAdditionalBanks": ["team-memory", "release-memory"],
"directoryBankMap": {
"project/acme": "acme-shared",
"shared/acme": "acme-shared"
},
"resolveWorktrees": true,
"sessionEndRetain": true,
"requestTimeoutSeconds": 10,
"retainRoles": ["user", "assistant"],
"recallRoles": ["user", "assistant"]
}
]
]
}Config File
Create ~/.hindsight/opencode.json for persistent settings:
{
"hindsightApiUrl": "http://localhost:8888",
"hindsightApiToken": "your-api-key",
"policyMode": "server",
"policyCanonicalAgentId": "opencode:workspace-1:builder",
"recallBudget": "mid",
"retainEveryNTurns": 3,
"debug": false
}Use this file for defaults you want across projects. Put project-specific overrides in opencode.json, then use environment variables when you want a quick local override.
Environment Variables
| Variable | Description | Default |
| --- | --- | --- |
| HINDSIGHT_API_URL | Hindsight API base URL | null |
| HINDSIGHT_API_TOKEN | API key for authentication | (none) |
| HINDSIGHT_REQUEST_TIMEOUT_SECONDS | Bounds outbound Hindsight requests in seconds. 0 keeps no configured request timeout | 0 |
| HINDSIGHT_POLICY_MODE | legacy or server | legacy |
| HINDSIGHT_POLICY_API_PREFIX | Policy endpoint prefix | /v1/opencode |
| HINDSIGHT_POLICY_TENANT_ID | Optional tenant override for policy calls | (none) |
| HINDSIGHT_POLICY_RUNTIME_ID | Optional runtime ID override | (none) |
| HINDSIGHT_POLICY_WORKSPACE_ID | Optional workspace override | (none) |
| HINDSIGHT_POLICY_CANONICAL_AGENT_ID | Optional canonical agent override | (none) |
| HINDSIGHT_BANK_ID | Static memory bank ID in legacy mode | null, plugin falls back to opencode |
| HINDSIGHT_AGENT_NAME | Agent name hint for legacy dynamic bank IDs and runtime naming | opencode |
| HINDSIGHT_AUTO_RECALL | Auto-recall toggle | true |
| HINDSIGHT_RECALL_ADDITIONAL_BANKS | Comma-separated extra bank IDs appended to recall reads | (none), parses to [] |
| HINDSIGHT_AUTO_RETAIN | Auto-retain toggle | true |
| HINDSIGHT_SESSION_END_RETAIN | Opt in to final retain on the idle/status completion mapping | false |
| HINDSIGHT_RETAIN_MODE | full-session or last-turn | full-session |
| HINDSIGHT_RETAIN_TAGS | Comma-separated tags attached to retained memories | (none), parses to [] |
| HINDSIGHT_RETAIN_ROLES | Comma-separated roles allowed into retained transcript candidates | (none), parses to [] |
| HINDSIGHT_RECALL_BUDGET | Recall budget, low, mid, high | mid |
| HINDSIGHT_RECALL_MAX_TOKENS | Max tokens for recall results | 1024 |
| HINDSIGHT_RECALL_TYPES | Comma-separated fact types included in recall/reflect operations | world,experience |
| HINDSIGHT_RECALL_MAX_QUERY_CHARS | Max recall query length | 800 |
| HINDSIGHT_RECALL_CONTEXT_TURNS | Recent turns used to shape recall query | 1 |
| HINDSIGHT_RECALL_ROLES | Comma-separated roles allowed into recall prior-context lines | (none), parses to [] |
| HINDSIGHT_RECALL_TAGS | Comma-separated recall tag filter | (none) |
| HINDSIGHT_RECALL_TAGS_MATCH | any, all, any_strict, all_strict | any |
| HINDSIGHT_DIRECTORY_BANK_MAP | JSON object string that maps directory prefixes to bank IDs | (none), parses to {} |
| HINDSIGHT_DYNAMIC_BANK_ID | Enable dynamic bank ID derivation in legacy mode | false |
| HINDSIGHT_RESOLVE_WORKTREES | Resolve linked git worktrees to the main worktree root before directory mapping or path-aware dynamic fields | false |
| HINDSIGHT_CHANNEL_ID | Optional channel segment used by dynamic bank IDs | (none), falls back to default when channel granularity is used |
| HINDSIGHT_USER_ID | Optional user segment used by dynamic bank IDs | (none), falls back to anonymous when user granularity is used |
| HINDSIGHT_BANK_MISSION | Legacy bank mission and context | (none) |
| HINDSIGHT_DEBUG | Enable debug logging | false |
Boolean environment variables treat true, 1, and yes as enabled. Other values are treated as false.
List environment variables use comma-separated values. HINDSIGHT_DIRECTORY_BANK_MAP must be a JSON object string such as {"project/acme":"acme-shared"}.
Configuration Priority
Settings are loaded in this order, later entries win:
- Built-in defaults
~/.hindsight/opencode.json- Plugin options from
opencode.json - Environment variables
After that merge, policyMode decides the runtime path. This means a shell export can override both the user config file and the inline plugin tuple.
Porting notes from omo integration
These five capabilities were ported as opt-in OpenCode behavior. Leaving them unset keeps the current defaults.
recallAdditionalBanksextends recall reads only. In legacy mode, the plugin recalls the primary bank first, then queries extra banks in parallel, deduplicates results, and keeps additional results insiderecallMaxTokens. In server mode, extra banks are appended only to an already scoped requested bank set or to snapshot-derived readable banks, so local config does not bypass server policy.directoryBankMapandresolveWorktreesaffect legacy bank selection before staticbankIdor dynamic derivation. Directory matching normalizes Windows drive letters to lowercase, converts backslashes to forward slashes, strips trailing slashes, and picks the longest matching prefix. WhenresolveWorktreesistrue, the plugin usesgit rev-parse --path-format=absolute --git-common-dirso linked worktrees can resolve to the main worktree root before directory matching or path-aware dynamic fields run.sessionEndRetainadds a final retain pass for new user-turn tail messages only. OpenCode does not expose a dedicatedsession.endevent, so this plugin maps the behavior tosession.statuswithstatus.type === "idle", withsession.idlekept as a compatibility fallback. The pass is backgrounded, opt in, and bounded.requestTimeoutSecondswraps outbound Hindsight requests with a positive-second bound.0means there is no configured request timeout. Positive values bound legacy and server calls alike. Session-end final retain still keeps a local safety bound even when this option stays at0.requestTimeoutSecondsremains a plugin runtime guard only, not a persisted AgentAccess or server policy setting.retainRolesandrecallRolesare role filters, not role rewriters. Empty arrays keep the current output. Non-empty arrays filter retained transcript candidate messages or recall prior-context lines before formatting. These role allowlists shape retained or recalled content only, they are not a server security boundary.
Common Setup Scenarios
Project-local legacy bank
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "http://localhost:8888",
"policyMode": "legacy",
"bankId": "frontend-team"
}
]
]
}Use this when one OpenCode project should always talk to one bank.
Legacy dynamic bank per repo
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@handsomecheese/opencode-hindsight",
{
"hindsightApiUrl": "http://localhost:8888",
"policyMode": "legacy",
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "gitProject"]
}
]
]
}Use this when linked worktrees for the same repo should share one memory bank.
Server mode with local defaults file
{
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
"hindsightApiToken": "your-api-key",
"policyMode": "server",
"policyCanonicalAgentId": "opencode:workspace-1:builder",
"autoRecall": true,
"autoRetain": true
}Save that in ~/.hindsight/opencode.json when most projects should use the same Hindsight server and policy identity.
Policy Modes
policyMode="legacy"
- Keeps the current single-bank and dynamic-bank behavior.
- One bank is selected when the plugin starts.
- Explicit tools and automatic hooks use that process-scoped bank.
HINDSIGHT_BANK_ID,HINDSIGHT_DYNAMIC_BANK_ID, andHINDSIGHT_AGENT_NAMEkeep their current meanings.
policyMode="server"
- Hindsight Server becomes the policy authority.
- Dashboard AgentAccess authors the desired server policy that OpenCode server mode consumes.
- The plugin performs explicit policy-aware tool calls to
/v1/opencode/*endpoints. - OpenCode tool context such as
agent,sessionID,messageID,directory, andworktreeis sent to the server. - Local legacy bank config is not an authorization source.
Normal pattern: keep hindsightApiUrl, hindsightApiToken, and policyCanonicalAgentId configured, then let the server decide readable and writable banks.
Startup Precedence
Mode selection is deterministic:
- Merge defaults, file config, plugin options, and environment variables.
- Read the resulting
policyMode. - If the result is
legacy, use the legacy bank path. - If the result is
server, use policy-aware server endpoints.
If policyMode="server" is mixed with legacy bank config such as bankId, bankIdPrefix, or dynamicBankId, startup emits a deterministic warning and server policy wins.
Legacy vs Server Notes
legacypicks a bank locally when the plugin starts.serverasks Hindsight Server to authorize every policy-aware operation.legacyhonorsbankId,bankIdPrefix, anddynamicBankId.servermay still accept those values in merged config, but they are not used as the authorization source.- In server mode, Dashboard AgentAccess and Hindsight Server policy are the source of truth. Local plugin config can identify the runtime, but it cannot grant access, widen read or write scope, or replace denied server policy.
- In server mode, legacy local bank fields (
bankId,bankIdPrefix,dynamicBankId,directoryBankMap,recallAdditionalBanks) and local naming/context fields (agentName,sessionID,messageID,policyCanonicalAgentId) are compatibility/configuration hints only. They cannot expand authorization, widen SubAgent filters, or stand in for missingcontext.agent; the server remains authoritative and filtered operations fail closed when the raw agent is absent or denied. serveris the right mode when you want centrally managed bank access, tenant policy, or dashboard bindings.
Troubleshooting
Missing HINDSIGHT_API_URL
If tools do not connect, make sure hindsightApiUrl or HINDSIGHT_API_URL is set. The plugin default is null, so there is no built-in server URL.
Missing token for cloud or protected servers
If your Hindsight deployment requires auth, set hindsightApiToken or HINDSIGHT_API_TOKEN. Do not put a real token in committed config files.
Mixed legacy settings in server mode
If policyMode is server and you also set bankId, bankIdPrefix, or dynamicBankId, the plugin warns and server policy still wins. Remove the legacy bank settings once your server bindings are ready.
Automatic hooks appear inactive in server mode
That can be expected. Auto-recall, auto-retain, and compaction fail closed when there is no reliable session policy snapshot. Check that the session registered successfully and that policyCanonicalAgentId maps to a valid server policy binding.
Server Policy Contract
No OpenCode Patch
No OpenCode Patch is part of v1. Hindsight does not patch OpenCode core.
Threat Model
V1 protects against model, tool, and integration misuse. It does not claim cryptographic isolation from a hostile local OS user who controls OpenCode and raw Hindsight credentials.
Plugin-side checks are convenience and early rejection only. Server-side policy enforcement is mandatory.
Authorization Boundary
experimental.chat.system.transform is not used as a per-agent authorization basis. That hook does not carry reliable agent identity. In server mode, explicit tools are the primary guaranteed path.
AgentAccess Desired Policy
In policyMode="server", the desired policy authored in Dashboard AgentAccess and stored on Hindsight Server is the source of truth for bank access, automatic retain behavior, role allowlists, deterministic document templates, SubAgent capture, and SubAgent bank filters. Local plugin config can help identify the runtime, but it cannot grant access or bypass a denied server policy.
Runtime Guard vs Persisted Policy
requestTimeoutSeconds remains a plugin runtime guard only. It bounds outbound Hindsight calls from the running OpenCode process, but it is not persisted in AgentAccess, not copied into SessionPolicySnapshot, and not treated as a server-side security control.
SessionPolicySnapshot
Every active OpenCode session is pinned to a SessionPolicySnapshot.
- Desired policy edits affect new sessions by default.
- Active sessions keep their immutable snapshot unless it is revoked, expires, or becomes broken because a required bank binding disappeared.
- Live propagation of desired policy edits into active sessions is not part of v1.
Role Allowlists Shape Content
retainRoles and recallRoles shape which transcript lines become retain content or recall query context. In server mode the plugin reads the immutable snapshot allowlists instead of local expansion config, but those role allowlists still shape content only. They do not authorize banks or create a server security boundary.
Deterministic Document IDs
Server mode keeps session and SubAgent document identity backend-owned. Desired policy templates are validated against safe token sets, copied into immutable session snapshots, and expanded by the server after bank authorization succeeds.
- Session retain templates may use
{sessionID},{start}, and{end}. - SubAgent capture templates may use
{parentSessionID},{subagentSessionID},{start}, and{end}. - Unsupported tokens or unsafe static templates fail closed with policy validation errors.
- Users do not need to invent timestamp-based document IDs to mimic snapshot behavior.
SubAgent Capture
SubAgent capture is off by default. When enabled, the plugin sends only sanitized public summary content, title, learnings, issues, problems, and decisions-style fields. It omits hidden reasoning, raw tool outputs, agent, agentName, canonicalAgentId, messageID, and documentId, and it fails closed when snapshot policy, target selection, or sanitization is not safe.
SubAgent Bank Filters
SubAgent filters are exact-match, case-sensitive narrowing rules on raw ToolContext.agent or context.agent values. They only narrow existing binding permissions, they do not expand them.
- Missing
context.agentfails closed for filtered banks. - Missing, denied, or non-matching SubAgent ids fail closed.
- Filter rules never fall back to
sessionID,messageID, display names, orpolicyCanonicalAgentId.
Emergency Revoke
Emergency revoke is the operator kill switch for an active snapshot. After revoke, later snapshot-based operations fail closed with SNAPSHOT_REVOKED.
Retain Targeting
Retain targeting is always resolved on the server in policyMode="server".
- Zero writable banks, deny retain.
- One writable bank, retain may use that bank implicitly.
- Multiple writable banks, retain requires
targetBankIdunless there is one unique default write bank. - A readable-only
targetBankIdis denied for retain. - A writable but not readable target may still accept retain, but the response does not expose bank contents.
Ambiguous retain returns TARGET_BANK_REQUIRED.
Fail Closed Automatic Behavior
In server mode, auto-recall, auto-retain, and compaction fail closed when there is no reliable per-agent snapshot.
- No reliable snapshot, no automatic recall.
- No reliable snapshot, no automatic retain.
- No reliable snapshot, no automatic compaction read or write.
This keeps plugin-side guesses out of the authorization path.
Redacted Local Identity
Server policy mode stores normalized hash and fingerprint keys, plus an optional basename, instead of full absolute local paths by default.
Migration From Legacy Config
| Legacy setting | policyMode="legacy" | policyMode="server" | Migration guidance |
| --- | --- | --- | --- |
| HINDSIGHT_BANK_ID | Used as the static bank ID | Ignored for authorization. Server policy wins if mixed | Remove after server bindings are authored |
| HINDSIGHT_DYNAMIC_BANK_ID | Enables dynamic bank derivation | Ignored for authorization. Server policy wins if mixed | Replace with server-side bindings |
| HINDSIGHT_AGENT_NAME | Used for legacy naming and defaults | Still useful as a naming hint, not as access control | Keep only if you want stable naming |
| autoRecall | Legacy automatic recall path stays active | Automatic recall is policy-gated and fail closed without a reliable snapshot | Keep the toggle, but expect the snapshot to decide whether recall runs |
| autoRetain | Legacy automatic retain path stays active | Automatic retain is policy-gated and fail closed without a reliable snapshot | Keep the toggle, but expect the snapshot to decide whether retain runs |
Examples
One Writable Bank
The agent has one writable bank, team-memory, and read access to that same bank.
{
"tool": "hindsight_retain",
"arguments": {
"content": "The rollout starts Tuesday at 09:00 UTC."
}
}The server accepts the retain and uses team-memory as the implicit target.
Multiple Writable Banks With Explicit targetBankId
The agent can write to team-memory and incident-memory.
{
"tool": "hindsight_retain",
"arguments": {
"content": "Incident 482 needs a customer update.",
"targetBankId": "incident-memory"
}
}If targetBankId is omitted here and there is no unique default write bank, the server denies the call with TARGET_BANK_REQUIRED.
Unknown Agent Denial
Unknown or unbound agents do not fall back to a legacy bank in server mode.
HTTP 403
error.code = AGENT_UNBOUNDTools
hindsight_retain
Stores information in long-term memory.
hindsight_recall
Searches long-term memory for relevant facts.
hindsight_reflect
Generates a synthesized answer from long-term memory.
Dynamic Bank IDs
Dynamic bank IDs are part of policyMode="legacy".
export HINDSIGHT_DYNAMIC_BANK_ID=trueThe bank ID is composed from granularity fields, default agent::project. Supported fields are agent, project, gitProject, channel, and user.
directoryBankMap runs before static or dynamic legacy bank selection. A mapped directory wins immediately, and the longest matching normalized prefix is used when multiple entries overlap.
projectuses the working directory basename. Separate git worktrees of the same repository can end up with different bank IDs because their paths differ.gitProjectresolves to the main worktree basename throughgit rev-parse --git-common-dir, so linked worktrees of the same repository can share one bank. It falls back to the working directory basename when git metadata is unavailable.
If resolveWorktrees is true, the plugin resolves the main worktree root before directory map matching and before any path-aware dynamic fields are evaluated.
{
"dynamicBankId": true,
"dynamicBankGranularity": ["agent", "gitProject"]
}The bank ID is derived once when the plugin starts. These dimensions are process-scoped, they do not change per session inside one running OpenCode process.
For per-user isolation in legacy mode:
export HINDSIGHT_CHANNEL_ID="slack-general"
export HINDSIGHT_USER_ID="user123"Development
npm install
npm test
npm run buildLicense
MIT
