dsh-openviking-memory
Đã xác minh@yadsh/dsh-openviking-memory · v0.5.1 · Apache-2.0 · Giao diện web
OpenViking memory integration for DeepSeek Harness with configurable automatic context injection; derived from the official OpenViking DSH plugin.
Cài đặt
dsh plugin add @yadsh/dsh-openviking-memory Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.
Mã nguồn
Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.
Thẻ
Readme
@yadsh/dsh-openviking-memory
OpenViking memory for DeepSeek Harness: durable memory tools, skills, conversation capture and commit — with automatic profile/recall injection you can switch off entirely.
This package is a community-maintained derivative of OpenViking's official
@openviking/dsh-memory-plugin. It is not maintained or endorsed by the OpenViking project.
Original upstream: https://github.com/volcengine/OpenViking/tree/main/examples/dsh-memory-plugin. Provenance is recorded in UPSTREAM.md.
What it is
A DSH bundle that connects one agent to an OpenViking server and keeps five capabilities independent of each other:
| Capability | What it does |
|---|---|
| Tools | Mounts DSH's MCP bridge against the OpenViking stdio proxy, publishing mcp__openviking__* |
| Skills | Serves the openviking-memory skill so the model knows how to search, read and write memory |
| Capture / commit | Mirrors conversation turns into an OpenViking session and commits it once it grows past a token threshold |
viking:// guard |
Denies any filesystem or shell tool call that is handed an OpenViking URI, with a hint naming the right tool |
| Automatic context presentation | Injects the stored profile at session start and/or before each step, and runs semantic recall per step |
The first four are always on. The fifth is what this fork adds control over.
Why this fork exists
Upstream always injects: every session gets the stored profile, and every step gets a recall round-trip. That is the right default, and it stays the default here. It is the wrong behaviour when you want the model to decide when memory is worth the latency and the tokens:
Now I need memory → call the OpenViking search/read tools.
autoInject: false gives exactly that. It is not "memory disabled": tools,
skills, capture, commit and the URI guard all keep working, so the agent still
learns from the conversation and can still recall on demand. Only the automatic
context presentation branch is switched off — and with it, every profile and
recall HTTP request (not "the request is made and the result dropped").
Differences from upstream
autoInjectplus three granular knobs —injectStartupProfile,injectStepProfile,autoRecall. See the behaviour matrix below.- Zero-work when disabled. With
autoInject: falsethe plugin never calls into the profile or recall machinery, so no profile or recall request is issued at all. - Typed, validated configuration. A Schemastery schema declares ranges and enums, and DSH refuses to load the plugin when a value is out of range instead of silently clamping it. Out-of-range defaults still resolve to the upstream behaviour.
- A settings card in the web UI. The plugin ships a browser bundle, so its configuration is editable from the Plugins page — on this plugin's own row — without touching a patch file. See Settings card.
- Memory per QA account. On a deployment with QA Surface mounted, each account gets its own OpenViking space, and its QA settings dialog shows what that space holds about it — read-only, because the switches that decide whether the assistant uses the memory belong to the deployment. See Per-account memory.
- Repository-conventional package layout. The upstream
.mjssources are ported to TypeScript undersrc/, with a Cordis service, a@yadshstructured log file under<$DSH_HOME>/logs/dsh-openviking-memory/, and the monorepo's build/release tooling. - No bundler group wrapper. Upstream's bundle patch nested the runtime in a
@deepseek-ai/cordis-plugin-grouprow withisolate: { openvikingMemory: true }. This fork uses the canonical single-row patch and provides the service from a CordisServicesubclass instead. See docs/upstream-sync.md for what that means when syncing upstream changes. - Dropped upstream code that no DSH caller used (cross-harness session bypass helpers, rollout-log ingestion, the doctor surface). The complete list is in SPEC.md.
Do not run both plugins at once
Remove or disable the official @openviking/dsh-memory-plugin before enabling
this package. Running both in one profile means duplicate capture, duplicate
recall, duplicate MCP registrations, conflicting services and duplicate skill
providers.
Install
dsh plugin --profile <profile> add @yadsh/dsh-openviking-memory
--profile is required. From a checkout instead:
pnpm nx run @yadsh/dsh-openviking-memory:build
dsh plugin --profile <profile> add ./plugins/dsh-openviking-memory
Quick start
Point the plugin at your OpenViking server and keep the upstream defaults:
- insert:
- id: dsh-openviking-memory
name: "@yadsh/dsh-openviking-memory"
config:
endpoint: http://openviking:1933
Credentials are resolved in upstream's order: explicit config → OPENVIKING_URL
/ OPENVIKING_API_KEY / OPENVIKING_ACCOUNT / OPENVIKING_USER /
OPENVIKING_PEER_ID → ~/.openviking/ovcli.conf → ~/.openviking/ov.conf.
Manual-only mode
Let the model ask for memory instead of injecting it every step:
- insert:
- id: dsh-openviking-memory
name: "@yadsh/dsh-openviking-memory"
config:
endpoint: http://openviking:1933
# No automatic profile or recall injection.
autoInject: false
# Keep learning from the conversation.
syncTurns: true
captureAssistantTurns: true
captureToolResults: false
Manual-only (autoInject: false) |
|
|---|---|
| Automatic startup profile | off |
| Automatic per-step profile | off |
| Automatic semantic recall | off |
| Conversation capture | on |
| Memory commit | on |
| OpenViking MCP tools | on |
| Skills | on |
viking:// guard |
on |
| Model-initiated recall | on |
Upstream-compatible mode — the default, and what you get by not writing any of these keys:
config:
autoInject: true
injectStartupProfile: true
injectStepProfile: true
autoRecall: true
Per-account memory on a QA deployment
One plugin serves every chat, so on a multi-user deployment the OpenViking space has to be split per account — otherwise recall hands one user another user's memories, and capture files one user's conversation where the next user's recall finds it.
With QA Surface (@yadsh/dsh-qa-surface) mounted, the plugin asks it who owns
the session and sends that account as X-OpenViking-User. The behaviour is on
by default and is the operator's: Multi-user memory → qaUserScoping on the
card below, applied to sessions that are already open.
- A chat root resolves to the account that attested it; a delegated child inherits the chat that created it.
- A session nobody has claimed yet is left entirely alone — no profile, no
recall, no capture — until the account's browser half claims it. A
conversation never reads from, or writes into, a space it does not belong to.
The log says
qa_memory_unattributedonce when a session is asked about too early andqa_memory_attributed, with the delay, when the claim lands: the first line on its own means only "not yet", and the ones that never get the second are the chats no account will own. - An admin viewing somebody else's chat resolves to nobody (QA Surface fails closed there), so that view neither reads nor writes memory.
- Without a QA Surface, or with
qaUserScoping: false, the plugin keeps the single deployment-wide identity it always had.
Each account's page shows what that space holds. The signed-in user's QA
settings dialog gets a Память page (backed by the openvikingMemory Remote
namespace): the profile the store keeps about the account, the sections it files
memories under with their entries, and the conversations it has learned from.
The page is read-only. Everything on it was written by conversations themselves, and the switches that decide whether the assistant uses the memory at all are the deployment's (the card above) — a person's settings dialog is the wrong place to switch the product's memory off.
It also says when the space is not what it looks like. The plugin sends the
account as X-OpenViking-User; a store running in API-key mode strips that
header and answers as its own user. The page compares what it asked for with
the identity the store reports (userMemoryOverview → accountApplies), and
names the shared space instead of presenting other accounts' memories as this
one's own.
A read that failed is reported the same way: the page shows its own sentence for
the kind of failure — the store does not answer, it refuses this deployment,
it could not open the account's space — and never the transport's line. What
crosses the Remote boundary is a code (userMemoryOverview → failure), while
the endpoint, the status and the store's own message go to the plugin log as
qa_memory_overview_failed, where an operator can grep them. A call refused
before it reached the plugin is answered from what the transport left in the
browser (HTTP 403 reads as "this session is not let through", an aborted
request as "the deployment is not answering"), and its detail goes to
console.debug.
Per-account overrides of the deployment's plan still exist and are read from
openviking-memory-qa-users.json under $DSH_HOME (qaUserSettingsPath
overrides the path). An override can only narrow the plan, never widen it — and
nothing in the browser writes that file any more: it is an operator's lever,
applied the next time a session asks for its plan.
# A deployment that prefers the old shared space, or a local install that
# mounts a QA surface for other reasons:
config:
qaUserScoping: false
Where the settings live. The configuration card opens from this plugin's row on the Host's Plugins page, and it edits the settings document of the profile that mounts this plugin. A browser reaching the deployment over the network is served that document in
memorymode — the Host's own contract calls it process-local and never writable — and while its form reports that state the card says so in one sentence instead of drawing controls, so the operator configures on the machine that serves the installation. That is how the tab this card left behaved too: the provider that decides it is shared, and §6.17 of SPEC.md carries the citations, plus the one thing a live stand still owes — whether the deployed page serves that row to a non-loopback connection at all. So the card is the operator's surface in access as well as in effect: the QA page above is the face a network browser is meant to use, and it only reports what the memory holds.There is no namespace for the card to register. Since 0.1.7 a field is an editable form field exactly when its schema node is volatile, and the profile entry id is the namespace;
ctx.configForms.get('dsh-openviking-memory')is how the card reaches it. A configuration that declares no volatile knob has no form to edit, and its card renders nothing.
Switching scoping on moves the memory — and nothing moves it back. The space is chosen by the
X-OpenViking-Userheader, so memories written before the switch were filed under the deployment-wide user and appear in no account's space afterwards. There is no merge operation in this plugin, and the QA page is read-only on purpose, so re-filing is an operator's act against the memory API. The path, in order:
- Read which identity the store answers with — the account page shows it (
userMemoryOverview→serverIdentity). Off, that is the shared space every chat uses; on, it is the account's own.- Confirm the store honours the header before moving anything. A store in
api_keymode strips it and answers as the key's own user (accountApplies: falseon every page), and then every write lands in that one space whatever the plugin sends — re-filing would relocate nothing until the store runs intrustedordevmode.- List what the shared space holds (the deployment's own page reads it, and the bridged
mcp__openviking__*tools see it as their caller), decide what is worth keeping, and write those notes into the owning account's space.- Leave the rest where it is. Turning
qaUserScopingoff again returns every chat to the deployment space, so the older memory becomes reachable as it was without two accounts' histories being stitched together.Turn scoping on at the start of a deployment and there is nothing to migrate.
The header is an assertion, not a guarantee. Whether a space is really per account is the memory store's decision: its
trustedanddevauth modes honourX-OpenViking-User, whileapi_keymode strips the header and answers as the key's own user. That is not this plugin's to fix — but it is the plugin's to report, which is why every account page says which space it is actually showing.
What is still deployment-wide. The bridged mcp__openviking__* tools are
one MCP server for the whole process, and DSH's MCP client carries a single
identity for it — so a model-initiated search, read or remember is
issued as the deployment identity, not as the account that asked for it. A fact
the model stored through a tool is therefore shared by every account on the
deployment, and no account's Память page will claim it as that account's
own. Automatic context (profile and recall) and everything the plugin writes for
a session are per account; a tool call the model makes on its own is not.
Closing that gap needs either a per-session MCP identity in DSH's MCP client or
native tool implementations in this plugin.
Behaviour matrix
| Configuration | Startup profile | Per-step profile | Automatic recall |
|---|---|---|---|
| defaults | yes | yes | yes |
autoInject: false |
no | no | no |
injectStartupProfile: false |
no | yes | yes |
injectStepProfile: false |
yes | no | yes |
autoRecall: false |
yes | yes | no |
all three granular false |
no | no | no |
Each granular knob is gated by autoInject, so it can narrow the master switch
but never widen it. When a capability is off, the plugin issues no request on
its behalf.
Configuration
Every key is optional; the default column is what resolveConfig uses when the
key is absent, and it matches upstream.
Settings card
The package ships a browser bundle, so the plugin gets a card on the Plugins
page, opened from its own row in the DSH web UI. It edits the plugin's
dsh-openviking-memory settings namespace directly — no patch file required:
- Sections follow the reference tables below: automatic context
presentation, connection, peer identity, recall, capture and commit, the
multi-user memory switch, plus an advanced group with
skipSubagentSessions, the timeouts and the deprecatedcaptureMode. - Writes are immediate. Toggles and selects apply on change; text and
number fields commit on blur or Enter. Emptying a field clears the override,
so the value falls back to the profile's composition layer — and for the
connection fields that means the
OPENVIKING_*environment variables and credential files stay in charge. A committed change is re-resolved and handed to the running runtime, so an edited switch reaches sessions that are already open; the bridgedmcp__openviking__*tools follow on the next reload, because they are a child process whose transport is fixed when it starts. - Overrides are visible. A field the profile's user layer carries is marked, and a reset action clears every override in one step.
- The card draws no chrome of its own. The Plugins page draws the row's card —
its surface, heading, description line and expand control — and this bundle
mounts the body inside it. The master switch therefore reads from its own
autoInjecttoggle rather than from a header badge. Runtime diagnostics live in the plugin log under<$DSH_HOME>/logs/dsh-openviking-memory/; the card shows the configuration and nothing else, because it has no Remote face. - The card edits the Host's document from loopback. The settings document a
card writes is the profile the Host serves, and a page that reaches the
deployment over the network is served it in
memorymode — process-local, and never writable, in the Host contract's own words — which this card reports as one sentence naming the state, not as a disabled form and not as an empty section. So the operator sets these switches on the machine that serves the installation, including a stand through its loopback port; the surface a network browser is meant to use is the read-only account page described in §Per-account memory. The split is deliberate: the switches below change one shared deployment (endpoint, credentials, which memory is injected), so they belong to the operator, not to whoever opens a chat.
Injection
| Option | Type | Default | Description |
|---|---|---|---|
autoInject |
boolean | true |
Master switch for automatic context presentation |
injectStartupProfile |
boolean | true |
Inject the stored profile once at session start |
injectStepProfile |
boolean | true |
Inject the profile before a step while undelivered |
autoRecall |
boolean | true |
Run automatic semantic recall before each step |
Connection
| Option | Type | Default | Description |
|---|---|---|---|
endpoint |
string | (from credentials, else http://127.0.0.1:1933) |
OpenViking base URL |
apiKey |
string | "" |
Bearer token |
account |
string | "" |
X-OpenViking-Account header |
user |
string | "" |
X-OpenViking-User header |
peerId |
string | "" |
Explicit actor peer id; skips workspace derivation |
workspacePeer |
boolean | true |
Derive a peer id from the workspace |
peerSource |
string | "git" chain |
Peer preset (git, cwd, none) or a template such as team-{dir} |
Recall and profile
| Option | Type | Default | Description |
|---|---|---|---|
recallPeerScope |
"all" | "actor" |
"all" |
Search every peer of the user, or only the caller's |
recallQueryExpansion |
"auto" | "off" |
"auto" |
Server-side query expansion (sent only when you name it) |
recallTokenBudget |
integer 200–50000 | 2000 |
Token budget for one recall block |
recallMaxContentChars |
integer 100–5000 | 500 |
Per-entry character cap |
recallPreferAbstract |
boolean | true |
Use the stored abstract instead of reading the body |
recallLimit |
integer 1–50 | 10 |
Maximum entries per step (naming it switches the server to the client's quota table) |
scoreThreshold |
number 0–1 | 0.35 |
Minimum relevance score |
minQueryLength |
integer 1–64 | 3 |
Shortest prompt that triggers a recall |
profileTokenBudget |
integer 500–50000 | 10000 |
Token budget for the profile block |
recallRewrite |
"off" | "auto" | "client" | "server" |
"off" |
Who builds the digest |
recallDedupTurns |
integer 0–1000 | 5 |
Turns the server de-duplicates against (0 disables) |
recallContextTimeoutMs |
integer 0–600000 | 0 |
Hard deadline for one context request (0 derives it) |
recallMaxTokens |
integer 64–1000000 | 1600 |
Token ceiling for a server-assembled block |
recallCompressMaxBullets |
integer 1–50 | 6 |
Bullet cap when a digest is produced |
Capture and commit
| Option | Type | Default | Description |
|---|---|---|---|
syncTurns |
boolean | true |
Capture conversation turns into the OpenViking session |
captureToolResults |
boolean | false |
Capture tool results as well |
captureMode |
"semantic" | "keyword" |
"semantic" |
Accepted for compatibility; not consumed by this plugin |
captureMaxLength |
integer 200–100000 | 24000 |
Character cap per captured turn |
captureToolMaxChars |
integer 200–1000000 | 1000000 |
Character cap per captured tool payload |
captureAssistantTurns |
boolean | true |
Capture assistant turns as well as user turns |
captureFilters |
string[] | [] |
Sed-style filters: s/pat/rep/, d|pat| (drop), k|pat| (keep only), optionally prefixed user: / assistant: |
skipSubagentSessions |
boolean | false |
Leave delegated subagent sessions entirely alone |
commitTokenThreshold |
integer 1000–1000000 | 20000 |
Commit once the session's pending tokens reach this |
commitKeepRecentCount |
integer 0–1000 | 10 |
Recent turns a commit keeps unsummarized |
Multi-user deployments
| Option | Type | Default | Description |
|---|---|---|---|
qaUserScoping |
boolean | true |
With a QA Surface mounted, keep one memory space per account; an unattributed session is left alone entirely |
qaUserSettingsPath |
string | <$DSH_HOME>/openviking-memory-qa-users.json |
Where the deployment's per-account plan overrides live (no browser writes it) |
Transport
| Option | Type | Default | Description |
|---|---|---|---|
requestTimeoutMs |
integer 1000–120000 | 10000 |
Timeout for one OpenViking request |
mcpToolCallTimeoutMs |
integer 1000–600000 | 60000 |
Timeout for one bridged MCP tool call |
Out-of-range or unknown enum values are rejected by DSH with a validation error
rather than silently coerced. Values supplied through OPENVIKING_* environment
variables keep upstream's clamping behaviour, because they bypass the schema.
OpenViking setup
The plugin talks to a running OpenViking server over HTTP and starts the
bundled stdio MCP proxy as a child process (process.execPath, so it works
inside DSH Desktop where Electron is the executable). Nothing else needs
installing: the MCP bridge itself ships with DSH.
Where memories are filed is OpenViking's decision, not this plugin's. A git
repository derives its peer from origin, so every clone and worktree of one
repository shares one memory; a directory that is neither a repository nor
marked gets no peer of its own. Upstream's ~/.openviking/ovcli.conf is the
usual place to pin actor_peer_id if you want a fixed identity.
Security and privacy
- Credentials never enter agent messages or the log file. API keys and
bearer tokens are only ever sent as request headers; the plugin log records
booleans (
hasApiKey) and never the values. - Capture is a write path, not a read grant.
syncTurnsand its companions control what this plugin sends to your OpenViking; they never widen what a tool call may do. viking://is still guarded. The guard is registered in every mode, includingautoInject: false, and denies filesystem or shell calls handed an OpenViking URI.viking_forgetand the other destructive tools are unchanged. This fork does not add, weaken or auto-approve any tool contract.autoInject: falseis not "memory disabled" — it turns off automatic context presentation only. Treat a profile you no longer want injected as something to remove in OpenViking, not something this knob hides.- The offline pending queue writes raw conversation payloads to
~/.openviking/pendingwith0700/0600permissions; pointOPENVIKING_PENDING_DIRelsewhere if that location does not suit your threat model.
Upstream & attribution
This package is derived from OpenViking's @openviking/dsh-memory-plugin
(Apache License 2.0), imported from a pinned upstream commit:
- Project: OpenViking
- Original package:
@openviking/dsh-memory-plugin(pathexamples/dsh-memory-plugin) - Original authors: OpenViking / Volcengine contributors
- Upstream revision:
688f78e923d2269d96c27096fe2dad10156ebdb8(version0.3.2) - License: Apache License 2.0 — see LICENSE
- Full provenance and the list of local modifications: UPSTREAM.md
- Upstream sync process: docs/upstream-sync.md
Files ported from upstream keep an attribution header naming the original project, and no upstream copyright or attribution notice was removed.
Compatibility
- DeepSeek Harness
>=0.1.7-rc.2 <0.2.0 - Node.js
^22.19.0 || >=24.0.0
See compatibility.json in the installed package for the machine-readable form.
Development
pnpm nx run @yadsh/dsh-openviking-memory:build
pnpm nx run @yadsh/dsh-openviking-memory:lint
pnpm nx run @yadsh/dsh-openviking-memory:typecheck
pnpm nx run @yadsh/dsh-openviking-memory:test
pnpm nx run @yadsh/dsh-openviking-memory:verify
The product contract lives in SPEC.md.
License
Apache License 2.0, inherited from the upstream package this fork is derived from. See LICENSE.