Integration Capability Reference
Reading guide
| What you want to know | Where to look |
|---|---|
| Which tools an agent can call autonomously per harness | §1.1 Active tool surface + §2.1 (MCP surface) + the profile cards |
| How memory archiving behaves under different shutdown methods | §3.3.3 Shutdown path × harness end-state matrix |
Whether auto-recall includes session_id, and its impact | §3.2.2 / §3.2.3 |
| How to enable recall digests, and the workload distribution between server and client | §3.2.5 |
Type boundaries for forget and delete operations | §3.5 |
| Which environment variables apply to specific harnesses | §3.1.4 Configuration layering + "Config" on each profile card |
| Whether the server provides an automatic commit fallback | §2.3 |
The complete ov CLI command set | §5 |
| How to integrate a custom agent with OpenViking | §6 |
| Installation, configuration, and troubleshooting for specific integrations | The integration's own page (linked in the first line of each profile card in §4) |
1. Capability overview
1.1 Active tool surface (agentic calls)
MCP-based harnesses (claude-code, codex/trae-cli, cursor, trae/trae-cn, zcode, opencode) share an identical active tool surface comprising 15 tools. The server centrally defines these tools. The plugin reads
~/.openviking/ovcli.confvia its proxy and establishes a connection to the server-defined MCP tools.trae-climeans TraeCode CLI 2.0 (2.0 only). It is installed via acodexplugin alias and maintains format compatibility withcodex. Therefore, it is consolidated into thecodexrow in the matrices below.
| harness | tool surface | tools (enabled by default) | search memory | search resource | search skill | write memory | write resource | write skill | delete type boundary |
|---|---|---|---|---|---|---|---|---|---|
| claude-code | MCP passthrough | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| codex / trae-cli | MCP passthrough | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| cursor | MCP passthrough | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| trae / trae-cn | MCP passthrough | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| zcode | MCP passthrough | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| opencode | MCP passthrough (host adds an openviking_ prefix) | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| dsh | MCP passthrough (@deepseek-ai/dsh-mcp-client → the shared stdio proxy; host adds an mcp__openviking__ prefix) | 15 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌¹ | no type distinction² |
| pi | native registration (7 × viking_*) | 7 (registration runs preflight checks⁴) | ✅ | ✅ | ✅ | ✅ viking_remember | ✅ viking_add_resource (URL only) | ❌ | no type distinction; delete by query needs score>0.8³ |
| openclaw | native registration (15 × memory_*/ov_* and friends) | 15 (14 on by default⁵) | ✅ memory_recall | ✅ ov_search (both scopes by default) | ✅ ov_search | ✅ memory_store | off by default⁵ | ✅ add_skill | memory-only allowlist + auto-delete only for a single candidate with score≥0.85 |
| hermes | native registration (6 × viking_*) | 6 (all on once the provider is active) | ✅ | ✅ | ✅ | ✅ viking_remember (writes the file directly, no extraction) | ✅ multi-protocol ingest (HTTP/Git/SSH/local file/directory zip) | ❌ | memory-only + .md leaf check |
| ov CLI | CLI commands | ~40 command groups | ✅ ov find | ✅ ov find | ✅ ov find | ✅ ov add-memory | ✅ ov add-resource | ✅ ov add-skill | ov rm executes directly (TUI deletion asks for confirmation and blocks root/scope deletes) |
¹ MCP write can target viking://resources, viking://user, or viking://agent. Adding skills via MCP is not yet supported; use the openclaw add_skill tool, ov add-skill CLI, or the REST API instead. ² MCP forget does not differentiate between memory, resource, and skill types. However, the storage layer protects namespace roots: deletion requests for bare viking://, viking://user, and viking://agent are rejected. See §3.5. ³ For viking_forget on pi: the recursive flag is strictly set to false (directories are never deleted), and deleting by query requires a match score > 0.8. ⁴ The pi harness registers its tools only if the session bypasses bypassPatterns, client.health() passes, and ensureSession succeeds (index.ts:66-113). If the health check fails, no tools are registered for that session. ⁵ The add_resource tool in openclaw requires a double opt-in before activation.
Skill addition/deletion boundaries: Skills can be added via the openclaw add_skill tool (enabled by default), the ov add-skill CLI command, or the REST API. Deletion operates across four tiers, detailed in §3.5.
1.2 Automatic hook surface (driven by the harness)
| harness | how it plugs in | auto-recall | recall carries session_id | digest (client)* | profile injection | takes over host compaction | offline compensation (pending queue) | statusline |
|---|---|---|---|---|---|---|---|---|
| claude-code | 9 hooks + MCP proxy + slash + statusline + skill | ✅ | ✅ | ✅ local claude -p / server-side rewrite (auto by default) | ✅ (10000) | ❌ (PreCompact only commits) | ✅ | ✅ |
| codex / trae-cli | 4 hooks + MCP proxy + skill | ✅ | ✅ | ✅ local codex exec (on by default) | ✅ (10000) | ❌ | ❌ no on-disk queue (the cursor stays put and the next turn resends) | ❌ |
| cursor | 7 hooks + MCP proxy + rule + skill | ✅ | ✅ | ❌ | ✅ (6000) | ❌ | ✅ | ❌ |
| trae / trae-cn | 4 hooks + MCP proxy | ✅ | ✅ | ❌ | ✅ (6000) | ❌ | ✅ | ❌ |
| zcode | 4 hooks + MCP proxy | ✅ | ✅ | ❌ | ✅ (6000) | ❌ | ✅ | ❌ |
| opencode | 7 plugin hooks + MCP proxy | ✅ | ✅ | ❌ | ✅ (10000) + repo list into the system prompt | ❌ (commits once before and once after compacting) | ✅ | ❌ (toast instead) |
| dsh | native Cordis plugin (same process) + MCP proxy + skill | ✅ | ✅ | ❌ | ✅ (10000, once per session) | ❌ | ✅ | ❌ |
| pi | native extension (8 events) | ✅ | ✅ | ❌ | ✅ (10000, rebuilt into systemPrompt every turn) | ✅ takeover (on by default) | ✅ | ✅ |
| openclaw | context-engine plugin (ownsCompaction:true) | ✅ | ❌ (goes through /find, which has no session_id field) | ❌ | ❌ | ✅ full ContextEngine takeover | ❌ failed turns are not replayed | ❌ |
| hermes | native MemoryProvider plugin | ✅ | partial (preferred search/search path only; the degraded /find path drops it) | ❌ | ❌ (static tool-guidance block) | ❌ | ✅ in-process queue (never written to disk) | ❌ |
| ov CLI | one-shot commands | ❌ (ov find/search are explicit commands) | — (ov search --session-id is an explicit argument) | ❌ | ❌ | ❌ | ❌ | ❌ |
* This column indicates whether the client performs its own local compression of recall results. On the server side, the context retrieval API provides digest capabilities equally to all callers via the rewrite parameter (see §3.2.5).
Current status of session_id: With the exception of openclaw (whose /find endpoint lacks this field) and the degraded path in hermes, auto-recall on all harnesses explicitly carries a session_id. This behavior is enforced by a cross-plugin regression test (examples/memory-plugin-shared/recall-session-wiring.test.mjs:16-39).
1.3 Grouping by form
- Full suite (hook automation + MCP tool surface + surrounding UX): claude-code and codex (
trae-cliis installed via an alias and is included here). - Thin hook (sharing
agent-hook-runtime, meaning core behaviors are essentially identical, with differences limited to host events and thresholds): cursor, trae/trae-cn, and zcode. - Plugin event: opencode (offers the richest host event surface;
disposehandles shutdown). - Native in-process: dsh (Cordis), pi (extension + compaction takeover), openclaw (full ContextEngine takeover), and hermes (MemoryProvider).
- Tool: ov CLI (all operations are explicit calls; no automatic background actions).
- Non-coding: Open WebUI (tool server), LangChain (SDK library), the Agent Plugins portable package (an MCP + skill spec bundle), generic direct MCP, log ingestion, and Helper (desktop).
2. Shared capability core
The individual harness sections (profile cards) focus exclusively on differences and specific implementations. All shared capabilities and universal behaviors are documented once in this section.
2.1 Server-side MCP tool surface
These tools are defined on the server side, and future updates will be centrally published there. Harnesses only need to proxy the MCP to obtain the latest ~/.openviking/ovcli.conf.
| # | Tool | What it does | Key parameters (definition line) |
|---|---|---|---|
| 1 | find | Fast semantic search requiring no session context | query, target_uri="", limit=10, min_score=0.35, level, context_type (:259) |
| 2 | search | Deep search, featuring optional session_id integration and intent analysis | The session is only loaded if the server has retrieval.enable_intent enabled (defaults to true) (:285, :302-304) |
| 3 | read | Read the full text of one or more viking:// files | Uses a concurrency semaphore of 10; a single failure yields (nothing found at <uri>) rather than raising an exception (:389) |
| 4 | list | List a directory (function name ls, explicitly registered as list) | recursive=False (:423) |
| 5 | tree | Recursive directory tree | level_limit=3, node_limit=1000, include_abstract=False (:449) |
| 6 | remember | Write long-term memory | Internally creates a one-shot session (mcp-store-<uuid12>) and immediately calls commit_async (:504-523). This is the only commit entry point on the MCP surface, as there is no explicit commit tool. |
| 7 | write | Write a viking:// file | mode=replace|append|create; replace falls back to create if not found. New files must use an extension from the allowlist: .md .txt .json .yaml .yml .toml .py .js .ts. Writable domains are limited to resources/user/agent; directories like skills/, peers/, privacy/, and sessions/ under the user root are read-only. Existing .abstract.md / .overview.md sidecars may be body-updated, but public APIs cannot create them (:529; content_write.py:60-81) |
| 8 | edit | Exact string replacement | Supplying an empty old_string, finding zero matches, or finding multiple matches without replace_all will raise an error and leave the file content unchanged (:569) |
| 9 | add_resource | Resource ingestion (remote URL / signed upload of a local file / Connector) | watch_interval is defined in minutes (0 disables watching). The local-path branch generates a signed upload URL (default TTL of 600s), and ingestion triggers automatically post-upload without requiring a subsequent API call (:723-947) |
| 10 | list_watches | List watch subscriptions; not yet supported on the commercial edition | Returns an error string if the scheduler is not running (:958) |
| 11 | cancel_watch | Cancel by to_uri; not yet supported on the commercial edition | Deliberately does not expose pause/resume/trigger/update (:990) |
| 12 | grep | Regex content search | Multiple patterns run concurrently (semaphore of 10), node_limit=10 (:1032) |
| 13 | glob | Filename glob | node_limit=100 (:1084) |
| 14 | forget | Permanently deletes a URI (unrecoverable) | recursive=False by default; type boundaries in §3.5 (:1110-1117) |
| 15 | health | Health check | No parameters (:1123) |
Supporting mechanisms:
- Portable schema rewriting (
:1149-1218): At module import time, every tool'sanyOf/$refis flattened into a plain type to ensure compatibility with clients that only support the OpenAPI 3.0 subset (such as Gemini). Runtime validation still relies on the original Python signature (for instance,readadvertises an array schema but continues to accept a bare string). All MCP clients receive the exact same server-produced schema; there are no client-specific variants. - Identity middleware (
:149-233): Sharesresolve_identitywith REST endpoints. It reads headers in the following order:x-api-key,authorization,x-openviking-account,x-openviking-user, andx-openviking-actor-peer. If absent, the account and user fields fall back to"default".
2.2 The memory-plugin-shared layer
The examples/memory-plugin-shared/lib/ directory contains 18 .mjs modules and serves as the single source of truth for all JS-based harnesses. These are consumed in two ways:
- Vendoring (copying): The
sync.mjsscript distributes these modules to 7 targets, prefixing every file with// GENERATED FROM ... DO NOT EDIT.. Because of this added line, a vendored copy's line number will be exactly one line greater than the library source (keep this in mind when cross-referencing line numbers). The distribution breakdown is as follows: 17 modules each for claude-code, codex, and opencode (the "HARNESS 13" +mcp-proxy-core+mcp-proxy-config+async-writer+batch-send); 15 for dsh (the "HARNESS 13" + the twomcp-proxy-*modules it needs for the stdio proxy); 13 for pi; all 19 for zcode; and 5 for agent-plugins. At the current HEAD, every target has zero drift from the original library source. - Direct import via relative path (no copying): Integrations like cursor, trae, and trae-cn directly
import "../../memory-plugin-shared/lib/...". To ensure this works, the installer copies the package alongside the shared library into~/.openviking/agent-integrations/{<client>,memory-plugin-shared}/, preserving the relative folder layout. At runtime, these harnesses share this directory, meaning reinstalling any one of them will overwrite the shared directory wholesale.
Core modules at a glance (detailed further in the per-dimension sections):
| Module | Responsibility | Consumers |
|---|---|---|
recall-core.mjs | Handles recall request construction, three-tier degradation, and local fallback ranking/injection | All JS-based harnesses |
agent-hook-runtime.mjs | All-in-one "thin hook" runtime handling 19 configuration environment variables, session ID derivation, cross-process locking, fetching, and commits | cursor / trae / trae-cn / zcode |
mcp-proxy-core.mjs | stdio ↔ streamable-HTTP MCP proxy core | All MCP-based integrations + agent-plugins |
pending-queue.mjs | On-disk offline queueing and replay at session start | cc / cursor / trae×2 / zcode / opencode / dsh / pi |
batch-send.mjs | Executes writes in batches of 100, handles per-message degradation on 404/405 errors, and queues the leading contiguous prefix | cc / codex / opencode + the agent-hook family |
profile-inject.mjs | Injects the profile and available-memory index at session start | 9 harnesses (all but openclaw / hermes) |
recall-compress-core.mjs | Manages the recall compression prompt, URI edit-distance repair, and caching | claude-code |
capture-utils.mjs | Handles message normalization, injection back-flow guarding, and capture filtering | codex / opencode / dsh / pi |
credentials.mjs | Credential resolution chain (see §3.1.3) | All JS-based |
session-model.mjs | Session ID prefix derivation and bypass globbing | All JS-based |
async-writer.mjs | Detaches the write path (drains stdin → spawn → approve → write → unref). Falls back to synchronous writing if spawn fails | cc / codex / zcode |
workspace-peer.mjs | Converts the cwd into an actor peer (replacing every non-alphanumeric character with -) | All JS-based |
uri-guard.mjs / agent-uri-guard.mjs | Intercepts cases where viking:// is incorrectly treated as a local path | Used in each harness's PreToolUse or tool.execute.before-style hooks |
plugin-config.mjs | Reads the plugin section of ovcli.conf | claude-code / codex |
setup-wizard.mjs | Interactively writes to ovcli.conf | cc, codex, opencode, and pi expose an entry point for this |
retryable.mjs | Handles retryability checks: allows status codes 0, 408, 429, and ≥500, as well as 409 if error.details.retryable===true. Standard 4xx errors (including 401/403) are not retried | All JS-based |
2.3 Server-side session and commit semantics
- Implicit session creation: Plugins generally do not call
POST /sessionsexplicitly (withdshbeing the only exception, as it sends a creation request carrying onlysession_id). Instead, the server creates the session usingauto_create=Trueupon receiving the firstPOST /sessions/{id}/messages(/batch)request. On the recall side, calling_load_session(auto_create=True)undermode="context"also creates a session, meaning the very first recall action will initialize the session on the server. - Two-phase commit:
POST /sessions/{id}/commitreturns only after Phase 1 (archiving) has completed synchronously. Phase 2 (memory extraction) runs as a background task, returning atask_id. The server default forkeep_recent_countis 0 (meaning it archives everything, leaving no live tail). - Server-side auto-commit is disabled by default, and the session creation paths used by plugins never enable it:
memory.session_auto_commit.default_enabled = falseandidle_enabled = false(memory_config.py:15-16). Auto-commit is disabled for sessions lacking a storage policy (session_service.py:637-638). Furthermore, the idle scanner isn't even instantiated whenidle_enabled=false(core.py:440-448), effectively acting as a double gate.- The
auto_createpath triggered byPOST /messagesaccepts no policy arguments; onlyPOST /sessions(creation) andPATCH /sessions/{id}/configcan set theauto_commit_policy. - Currently, no plugin sends an
auto_commit_policy. Among first-party clients, only the ov CLI does this (ov session new --auto-commit-policy-json/--no-auto-commit,ov session config set). - When a policy is explicitly enabled, the server defaults are:
pending_token_threshold=150000(strictly greater than),message_count_threshold=100,idle_timeout_seconds=86400,keep_recent_count=0, andmin_commit_interval_seconds=0. Note that this set of server defaults operates independently of the plugin clients' configuration (which is typically 20000/10).
- Consequence: As it stands, every auto-commit relies on threshold logic implemented individually by each client (§3.3), with no server-side fallback. If a process dies abnormally, any lingering pending messages are only archived and extracted when a subsequent commit is manually triggered in that same session.
- Externalized tool output: The server sets
tool_output_externalization.enabled=Truewiththreshold_chars=20000(server/config.py:257-258). Clients commonly raisecaptureToolMaxCharsto 1000000 purely as a fallback mechanism, since the actual truncation and externalization occur on the server. Externalized results are then referenced viatool_output_ref(for context, openclaw provides three dedicated tools specifically for reading these references). - Server-side recall timeout fuses: Configured via
retrieval.recall_intent_timeout_s=5.0(for query expansion),recall_rewrite_timeout_s=30.0(for digests, §3.2.5), andenable_intent=true. Client timeout budgets are derived directly from these two timeout values (§3.2.4).
3. Dimensions in detail
3.1 Integration forms, installation, and configuration
3.1.1 Decision matrix
| Harness | Integration Form | Install Channel | Session ID Prefix/Format | Config Source | Standalone Setup Wizard |
|---|---|---|---|---|---|
| claude-code | CC plugin (marketplace): 9 hooks + MCP proxy + slash + statusline + skill | One-line install.sh --harness claude (supports both the modern plugin path and the legacy claude mcp add compatibility path) / manual marketplace / TOS mirror | cc-<CC session_id verbatim>; subagents use …__subagent-<agent_id> | env + ovcli.conf plugin.claude_code + ov.conf claude_code | ✅ scripts/setup.mjs |
| codex | Codex plugin (marketplace): 4 hooks + MCP proxy + skill | One-line --harness codex / codex plugin marketplace add (the TOS channel uses dumb-HTTP git to ensure remote updates continue working) | cx-<safeId> (deterministically derived, without reading state) | env + ovcli.conf plugin.codex + ov.conf codex | ✅ |
| trae-cli | Installed as a codex plugin alias (TraeCode CLI 2.0, 2.0 only; a Codex-family CLI: uses the traecli binary and ~/.trae/traecli.toml config; its capability surface is identical to codex) | One-line --harness trae-cli (reuses the codex install flow; marketplace commands run against the targeted binary, e.g., traecli plugin marketplace add) | Same derivation rule as codex | Same as codex (env + ovcli.conf plugin.codex + ov.conf) | ✅ (same as codex) |
| cursor | Config-driven (writes ~/.cursor/hooks.json+mcp.json) + rule + skill | One-line --harness cursor | cu-<conversation_id> | env only | ❌ (shares the installer TUI) |
| trae / trae-cn | Config-driven (~/.trae{,-cn}/hooks.json + platform-specific mcp.json) | One-line --harness trae,trae-cn | tr- / trcn- | env only | ❌ |
| zcode | Config-driven (merged into ~/.zcode/cli/config.json, forcing hooks.enabled=true) | One-line --harness zcode | zc-<sess_…> | env only | ❌ |
| opencode | npm plugin @openviking/opencode-plugin (its config hook dynamically injects the MCP entry) | One-line --harness opencode (uses npm registration with a proxy snapshot fallback) / manual npm / from source | oc-<id>; subagents use oc-<parent>__subagent-<child> | openviking-config.json (searched at 4 levels) + env | ✅ |
| dsh | In-process Cordis plugin (cordis.patch.yml plugin group) | Unified installer (asks for the profile, default web), or dsh plugin --profile web add @openviking/dsh-memory-plugin | dsh-<session.id as-is>; each subagent is assigned its own session | cordis patch config + 4 env vars (credentials: patch > env; behavior toggles: env > patch) | ❌ |
| pi | Native pi extension (loaded from a directory, with TypeScript transpiled on the fly via jiti) | One-line --harness pi (including pi install registration) | pi-<piSessionId> | config.json (credential fields are resolved via the shared credential chain) + env | ✅ |
| openclaw | context-engine plugin (ownsCompaction:true) + 15 tools + 5 slash + 4 hooks + HTTP routes | ClawHub: run openclaw plugins install clawhub:@openviking/openclaw-plugin alongside openclaw openviking setup / npm installer / TOS offline bundle | A UUID is lowercased as-is, otherwise sha256(sessionKey); memory_store temporary sessions use memory-store-<ts>-<rand> | plugins.entries.openviking.config in openclaw.json (strictly validated: unknown keys or invalid values force the plugin into setup-only mode) + a few env vars | ✅ openclaw openviking setup (interactive/non-interactive + key role probing + version compatibility check) |
| hermes | Hermes bundled MemoryProvider (ships with Hermes; no plugin installation required) | Run hermes memory setup openviking (interactive curses wizard), or manually run config set memory.provider openviking + .env | Hermes generates %Y%m%d_%H%M%S_<hex6>; the plugin uses it verbatim | .env (OPENVIKING_*) or linked ovcli.conf (use_ovcli_config mode clears the 5 corresponding variables from .env) + config.yaml | ✅ (multi-level menu) |
| ov CLI | Native Rust binary | npm @openviking/cli / uv tool install openviking / cargo / GitHub Releases | Does not manage its own session (ov chat defaults to the machine-uid) | ovcli.conf (multiple profiles) + a few env vars | ✅ ov config (TUI wizard) |
3.1.2 Unified installer
The unified install script (examples/memory-plugin-shared/install.sh, 3424 lines) supports ten harness IDs: claude, codex, cursor, trae, trae-cn, trae-cli, zcode, opencode, pi, dsh. (Note that openclaw uses its own distribution channel, while trae-cli reuses the codex install flow, as detailed in §3.1.1). Key highlights:
- Interactive prompts: Two distributions (
--dist github|tos) and three sources (--source remote|archive|dev) are available. When executed viabash <(curl …), it reads input directly from/dev/ttyto ensure prompts remain interactive. - Usage: In the official documentation, the canonical one-line command omits the
--harnessflag, which launches a TUI multi-select menu. However, the setup-helper forwarding scripts bundled with each plugin append the--harnessflag automatically. - Idempotent merging: Hooks and MCP entries are identified by the
OPENVIKING_INTEGRATION_IDmarker. This ensures stale entries are pruned and new ones are appended without affecting third-party configurations. Writes are atomic: the script creates a.bakbackup, writes to a temporary file, and then renames it over the target with0600permissions. - Credential wizard: This writes to
~/.openviking/ovcli.conf. Users can select from three targets (localhttp://127.0.0.1:1933/ Volcengine Cloudhttps://api.vikingdb.cn-beijing.volces.com/openviking/ custom). If a configuration already exists, the script displays the current values first and asks whether to keep or reconfigure them, masking the API key for security. - Uninstallation: The
--uninstallflag coverscursor,trae,trae-cn, andzcode, and also cleans up any legacy trae-cli hook config. The Codex-format and host-managed plugins (claude,codex,trae-cli,opencode, andpi) are removed via their respective host's plugin management systems. - Post-installation self-check: This includes grepping the configuration, running
node --check, and executing a smoke test withOPENVIKING_MEMORY_ENABLED=0. - Node.js requirement: The installer enforces a minimum version of Node 18+.
3.1.3 Credential systems
Four parallel credential-resolution systems coexist within the codebase, each utilizing its own environment variable names and authentication headers. When troubleshooting, your first step should be identifying which system is currently in use:
| Family | Consumers | URL Env | Key Env | Identity Env | Auth Header |
|---|---|---|---|---|---|
A. Shared JS core (credentials.mjs) | claude-code / codex (including trae-cli) / cursor / trae×2 / zcode / opencode / pi / dsh / agent-plugins | OPENVIKING_URL → OPENVIKING_BASE_URL | OPENVIKING_BEARER_TOKEN → OPENVIKING_API_KEY | OPENVIKING_ACCOUNT / OPENVIKING_USER / OPENVIKING_PEER_ID | Authorization: Bearer. Note: codex's four hook scripts also send an X-API-Key compatibility header. |
B. openclaw (its own config.ts) | openclaw | OPENVIKING_BASE_URL → OPENVIKING_URL | OPENVIKING_API_KEY (supports SecretRef env/file) | OPENVIKING_ACCOUNT_ID / OPENVIKING_USER_ID (note the _ID suffix here) | X-API-Key. (When pointing to OV Cloud, note that it actually authenticates using Bearer). |
| C. hermes (Python) | hermes | OPENVIKING_ENDPOINT | OPENVIKING_API_KEY | OPENVIKING_ACCOUNT / OPENVIKING_USER / OPENVIKING_AGENT (= actor peer) | Sends both X-API-Key and Bearer. When a key is present, it omits tenant headers by default (if the server rejects the call with a trusted error, it appends them and retries once). |
| D. ov CLI (Rust) | ov | Primarily the conf file | conf | --account/--user/--actor-peer-id | X-API-Key. Toggles between LDAP Basic and OIDC Bearer based on auth_mode; an api_key containing two or more . characters automatically receives a Bearer header as well (JWT fallback). |
Family A resolves credentials in the following order (refer to individual profile cards for other families):
- The resolution mode is controlled by
OPENVIKING_CREDENTIAL_SOURCE(aliased as_CREDENTIALS_SOURCE), accepting values ofenv|cli|auto(defaults toauto). autoprioritizes environment variables: If any environment credential field is present, the entire resolution process relies on the environment. Only when all environment fields are empty, and anovcli.conffile exists with credential fields, does the system fall back to the configuration file (meaning the key, account, user, and peer are then exclusively sourced from the file).- baseUrl: Resolves via environment variables →
ovcliurl→ov.confserver.url→http://{server.host|127.0.0.1}:{server.port|1933}(where0.0.0.0normalizes to127.0.0.1), with a final fallback tohttp://127.0.0.1:1933. - apiKey: Resolves via
BEARER_TOKEN→API_KEY→ovcliapi_key→ov.confcodex.apiKey→server.root_api_key. - mcpUrl: Resolves via
OPENVIKING_MCP_URL(when outside CLI mode) →${baseUrl}/mcp. - Common request headers:
Authorization: Bearer+X-OpenViking-Account/User/Actor-Peer+User-Agent: openviking-memory-<harness>/<version>.
Workspace peer (applies to all of Family A + agent-plugins): If no explicit peerId is provided and OPENVIKING_WORKSPACE_PEER≠0, the peer is derived from the current working directory (cwd). Every non-alphanumeric character in the path is replaced with a hyphen (-) (e.g., /Users/x/Dev/OpenViking becomes -Users-x-Dev-OpenViking), and this value is sent as the X-OpenViking-Actor-Peer. The server validates this header and returns a 400 error if it contains / or \. For openclaw, the peer is derived from peer_role/peer_prefix (note that if peer_role=person, sender information must be available, otherwise tool calls will fail). The hermes peer defaults to OPENVIKING_AGENT (defaulting to hermes).
3.1.4 Configuration layers
| Config Layer | Applies To | Notes |
|---|---|---|
env OPENVIKING_* | Per family, see above; behavior knobs are listed on each profile card | The only layer that spans every JS-based integration. |
ovcli.conf plugin section (plugin.claude_code / plugin.codex / shared scalars) | claude-code / codex | plugin.<x> entries named after any other harness are ignored. Note: ov config add/edit rewrites the entire file from the Rust Config struct, thereby dropping any plugin sections it does not recognize; however, ov config switch simply copies bytes and remains unaffected. |
ov.conf harness sections (claude_code.* / codex.*) | claude-code / codex (legacy fallback) | |
| The harness's own config file | opencode openviking-config.json, pi config.json, dsh cordis patch, openclaw openclaw.json, hermes config.yaml+.env |
Quick scope reference (these settings only take effect on the specified harnesses):
OPENVIKING_COMMIT_TURN_THRESHOLD: cursor only (trae,trae-cn, andzcodecommit on every Stop and ignore this threshold).OPENVIKING_WRITE_PATH_ASYNC: claude-code / codex / zcode.- Recall digest settings (
OPENVIKING_RECALL_COMPRESS,OPENVIKING_RECALL_REWRITE, and their companions): claude-code / codex (note that the server-siderewriteparameter is available to all callers, see §3.2.5). OPENVIKING_RECALL_DEDUP_TURNS,OPENVIKING_RECALL_QUERY_EXPANSION: claude-code / codex.- ovcli.conf
pluginsection: claude-code / codex.
3.2 Automatic recall and injection
3.2.1 Mechanism foundation: one shared pipeline, two server-side paths
Recall for the JS-family harnesses is managed through a three-level degradation chain within recall-core.mjs:
- Context face: Calls
POST /api/v1/search/searchwithmode:"context"andpurpose:"coding". Its core design principle is "declare intent only, leave the mechanism to the server." Parameters likequotas,max_tokens,query_expansion, andrewrite_max_bulletsare transmitted only if explicitly configured by the user (indicated by a sentinel field); otherwise, server defaults are applied. - Legacy
/recall: If the context face request returns a 400 or 422 error and the response body contains marker fields likeextra,mode, orunexpected, the server is identified as an older version. A 6-hour negative cache is then written locally to~/.openviking/state/context-face.json. Because this is a machine-wide shared file, once one harness flags it, every JS-family harness on that machine will bypass the context face stage. The call then degrades to the deprecated/api/v1/search/recallendpoint. Ifpeer_scopeis rejected, it retries once without that parameter. - Raw find fallback: Concurrently requests
viking://~/memoriesandviking://~/skillsby callingPOST /search/findtwice. (Note: resources are deliberately excluded from automatic recall; resource documents are fetched by the model invokingsearchitself). The client then re-ranks the results locally (using weight rules: leaf +0.12, time intent +0.10, preference intent +0.08, and lexical overlap ≤0.2), deduplicates them, and fills up to the client token budget. TherecallTokenBudget,recallMaxContentChars, andrecallPreferAbstractconfigurations take effect only at this level. Under the context face, the injection budget is dictated by the server'smax_tokens(which defaults to 1600).
On the server side, session_id handling diverges into two distinct execution paths:
- Path A:
mode="context"(utilized by the context face and the/recallpreset). This path manages query expansion and the cross-turn deduplication ledger. Query expansion requires passing three gates:retrieval.enable_intentmust be enabled (default is true) → the session must be materialized (meaning themessages.jsonlfile exists) → and eitherlatest_archive_overvieworcurrent_messagesmust be non-empty. Following expansion, the original query always ranks first, followed by a maximum of 3 appended planned queries. The ledger (.recall_log.json) applies a cooldown to URIs whose bodies have already been sent, lasting fordedup_turns. If a turn "sent only the URI and not the body," that record bypasses the cooldown. Similarly, nothing is recorded if the digest determines the memory isno_relevant. - Path B:
mode="list"(the default behavior when the mode is omitted). In this path,IntentAnalyzercompletely replacestyped_queries(meaning the original query is not guaranteed to survive), bypassing both the ledger and the original-query baseline. Callers that land in this path include codex's second-level degradationsearchScope, hermes'sviking_search(mode="deep"), and the preferred prefetch path. Although they carry asession_id, they do not benefit from the context face's query expansion or deduplication features.
Three key notes on dedup_turns: ① Server default: The server default for the context face is 0. The familiar default of "5" actually originates from the recall-core.mjs client fallback and the /recall preset (the latter applying only when a session_id is present). Therefore, a third-party application hitting the API directly without the shared library must explicitly send dedup_turns to enable cross-turn deduplication, even if a session_id is provided. ② Turn counting: A "turn" counts individual messages, not full conversation rounds (since _resolve_turn relies on total_message_count). For a harness that pushes user and assistant messages simultaneously, the default of 5 roughly equals 1-2 actual conversation rounds. ③ Edge cases with auto-settings: If autoCapture=0 and autoRecall=1, the message count remains at 0, meaning the ledger clock never advances. As a result, URIs whose bodies were previously sent remain cooled down for the entire session. To disable deduplication entirely (e.g., for claude-code or codex), use OPENVIKING_RECALL_DEDUP_TURNS=0.
3.2.2 Decision matrix
| harness | trigger | query construction | session_id | server path | injection format / location | digest (client)* |
|---|---|---|---|---|---|---|
| claude-code | every UserPromptSubmit | prompt verbatim, trimmed | ✅ cc- | A (context face) | <openviking-context> → hookSpecificOutput.additionalContext | ✅ local/server (default auto, §3.2.5) |
| codex / trae-cli | every UserPromptSubmit (hard 120s deadline for the whole hook) | prompt verbatim | ✅ cx- (derived deterministically, no state read) | A; second-level degradation searchScope lands in B | <openviking-context source="auto-recall" format="digest"> | ✅ local codex exec (§3.2.5) |
| cursor | beforeSubmitPrompt | prompt verbatim; deduped by event id and a 500ms window, reusing the cached block for the same promptHash | ✅ cu- | A | additional_context | ❌ |
| trae / trae-cn | UserPromptSubmit | prompt with prior injection blocks stripped (reads input.prompt only) | ✅ tr-/trcn- | A | additionalContext | ❌ |
| zcode | UserPromptSubmit | three kinds of injection block stripped (including <system-reminder>) | ✅ zc- | A | additionalContext (strict JSON) | ❌ |
| opencode | the user message in every chat.message | concatenates non-synthetic text parts; skips recall for the turn if the body already contains <openviking-context | ✅ oc- | A (timeoutMs=30000) | builds a synthetic part and unshifts it to the front of parts | ❌ |
| dsh | agent/pre-step waterfall (await next first, then append) | every message in the claimed batch (filtering out its own injected content) | ✅ dsh- | A | appended to the end of decision.messages via createUserMessage (source: plugin/openviking-memory) | ❌ |
| pi | queued during before_agent_start; retrieval runs inside the context event (this turn's prompt gets this turn's memories) | prompt verbatim | ✅ pi- (omitted before the session exists) | A | prepended to the last real user message (idempotency checked via <openviking-context) | ❌ |
| openclaw | context-engine transformContext assemble (7 passthrough gates) | plain text of the last user message, cleaned and cut to 4000 characters | ❌ (/find has no such field) | /find | prepended into the last user message as <relevant-memories> + Source: openviking-auto-recall | ❌ |
| hermes | prefetch runs synchronously before every API call | raw user input, with two layers of skill scaffolding stripped; skipped under 5 characters | partial (only on the preferred search/search path, which lands in B; omitted when degrading to /find) | B / find | <memory-context> fenced block appended to the current user message (request body only, never written back to storage) | ❌ |
* As in §1.2, "digest" in this column refers to client-side local compression, while the server digest is available to every caller (§3.2.5). The ov CLI has no automatic recall and is not included in this table.
3.2.3 Profile / opening injection
- Implementation:
profile-inject.mjsreads the fullviking://user/<space>/memories/profile.md, alongside a recursive listing of thepreferences/andentities/directories (abs_limit=512). The budget estimation logic is CJK-aware: characters ≥U+3000 count as 1.5 tokens per character, while all others are calculated as characters divided by 4. The profile consumes half of the available budget. If the limit is exceeded, the middle section is elided, preserving "the first 8 lines + the tail." If a directory listing exceeds the limit, a... +N morenote is appended. - Who injects, when, and with what budget:
- claude-code: On
SessionStart(all sources, 10000 budget). - codex: On
SessionStart(startup/clear/resume, 10000 budget). - cursor / trae×2 / zcode: On
SessionStart(6000 budget, 2s debounce). - opencode: Once per session on the first
chat.message(10000 budget, deduplicated by an in-processSet, subagent sessions skipped). Note that opening injection is attempted only once per session and does not retry in-process after a failure. - dsh: Posts
profileDeliveredonce per session (10000 budget; not re-posted after compaction). - pi: Injected into the
systemPrompt, re-assembled for every prompt (10000 budget, always resident). - Note:
openclawandhermesdo not perform profile injection.
- claude-code: On
- Archive injection (pulls the previous archive summary back upon resume):
- claude-code:
source=resume/compact,token_budget=32000, ≤5pre_archive_abstracts. - codex: On resume, when the local
ovSessionIdhas already been cleared (32000 budget / truncated to 6000 characters). - opencode: Executed as part B of the opening injection (32000 budget).
- pi: Active in non-takeover mode (32000 budget).
- claude-code:
- Repo context injection: Unique to
opencode. It inserts the list of indexed repositories into the system prompt viaexperimental.chat.system.transform.
3.2.4 Timeout and budget chain
- Family A client derivation: With rewrite, the timeout is
max(timeoutMs, 45000); with expansion, it ismax(timeoutMs, 15000). The corresponding server-side fuses are 5s (expansion) and 30s (rewrite). By design, the client budget accommodates every server stage, ensuring the client never aborts early and loses the entire response. - Actual timeout values:
- claude-code (cc): 15s (against a 60s hook budget).
- codex: Recall enforces a hard 120s deadline for the entire hook, plus a 110s compression subprocess.
- cursor / trae×2 / zcode: 15s (against a 20s host hook budget).
- opencode / dsh / pi: 15s (
dshblocks the pre-step). - openclaw: Imposes a 5s hard timeout around the entire recall flow (including a 500ms health precheck). Since the default
recallPreferAbstract=falsemeans every leaf memory costs one extra read, this budget allows at most 1 find + 6 reads + 1 health check. - hermes: 4s total / 3s per request (configurable).
- Injection budget: The server's
max_tokensdefaults to 1600 (Family A harnesses do not send this by default, allowing the server to dictate the limit). Bothopenclawandhermesutilize a 4000-character budget, opting to "skip an entry that does not fit" rather than truncating it.
3.2.5 Recall digest
Server implementation (available to all callers): The context retrieval face (covering REST mode="context" and legacy /recall) accepts a rewrite parameter—which can be false, true, or "auto" (defaulting to false)—alongside rewrite_max_bullets (defaulting to 6, with a range of 1-20). When enabled, the server leverages the query_planner model to rewrite recall results into a digest with citations. (If rewrite=true but query_planner is unconfigured, it falls back to the main vlm; "auto" only takes effect if query_planner is explicitly configured). The digest features an OpenViking memory digest: header followed by bullet points (- ). Each bullet must be ≤500 characters and must cite a valid viking:// URI from the hit set (bullets with missing or out-of-range citations are dropped). If the model determines there are no relevant memories, it emits a sentinel value and clears the injection block, ensuring that turn is not recorded in the deduplication ledger. This model call is protected by a fuse (retrieval.recall_rewrite_timeout_s=30s). On timeout, it falls back to providing the un-rewritten, rendered block (rewrite.py:78-141, pipeline.py:122-130, search.py:195-196).
Client-side status:
- claude-code:
recallRewritesupports four states:off,client,server, andauto(defaulting to auto). It initially probes for a local compressor viaclaude --version(caching the result for 7 days). If available, compression runs in a local subprocess:claude -p --model sonnet --effort low --strict-mcp-config. This subprocess has a 30s timeout, skips compression for inputs under 1500 characters, uses per-digest caching, and force-degrades its environment to prevent recursion. Subprocess failures fall back to the uncompressed block. URIs are snapped back to valid URIs using edit distance, and any irreparably broken bullets are dropped. If no local compressor is found, it sendsrewrite:"auto", deferring to the server. Notably, this is the only harness actively wired to the server-side rewrite. - codex: The boolean
recallCompressdefaults to true and relies entirely on local compression (it does not utilize the server rewrite). The model profile is read from~/.codex/models_cache.json(candidates range fromgpt-5.3-codex-sparktogpt-5.6-luna, cached for 7 days). The execution command iscodex --sandbox read-only --ask-for-approval never exec --ephemeral --ignore-user-config --skip-git-repo-check --output-last-message <tmp> -, with a 110s timeout. If a runtime failure occurs, compression is disabled for the remainder of the session and re-probed at the nextSessionStart. Outputs are normalized and truncated to 4000 characters. When compression is turned off or fails, a deterministicfallbackDigesttakes over. - Other harnesses: None of the other harnesses send the
rewriteflag or compress locally; they simply inject the raw recall block returned by the server. Third-party applications directly calling the API can passrewritethemselves to leverage the server digest.
3.2.6 Injection backflow protection
To prevent injected content from being captured a second time, the injection process wraps the content in deterministic tags (like <openviking-context>), which the capture mechanism then mechanically strips. Specifically, capture-utils' sanitizeCapturedText function removes injection blocks, digest blocks, metadata fences, and timestamp prefixes.
Per-harness specifics:
- trae / zcode: Utilize their own cleaning functions (zcode's strips three distinct types of injection blocks).
- openclaw: Strips
<relevant-memories>twice—once when writing data back duringafterTurn, and again when constructing the query for the next turn. - hermes: Goes a step further by entirely dropping the
tool_callandresultof all three recall-type tools from the sync batch (while retaining write-type tools).
3.3 Session and commit lifecycle
3.3.1 Mechanism foundations
- Write path: The JS family routes all writes through
batch-send.mjs(endpointPOST /messages/batch, capped at 100 messages per batch to match the server'smax_length=100limit; on a 404/405 error, it gracefully degrades to sending one message at a time). Incremental cursors are implemented per integration (e.g.,ccandcodexcompute a cursor from the transcript turn index;cursorusessha256(index+role+content);zcoderelies on the rolloutturn_id;opencodeuses an event-stream Map;dshuses an event allowlist;piuses the branch entry watermark; andhermesslices by the current turn). - Commits are client-triggered (see §2.3): The server does not auto-commit by default. Any "threshold/trigger" condition mentioned in the tables below refers strictly to client-side logic.
- Differences in
keep_recent_count(determining how much of a "live tail" a commit leaves for the host): The server default is 0. Here is what each integration passes:cc/codexpass 10 on threshold commits;cursor,trae×2, andzcodesend an empty body{}, meaning 0 (every commit acts as a full archive);opencodeanddshpass 10;pipasses 10 outside takeover mode and 3 within it (locally, this means "keep 3 user turns," but the server interprets it as a raw message count, so fewer messages are actually retained);openclawpasses 10 on anafterTurnthreshold trigger and 0 oncompact/reset/memory_storeoperations;hermesalways passes 0. - Write-path detachment (
async-writer.mjs, enabled by default forcc/codex/zcodeonStop): The execution flow is drain stdin → spawn detached worker → approve → write payload → unref. (Note: If spawning fails, approval has not yet occurred, ensuring the synchronous fallback executes exactly once). The detached worker forms its own process group, immunizing it against terminal signals. This is the crucial mechanism that makesccreliable during shutdown and preventszcodefrom losing writes uponCtrl+C. Side effect: Once detachment is active,Stopno longer prints theappended N turn(s)notice (to restore this, setOPENVIKING_WRITE_PATH_ASYNC=0).
3.3.2 Regular commit triggers
| harness | turn-level threshold | explicit / boundary trigger | compaction trigger |
|---|---|---|---|
| claude-code | Stop: pending_tokens ≥ 20000 (reads the server value), keep 10 | SessionEnd: unconditional; SubagentStop: unconditional (no threshold); SessionStart: replays pending | PreCompact: unconditional (runs synchronously, no detach) |
| codex / trae-cli | Stop: same as above, 20000 / keep 10 | SessionStart(startup|clear): an active-window heuristic (exactly 1 state within 2min → commit; ≥2 → skip) + an idle-TTL sweep (>30min always commits and cleans up) | PreCompact: full commit (sends an empty body {}), then sets ovSessionId=null |
| cursor | stop: capturedSinceCommit ≥ 8 (counted in messages, ~4 Q&A turns; purely client-side counting), keep 0 | sessionEnd: registered (but never reached in practice, see §3.3.3) | preCompact: unconditional |
| trae / trae-cn | Every Stop with content commits (no threshold), keep 0 | — | None (no PreCompact event upstream) |
| zcode | Same as trae (every Stop commits, keep 0; the rollout incremental cursor advances conservatively, so any missed turns are caught up on the next Stop in the same session) | — | None (no PreCompact event upstream) |
| opencode | session.idle path: after the flush runs, pending_tokens ≥ 20000 must hold before it commits, keep 10 | session.deleted / session.error: forced commit; dispose: forced commit | Fires once before experimental.session.compacting and once after session.compacted (so one host compaction = two commits) |
| dsh | turn/end: pending_tokens ≥ 20000 (30s timeout), keep 10 | Teardown (see §3.3.3) | None (does not listen for compaction events) |
| pi (takeover on by default) | onTurnSynced: when the locally estimated pendingTokens ≥ 30000 and lastSeenUserTurns > 3, runs commitAndAdvance (keep 3; the overview polls 15 times at 2s intervals, and if it returns empty, the boundary does not advance, but pendingTokens is zeroed and retried once it has accumulated again) | Run /viking commit manually | session_before_compact (requires a non-empty firstKeptEntryId) |
| pi (takeover off) | After syncBranch runs: server-side pending_tokens ≥ 20000, keep 10 | session_shutdown: unconditional commit; run /viking commit manually | session_before_compact: unconditional commit |
| openclaw | afterTurn: pending_tokens ≥ floor(tokenBudget × 0.5) (ratio defaults to 0.5, tokenBudget defaults to 128000, making the threshold ~64000), wait=false, keep 10 | before_reset (running /new /reset): wait=true, keep 0; the memory_store tool: wait=true, keep 0 | compact(): wait=true, keep 0 (Phase2 polls for up to 5 minutes) |
| hermes | No threshold commit — every trigger is a session boundary: on_session_end (10s drain; if incomplete, this round aborts the commit), on_session_switch (covers /new, /resume, /branch, and compaction forks, async drain budget 65s), gateway cache eviction; /undo and in-place compaction do not commit. An idempotency set prevents duplicate commits; keep 0 | atexit fallback | Commits at fork-style compaction boundaries; in-place compaction does not commit |
| ov CLI | None | ov session commit; ov add-memory always commits in step 3 | — |
| ingest | pending ≥ 6000 or 5s idle, keep 0; backfill runs commit_if_needed at the end of every session | Runs _flush_all() on exit | — |
| LangChain | CommitPolicy.mode defaults to never; the pending_tokens mode threshold is 8000; the always mode triggers on every record | Up to the caller | — |
3.3.3 Shutdown method × harness outcome matrix
Legend: C = commits; C* = commits, with a precondition (see notes); — = does not commit (messages already POSTed stay in the server's live area: the message bodies are not lost, they wait for a later trigger to archive and extract them); n/a = not applicable. The server-side behavior is — on every row (§2.3).
| harness | normal exit | Ctrl+C | SIGTERM | SIGHUP / close terminal / close window·tab | kill -9 / crash | recovery path |
|---|---|---|---|---|---|---|
| claude-code | C (SessionEnd → a detached child process commits, so the user does not wait) | C | C | C (the detached worker forms its own process group and is unaffected by SIGHUP) | — | Next Stop over the threshold / /compact / next SessionEnd |
| codex / trae-cli | — (no SessionEnd-style event upstream) | — | — | — | — | The heuristic on the next SessionStart(startup|clear) (exactly 1 active state within 2min → commit) or the 30min idle-TTL sweep; with ≥2 concurrent sessions, the heuristic defers to the TTL sweep |
| cursor | — (closing a chat or opening a new chat fires no event) | — | — | — (sessionEnd is registered and only fires on window_close, but by then the host has destroyed the shell-exec host, causing the hook to abort before spawn) | — | A session ending below the 8-message watermark leaves its tail waiting for later messages in the same session to trigger a commit |
| trae / trae-cn | — (no session-end-style event) | — | — | — | — | Every Stop has already committed, meaning the most data left to archive equals the last in-flight turn |
| zcode | — (no session-end-style event) | C* | — | — | — | C* precondition: The Stop for that turn had already fired when Ctrl+C arrived (the detached worker finishes writing as usual); every Stop has already committed, and missed turns are recovered by the rollout cursor on the next Stop in the same session |
| opencode | C* (≥1.15.11 dispose calls flushAll({commit:true}), covering all four shutdown paths; <1.15.11 has no such hook → —) | C* | C* | C* | — | C* precondition: The host shutdown budget is 5s, while a single session takes up to 5s for health + 10s for batch + 30s for commit, and multiple sessions process serially. A commit overrunning the budget is cut off, and the pending queue does not cover this (unsettled fetches are never queued); after a restart, init() does not proactively flush leftover sessions |
| dsh | C (Cordis teardown triggers one 3s-timeout commit per session, no threshold) | C (the first one; a second Ctrl+C force-quits → —) | C | — (no SIGHUP listener) | — | The teardown commit and the threshold commit share a serial write chain, meaning it may not fit inside the 5s process grace period if a slow request precedes it; in the web form, closing the browser tab does not trigger a teardown |
| pi (takeover default) | — (session_shutdown fires on every shutdown path and is awaited, but the handler persists local takeover state and does not commit) | — | — | — | — | The next run accumulating 30000, or a manual /viking commit |
| pi (takeover off) | C (await sync.commit(), failures go to the pending queue) | C | C | C | — | — |
| openclaw | — (upstream sends an awaited session_end(reason=shutdown|restart); the handler caches agentId and returns without triggering a commit; gateway_stop is not registered; no signal handling) | — | — | — | — | Explicit /new /reset and the ~50% threshold; sessions below the threshold rely on these two paths for archiving |
| hermes | C (atexit _run_cleanup → 10s flush → on_session_end) | C (both interactive and non-interactive trigger atexit) | C (_signal_handler, grace period defaults to 1.5s → clean exit → atexit) | C (SIGHUP follows the same path as SIGTERM) | — (atexit does not run) | If the drain does not finish, this round aborts the commit (avoiding a half-written commit); an exit watchdog kills slow commits at 30s |
| ov CLI | n/a (one-shot command) | n/a | n/a | n/a | n/a | No pending queue; just re-run a failed command |
| ingest | C (finally _flush_all) | C (SIGINT → stop) | C | — (no SIGHUP handler) | — | The only write path offering crash recovery: needs_commit is persisted in the cursor store, and the subsequent run's reconciliation completes the commit |
| LangChain / Open WebUI / Agent Plugins / generic MCP | — (no session lifecycle hooks; the DELETE /mcp sent by an MCP proxy on exit only releases the protocol session and does not trigger memory commits) | — | — | — | — | LangChain relies on the caller's close(); the in-process pending-commit set disappears alongside the process |
Three reading notes:
- Five integrations commit on a normal exit:
claude-code,opencode(≥1.15.11),dsh,pi(takeover off), andhermes. The rest rely on the recovery mechanisms detailed in the "recovery path" column. - No integration commits under
kill -9— messages already submitted remain in the server's live area and are archived the next time the same session triggers a commit. The server offers a per-session idle fallback (§2.3), which the current plugins do not utilize by default. trae×2andzcode, which commit on every turn, offer the simplest shutdown semantics (the maximum data left to archive equals the last round that never reached Stop), at the cost of a full archive and memory extraction on every Stop (keep 0).
3.3.4 pending queue / offline compensation comparison
| harness | mechanism | notes |
|---|---|---|
| cc / cursor / trae×2 / zcode / opencode / dsh / pi | On-disk queue ~/.openviking/pending (0700/0600) | Only retryable failures are queued (4xx errors, including 401/403, are considered non-retryable and are not queued, though they appear in debug logs); replay runs at session start: ≤50 entries per run, ≤3 attempts per entry, TTL 7 days; .processing claims entries atomically, with a 10min stale reclaim; an addMessage failure breaks execution immediately to preserve order |
| codex / trae-cli | No on-disk queue | When the server is unreachable, compensation occurs because the capturedTurnCount cursor does not advance, prompting the next Stop to resend the same batch. This works provided the process survives and a subsequent turn occurs |
| openclaw | No local queue | An addSessionMessage failure is caught, and that turn's messages are not replayed |
| hermes | In-process daemon-thread queue | The drain operates on a strict budget (10s/65s); nothing is written to disk |
| LangChain | In-process _pending_commit_sessions set | A failed commit is retried automatically during the next record; nothing is written to disk. On partial success, it raises an OpenVikingPartialWriteError (carrying messages_written, input_messages_consumed, and context_attached, allowing the caller to slice by position and retry the suffix) — making this the only protocol across all integrations that reports partial success |
| ingest | SQLite cursor store + single-instance lock | The intent is persisted before appending. After a crash, a reconciliation process checks the server's message count to determine if the batch landed—making this the only write path with true crash-recovery semantics |
3.3.5 subagent session comparison
| harness | handling |
|---|---|
| claude-code | Offers the most complete isolation: SubagentStart derives a separate cc-<sid>__subagent-<agent_id> session, and SubagentStop reads the subagent transcript, pushes it, commits unconditionally, and clears the state |
| codex / trae-cli | No separate session: Subagent output (agent_message / sub_agent_activity) is folded into the main session's assistant/tool components |
| opencode | oc-<parent>__subagent-<child> hangs under the parent namespace; the session-start injection skips subagents (though recall does not); ID derivation is sensitive to event order — when chat.message arrives before session.created, the __subagent- suffix is lost |
| dsh | Each subagent operates as a separate dsh-<id> session, preserving no parent-child relationship; N subagents = N profile injections + N separate sessions |
| hermes | delegate_task passes skip_memory=True → the subagent is disconnected from OV (no session, recall, or tool surface); subtask output is not fed back |
| cursor / trae×2 / zcode / pi / openclaw | No specific subagent handling (anything generating its own session ID becomes its own session; otherwise, it mixes into the main session. openclaw can mask this behavior using bypassSessionPatterns) |
| ingest | The claude_code adapter skips isSidechain / isMeta records, meaning subagent conversations are not ingested |
3.4 Compaction takeover
3.4.1 Decision matrix
| harness | Stance on host compaction | Before compaction | After compaction |
|---|---|---|---|
| claude-code | No takeover | PreCompact commits synchronously. This is the only write path that does not detach, as CC rewrites the transcript immediately afterward. | A SessionStart with source="compact" re-injects OV's latest_archive_overview plus ≤5 abstracts. |
| codex / trae-cli | No takeover | PreCompact backfills uncaptured turns → full commit → ovSessionId=null. If the backfill is incomplete, no commit occurs and it is left for retry. There is no PostCompact wiring; it relies instead on the transcript shrinkage observed at Stop for defensive correction. | Injects the archive digest upon resume. |
| cursor / trae×2 / zcode | No takeover | Cursor: preCompact commits unconditionally (Trae×2/Zcode lack this upstream event). | — |
| opencode | No takeover | Flush and commit prior to compaction. | Flush and commit again once session.compacted fires (two commits in total). |
| dsh | Unaware. It does not listen for compaction events; injection rides on a pre-step user message and shrinks alongside the host's compaction. The profile is not re-sent. | — | — |
| pi | Two-layer takeover (on by default, §3.4.2) | session_before_compact: flush → commit → pollOverview. On success, it returns a custom compaction summary that overrides Pi's. On failure, it fails open and falls back to Pi's default compaction. | Calls resetBoundary upon success. |
| openclaw | Full takeover: ownsCompaction: true, the host no longer runs its own summary (§3.4.3) | compact() = commit(wait=true, keep 0) → reads the overview back and utilizes it as the summary. | The main assemble rebuilds context with [Session History Summary]. |
| hermes | No takeover (The on_pre_compress interface is reserved but currently plays no role in compaction summaries.) | A fork-style compaction boundary triggers a commit of the old session, whereas in-place compaction does nothing. | — |
3.4.2 pi takeover
- The takeover surface involves rewriting the messages of the
contextevent; Pi's native history storage remains untouched. The trigger is token pressure (30000 tokens + keep 3 turns) rather than Pi's native compaction event. - The replacement process: first locate the boundary, then replace every preceding message with a single synthetic user message,
[OpenViking Session Context]. The overview within this message is truncated at 3000 tokens. Its timestamp is set to the first kept message minus 1, which stabilizes the provider payload to ensure prompt cache hits. - The data source is
latest_archive_overviewfetched viaGET /sessions/{id}/context(polled 15 times at 2-second intervals). This state is persisted in Pi's own branch via the custom entryov-takeover. - Failure stance: fail-open, reverting to the full history. This occurs under three fallback conditions: a fingerprint mismatch, a history shorter than the boundary, or an unavailable overview.
- Relationship to Pi's native compaction: upon success,
session_before_compactreturns{compaction: {summary, firstKeptEntryId, …, details: {source: "openviking"}}}to override Pi's summary. IffirstKeptEntryIdis missing, it falls through to Pi's default compaction behavior.
3.4.3 openclaw ContextEngine
- Implements the host's
ContextEngineinterface. Theassemble()function splits into two branches:transformContext(handling recall pre-injection only, protected by 5 passthrough guards) and mainassemble(callsgetSessionContext(tokenBudget)→ replaces the host's live history with the server response using a four-tier budget split, protected by 3 passthrough guards and a provider-message sanitization pipeline). compact()executescommit(wait=true, keep 0)(utilizing 500ms polling, with Phase 2 capped at 5 minutes) →latest_archive_overviewbecomes the summary, and the last segment ofarchive_uriserves asfirstKeptEntryId. Note thatcustomInstructionsandcompactionTargetare reserved interfaces that currently play no role in the compaction output.ingest()andingestBatch()are deliberate no-ops; all writes are routed throughafterTurn.- If an archive exists, a 20-line "Session Context Guide" is injected via
systemPromptAddition. This instructs the model to re-read the summary before claiming it has "no information" and to attempt at least two different keyword sets usingov_archive_search.
3.4.4 pi vs. openclaw takeover
| Dimension | pi takeover | openclaw ContextEngine |
|---|---|---|
| Host contract | Rewrites the messages of a single context hook | Registers a ContextEngine with ownsCompaction: true |
| Source of truth for history | Pi's local branch | OV server-side getSessionContext |
| Trigger | Client-side token threshold (30000) + keep 3 turns | Host invocations of assemble or compact |
| Compaction output | A single synthetic user message (truncated at 3000 tokens) | The fully rebuilt messages array plus a compaction summary |
| Failure stance | Fail-open, reverting to the full history | Passthrough, reverting to the host's live messages |
| Recall and session | The context interface carries session_id | /find does not (expansion and ledger are excluded) |
3.5 Type boundaries for writes and deletes
3.5.1 Write boundary
There are three primary guards on MCP write and REST content/write (content_write.py). First, the writable domain is strictly limited to viking://resources, viking://user, and viking://agent. Second, file extensions for new files must match the whitelist (.md, .txt, .json, .yaml, .yml, .toml, .py, .js, .ts). Third, the four managed subtrees (skills/, peers/, privacy/, sessions/) under the user root are designated as read-only (_USER_MANAGED_SUBTREES). Existing .abstract.md and .overview.md sidecars can be body-updated, but public write APIs cannot create them.
3.5.2 Delete boundary
Tier 1: The universal server-side defense (shared by every delete entry point). The first statement of VikingFS.rm, _ensure_delete_access (_access.py:182-229), enforces five checks: namespace accessibility; ongoing user deletions (returns FailedPrecondition); actor-peer hidden views (returns PermissionDenied); namespace root protection (bare viking://, along with the viking://user and viking://agent roots, are unconditionally refused); and restricting deletes in viking://temp to ROOT only. This tier defends exclusively at the namespace root level and does not differentiate between memory, resource, or skill types. Those type-level distinctions are enforced on the client side by the subsequent three tiers.
Tier 2: No client-side additions (the MCP surface + dsh / pi / langchain / ov rm). Here, the differences lie purely in the parameters. In the viking_forget implementation for dsh and Pi, recursive is hardcoded to false (preventing directory deletion), and semantic query deletions require a score > 0.8. LangChain's viking_forget exposes recursive as a model-controllable parameter, although the tool is not exposed by default. The CLI command ov rm -r explicitly enables recursion without a confirmation prompt, whereas the TUI's d key enforces a y/n confirmation and bans the deletion of root or scope directories.
Tier 3: The two memory-only delete surfaces.
- In openclaw's
memory_forget, three regex whitelists restrict deletions strictly toviking://user/[…/]memories,viking://user/<u>/peers/<p>/memories, andviking://agent/[…/]memories. Explicit URIs failing to match these are refused outright. Search-path candidates must pass this same guard first; automatic deletion only proceeds if a candidate is unique and scores ≥ 0.85. Otherwise, candidates are listed so the agent can explicitly select one. The underlying URL always pinsrecursive=false. - In hermes'
viking_forget, there are six sequential validations: non-string or empty inputs are refused; a scheme other thanviking://is refused; queries or fragments are refused; directories or any files not ending in.mdare refused; the path must match one of four permitted memory path structures and contain at least two additional segments after thememoriessegment (ensuringmemories/and its category directories are never deleted); finally, the filename must not be.abstract.mdor.overview.md.
Tier 4: No deletion offered by default (LangChain / Open WebUI). LangChain's viking_forget is only exposed as a tool when configured with profile="admin" or allow_forget=True. Open WebUI does not offer any deletion tools.
Add/delete boundary for skills: The entry points for adding skills are openclaw's add_skill (enabled by default), ov add-skill, and REST. The delete surfaces that remain entirely read-only for skills (permitting neither addition nor deletion) are openclaw's memory_forget and hermes' viking_forget. Conversely, the MCP surface, dsh, Pi, and ov rm cannot add a skill but can delete one. This is because addition is blocked by _USER_MANAGED_SUBTREES on the write path, whereas deletion succeeds because the delete path does not check that specific constraint.
3.6 Degradation and fault tolerance
3.6.1 Decision matrix
| harness | When the server is unreachable | Negative cache | HTTP retry | Failure blocks the host |
|---|---|---|---|---|
| claude-code | All hooks catch exceptions → approve (never blocks); at session-start, even pending replays are skipped | context-face 6h + host-cli probe 7d + health 5s | None (relies on pending replay); peer_scope degrades once; batch falls back to sequential | No (uri-guard deny is by design) |
| codex / trae-cli | All hooks catch exceptions → noop | context-face 6h + compressor runtime_failed (until the next startup) | Same as above (no on-disk pending; resends from the cursor) | No |
| cursor/trae×2/zcode | Fetch errors are swallowed as status:0, and catch returns an empty injection; silently skips if the lock isn't acquired within 5s | context-face 6h (no negative cache for unreachable servers; every turn waits the full 15s) | None | No |
| opencode | All paths catch exceptions → WARN; the event/dispose hooks lack try/catch blocks (non-retryable commit failures bubble up to the host) | context-face 6h only; /health is uncached (one round trip per turn) | No synchronous retry; the MCP proxy retries once each for 401/403 and 400/404 | Mostly no (except event/dispose) |
| dsh | The client swallows all exceptions; ensureState failures are not cached (when the server is unreachable, each pre-step makes two 5s health calls) | context-face 6h + an in-process user-space cache that never expires | None; pending queue replays 3 times across processes | Yes (pre-step runs profile+recall serially; session/flush blocks) |
| pi | On health failure, start() returns early; subsequent prompts silently retry the connection | context-face 6h | None; pending queue only | Partly (session_shutdown is awaited: ~0s with takeover, max 30s without; on a turn_end network error, each message waits 10s) |
| openclaw | Client construction never fails; health checks swallow exceptions; recall is skipped if the 500ms precheck fails | No negative cache (one 500ms health precheck per turn) | None (a single fetch); Phase2 polling in commit/afterTurn | No (except when memory_store re-raises; compact() blocks for up to 5 minutes) |
| hermes | _client=None acts as the runtime negative cache (stops retrying in the current process, except for the local auto-start waiter) | No separate structure (_client=None handles this) | One retry with a trusted identity, one retry with a fresh sync client, multi-tier degradation; failed commits are not retried | No (a single background worker + per-provider try/except) |
| ov CLI | Mostly exits with 1; ov status in table mode always exits with 0; ov health exits with 0 even when unhealthy | None | Only one retry on a gateway 401 challenge | n/a (no host) |
3.6.2 Common timeouts
General HTTP timeouts are 15000ms (with a 1000ms floor). MCP proxy requests time out at 15000ms, while DELETE requests are pinned to 2000ms. Cross-process lock waits are 5s, becoming stale at 60s; pending .processing entries are reclaimed after 10 minutes. Note that the MCP proxy does not register SIGHUP signals (closing the terminal does not send a DELETE /mcp request), but since the server runs with stateless_http=True, the impact is minimal.
3.7 Additional UX comparison
| harness | statusline | slash command | rule/skill | setup wizard | other |
|---|---|---|---|---|---|
| claude-code | ✅ A separate process writes to settings.json (rich segments, 1-min TTL) | ✅ /openviking-memory:ov (server status + identity + injection provenance) | 1 experience skill | ✅ Line-based Q&A | Diagnostic scripts (debug-recall/debug-capture); uri-guard is not gated by the plugin toggle |
| codex / trae-cli | ❌ | ❌ | 1 experience skill | ✅ | 8-step SOP in VERIFICATION.md |
| cursor | ❌ | ❌ | Rule (alwaysApply) + skill | ❌ (Shares the installer TUI) | Standalone uri-guard, independent of the plugin toggle |
| trae/trae-cn | ❌ | ❌ | None | ❌ | — |
| zcode | ❌ | ❌ | None | ❌ | — |
| opencode | ❌ (Has toasts) | ❌ | None (Deliberately omitted) | ✅ | — |
| dsh | ❌ | ❌ | 1 openviking-memory skill (own isolated ctx.skills provider) | ❌ | ctx.provide("openvikingMemory") allows other Cordis plugins to build upon it |
| pi | ✅ ctx.ui.setStatus | ✅ /viking /viking commit | None | ✅ | e2e-live.sh |
| openclaw | ❌ | ✅ 5 commands (/add-resource, /add-skill, /ov-search, /ov-query-config, /ov-recall-trace) | 3 skills shipped with the plugin | ✅ (Key role detection, version compatibility checks, and a status command) | Gateway HTTP routes for visualizing recall traces; feature-gated RPCs; health-check script |
| hermes | ❌ | ❌ | None | ✅ Multi-level curses menus | hermes memory status (includes env override lists); hermes backup covers ovcli.conf |
| ov CLI | ❌ | ❌ (The CLI itself acts as the command) | None | ✅ TUI wizard | Comprehensive help system (63 curated entries); language gating; ov tui full-screen file browser (includes in-terminal image previews) |
4. Harness profile cards
Each card serves as a quick-reference entry point. It records only the facts and differences unique to a specific harness, linking back to the dimension chapters for shared mechanisms. All cards follow the same structure: Form / Capability highlights / Behavior notes / Configuration / Dimension index.
claude-code
- Integration docs: Claude Code Memory Plugin
- Form: A Claude Code plugin (marketplace) featuring a four-in-one architecture: 9 hooks, an MCP proxy (passing through 15 tools), a slash command, a statusline, and 1 experience skill. Version 0.4.4.
- Capability highlights: The harness with the broadest hook coverage —
SessionStart(120s) /UserPromptSubmit(60s) /PostToolUse:Read(5s, the skill-experience hook, off by default) /PreToolUse:Read|Glob|Grep(5s,uri-guard) /Stop(45s) /PreCompact(30s) /SessionEnd(30s) /SubagentStart(10s) /SubagentStop(45s). Recall digesting is enabled by default (localclaude -p, falling back to the server-side rewrite automatically when the local CLI is unavailable, §3.2.5). Provides full sub-session isolation viaSubagentStart/Stop(§3.3.5), along with a statusline, slash command, anduri-guard. All shutdown paths exceptkill -9trigger a commit (§3.3.3). - Behavior notes: The session ID format is
cc-<raw_CC_session_id>, while subagents use…__subagent-<agent_id>.Stopcommits at a threshold of 20000 (keep 10), andPreCompactcommits synchronously. Automatic recall excludes resources (§3.2.1). The incremental cursor is stored in/tmp(if cleared by the system, the entire session is pushed again). - Configuration: Configured via environment variables,
ovcli.conf(plugin.claude_code), andov.conf(claude_codeas per §3.1.4), offering roughly 40 tunable parameters. The compressor command and model are hardcoded toclaude/sonnet/low/30s. - Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3 | compaction §3.4 | degradation §3.6 | UX §3.7.
codex
- Integration docs: Codex Memory Plugin
- Form: A Codex plugin (marketplace). Features 4 hooks (
SessionStart70s /UserPromptSubmit130s /Stop30s /PreCompact60s), an MCP proxy, and 1 experience skill. Version 0.7.5. - Capability highlights: A local recall-compression pipeline (
codex exec, §3.2.5). TheSessionStartactive-window heuristic, combined with an idle-TTL scan, automatically reclaims messages left unarchived by earlier sessions (§3.3.3). - Behavior notes: The session ID format is
cx-<safeId>(derived deterministically without reading state). Lacks a shutdown hook (upstream provides noSessionEnd), so commits rely on the reclaim path during the next startup. Has no on-disk pending queue (while offline, the cursor simply doesn't advance, and the next turn resends to compensate, §3.3.4). Active-window is 120000ms / idle-TTL is 1800000ms (configured via env).Stopdetaches by default (theappended N turn(s)notice is hidden by default). - Configuration: Configured via environment variables,
ovcli.conf(plugin.codex), andov.conf(codex). Hooks transmit bothBearerand theX-API-Keycompatibility headers (§3.1.3). - Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3 | degradation §3.6.
trae-cli (TraeCode CLI 2.0)
- Integration docs: TRAE Memory Integration
- Form: TraeCode CLI 2.0 is a Codex-family CLI (binary
traecli, user config~/.trae/traecli.toml, TUI support for/plugins,/skills, and/mcp). OpenViking integrates via a codex plugin alias install: using--harness trae-clireuses the Codex installation flow, redirecting only the install parameters (binary, home, and config paths) to TraeCode CLI. - Capability surface: Identical to Codex: 4 hooks, an MCP proxy, an experience skill, local recall compression, active-window/idle-TTL commit reclamation, and resume-archive injection. Refer to the Codex profile card.
- Version support: TraeCode CLI 2.0 only. 1.0 and 2.0 are not the same CLI — only 2.0 is Codex-family, and only 2.0 can use the codex plugin alias install. The earlier standalone plugin for 1.0,
examples/trae-cli-memory-hooks(the~/.trae/cli/hooks.json+[mcp_servers."openviking-memory"]approach), is deprecated. - Dimension index: Same as the Codex card.
cursor
- Integration docs: Cursor Memory Integration
- Form: Config-driven (modifies
~/.cursor/hooks.jsonandmcp.json), featuring an MCP proxy, an always-on rule, and a skill. Includes 7 hooks:sessionStart(30s) /beforeSubmitPrompt(20s) /beforeReadFile(5s) /beforeShellExecution(5s) /stop(30s) /preCompact(30s) /sessionEnd(30s). The shared library is loaded via relative imports (no vendoring). - Capability highlights: Features a dual
uri-guardonbeforeReadFileandbeforeShellExecution(independent of the plugin toggle). The rule and skill are installed alongside it. - Behavior notes: The session ID format is
cu-<conversation_id>.stopcommits every 8 messages (commitTurnThreshold=8, counted in messages, keep 0).sessionEndonly fires onwindow_close. By then, the host has already destroyed the shell-exec host, so it practically never runs (§3.3.3) — sessions ending below the 8-message watermark leave their tail to be archived by a later message in the same session (§3.3.3). If the server is unreachable, every turn waits out the full 15s recall timeout. - Configuration: Configured strictly via environment variables (the
pluginsection is ignored). - Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3 | degradation §3.6.
trae / trae-cn (IDE editions)
- Integration docs: TRAE Memory Integration
- Form: Config-driven (
~/.trae{,-cn}/hooks.json+ a platform-specific mcp.json) utilizing an MCP proxy. It features 4 hooks: SessionStart(30s) / UserPromptSubmit(20s) / PreToolUse:Read|Glob|Grep|Bash|RunCommand(5s) / Stop(30s). The shared library is introduced via relative imports, and the MCP server is namedopenviking. - Capability highlights: Features the simplest and most direct behavior of the group. Every Stop that carries content is committed (keep 0). Consequently, upon shutdown, the largest possible backlog is merely the last in-flight turn (§3.3.3).
- Behavior notes: trae and trae-cn differ only in their session ID prefixes (
tr-vs.trcn-) and installation paths. The same workload lands in two separate sets of sessions across the two clients, and cross-client sharing occurs via the server-side memory space after extraction rather than by reusing sessions. Note that there is no handling for PreCompact, status lines, skills, or subagents. - Configuration: Environment variables only.
- Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3.
zcode
- Integration docs: Community Integrations → ZCode
- Form: Config-driven (merged into
~/.zcode/cli/config.json, forcinghooks.enabled=true) utilizing an MCP proxy. It features 4 hooks: SessionStart(30s) / UserPromptSubmit(20s) / PreToolUse:Read|Glob|Grep(5s) / Stop(30s). This is the only harness that fully vendors all 18 shared files. Version 0.1.1. - Capability highlights: The rollout file
~/.zcode/cli/rollout/model-io-<sid>.jsonlserves as the source of truth for increments (alastTurnIddiff backfills any missed Stop events). Stop detaches by default, meaning Ctrl+C does not result in lost writes. - Behavior notes: Commits on every Stop (keep 0). The capture path only strips the three types of injection blocks without performing any further text cleanup (§3.2.6). The initial capture reads the entire rollout at once, meaning that installing it into a long-running session will produce a single large push.
- Configuration: Environment variables only;
OPENVIKING_WRITE_PATH_ASYNCtakes effect for zcode. - Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3.
opencode
- Integration docs: OpenCode Plugin
- Form: Provided as the npm plugin
@openviking/opencode-plugin, featuring a config hook that injects the MCP entry itself (tools carry theopenviking_prefix). It exposes 7 plugin hooks: config / event / tool.execute.before / experimental.chat.system.transform / chat.message / experimental.session.compacting / dispose. Version 0.2.4. - Capability highlights: The
disposehook covers all four standard shutdown paths (on hosts ≥1.15.11). The repository list is injected into the system prompt (§3.2.3). It boasts the richest host event surface of any integration, withsession.idle,compacted,deleted, anderroreach carrying their own specific semantics. - Behavior notes: Features a
commitTokenThresholdof 20000 (positive values only; 0 falls back to the default) and a commit timeout of 30000ms. A single host compaction equates to two commits. Within thedisposehook's 5-second host budget, a slow commit spanning several sessions might be cut short, and the pending queue does not cover this scenario (§3.3.3). On host versions below 1.15.11, thedisposehook is unavailable, meaning shutdowns do not trigger a commit. Leftover sessions are not flushed upon restart. The opening injection is attempted once per session (§3.2.3). Finally, directory matching inbypassSessionPatternsdoes not apply to opencode, as the input does not carry a current working directory (cwd). - Configuration: Handled via
openviking-config.json(searched at 4 levels) + environment variables. - Dimension index: tool surface §2.1 | recall §3.2 | commit §3.3.2/§3.3.3 | subagent §3.3.5.
dsh (DeepSeek Harness)
- Form: This is the only in-process, native Cordis plugin (
export function apply). It natively registers 7viking_*tools (viking_search/read/browse/remember/forget/add_resource/archive_expand) and communicates directly via REST. It features 4 events: agent/session-start (emit) / agent/pre-step (waterfall) / session/event / session/flush. Version 0.1.0. - Capability highlights:
ctx.provide("openvikingMemory")allows other Cordis plugins to build upon it. Pre-step injection is transmitted as a user message, aligning with thecomplete:truerendering mode of the DSH persona. - Behavior notes: The unified installer covers dsh and asks which profile to install into (default
web, overridable with--dsh-profile); npm is the bundle's only distribution channel, so the github/tos choice does not apply and every mode exceptdevinstalls the published package;devpacks the checkout first, becausedsh pluginforwards to pnpm and a linked source tree cannot resolve the dsh peers the bundle imports. The teardown commit is allocated 3 seconds with no threshold, and it does not trigger on SIGHUP or a consecutive Ctrl+C (§3.3.3). Compaction remains invisible to the plugin: injected content shrinks alongside the host's compaction, and the profile is not re-injected. Each subagent is assigned its own session (§3.3.5). The tool surface is the server's own MCP surface, reached through the same stdio proxy the other integrations use and published undermcp__openviking__*, so a server upgrade adds tools without a bundle release; the trade-off is that the proxy runs once per profile, so tool calls carry a process-level actor peer andrememberis not session-scoped (recall, capture, and commit still resolve a peer per session). The bundle also ships the sharedopenviking-memoryskill. Additionally,uri-guardmatches tool names without normalizing case. - Configuration: Configured via cordis patch + 4 environment variables. For credentials, the patch overrides env vars; for behavior toggles, env vars override the patch.
- Dimension index: tool surface §1.1 | recall §3.2 | commit §3.3.2/§3.3.3 | degradation §3.6.
pi (pi Coding Agent Extension)
- Integration docs: pi Coding Agent Extension
- Form: Operates as a native pi extension (loaded from a directory and dynamically transpiled from TS by jiti). It registers 7 native
viking_*tools and uses direct REST communication (since pi lacks MCP support). It features 8 events + a/vikingcommand. Version 0.1.0. - Highlights: Features takeover compaction (enabled by default, §3.4.2). Employs a two-stage recall system: it queues at
before_agent_startand performs synchronous retrieval during the context event, ensuring the current turn's prompt receives its corresponding memories. It also includes a status line. Thesession_shutdownevent triggers across all shutdown paths and is properly awaited. - Behavior: When takeover is enabled, exiting does not trigger a commit. Instead, the handler persists local state, and archiving waits either for the next resumed run to hit the threshold or for a manual
/viking commitcommand (§3.3.3). The takeover threshold is set to 30000 tokens while keeping the last 3 turns (keep 3, which the server interprets as a message count). When takeover is disabled, the threshold is 20000 tokens (keep 10), and exiting triggers an unconditional commit. Tool registration requireshealthandensureSessionto be established first (§1.1). Theviking_add_resourcetool exclusively accepts HTTP URLs (the guard resides on the server). If takeover is off, resuming viapi -cwill re-report the entire branch. - Config: Behavior toggles are managed in
config.json+ environment variables (credentials always pass through the credential chain, §3.1.3). Note thatbypassPatternsuses prefix matching rather than globs. - Dimension index: tool surface §1.1 | recall §3.2 | takeover §3.4.2 | commit §3.3.2/§3.3.3.
openclaw
- Integration docs: OpenClaw Plugin
- Form: Represents the only full ContextEngine takeover (
ownsCompaction:true). Features 15 native tools (14 enabled by default) + 5 slash commands + 4 hooks + Gateway HTTP routes + feature-gate RPC. It operates entirely remotely. Version 2026.6.18. - Highlights: Retrieval is split into two non-overlapping entry points by default:
memory_recallsearches memory, whileov_searchtargets resources and user skills (though either can cross over if explicit parameters are provided). Theadd_skilltool is enabled by default, andmemory_forgetis whitelisted exclusively for memory (§3.5). Three tool-result tools read server-side externalized outputs (safeguarded across sessions). It features complete ContextEngine takeover (§3.4.3), and the setup wizard actively probes the key's role while verifying version compatibility. - Behavior: Recall invokes
/findwithout a session ID. Consequently, there is no expansion or deduplication ledger, meaning the same memory might be repeatedly injected during a long session. Shutdowns do not trigger a commit; archiving relies on an explicit/newor/resetcommand alongside an approximate 50% threshold (§3.3.3). There is no local pending queue, so failed turns are not replayed. Thecompact()function can block for up to 5 minutes. By default, recall issues one additional read per leaf memory (recallPreferAbstract=false). Configuration validation is exceptionally strict: any unknown key or invalid value immediately forces the plugin into a setup-only mode. - Config: Configured via
plugins.entries.openviking.configinopenclaw.jsonalongside a few environment variables. Utilizes theX-API-Keyauth header (§3.1.3). The commit threshold is controlled bycommitTokenThresholdRatio(default 0.5), and the recall character budget is 4000. - Dimension index: tool surface §1.1 | recall §3.2 | ContextEngine §3.4.3 | deletion §3.5 | commit §3.3.2/§3.3.3.
hermes (Nous Research)
- Integration docs: Hermes Agent
- Form: Implemented as a MemoryProvider bundled directly with Hermes (a single-file Python implementation of 3725 lines, shipped alongside Hermes). It connects directly via
httpx, eliminating the need for additional plugin installations. It provides 6 tools and over 10 lifecycle hooks (includingprefetch,sync_turn,on_session_end,on_session_switch, andon_memory_write). The baseline is releasee12626b3(equivalent to brew 2026.7.7.2). - Highlights: Boasts the most comprehensive resource-ingestion surface:
viking_add_resourcesupports HTTP, Git, SSH,file://, temporary uploads of local files, and zip-packing local directories for upload (automatically skipping symlinks and out-of-tree files). Theviking_remembertool writes memory files directly, bypassing session commits or extractions. A local server can be initialized on demand; if the configured local endpoint is unreachable, it is automatically launched viasubprocess.Popen openviking-server. It supports retries with an injected trusted identity. Commits are guaranteed to complete whether triggered by a normal exit, Ctrl+C, SIGTERM, or SIGHUP (§3.3.3). - Behavior: During recall, only the preferred
search/searchpath carries a session ID and routes to path B (mode="deep"). Conversely, theautoandfastmodes call/findwithout a session ID (§3.2.2). Thequeue_prefetchhook is implemented synchronously without warm-up. Subagents configured withskip_memory=Truewill not interact with OpenViking (§3.3.5). This integration lacks profile injection, a status line, and slash commands. Commits strictly follow a "keep 0" policy; if the queue drain does not finish cleanly, the commit is skipped for that round. Additionally, the in-process queue is never written to disk. Session IDs follow the%Y%m%d_%H%M%S_<hex6>format. Recall parameters are strictly defined: 6 results, a 0.15 threshold, a 4000-character budget, and a 4-second total timeout. Memory URIs are formatted asviking://~/peers/{agent}/memories/{subdir}/mem_<uuid12>.md. The shutdown sequence incorporates a 1.5-second SIGTERM grace period alongside a 30-second exit watchdog. Finally, a complementary path (openviking-server ingest hermes) enables offline replays, though this is disabled by default (§7 E). - Config: Configured through
OPENVIKING_ENDPOINT(not_URL), 8OPENVIKING_RECALL_*environment variables, andconfig.yaml. Inuse_ovcli_configmode, the corresponding variables in.envare cleared. - Dimension index: tool surface §1.1 | recall §3.2 | commit §3.3.2/§3.3.3 | deletion §3.5.
ov CLI
- Integration docs: Deployment Guide → CLI
- Form: A native Rust binary that wraps the server's REST API into a command-line interface. It operates without host events, automatic recall, or compaction takeover.
- Highlights: The only first-party client capable of sending an
auto_commit_policy(ov session new --auto-commit-policy-json,ov session config set). It also offers multi-profile management, admin tools, privacy controls, snapshot management, and TUI capabilities unavailable in any plugin (see §5.3). - Dimension index: See §5 for the comprehensive command reference.
5. ov CLI Command Reference
ov (internally identified as openviking in clap) is a Rust-based HTTP client. Because all core capabilities reside on the server, the CLI's role is strictly to assemble parameters, pack and upload local files, render output, and manage multiple profiles. It is a standalone tool rather than a harness integration—meaning it does not handle host events, automatic recall, or compaction takeover. This chapter details its complete command surface for using OV directly (either manually or via scripts) and highlights features exclusive to the CLI (such as the TUI, multi-profile management, admin tools, --sudo, privacy controls, and snapshots).
Version note: This guide reflects the HEAD source (with the HEAD tag at
cli@0.4.14); notable differences from older versions, such as 0.4.10, are explicitly mentioned. Please note thatov doctoris not included in the native Rust binary. Instead, the Python wrapper installed via pip interceptsargv[1]=="doctor"and routes it toopenviking_cli.doctor, reading the server'sov.confrather thanovcli.conf. The pure Rustovbinary distributed via npm/cargo does not include this subcommand.
5.1 Command tree
Doc-comment prefixes like [Data], [Interactive], [Admin], or [Experimental] only influence the help menu rendering and do not affect runtime behavior. For instance, even [Experimental] commands are available by default.
Writing data: add-resource (supports local files/directories/URLs/Git/sitemaps/RSS; pick one target from --to/--parent/-p; manifest mode via -m; Connector via --add-type; note that specifying a local path with watch>0 triggers an error) | add-skill | write (--content and --from-file are mutually exclusive) | mkdir | rm (aliases: del/delete; executes without a confirmation prompt; use -r for explicit recursion) | mv (alias: rename) | set-tags (hidden at the top level; use attrs set-tags instead) | add-memory (experimental; executes a sequence of three serial steps: create session → bulk add → commit)
Reading/retrieval: ls (alias: list) | tree (-L defaults to 3) | stat / attrs get | read / abstract / overview (L2/L0/L1 respectively) | get (download) | find (requires at least one non-empty query or --image; --image accepts local paths, data URIs, HTTP URLs, and viking:// schemas) | search (experimental; adds --session-id on top of find capabilities) | grep | glob
Skills: skills add (supports local paths, Git, and GitHub tree URLs; use -s to select or * for all; prompts for interactive confirmation) | skills list/find/show/update/remove | skills validate (the only fully offline command)
Sessions/memory: session new (--auto-commit-policy-json and --no-auto-commit are mutually exclusive—this is the only first-party client that sends a policy) | session list/get/delete | session get-session-context (--token-budget defaults to 128000) | session get-session-archive | session add-message(s) | session config set (modifies mutable configurations) | session commit
Import/export & snapshots: export / backup / import / restore (.ovpack formats) | snapshot commit/restore/show/log/diff/ignore-* (workspace snapshots; rollbacks are achieved by committing forward)
Privacy: privacy categories/list/get/versions/version/activate/upsert (offers --key-<name> syntactic sugar)
Status/observability: health (exits with 0 even when healthy=false) | status (table mode always exits with 0) | observer {queue,vikingdb,models,retrieval,filesystem,system} | wait | task status/cancel/list | task watch {ls,show,rm,pause,resume,update,trigger} | version (probes the server utilizing its own 3-second timeout)
Config/interactive: config (launches a TUI wizard with a 5-item menu, including User Management) | config show (always outputs compact JSON) | config validate | config list/switch/add/edit/delete (Agent-facing; exit codes 2-6 carry semantic meaning) | config add ov-service (cloud) / config add custom | language (alias: lang) | tui (provides a full-screen file browser, in-terminal image previews, a vector view, and deletion confirmations) | chat (launches VikingBot with a 300-second timeout) | compile (organizes material via a Skill; --wait enables local polling)
Administration (mostly ROOT/--sudo): admin create-account/list-accounts/delete-account/set-role/migrate/register-user/list-users/remove-user/regenerate-key | system wait/status/health/consistency | system crypto init-key (generates a 32-byte root key entirely locally with 0600 permissions) | system backend sync-status/sync-retry | reindex (--mode defaults to vectors_only; --wait defaults to true, making it the only CLI command with this default behavior) | doctor (exclusive to the Python wrapper)
5.2 Global options and unique mechanisms
-o/--output table|json(defaults tooutputin the config file) |-c/--compact(defaults to true) |--account/--user/--actor-peer-id|--sudo(utilizes theroot_api_key; permitted only for admin, system, reindex, task status, and task list commands) |--profile(hidden).- Multi-profile: The active profile is stored at
~/.openviking/ovcli.conf, with alternative profiles namedovcli.conf.<name>. Theswitchcommand performs a direct byte copy (preserving thepluginsection). Conversely,addandeditrewrite the file via serde, which drops any keys unrecognized by the Rust Config structure (including thepluginsection, as detailed in §3.1.4). - Language gate: A display language must be configured before executing any commands (if unconfigured in a non-interactive environment, the CLI exits with code 2). As of the HEAD version,
--helpbypasses this gate (unlike version 0.4.10, which lacked this exemption), though--versionstill requires a configured language to run. - Three JSON output shapes, categorized by command group: Under compact mode, standard commands emit
{"ok":true,"result":…}upon success, a bare payload when-c falseis passed, and{"ok":false,"error":…}upon failure. The configuration command family outputs{"status":"ok","result":…}. When a profile is active,"profile":[…]is appended to the response. These variations require careful handling when parsing outputs in scripts. Additionally,echo_commanddefaults to true and is not suppressed by-o json(meaning the first line of stdout will typically becmd: …). - Environment variables:
OPENVIKING_CLI_CONFIG_FILE,OPENVIKING_UPLOAD_MODE(local/shared),OPENVIKING_ASSETS_CREDENTIALS_FILE,OPENVIKING_LANG/LC_*/LANG, along withVIKINGBOT_ENDPOINT/VIKINGBOT_API_KEY/OPENVIKING_URLfor chat functionality.
5.3 Capabilities only the CLI has
The following operations are inaccessible via the plugin surface and remain exclusive to the CLI/TUI: multi-profile switching and the configuration wizard, the complete admin account and user management suite, root operations via --sudo, privacy policy CRUD and versioning, data movement commands (snapshot/backup/restore/export/import), index rebuilds via reindex, root key generation via system crypto init-key, interactive browsing with ov tui, VikingBot-dependent features like ov chat and ov compile, and ov session config set for explicitly defining the auto_commit_policy (this serves as the only first-party entry point capable of enabling server-side auto-commits).
6. Custom agent integration guide
If your preferred agent or harness is not among the 11 listed previously, you can integrate it using one of three methods, arranged below from lowest to highest implementation effort.
6.1 Integration path × capabilities you get
| Path | Effort | Agent-initiated tool surface | Auto recall/capture hooks | Session/commit | Compaction takeover |
|---|---|---|---|---|---|
| ① Direct MCP connection | Minutes (fill in one config block) | ✅ All 15 tools | ❌ The model calls them itself | Only remember creates a temporary session | ❌ |
| ② HTTP API / SDK / LangChain | Hours (requires code) | Flexible (call REST as needed) | Custom implementation | Custom implementation (or use the LangChain middleware) | ❌ |
| ③ Reuse shared-core / the Agent Plugins portable package | Days (requires hook adapters) | ✅ 15 tools (through the MCP proxy) | ✅ Full recall/capture/commit/pending set | ✅ | Depends on which events you wire up |
6.2 Path ①: Direct MCP connection (recommended starting point)
Any MCP-capable agent simply needs to point its mcpServers configuration to the server's /mcp endpoint (refer to MCP Clients to locate this configuration for each client). Doing so instantly unlocks all 15 tools (§2.1). The minimal configuration looks like this:
{
"mcpServers": {
"openviking": {
"url": "http://127.0.0.1:1933/mcp",
"headers": {
"Authorization": "Bearer <api_key>",
"X-OpenViking-Account": "<account>",
"X-OpenViking-User": "<user>",
"X-OpenViking-Actor-Peer": "<workspace-peer>"
}
}
}
}The last three headers are optional; however, omitting them disables workspace peer isolation and tenant routing. For stdio-only clients, you can utilize the portable proxy (Path ③) to bridge stdio to streamable HTTP. This path provides a pure tool surface—meaning it does not include automatic recall, capture, or commits (unless the model explicitly invokes the remember tool).
6.3 Path ②: Programmatic integration
- Direct REST: Trigger recall via
POST /api/v1/search/search(note that expansion and deduplication requiremode:"context"alongside asession_id, as per §3.2.1; passrewriteto generate a server-side digest, see §3.2.5). Write data usingPOST /api/v1/sessions/{id}/messages/batch(supports up to 100 messages per batch withauto_create). Finalize sessions viaPOST /api/v1/sessions/{id}/commit, and retrieve content usingGET /api/v1/content/readand related endpoints. To enable server-side auto-commits, explicitly pass theauto_commit_policyduringPOST /api/v1/sessions, or modify it later viaPATCH /{id}/config(§2.3). - LangChain / LangGraph SDK (
pip install langchain-openviking): TheOpenVikingContextMiddlewareprovideswrap_model_call(which injects recalled content into<openviking_context>) andafter_agent(handling capture and commit actions according to theCommitPolicy, which defaults tonever). Within this group, this is the only out-of-the-box automatic recall solution that includes both session management and a token budget. Its fault-tolerance strategy is to retry read-only methods once and never retry writes. Partial successes raise anOpenVikingPartialWriteError, allowing you to retry a specific slice based oninput_messages_consumed. See §7 B for more details. - Open WebUI (OpenAPI tool server): Running
python -m openviking_openwebuilaunches a standalone process. By adding the resulting Tool Server URL to Open WebUI, you gain access to 7 tools (note that deletion and hooks are unsupported). See §7 A for more details.
6.4 Path ③: Reuse a Reference Implementation for Automatic Hooks
If you need the full spectrum of automation—recall, capture, commit, and pending state management—there is no need to build it from scratch. Consider studying and reusing one of these two existing implementations:
examples/memory-plugin-shared/lib/(Node): This provides a complete set of core modules. These includerecall-core(three-tier degradation recall),profile-inject,capture-utils(message normalization and injection-echo protection),pending-queue(offline replay),batch-send,mcp-proxy-core(stdio↔HTTP proxy),session-model(session ID derivation), andcredentials. To build a lightweight integration harness, you only need to implement an adapter layer that maps host lifecycle events to these modules. For example,agent-hook-runtime.mjsis a ready-made, all-in-one runtime shared by Cursor, Trae, and zcode. Wiring up a new host is usually just a matter of parsing its stdin JSON field names.- The Agent Plugins 1.0 Portable Package (under
agent-plugins/): This offers a standardized portable format comprisingplugin.json, theskills/directory, andmcp.json(stdio→HTTP proxy). It intentionally excludes automatic hooks—instead, recall and persistence rely on a skill that teaches the model to invoke the tools autonomously. This design makes it highly suitable for clients that follow the Agent Plugins specification for direct loading. Additionally,plugin.test.mjsdefines spec-conformance checks (such as schema URL validation, naming rules, ensuring no secrets in static headers, and preventingmcp.jsonreferences from escaping the plugin root), which you can use as a linting baseline when packaging your own plugin.
Three conventions you must follow (to ensure behavior remains consistent with existing harnesses): ① The recall call site must forward the session_id, which enables server-side expansion and cross-turn deduplication (see §3.2.1); ② Do not allow the adapter's own timeout to override the deadline dictated by the helper; ③ You must arrange a commit path at shutdown. Otherwise, any remaining conversation tail that falls below the threshold will remain unarchived until a subsequent trigger occurs (see §3.3.3). If the host does not provide a shutdown event, rely on the server-side idle fallback (enable memory.session_auto_commit.idle_enabled on the server and pass down a per-session policy). These three rules are exactly what recall-session-wiring.test.mjs enforces using cross-plugin regexes.
7. Appendix: Non-Coding Integrations at a Glance
| Integration | Form | Tool Surface | Session/Commit | Fault Tolerance | Default State |
|---|---|---|---|---|---|
| A. Open WebUI | Standalone FastAPI OpenAPI tool server | 7 local OpenAPI routes (ov_search/ov_recall_memories/ov_add_memory/ov_list_memories/ov_read_resource/ov_add_resource/ov_session_status), no deletion tool | No session concept | Most lightweight: bare httpx, no retries or negative caching; /health only echoes the config and does not probe OpenViking. | Inactive until the process is explicitly started. |
| B. LangChain/LangGraph | Python SDK adapter layer (retriever/tools/store/middleware/recorder) | create_openviking_tools() provides 12 StructuredTools (viking_forget is not in the agent profile by default) | thread_id/session_id come from the caller; CommitPolicy defaults to never | Most robust in this group: read-only methods automatically retry once, writes never retry (to prevent duplicates), and partial successes raise a structured exception that can be retried as a slice. | Requires explicit construction before taking effect. |
| C. Agent Plugins 1.0 | Portable package: plugin.json + skills/ + mcp.json (stdio→HTTP proxy) | MCP passthrough for all 15 tools; intentionally excludes hooks (recall relies on a skill instructing the model). | Only remember creates a temporary session | Retries handled at the MCP proxy layer (401/403 triggers credential swap, 400/404 triggers re-initialization; max 1 retry each). | Active as soon as loaded by the client. |
| D. Direct MCP Connection | No local components, connects straight to /mcp | Same as C (15 tools) | Same as C | Depends entirely on the client. | /mcp is always on. |
| E. Log Ingestion | openviking-server ingest CLI (runs on the machine hosting the logs, importing them in reverse) | None (write-only, no recall) | Session ID {prefix}__{harness}__{sanitized native id}; commit token 6000 / idle 5s / keep 0 | The only integration featuring crash recovery: utilizes a SQLite cursor store, single-instance locking, and a reconciliation process to verify batch delivery. | Disabled by default on two levels (ingest.enabled and each harness's individual enabled flag are both false); adapters available for claude_code, codex, hermes, opencode, openclaw, and cursor. |
| F. OpenViking Helper | Closed-source desktop app | — | — | — | Outside the scope of this codebase. |
Installation, configuration, and troubleshooting for each integration are governed by their respective integration pages. In the event of a discrepancy between this summary and the individual page, the individual page takes precedence.
