examples/codex-memory-plugin/README.md
Long-term semantic memory for Codex, powered by OpenViking.
This is the Codex counterpart to claude-code-memory-plugin. It hooks Codex's lifecycle to:
UserPromptSubmit and inject them via hookSpecificOutput.additionalContextStop (turn end): append the new user/assistant turns to a deterministic OpenViking session id cx-<codex_session_id>. When pending_tokens reaches OPENVIKING_COMMIT_TOKEN_THRESHOLD, commit while keeping a recent live tail.PreCompact: trigger OpenViking's memory extractor on the full pre-compact transcript before Codex summarizes it.SessionStart (source=startup|clear): active-window heuristic — if exactly one other state file was touched within the last 2 min, commit it (the just-ended session). On ≥2, defer to idle-TTL sweep at the tail. source=resume never commits or sweeps; if the live OV session was already committed, it may inject the latest archive summary for continuity. See DESIGN.md for the full decision tree.It also starts a local stdio MCP proxy that forwards to OpenViking's native /mcp endpoint with credentials resolved from env / ovcli.conf, so the model has direct access to the search, store, read, list, grep, glob, forget, add_resource, and health tools.
There are two install paths. Pick one — don't mix them (both surface the same openviking-memory plugin; enabling it from both would run the hooks twice). The one-line installer (A) is the recommended path for most users; the marketplace install (B) is useful when you already manage ~/.openviking/ovcli.conf yourself.
curl | bash (recommended)bash <(curl -fsSL https://raw.githubusercontent.com/volcengine/OpenViking/main/examples/memory-plugin-shared/install.sh) --harness codex
Claude Code and Codex share this installer (drop --harness codex to pick interactively). It asks for your language (English/中文), the download source (GitHub, or a TOS mirror for GitHub-blocked regions — pass --dist tos; Codex on TOS installs from a TOS-hosted git repo and keeps remote updates), and your OpenViking credentials. It:
codex and Node.js 18+ (the plugin itself wants Codex's bundled Node 22+ at runtime)~/.openviking/ovcli.conf interactivelyopenviking marketplace — remote git by default (codex plugin marketplace add https://github.com/volcengine/OpenViking.git), or this checkout / a TOS archive in dev/archive mode — and enables openviking-memory@openviking with features.plugin_hooks = true.mcp.json intact; servers/mcp-proxy.mjs reads your active ovcli.conf at runtimeopenviking-plugins-local marketplace when foundAfter install:
codex # first run: review /hooks once
This path uses the same checked-in stdio MCP proxy as the installer path. Authenticated and remote/cloud servers work when ~/.openviking/ovcli.conf or the relevant OPENVIKING_* env vars are present in Codex's environment.
The repo ships a Codex marketplace catalog at .agents/plugins/marketplace.json, so you can install with Codex's native commands:
# 1. add the OpenViking marketplace (use volcengine/OpenViking once merged
# upstream, or <your-fork>/OpenViking while testing a fork)
codex plugin marketplace add volcengine/OpenViking
# 2. install the plugin from that marketplace
# (older Codex builds spell this `codex plugin install`)
codex plugin add openviking-memory@openviking
Then enable plugin hooks (if your Codex build doesn't already) by adding to ~/.codex/config.toml:
[features]
plugin_hooks = true
Finally start Codex and trust the plugin hooks once:
codex # then run /hooks inside Codex to review & approve the hooks
Requirements & notes
- Codex version: this path relies on Codex injecting and inline-substituting
${PLUGIN_ROOT}in plugin hook commands (current Codex does both). On an older Codex that doesn't substitute${PLUGIN_ROOT}, the hook script paths won't resolve — use path A.- Catalog source: the catalog entry (
.agents/plugins/marketplace.json) uses a relative source (./examples/codex-memory-plugin).codex plugin addtherefore installs the plugin from the same marketplace snapshot/ref that you added. This keeps fork, branch, tag, and upstream-main installs reproducible and testable without rewriting the catalog.
This path works out of the box against an unauthenticated local OpenViking at http://127.0.0.1:1933. For remote/cloud servers, create ~/.openviking/ovcli.conf with url, api_key, and optional account / user; the proxy reads it when Codex starts.
If you don't want the installer touching your rc, do these things yourself:
Write ovcli.conf once so hooks and MCP share the same connection:
{
"url": "https://your-openviking-server.example.com",
"api_key": "<your-api-key>",
"account": "my-team",
"user": "alice"
}
Or run the bundled interactive wizard: node scripts/setup.mjs (from the plugin directory).
Add the plugin via the remote marketplace (path B above), or via a local directory marketplace: codex plugin marketplace add <checkout>/examples reads examples/.agents/plugins/marketplace.json and yields the same openviking-memory@openviking id. hooks/hooks.json needs no rendering on modern Codex: it uses the native ${PLUGIN_ROOT} token, which Codex injects into the hook env and substitutes inline.
Connection / identity source (applies to hooks, MCP, and ov commands run inside Codex):
ovcli.conf wins when present: OPENVIKING_CLI_CONFIG_FILE or ~/.openviking/ovcli.conf. Use ov config switch <name> to change the active credentials for the CLI, hooks, MCP, and child ov commands together.OPENVIKING_CREDENTIAL_SOURCE=env to force OPENVIKING_URL / OPENVIKING_BASE_URL, OPENVIKING_API_KEY / OPENVIKING_BEARER_TOKEN, OPENVIKING_ACCOUNT, OPENVIKING_USER, and OPENVIKING_PEER_ID.ov.conf (server.url / server.root_api_key plus legacy codex.* tuning); then http://127.0.0.1:1933 unauthenticated.Hooks and the MCP proxy call the same resolver directly, so the model tools and lifecycle hooks follow the same target.
Auth is sent as Authorization: Bearer <api_key> to both the REST API (used by hooks) and the /mcp endpoint (used by the model).
By default the plugin derives a peer from the current workspace path using Claude's project-directory naming rule: every non-letter-or-digit character becomes -, with no path normalization. For example, /Users/x/Dev/OpenViking becomes -Users-x-Dev-OpenViking. Hooks pass the effective peer as peer_id for captured session messages and as X-OpenViking-Actor-Peer for retrieval/filesystem calls; MCP gets the same header mapping.
Set actor_peer_id in ovcli.conf (or OPENVIKING_PEER_ID with OPENVIKING_CREDENTIAL_SOURCE=env) to override the workspace-derived peer. The legacy codex.peerId / codex.peer_id fields in ov.conf still resolve as a fallback. Set OPENVIKING_WORKSPACE_PEER=0 or codex.workspacePeer=false to turn off workspace-derived peers.
Recall defaults to the broad mode: global memory, the current workspace, and other workspace memories can all be recalled, with other workspaces penalized and rendered later. Set OPENVIKING_RECALL_PEER_SCOPE=actor or codex.recallPeerScope="actor" for the isolation mode, which only sees global memory plus the current workspace. In deployments where one bot serves multiple real people, such as zouk, vikingbot, or AstrBot, use the isolation mode with an explicit actor peer so one person's memories are not recalled into another person's session.
The checked-in .mcp.json contains only a stdio command. It never stores server URLs, bearer-token env mappings, or identity headers, so switching ovcli.conf changes the MCP target on the next Codex launch without cache rendering.
All plugin behavior is controlled by OPENVIKING_* environment variables. Connection and identity should normally live in ovcli.conf; tuning vars can be exported in your shell rc when you want every Codex launch to pick them up.
# ~/.zshrc — examples
export OPENVIKING_RECALL_LIMIT=6
export OPENVIKING_RECALL_COMPRESS=1
export OPENVIKING_RECALL_COMPRESS_MODEL=gpt-5.3-codex-spark
export OPENVIKING_RECALL_COMPRESS_THINKING=default
export OPENVIKING_RECALL_TIMEOUT_MS=120000
export OPENVIKING_CAPTURE_ASSISTANT_TURNS=1
export OPENVIKING_AUTO_COMMIT_ON_COMPACT=1
export OPENVIKING_DEBUG=1
Full list: see the Misc env vars block in scripts/config.mjs. Tuning fields have OPENVIKING_* counterparts and env vars win for those tuning fields.
codex block in ov.confEarlier plugin versions configured tuning fields under a codex block in ~/.openviking/ov.conf. That still works for backward compat — every env var above has a camelCase counterpart (OPENVIKING_RECALL_LIMIT → codex.recallLimit, etc.) — but new deployments should prefer env vars: this is the codex CLI's per-machine plugin tuning, and the server-side ov.conf is the wrong place for it. (It's read from ov.conf, not ovcli.conf, by historical accident in scripts/config.mjs.)
┌──────────────────────────────────────────────────────────────┐
│ Codex │
└──┬─────────────────┬────────────────┬───────────────────┬────┘
│ │ │ │
SessionStart UserPromptSubmit Stop PreCompact
(startup|clear|resume) │ (per turn) │
│ │ │ │
┌────▼──────────┐ ┌────▼──────┐ ┌──────▼──────┐ ┌──────────▼──────┐
│ session-start │ │ auto- │ │ auto- │ │ pre-compact- │
│ -commit.mjs │ │ recall.mjs│ │ capture.mjs │ │ capture.mjs │
│ (active-win │ │ (search + │ │ (append + │ │ (commit + reset │
│ heuristic + │ │ compress) │ │ threshold │ │ ovSessionId) │
│ idle TTL + │ │ │ │ │ │ │
│ resume inject)│ │ │ │ │ │ │
└────┬──────────┘ └────┬──────┘ └──────┬──────┘ └──────────┬──────┘
│ │ │ │
│ ┌───▼────────────────▼───────────────────▼──┐
└────────────►│ OpenViking REST API │
│ /api/v1/search/search │
│ /api/v1/sessions [+/{id}/{messages,commit}]│
│ /api/v1/content/read │
└─────────────────┬─────────────────────────┘
│
Codex ◄── stdio MCP proxy ──► /mcp (search, store, read, list,
(env/ovcli.conf) grep, glob, forget,
add_resource, health)
The checked-in .mcp.json starts servers/mcp-proxy.mjs with node. The proxy keeps stdout protocol-clean, reads the same credential sources as the hooks, sends auth and identity headers to /mcp, caches the server mcp-session-id, and transparently reinitializes once if the server restarts.
For details on OpenViking's MCP endpoint, tools, and protocol, see the MCP Integration Guide. The tools list and per-tool semantics are documented there once, not duplicated here.
See
DESIGN.mdfor the commit decision tree — it's the source of truth for which OpenViking session is sealed by which hook event.
Codex fires SessionStart with one of three source values: startup (fresh process / /new / zouk daemon spawn-without-sessionId), resume (/resume or short reconnect), and clear (/clear — the previous transcript is orphaned and a new session_id is created). resume never commits or sweeps; on startup and clear we run the same active-window heuristic.
hooks.json registers SessionStart with matcher: "clear|startup|resume" so codex's dispatcher invokes the script on all three relevant sources. session-start-commit.mjs gates internally so only startup and clear commit/sweep.
On startup or clear, the script:
lastUpdatedAt is within OPENVIKING_CODEX_ACTIVE_WINDOW_MS (default 2 min) of "now":
OPENVIKING_CODEX_IDLE_TTL_MS (default 30 min) gets committed and cleared.On any /commit failure (OV unreachable, non-2xx, timeout) we preserve state (don't clearState) so the next sweep can retry.
On resume, the script skips commit/sweep. If local state has no live ovSessionId, it reads /api/v1/sessions/{cx-session-id}/context and injects the latest committed archive overview. The injected block includes a viking://user/sessions/{cx-session-id}/history/ URI and tells the model to use the OpenViking MCP read/search tools for exact prior commands, file paths, tool outputs, or messages. Set OPENVIKING_RESUME_ARCHIVE_INJECT=0 to disable this.
auto-recall.mjs reads prompt and session_id from stdin, derives the long-lived OpenViking session id (cx-<safe-session-id>) directly from the Codex session id (no plugin state read, so a corrupt state file can't crash recall), calls /api/v1/search/search with that session_id, ranks results, reads full content for top-ranked leaves, and emits:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "<openviking-context source=\"auto-recall\" format=\"digest\">\nOpenViking memory digest:\n- ...\n</openviking-context>" } }
Codex injects additionalContext into the model turn, so memories arrive without an extra tool call. By default the hook runs a Codex compression pass over recalled candidates before injection, dropping weakly-related memories and preserving only a short digest. If the compressor returns NO_RELEVANT_MEMORY, empty text, or non-digest chatter, the hook emits {} and injects nothing. The whole hook has its own OPENVIKING_RECALL_TIMEOUT_MS deadline (default 120s); the bundled hooks.json gives Codex 130s so the script can return {} before Codex kills it. Digests may keep viking:// source URIs and point the model at the OpenViking MCP read/search tools for details when the inline bullet is intentionally short. The outer <openviking-context ...> wrapper is deterministic, not compressor-generated; capture strips it to distinguish recalled context from the user's prompt. Set OPENVIKING_RECALL_COMPRESS=0 to fall back to deterministic short formatting.
The compressor profile is recreated on every SessionStart and cached under OPENVIKING_CODEX_STATE_DIR so cross-session config changes are picked up but each UserPromptSubmit does not probe models. Default fallback order:
OPENVIKING_RECALL_COMPRESS_MODEL + OPENVIKING_RECALL_COMPRESS_THINKINGgpt-5.3-codex-spark with thinking defaultgpt-5.5 with thinking lowcodex exec compression)Config knobs:
| Env var | Default | Meaning |
|---|---|---|
OPENVIKING_RECALL_COMPRESS | 1 | Set 0 / off to disable codex exec compression. |
OPENVIKING_RECALL_COMPRESS_MODEL | unset | Custom first-choice compressor model. Set off to disable compression. |
OPENVIKING_RECALL_COMPRESS_THINKING | unset | Custom model_reasoning_effort; default omits the Codex config override. Alias: OPENVIKING_RECALL_COMPRESS_REASONING_EFFORT. |
OPENVIKING_RECALL_COMPRESS_DETECT_ON_STARTUP | 1 | Recreate/cache compressor profile in SessionStart. |
OPENVIKING_RECALL_COMPRESS_DETECT_TIMEOUT_MS | 15000 | Per-candidate startup probe timeout. |
OPENVIKING_RECALL_COMPRESS_DETECT_TTL_MS | 604800000 | Cache TTL used by UserPromptSubmit when reading the latest profile. |
add_message, threshold commit)auto-capture.mjs derives one long-lived OpenViking session id per Codex session_id as cx-<safe-session-id> and incrementally appends every new user/assistant turn via /api/v1/sessions/{id}/messages. The /messages endpoint auto-creates the session on first append. Per-codex-session state lives at ~/.openviking/codex-plugin-state/<safe-session-id>.json. Capture sanitizes obvious hook noise, metadata wrappers, and plugin-injected <openviking-context ...> blocks before append; tool calls/results are retained as compact [tool-call ...] / [tool-result ...] lines capped by OPENVIKING_CAPTURE_TOOL_MAX_CHARS (default 2000).
After a successful append, Stop reads the session meta and commits when pending_tokens >= OPENVIKING_COMMIT_TOKEN_THRESHOLD (default 20000). Threshold commits pass keep_recent_count=OPENVIKING_COMMIT_KEEP_RECENT_COUNT (default 10) so the newest turns remain live for continuity while older context is archived and extracted. PreCompact still commits everything before compaction.
pre-compact-capture.mjs:
capturedTurnCount)ovSessionId to null so the next Stop re-derives the same cx-<safe-session-id> and appends the post-compact half under that deterministic OV session id/exit are silentCodex fires no hook on process exit. /compact is the only fully-deterministic "context disappearing" signal. If you /exit without /compact, the OV session for that codex session_id stays open. Two fallbacks recover the orphan:
SessionStart commits any state file older than 30 min/new or /clear shortly afterCodex's hook output schema differs from Claude Code's. Notably:
| Hook | Input field of interest | Output channel for context injection |
|---|---|---|
SessionStart | source (startup/resume/clear), session_id | hookSpecificOutput.additionalContext |
UserPromptSubmit | prompt, session_id | hookSpecificOutput.additionalContext |
Stop | last_assistant_message, transcript_path, session_id | systemMessage (only) |
PreCompact | trigger (manual/auto), transcript_path, session_id | systemMessage (only) |
Unlike Claude Code, Codex does not support decision: "approve"; only decision: "block". A no-op is {} (which is what these scripts emit when there's nothing to add).
codex-memory-plugin/
├── .codex-plugin/
│ └── plugin.json # Plugin manifest (hooks + mcp wiring)
├── hooks/
│ └── hooks.json # SessionStart + UserPromptSubmit + Stop + PreCompact
│ (uses Codex's native ${PLUGIN_ROOT} token; no
│ rendering needed on modern Codex)
├── scripts/
│ ├── config.mjs # Shared config loader (ovcli.conf + env)
│ ├── capture-utils.mjs # Transcript text extraction, filtering, tool compression
│ ├── debug-log.mjs # Structured JSONL logger
│ ├── recall-compressor-profile.mjs # Compressor profile detection/cache
│ ├── session-state.mjs # Per-codex-session OV session state
│ ├── auto-recall.mjs # UserPromptSubmit hook (REST /search/search)
│ ├── auto-capture.mjs # Stop hook (append + threshold commit)
│ ├── session-start-commit.mjs # SessionStart hook (active-window + idle TTL)
│ └── pre-compact-capture.mjs # PreCompact hook
├── servers/
│ ├── mcp-proxy.mjs # stdio -> OpenViking /mcp bridge
│ └── mcp-proxy.test.mjs # proxy contract tests
├── setup-helper/
│ └── install.sh # One-line installer
├── .mcp.json # stdio MCP wiring
├── DESIGN.md
├── VERIFICATION.md
└── README.md
No src/ or package.json: there is no build step. Hook scripts and the MCP proxy are zero-dep .mjs files running on Codex's bundled Node 22 or a compatible system Node.
The Codex marketplace catalog that exposes this plugin for codex plugin marketplace add lives at the repo root in .agents/plugins/marketplace.json (Codex resolves a marketplace manifest from the source root, not from this subdirectory). The catalog points at ./examples/codex-memory-plugin using a relative source, so the installed plugin follows the same marketplace snapshot/ref that the user added.
| Aspect | Claude Code Plugin | Codex Plugin |
|---|---|---|
| Plugin root env var | CLAUDE_PLUGIN_ROOT (expanded by CC) | ${PLUGIN_ROOT} (injected into hook env + substituted inline by modern Codex; installer also renders it to absolute paths for older Codex) |
UserPromptSubmit injection | decision: "approve" + hookSpecificOutput.additionalContext | hookSpecificOutput.additionalContext only — approve is not a Codex output |
Stop decision | decision: "approve" no-op | {} no-op — only block is a valid Codex decision |
| Compaction hook | n/a (Claude Code does not expose one) | PreCompact — full-transcript commit before context loss |
| Config section | claude_code | codex |
| Default config file | ~/.openviking/ov.conf | ~/.openviking/ovcli.conf, falls back to ov.conf |
| MCP server | Local stdio proxy to OpenViking /mcp | Local stdio proxy to OpenViking /mcp |
Apache-2.0 — same as OpenViking.