DSH's plugin manager lists every bundle row, group rows included, but resolves row state and toggles through the running-plugin inventory, which skips group entries. The outer `@deepseek-ai/cordis-plugin-group` row therefore always showed as disabled, and enabling it failed with `unknown-plugin`. The group only isolated the `openvikingMemory` service, which nothing consumes, so the bundle now inserts the runtime row directly under the same row id. Desktop toggles and id-targeted overrides keep applying, and the docs show the id-targeted override form, which also applies to the old nested layout.
Memory Plugin Shared Library
For contributor guidance on adding and maintaining hook + MCP integrations, see the Agent plugin development and maintenance standard (中文). When using a coding agent, have it read and follow this standard before making changes.
This directory contains shared JavaScript modules. sync.mjs vendors each module into the plugins whose code imports it — Claude Code, Codex, OpenCode, dsh, pi, openclaw and the bundled agent-plugins servers — together with the matching lib/*.d.mts declaration for the targets written in TypeScript. cursor, trae, trae-cn and zcode vendor nothing: the installer copies the modules lib/MANIFEST names — the same sync writes it — to $OV_HOME/agent-integrations/memory-plugin-shared/lib, and they import it from there.
lib/install/ is the exception: it holds the installer's own JavaScript — the JSONC editor OpenCode's config needs and the hooks/mcp merge cursor, trae, trae-cn and zcode install through — which runs from install.sh and never from a hook. No shared module imports it, so it stays out of every closure and out of lib/MANIFEST.
When the copies are made follows how the plugin is delivered. Claude Code, Codex and agent-plugins are installed by pointing a host at a directory in this repository, so their copies are committed and a push to main regenerates them. OpenCode, dsh and openclaw publish as npm packages and pi is tarred by the installer, so those build their copies at pack time and keep none in git — run node examples/memory-plugin-shared/sync.mjs once in a fresh checkout before running their tests.
Requires an OpenViking server with
viking://~home-alias support. Recall targets the caller's own context space throughviking://~/memoriesandviking://~/skills; the uid-lessviking://user/memoriesshorthand is rejected by newer servers.
Workspace Peers
lib/workspace-peer.mjs decides which peer a workspace writes its memories under; lib/workspace-identity.mjs derives the values it substitutes.
The peer used to be the working directory with every non-alphanumeric character replaced by -, so /Users/x/Dev/OpenViking became -Users-x-Dev-OpenViking. That made the identity an accident of where the repository sat: a clone on another machine, a rename, a git worktree, or simply working from a subdirectory each minted a separate, empty namespace. The default is now git's own identity, so one project keeps one memory wherever it is checked out. A directory that is not a repository sends no peer at all, and what is remembered there goes to your user-level space at viking://user/<you>/memories — an application that opens a fresh directory for every task would otherwise mint a fresh, empty peer for every task.
peer.source picks the rule. It is read from OPENVIKING_PEER_SOURCE, from plugin.peerSource / plugin.<harness>.peerSource in ovcli.conf, or from peer.source in a workspace config file:
git— the default. Equivalent to the template list["{git_remote}", "{git_root}"]: the normalizedoriginURL, else the repository root path. Outside a repository nothing is sent. No prefix is added.cwd— the previous behaviour, byte for byte.none— send no peer at all.OPENVIKING_WORKSPACE_PEER=0still means this.- A template such as
"git-{git_remote}"or"team-{dir}", or a list of templates tried in order. A template whose variables are empty falls through to the next one, so a half-substituted id is never sent.
The variables a template may use:
{git_remote}— the normalizedoriginURL asgithub.com-org-repo; empty outside a git repository, or when there is noorigin.{git_root}— the repository root path, legacy sanitation; empty outside a git repository. A marker file inside a repository still leaves this the repository's own root, so marking a subdirectory does not split the default peer.{cwd}— the working directory, legacy sanitation; never empty, and in no default chain, so a bare path becomes a peer only when you ask for one.{dir}— the workspace root's directory name: the repository root, or the directory holding.openviking/config.json; empty when the directory is not a workspace.{harness}— the name of the agent running, the same one the User-Agent carries; never empty. No preset uses it, so agents share one repository's memory unless a template such as"{git_remote}-{harness}"asks them not to. The MCP proxy takes no part in derivation, so a read path that goes only through it cannot resolve it.
To give a directory that is not a repository its own peer, create .openviking/config.json there holding {"version": 1, "peer": {"id": "my-project"}}.
So in /Users/x/Dev/OpenViking/examples/codex-memory-plugin with origin git@github.com:volcengine/OpenViking.git, the peer is github.com-volcengine-openviking — the same from any subdirectory, any worktree, any machine, any clone.
Every clone of one repository therefore shares one peer: project memory follows the project. A fork has a different origin, so it stays separate by default; reviewing an external PR through gh pr checkout leaves origin alone and does not move the identity. Worktrees converge through commondir, a submodule keeps its own identity, and $HOME and / are never workspace roots. The URL is normalized so ssh and https spellings of one repo agree, and an embedded token can never reach the peer id. The whole derivation is pure filesystem work — no git subprocess — so it also holds where git is absent from PATH or would refuse the repository over dubious ownership.
Resolution order is:
- Explicit peer:
OPENVIKING_PEER_ID, thenpeer.idin a workspace layer orpeerIdinovcli.conf'spluginsection, thenactor_peer_id/peer_idinovcli.conf, then the harness's own section ofov.conf. - The peer derived by
peer.source, whenworkspacePeeris notfalse. - No peer.
Migrating from the path-derived peer needs no action. The old id can always be recomputed locally, so recall still reaches memories written under it: with the default peer_scope: "all" the server's existing cross-peer sweep already covers them at zero cost, and with peer_scope: "actor" the plugin asks that peer separately. There is no deadline on this, and peer.source: "cwd" restores the old id outright.
Recall Peer Scope
lib/recall-core.mjs defaults to the broad recall mode and does not send a
peer_scope field. In that mode, the server can recall global memory, the
current workspace, and other workspace memories; other workspaces are penalized
and rendered later.
When recallPeerScope is actor, the helper sends peer_scope:"actor". This
is the isolation mode: recall only sees global memory plus the current
workspace. If an older server rejects that field with 400 or 422, postRecall
removes peer_scope and retries once.
For deployments where one bot serves multiple real people, such as zouk, vikingbot, or AstrBot, configure an explicit actor peer and use the isolation mode so one person's memories are not recalled into another person's session.
Skill Catalog
lib/profile-inject.mjs ends the session-start block with <available-skills>, built from one GET /api/v1/skills?node_limit=200, which caps each root rather than the merged list: the user's own skills first, then the ones shared under viking://agent/skills (a shared skill whose name the user also owns is left out), each description cut to about 40 tokens. Nothing trims the list again on this side; the token budget decides what fits. Callers turn it on by passing their resolved config as buildProfileBlock()'s fourth argument, and claude-code, codex, cursor, trae, trae-cn, zcode, opencode, dsh and pi all do, so they build it the same way; openclaw and hermes inject no profile, and pi-experimental-context-management calls buildProfileBlock() without that argument.
skillCatalog (default true, OPENVIKING_SKILL_CATALOG) switches it, and skillCatalogTokenBudget (default 1200, range 0-20000, OPENVIKING_SKILL_CATALOG_TOKEN_BUDGET) sizes it on its own, apart from profileTokenBudget; a budget of 0 also switches it off. When the descriptions do not fit, the block lists names only, ending with a ... +N more tail if even the names do not all fit; when not even one name fits, it becomes a one-line count. With no skills, or a server without the endpoint, there is no block.
Workspace Configuration
lib/workspace-config.mjs and lib/workspace-registry.mjs give a repository three configuration layers of its own: <repo-root>/.openviking/config.json, which the team commits, <repo-root>/.openviking/config.local.json, which stays private and gitignored, and a per-machine entry at ~/.openviking/workspaces/<slot>.json. Precedence, highest first:
OPENVIKING_*environment variables- the machine registry,
~/.openviking/workspaces/<slot>.json .openviking/config.local.json.openviking/config.jsonovcli.confplugin.<harness>ovcli.confplugin- the block in
ov.confnamed after the harness (legacy). Both its credential fields and its tuning knobs reach every harness, and for dsh it sits under the settings the cordis host hands the plugin - built-in defaults
Every harness resolves every knob through that order: claude-code, codex, cursor, trae, trae-cn, zcode, opencode, dsh and pi. One plugin section therefore configures all of them, and ov config switch moves behaviour along with credentials. Inside plugin, a per-harness override is found under either spelling of the harness name, so claude_code and claude-code, trae_cn and trae-cn both reach the same object.
The knobs themselves are declared once in lib/config-schema.mjs, with each one's type, default, range, OPENVIKING_* variable and older spellings. An older spelling keeps working: syncTurns sets autoCapture, bypassPatterns sets bypassSessionPatterns, recallRewrite sets recallCompress, requestTimeoutMs sets timeoutMs, recallBudget sets recallTokenBudget, recallScoreThreshold sets scoreThreshold, recallMinQueryLength sets minQueryLength, profileBudget sets profileTokenBudget, recallCompressReasoningEffort sets recallCompressThinking, auth_mode sets authMode, and peer_id sets peerId.
Every file declares version: 1; one declaring another version is skipped with a warning. Schema v1 is peer.source, peer.id, recall.enabled, recall.peer_scope, recall.dedup_turns, recall.max_items, recall.score_threshold, capture.enabled, capture.commit_token_threshold, bypass.session_patterns, and labels. Lists union across layers, and a leading "!reset" clears what was inherited. Unknown keys are kept and ignored.
Workspace files are trusted without a prompt: a hook is non-interactive, and an approval gate would degrade into one command per workspace. What is refused instead is structural — connection and credential keys (url, api_key, account, user, extra_headers, …) are stripped with a warning and ${VAR} is never expanded in these files. What a committed file switches off is announced in ov-memory-doctor rather than blocked.
.gitignore must not ignore all of .openviking/, or config.json can never be committed. Narrow the rule to .openviking/media/ and .openviking/downloads/; doctor warns while it is still blanket.
Cloud recall compression
OPENVIKING_RECALL_COMPRESS=server enables cloud compression across Claude Code,
Codex, OpenCode, DSH, Pi, the Cursor/Trae/Trae CN/ZCode hooks, OpenClaw, and Hermes.
The context-search request sends rewrite: true; the server digest is preferred
over raw rendered context, and stats.rewrite: "no_relevant" suppresses injection.
Claude Code and Codex also support client for local-only compression. Their
auto mode uses local compression when available and otherwise sends
rewrite: "auto". Harnesses without local compressors send rewrite: "auto"
directly. Their existing default remains off; cloud compression is opt-in.
off omits rewrite. The legacy boolean aliases 1 and 0 mean auto and off.
These settings apply to automatic recall. Explicit MCP search calls retain the arguments supplied by the caller. Cloud compression requires a server that supports the context-search rewrite API; older-server fallback behavior remains specific to each integration.