@openviking/dsh-memory-plugin 0.3.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -22
- package/capture.mjs +14 -0
- package/client.mjs +6 -155
- package/config.mjs +36 -166
- package/index.mjs +17 -4
- package/mcp-env.mjs +33 -0
- package/mcp.mjs +2 -33
- package/package.json +9 -6
- package/runtime.mjs +115 -28
- package/servers/mcp-proxy.mjs +13 -19
- package/shared/capture-utils.mjs +146 -7
- package/shared/config-schema.mjs +359 -0
- package/shared/credentials.mjs +238 -102
- package/shared/input-filters.mjs +219 -0
- package/shared/mcp-proxy-config.mjs +171 -9
- package/shared/mcp-proxy-core.mjs +35 -15
- package/shared/ov-http.mjs +112 -0
- package/shared/pending-queue.mjs +41 -5
- package/shared/plugin-config.mjs +239 -29
- package/shared/recall-compress-core.mjs +17 -6
- package/shared/recall-core.mjs +128 -10
- package/shared/uri-guard.mjs +173 -1
- package/shared/workspace-config.mjs +433 -0
- package/shared/workspace-identity.mjs +334 -0
- package/shared/workspace-peer.mjs +189 -8
- package/shared/workspace-registry.mjs +87 -0
- package/skills/openviking-memory/SKILL.md +19 -0
- package/skills.mjs +3 -4
- package/uri-guard.mjs +20 -10
- package/shared/setup-wizard.mjs +0 -114
package/README.md
CHANGED
|
@@ -29,10 +29,12 @@ be a real package. Linking a source checkout (`dsh plugin --profile web add
|
|
|
29
29
|
./examples/dsh-memory-plugin`) only works when that checkout has its own
|
|
30
30
|
`node_modules`, because Node resolves the bundle's dsh peers from the source
|
|
31
31
|
tree's realpath rather than from the profile.
|
|
32
|
+
It also needs `node examples/memory-plugin-shared/sync.mjs` run first: the
|
|
33
|
+
`shared/` modules the bundle imports are generated, not committed.
|
|
32
34
|
|
|
33
35
|
## Requirements
|
|
34
36
|
|
|
35
|
-
- `@deepseek-ai/dsh` `0.1.0-rc.6` or
|
|
37
|
+
- `@deepseek-ai/dsh` `0.1.0-rc.6`, `0.1.5-rc.1`, or `0.1.5-rc.2`; stable `0.1.x` releases are also admitted by the peer range
|
|
36
38
|
- Node.js `^22.19.0` or `>=24`
|
|
37
39
|
- A reachable OpenViking server
|
|
38
40
|
|
|
@@ -40,17 +42,62 @@ The bundle has no runtime npm dependencies. Its message structures come from
|
|
|
40
42
|
`createUserMessage` (`@deepseek-ai/dsh-llm`), its tool surface from
|
|
41
43
|
`@deepseek-ai/dsh-mcp-client`, and its skill provider from
|
|
42
44
|
`@deepseek-ai/dsh-skill-filesystem` — peerDependencies that DSH itself already
|
|
43
|
-
installs
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
installs. Keep those packages in the host installation rather than adding
|
|
46
|
+
individual DSH core packages to the profile. DSH initializes profiles with
|
|
47
|
+
`nodeLinker: hoisted` and `autoInstallPeers: false`, then makes the host packages
|
|
48
|
+
available through its module fallback at boot. A missing-peer warning during
|
|
49
|
+
`dsh plugin add` alone does not prove startup is broken.
|
|
50
|
+
|
|
51
|
+
The peer range is `>=0.1.0-rc.6 <0.2.0 || ^0.1.5-rc.1`. The second clause
|
|
52
|
+
explicitly admits the `0.1.5` release candidates: semver does not include them
|
|
53
|
+
in the first clause merely because they compare above `0.1.0-rc.6`. Other
|
|
54
|
+
pre-release series need separate verification. Local devDependencies and
|
|
55
|
+
`overrides` stay pinned to rc.6 to exercise the minimum supported contract.
|
|
48
56
|
|
|
49
57
|
Everything under `shared/` and `skills/` is generated by
|
|
50
58
|
`node examples/memory-plugin-shared/sync.mjs` — edit
|
|
51
59
|
`examples/memory-plugin-shared/lib/` and `examples/skills/`, never the vendored
|
|
52
60
|
copies.
|
|
53
61
|
|
|
62
|
+
## Troubleshooting startup
|
|
63
|
+
|
|
64
|
+
If startup reports that `@deepseek-ai/dsh-llm` does not export `assertNever`,
|
|
65
|
+
inspect the affected profile before changing dependencies:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
dsh --version
|
|
69
|
+
dsh plugin --profile web why @deepseek-ai/dsh-skill
|
|
70
|
+
dsh plugin --profile web why @deepseek-ai/dsh-skill-filesystem
|
|
71
|
+
dsh plugin --profile web why @deepseek-ai/dsh-llm
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Replace `web` with the affected profile name. Check its `package.json`,
|
|
75
|
+
`pnpm-lock.yaml`, and `pnpm-workspace.yaml` under
|
|
76
|
+
`${DSH_HOME:-$HOME/.dsh}/profiles/<profile>/`, including dependency overrides.
|
|
77
|
+
`dsh-skill@0.0.1-rc.1` imports `assertNever` from the old location. A profile-local
|
|
78
|
+
copy can shadow the newer host package and break both DSH's own skill loader
|
|
79
|
+
and this plugin. As of 2026-09-14, the skill and skill-filesystem npm `latest`
|
|
80
|
+
tags still point to `0.0.1` release candidates; installing either without a
|
|
81
|
+
version is not an upgrade strategy.
|
|
82
|
+
|
|
83
|
+
If these old packages are direct profile dependencies added only to satisfy
|
|
84
|
+
this plugin, remove those constraints and let DSH supply its matching packages:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dsh plugin --profile web remove @deepseek-ai/dsh-skill @deepseek-ai/dsh-skill-filesystem
|
|
88
|
+
dsh plugin --profile web add @openviking/dsh-memory-plugin@latest
|
|
89
|
+
dsh --profile web
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If another plugin requires them, update that plugin's constraints instead of
|
|
93
|
+
removing them blindly. Keep `autoInstallPeers: false` and `nodeLinker: hoisted`
|
|
94
|
+
in the profile's pnpm settings. Do not patch the import in `node_modules`: a
|
|
95
|
+
mixed DSH package family can have other incompatible interfaces. Updating this
|
|
96
|
+
memory plugin alone does not remove conflicting profile-local packages.
|
|
97
|
+
|
|
98
|
+
Use `dsh --profile <profile>` to start a named profile. `dsh web` is an alias for
|
|
99
|
+
the stock `web` profile, not a subcommand to append after `--profile`.
|
|
100
|
+
|
|
54
101
|
## Design notes
|
|
55
102
|
|
|
56
103
|
### Why injection uses pre-step user messages, not the system prompt
|
|
@@ -77,18 +124,17 @@ SDK client opens that standalone stream it stops resolving POST responses, so
|
|
|
77
124
|
`tools/list` never returns. The proxy owns the transport itself and is
|
|
78
125
|
unaffected.
|
|
79
126
|
|
|
80
|
-
The bundle's resolved
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
The bundle's resolved connection — url, MCP url, key, account, user and auth
|
|
128
|
+
mode — travels to the proxy through the child environment, because DSH scrubs
|
|
129
|
+
credential-shaped names out of the inherited env and a subprocess cannot see the
|
|
130
|
+
Cordis patch. It goes with `OPENVIKING_CREDENTIAL_SOURCE=env`, so the proxy reads
|
|
131
|
+
no config file and reaches the server exactly as the runtime does, including
|
|
132
|
+
with no key when the runtime has none. A changed `ovcli.conf` therefore takes
|
|
133
|
+
effect for the tools when DSH restarts the bundle, as it does for the runtime.
|
|
85
134
|
|
|
86
135
|
Two consequences follow from the proxy being one process per profile:
|
|
87
136
|
|
|
88
|
-
- **The actor peer is process-level.** Recall, capture, and commit still resolve
|
|
89
|
-
a peer per session from that session's workspace, but tool calls carry the
|
|
90
|
-
peer resolved at boot. Set `OPENVIKING_PEER_ID` when one process serves
|
|
91
|
-
several workspaces and you need tool calls attributed exactly.
|
|
137
|
+
- **The actor peer is process-level.** Recall, capture, and commit still resolve a peer per session from that session's workspace repository, but tool calls carry the peer resolved at boot. Set `OPENVIKING_PEER_ID` when one process serves several repositories and you need tool calls attributed exactly.
|
|
92
138
|
- **`remember` is not session-scoped.** The server's MCP `remember` stores into
|
|
93
139
|
its own short-lived session rather than the live `dsh-<session-id>` stream —
|
|
94
140
|
the same behavior the Claude Code, Codex, and Cursor integrations have.
|
|
@@ -100,9 +146,11 @@ lifecycle registration, so a proxy that cannot start holds up nothing above it.
|
|
|
100
146
|
### Why the skill gets its own provider
|
|
101
147
|
|
|
102
148
|
`skills.mjs` registers a second `ctx.skills` provider with
|
|
103
|
-
`includeDefaultRoots: false
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
`includeDefaultRoots: false` and `bundledSkillDir` pointing at `skills/`. DSH
|
|
150
|
+
reads this host-owned bundle directly rather than asking a workspace filesystem
|
|
151
|
+
to resolve a path outside its root. Its existing `filesystem` provider still
|
|
152
|
+
owns the project and user skill roots; the bundle does not duplicate that
|
|
153
|
+
catalog or override higher-priority project skills.
|
|
106
154
|
|
|
107
155
|
## Configuration
|
|
108
156
|
|
|
@@ -121,7 +169,7 @@ Common environment variables:
|
|
|
121
169
|
| `OPENVIKING_ACCOUNT` | Trusted-mode account |
|
|
122
170
|
| `OPENVIKING_USER` | Trusted-mode user |
|
|
123
171
|
| `OPENVIKING_PEER_ID` | Explicit actor peer |
|
|
124
|
-
| `OPENVIKING_WORKSPACE_PEER` | Derive a peer from each DSH session workspace by default |
|
|
172
|
+
| `OPENVIKING_WORKSPACE_PEER` | Derive a peer from each DSH session workspace's git identity by default; `0` sends no peer |
|
|
125
173
|
| `OPENVIKING_RECALL_PEER_SCOPE` | `all` for cross-workspace recall or `actor` for isolation |
|
|
126
174
|
|
|
127
175
|
The patch can also carry plugin config:
|
|
@@ -153,10 +201,12 @@ The patch can also carry plugin config:
|
|
|
153
201
|
- `session/event` captures user, assistant, and optionally tool-result messages without scraping a transcript.
|
|
154
202
|
- `turn/end` checks the OpenViking pending-token threshold and commits when required.
|
|
155
203
|
- `skipSubagentSessions: true` excludes sessions marked with `header.origin: subagent` from automatic profile, recall, capture, and commit; it defaults to `false`.
|
|
156
|
-
-
|
|
157
|
-
- `
|
|
204
|
+
- `syncTurns: false` stops every new write: no captured messages, no threshold or shutdown commit. Writes queued while the toggle was on are still replayed by the background drainer once the server recovers — they were captured with the toggle on. Profile injection and recall are unaffected; it defaults to `true`.
|
|
205
|
+
- Failed writes enter the shared OpenViking pending queue. A background drainer (default every 60s, `OPENVIKING_PENDING_DRAIN_INTERVAL_MS`) probes the server health and replays the queue in-process, so a transient write failure recovers without restarting dsh; it does not consume the session-start retry budget. Session-start replays keep consuming retries as before.
|
|
206
|
+
- `tools/pre-execute` denies a DSH filesystem tool (`read`, `glob`, `grep`, `edit`, `write`, `str_replace_editor`) whose path argument is a `viking://` URI, pointing the model at the bridged `mcp__openviking__*` tools instead. A `grep` whose pattern is `viking://` text still runs.
|
|
207
|
+
- `tools/post-execute` lets a `bash` command that carries a `viking://` URI run unchanged and attaches a notice for the model: use the bridged tools if it meant OpenViking content, or ignore the notice when the URI is intentional data such as an `ov` argument or an HTTP payload.
|
|
158
208
|
|
|
159
|
-
Each DSH session maps to `dsh-<session-id>` in OpenViking. Workspace-derived actor peers are resolved per session and sent on every session-specific request.
|
|
209
|
+
Each DSH session maps to `dsh-<session-id>` in OpenViking. Workspace-derived actor peers are resolved per session and sent on every session-specific request: the peer is the git identity of the session's workspace — the normalized `origin` URL (`git@github.com:volcengine/OpenViking.git` becomes `github.com-volcengine-openviking`), else the repository root path, that fallback keeping the older rule where every non-letter-or-digit character becomes `-`. Outside a git repository no peer is sent at all, and what is remembered there goes to the user-level space `viking://user/<you>/memories`. One repository therefore keeps one peer across subdirectories, worktrees, clones and machines, while a fork's different origin keeps it separate. DSH does not read workspace `.openviking/config.json` files, so a `peer.id` written there has no effect; pin a peer with `OPENVIKING_PEER_ID` instead. Memories written under the older path-derived peer stay reachable: the default `recallPeerScope: all` sweeps every peer under the user.
|
|
160
210
|
|
|
161
211
|
## Tools
|
|
162
212
|
|
|
@@ -184,6 +234,20 @@ npm run check # syntax check every shipped module + package/PLUGIN_VERSION agr
|
|
|
184
234
|
npm test # node --test *.test.mjs — runs in the repo's PR workflow
|
|
185
235
|
```
|
|
186
236
|
|
|
237
|
+
The `0.3.2` compatibility check installed the packed bundle through
|
|
238
|
+
`dsh plugin add` in isolated profiles and exercised DSH `0.1.0-rc.6`,
|
|
239
|
+
`0.1.5-rc.1`, and `0.1.5-rc.2` with matching core packages. It covered Web startup
|
|
240
|
+
and browser loading, skill discovery/read, MCP tool calls, profile/recall
|
|
241
|
+
injection, user/assistant capture, resume deduplication, threshold commits, and
|
|
242
|
+
disposal commits. The check used a local HTTP/MCP fixture and a deterministic
|
|
243
|
+
model adapter on macOS with Node.js 26.5.0; it verifies harness integration, not
|
|
244
|
+
real-server memory extraction. The rc.2 CLI also exited cleanly on SIGTERM.
|
|
245
|
+
|
|
246
|
+
The startup failure from issue #4944 was reproduced by adding the reported
|
|
247
|
+
old skill packages to an rc.2 profile; removing those direct dependencies and
|
|
248
|
+
reinstalling the bundle restored the same integration checks. A clean rc.2
|
|
249
|
+
profile with the published `0.3.0` bundle did not reproduce that import error.
|
|
250
|
+
|
|
187
251
|
`live-recall.test.mjs` is an opt-in end-to-end gate against a real OpenViking
|
|
188
252
|
server: it stores a sentinel memory through a session commit, waits for
|
|
189
253
|
extraction, and asserts recall returns that sentinel — the property no stub
|
package/capture.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
1
2
|
import {
|
|
2
3
|
extractPartsFromPayload,
|
|
3
4
|
extractTextFromPayload,
|
|
@@ -6,6 +7,19 @@ import {
|
|
|
6
7
|
|
|
7
8
|
export const OPENVIKING_PLUGIN_SOURCE = "openviking-memory";
|
|
8
9
|
|
|
10
|
+
export function pluginMessage(content, source) {
|
|
11
|
+
// dsh's own constructor: identity, normalization, and any future Message
|
|
12
|
+
// invariants come from the pinned peer instead of a hand-built object.
|
|
13
|
+
return createUserMessage({
|
|
14
|
+
content: [{ type: "text", text: content }],
|
|
15
|
+
source: {
|
|
16
|
+
kind: "plugin",
|
|
17
|
+
plugin: OPENVIKING_PLUGIN_SOURCE,
|
|
18
|
+
...source,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function captureEvent(event, config, toolNames = new Map()) {
|
|
10
24
|
if (!event || typeof event !== "object") return null;
|
|
11
25
|
if (event.type === "tool/call") {
|
package/client.mjs
CHANGED
|
@@ -1,67 +1,13 @@
|
|
|
1
|
+
import { createOvHttp } from "./shared/ov-http.mjs";
|
|
2
|
+
|
|
1
3
|
export class OpenVikingClient {
|
|
2
4
|
constructor(config) {
|
|
3
5
|
this.config = config;
|
|
4
6
|
this.connected = false;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (this.config.apiKey) headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
10
|
-
if (this.config.account) headers["X-OpenViking-Account"] = this.config.account;
|
|
11
|
-
if (this.config.user) headers["X-OpenViking-User"] = this.config.user;
|
|
12
|
-
const actorPeerId = options.actorPeerId ?? this.config.peerId;
|
|
13
|
-
if (actorPeerId) headers["X-OpenViking-Actor-Peer"] = actorPeerId;
|
|
14
|
-
if (this.config.userAgent) headers["User-Agent"] = this.config.userAgent;
|
|
15
|
-
return headers;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async fetchJSON(path, init = {}, options = {}) {
|
|
19
|
-
const timeoutMs = options.timeoutMs ?? this.config.requestTimeoutMs;
|
|
20
|
-
const controller = new AbortController();
|
|
21
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
-
try {
|
|
23
|
-
const response = await fetch(`${this.config.endpoint}${path}`, {
|
|
24
|
-
...init,
|
|
25
|
-
headers: {
|
|
26
|
-
...this.headers(options),
|
|
27
|
-
...(init.headers || {}),
|
|
28
|
-
},
|
|
29
|
-
signal: controller.signal,
|
|
30
|
-
});
|
|
31
|
-
const body = await response.json().catch(() => ({}));
|
|
32
|
-
const traceId = body?.result?.trace_id
|
|
33
|
-
|| body?.error?.trace_id
|
|
34
|
-
|| body?.trace_id
|
|
35
|
-
|| undefined;
|
|
36
|
-
if (!response.ok || body?.status === "error") {
|
|
37
|
-
return {
|
|
38
|
-
ok: false,
|
|
39
|
-
result: null,
|
|
40
|
-
status: response.status,
|
|
41
|
-
error: body?.error || { message: `HTTP ${response.status}` },
|
|
42
|
-
traceId,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
ok: true,
|
|
47
|
-
result: body?.result ?? body,
|
|
48
|
-
status: response.status,
|
|
49
|
-
traceId,
|
|
50
|
-
};
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
result: null,
|
|
55
|
-
status: 0,
|
|
56
|
-
error: { message: error instanceof Error ? error.message : String(error) },
|
|
57
|
-
};
|
|
58
|
-
} finally {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async health() {
|
|
64
|
-
return (await this.healthResult()).ok;
|
|
7
|
+
this.fetchJSON = createOvHttp(config, {
|
|
8
|
+
defaultTimeoutMs: config.requestTimeoutMs,
|
|
9
|
+
resolveActorPeerId: () => this.config.peerId,
|
|
10
|
+
});
|
|
65
11
|
}
|
|
66
12
|
|
|
67
13
|
async healthResult() {
|
|
@@ -70,12 +16,6 @@ export class OpenVikingClient {
|
|
|
70
16
|
return response;
|
|
71
17
|
}
|
|
72
18
|
|
|
73
|
-
async ensureSession(sessionId, actorPeerId) {
|
|
74
|
-
const response = await this.ensureSessionResult(sessionId, actorPeerId);
|
|
75
|
-
return response.ok
|
|
76
|
-
|| (response.status === 409 && response.error?.code === "ALREADY_EXISTS");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
19
|
async ensureSessionResult(sessionId, actorPeerId) {
|
|
80
20
|
const response = await this.fetchJSON("/api/v1/sessions", {
|
|
81
21
|
method: "POST",
|
|
@@ -93,15 +33,6 @@ export class OpenVikingClient {
|
|
|
93
33
|
return response.ok ? response.result : null;
|
|
94
34
|
}
|
|
95
35
|
|
|
96
|
-
async getSessionArchive(sessionId, archiveId, actorPeerId) {
|
|
97
|
-
const response = await this.fetchJSON(
|
|
98
|
-
`/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`,
|
|
99
|
-
{},
|
|
100
|
-
{ actorPeerId },
|
|
101
|
-
);
|
|
102
|
-
return response.ok ? response.result : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
36
|
async addMessage(sessionId, payload, actorPeerId) {
|
|
106
37
|
return this.fetchJSON(
|
|
107
38
|
`/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
@@ -123,84 +54,4 @@ export class OpenVikingClient {
|
|
|
123
54
|
{ timeoutMs: options.timeoutMs ?? 30000, actorPeerId },
|
|
124
55
|
);
|
|
125
56
|
}
|
|
126
|
-
|
|
127
|
-
async find(query, options = {}) {
|
|
128
|
-
const body = { query };
|
|
129
|
-
if (options.targetUri) body.target_uri = options.targetUri;
|
|
130
|
-
if (options.limit) body.limit = options.limit;
|
|
131
|
-
if (options.scoreThreshold !== undefined) body.score_threshold = options.scoreThreshold;
|
|
132
|
-
const response = await this.fetchJSON("/api/v1/search/find", {
|
|
133
|
-
method: "POST",
|
|
134
|
-
body: JSON.stringify(body),
|
|
135
|
-
}, { actorPeerId: options.actorPeerId });
|
|
136
|
-
if (!response.ok || !response.result) return [];
|
|
137
|
-
|
|
138
|
-
const results = [];
|
|
139
|
-
for (const bucket of ["memories", "resources", "skills"]) {
|
|
140
|
-
const entries = response.result[bucket];
|
|
141
|
-
if (!Array.isArray(entries)) continue;
|
|
142
|
-
for (const entry of entries) {
|
|
143
|
-
results.push({
|
|
144
|
-
uri: entry?.uri || "",
|
|
145
|
-
contextType: entry?.context_type
|
|
146
|
-
|| (bucket === "memories" ? "memory" : bucket === "skills" ? "skill" : "resource"),
|
|
147
|
-
score: Number(entry?.score || 0),
|
|
148
|
-
abstract: entry?.abstract || "",
|
|
149
|
-
overview: entry?.overview || null,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return results;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async read(uri, level, actorPeerId) {
|
|
157
|
-
const endpoint = level === "abstract"
|
|
158
|
-
? "abstract"
|
|
159
|
-
: level === "overview"
|
|
160
|
-
? "overview"
|
|
161
|
-
: "read";
|
|
162
|
-
const response = await this.fetchJSON(
|
|
163
|
-
`/api/v1/content/${endpoint}?uri=${encodeURIComponent(uri)}`,
|
|
164
|
-
{},
|
|
165
|
-
{ actorPeerId },
|
|
166
|
-
);
|
|
167
|
-
return response.ok ? response.result : null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async list(uri, actorPeerId) {
|
|
171
|
-
const response = await this.fetchJSON(
|
|
172
|
-
`/api/v1/fs/ls?uri=${encodeURIComponent(uri)}&output=original`,
|
|
173
|
-
{},
|
|
174
|
-
{ actorPeerId },
|
|
175
|
-
);
|
|
176
|
-
return response.ok && Array.isArray(response.result) ? response.result : [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async stat(uri, actorPeerId) {
|
|
180
|
-
const response = await this.fetchJSON(
|
|
181
|
-
`/api/v1/fs/stat?uri=${encodeURIComponent(uri)}`,
|
|
182
|
-
{},
|
|
183
|
-
{ actorPeerId },
|
|
184
|
-
);
|
|
185
|
-
return response.ok ? response.result : null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async forget(uri, recursive = false, actorPeerId) {
|
|
189
|
-
const response = await this.fetchJSON(
|
|
190
|
-
`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=${recursive}`,
|
|
191
|
-
{ method: "DELETE" },
|
|
192
|
-
{ actorPeerId },
|
|
193
|
-
);
|
|
194
|
-
return response.ok;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async addResource(path, reason, actorPeerId) {
|
|
198
|
-
const body = { path };
|
|
199
|
-
if (reason) body.reason = reason;
|
|
200
|
-
const response = await this.fetchJSON("/api/v1/resources", {
|
|
201
|
-
method: "POST",
|
|
202
|
-
body: JSON.stringify(body),
|
|
203
|
-
}, { timeoutMs: 30000, actorPeerId });
|
|
204
|
-
return response.ok ? response.result : null;
|
|
205
|
-
}
|
|
206
57
|
}
|
package/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { buildPluginConfig } from "./shared/plugin-config.mjs";
|
|
2
|
+
import { loadCredentialFiles } from "./shared/credentials.mjs";
|
|
3
3
|
|
|
4
|
-
export const PLUGIN_VERSION = "0.3
|
|
4
|
+
export const PLUGIN_VERSION = "0.4.3";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Namespace for the bridged OpenViking MCP tools. DSH publishes every MCP tool
|
|
@@ -10,168 +10,38 @@ export const PLUGIN_VERSION = "0.3.0";
|
|
|
10
10
|
*/
|
|
11
11
|
export const MCP_SERVER_NAME = "openviking";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
syncTurns: true,
|
|
25
|
-
recallTokenBudget: 2000,
|
|
26
|
-
recallMaxContentChars: 500,
|
|
27
|
-
recallPreferAbstract: true,
|
|
28
|
-
recallLimit: 10,
|
|
29
|
-
recallLimitConfigured: false,
|
|
30
|
-
scoreThreshold: 0.35,
|
|
31
|
-
minQueryLength: 3,
|
|
32
|
-
profileTokenBudget: 10000,
|
|
33
|
-
commitTokenThreshold: 20000,
|
|
34
|
-
commitKeepRecentCount: 10,
|
|
35
|
-
captureToolResults: false,
|
|
36
|
-
captureMode: "semantic",
|
|
37
|
-
captureMaxLength: 24000,
|
|
38
|
-
captureToolMaxChars: 1000000,
|
|
39
|
-
captureAssistantTurns: true,
|
|
40
|
-
skipSubagentSessions: false,
|
|
41
|
-
requestTimeoutMs: 10000,
|
|
42
|
-
mcpToolCallTimeoutMs: 60000,
|
|
43
|
-
});
|
|
44
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the plugin's configuration.
|
|
15
|
+
*
|
|
16
|
+
* `input` is what the cordis host hands the plugin — this harness has no config
|
|
17
|
+
* file of its own. Every knob is declared in `shared/config-schema.mjs`, and
|
|
18
|
+
* `ovcli.conf`'s `plugin` section outranks the host's input so one file
|
|
19
|
+
* configures every harness and `ov config switch` moves them together.
|
|
20
|
+
* Connection fields are the exception: an endpoint, key, identity or auth mode
|
|
21
|
+
* named by the host is the more specific answer and stays ahead of the
|
|
22
|
+
* credential chain.
|
|
23
|
+
*/
|
|
45
24
|
export function resolveConfig(input = {}, env = process.env, cwd = process.cwd()) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (env.OPENVIKING_RECALL_PEER_SCOPE) {
|
|
69
|
-
config.recallPeerScope = env.OPENVIKING_RECALL_PEER_SCOPE;
|
|
70
|
-
}
|
|
71
|
-
if (env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
72
|
-
config.recallQueryExpansion = env.OPENVIKING_RECALL_QUERY_EXPANSION;
|
|
73
|
-
config.recallQueryExpansionConfigured = true;
|
|
74
|
-
}
|
|
75
|
-
if (env.OPENVIKING_RECALL_LIMIT) {
|
|
76
|
-
config.recallLimit = env.OPENVIKING_RECALL_LIMIT;
|
|
77
|
-
config.recallLimitConfigured = true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
config.endpoint = String(config.endpoint || DEFAULT_CONFIG.endpoint).replace(/\/+$/, "");
|
|
81
|
-
config.workspacePeer = config.workspacePeer !== false;
|
|
82
|
-
config.peerId = resolveEffectivePeerId({ cfg: config, cwd }).peerId;
|
|
83
|
-
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all";
|
|
84
|
-
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto";
|
|
85
|
-
config.recallLimit = clampInteger(config.recallLimit, 1, 50, DEFAULT_CONFIG.recallLimit);
|
|
86
|
-
config.recallMaxContentChars = clampInteger(
|
|
87
|
-
config.recallMaxContentChars,
|
|
88
|
-
100,
|
|
89
|
-
5000,
|
|
90
|
-
DEFAULT_CONFIG.recallMaxContentChars,
|
|
91
|
-
);
|
|
92
|
-
config.recallTokenBudget = clampInteger(
|
|
93
|
-
config.recallTokenBudget,
|
|
94
|
-
200,
|
|
95
|
-
50000,
|
|
96
|
-
DEFAULT_CONFIG.recallTokenBudget,
|
|
97
|
-
);
|
|
98
|
-
config.scoreThreshold = clampNumber(
|
|
99
|
-
config.scoreThreshold,
|
|
100
|
-
0,
|
|
101
|
-
1,
|
|
102
|
-
DEFAULT_CONFIG.scoreThreshold,
|
|
103
|
-
);
|
|
104
|
-
config.minQueryLength = clampInteger(
|
|
105
|
-
config.minQueryLength,
|
|
106
|
-
1,
|
|
107
|
-
64,
|
|
108
|
-
DEFAULT_CONFIG.minQueryLength,
|
|
109
|
-
);
|
|
110
|
-
config.profileTokenBudget = clampInteger(
|
|
111
|
-
config.profileTokenBudget,
|
|
112
|
-
500,
|
|
113
|
-
50000,
|
|
114
|
-
DEFAULT_CONFIG.profileTokenBudget,
|
|
115
|
-
);
|
|
116
|
-
config.commitTokenThreshold = clampInteger(
|
|
117
|
-
config.commitTokenThreshold,
|
|
118
|
-
1000,
|
|
119
|
-
1000000,
|
|
120
|
-
DEFAULT_CONFIG.commitTokenThreshold,
|
|
121
|
-
);
|
|
122
|
-
config.commitKeepRecentCount = clampInteger(
|
|
123
|
-
config.commitKeepRecentCount,
|
|
124
|
-
0,
|
|
125
|
-
1000,
|
|
126
|
-
DEFAULT_CONFIG.commitKeepRecentCount,
|
|
127
|
-
);
|
|
128
|
-
config.captureMaxLength = clampInteger(
|
|
129
|
-
config.captureMaxLength,
|
|
130
|
-
200,
|
|
131
|
-
100000,
|
|
132
|
-
DEFAULT_CONFIG.captureMaxLength,
|
|
133
|
-
);
|
|
134
|
-
config.captureToolMaxChars = clampInteger(
|
|
135
|
-
config.captureToolMaxChars,
|
|
136
|
-
200,
|
|
137
|
-
1000000,
|
|
138
|
-
DEFAULT_CONFIG.captureToolMaxChars,
|
|
139
|
-
);
|
|
140
|
-
config.requestTimeoutMs = clampInteger(
|
|
141
|
-
config.requestTimeoutMs,
|
|
142
|
-
1000,
|
|
143
|
-
120000,
|
|
144
|
-
DEFAULT_CONFIG.requestTimeoutMs,
|
|
145
|
-
);
|
|
146
|
-
config.mcpToolCallTimeoutMs = clampInteger(
|
|
147
|
-
config.mcpToolCallTimeoutMs,
|
|
148
|
-
1000,
|
|
149
|
-
600000,
|
|
150
|
-
DEFAULT_CONFIG.mcpToolCallTimeoutMs,
|
|
151
|
-
);
|
|
152
|
-
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic";
|
|
153
|
-
config.syncTurns = config.syncTurns !== false;
|
|
154
|
-
config.captureAssistantTurns = config.captureAssistantTurns !== false;
|
|
155
|
-
config.captureToolResults = config.captureToolResults === true;
|
|
156
|
-
config.skipSubagentSessions = config.skipSubagentSessions === true;
|
|
157
|
-
return config;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function environmentBoolean(value, fallback) {
|
|
161
|
-
const normalized = String(value || "").trim().toLowerCase();
|
|
162
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
163
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
164
|
-
return fallback;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function clampInteger(value, minimum, maximum, fallback) {
|
|
168
|
-
const number = Math.round(Number(value));
|
|
169
|
-
if (!Number.isFinite(number)) return fallback;
|
|
170
|
-
return Math.max(minimum, Math.min(maximum, number));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function clampNumber(value, minimum, maximum, fallback) {
|
|
174
|
-
const number = Number(value);
|
|
175
|
-
if (!Number.isFinite(number)) return fallback;
|
|
176
|
-
return Math.max(minimum, Math.min(maximum, number));
|
|
25
|
+
// ov.conf's `dsh` section is the legacy layer here as everywhere else, but
|
|
26
|
+
// the cordis patch shares that slot; the host named this process's settings,
|
|
27
|
+
// so it wins the overlap.
|
|
28
|
+
const ovSection = loadCredentialFiles(env).ovFile.dsh;
|
|
29
|
+
const config = buildPluginConfig("dsh", {
|
|
30
|
+
env,
|
|
31
|
+
cwd,
|
|
32
|
+
legacy: { ...(ovSection && typeof ovSection === "object" ? ovSection : {}), ...input },
|
|
33
|
+
version: PLUGIN_VERSION,
|
|
34
|
+
hostInput: {
|
|
35
|
+
peerId: input.peerId,
|
|
36
|
+
account: input.account,
|
|
37
|
+
user: input.user,
|
|
38
|
+
apiKey: input.apiKey,
|
|
39
|
+
baseUrl: input.endpoint,
|
|
40
|
+
// The legacy knob layer this used to ride accepted both spellings.
|
|
41
|
+
authMode: input.authMode || input.auth_mode,
|
|
42
|
+
},
|
|
43
|
+
deriveEffectivePeer: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { ...config, peerId: config.effectivePeer.peerId };
|
|
177
47
|
}
|
package/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { injectStartupProfile } from "./lifecycle.mjs";
|
|
|
4
4
|
import { mountOpenVikingMcp } from "./mcp.mjs";
|
|
5
5
|
import { OpenVikingRuntime } from "./runtime.mjs";
|
|
6
6
|
import { mountOpenVikingSkills } from "./skills.mjs";
|
|
7
|
-
import { guardVikingUri } from "./uri-guard.mjs";
|
|
7
|
+
import { guardVikingUri, noticeVikingUri } from "./uri-guard.mjs";
|
|
8
8
|
|
|
9
9
|
export const name = "openviking-memory";
|
|
10
10
|
export const inject = ["agents", "sessions", "tools"];
|
|
@@ -21,6 +21,14 @@ export function apply(ctx, input = {}) {
|
|
|
21
21
|
() => () => runtime.disposeAll(),
|
|
22
22
|
"openvikingMemory.disposeAll()",
|
|
23
23
|
);
|
|
24
|
+
// The pending-queue drainer is the in-process recovery path: without it a
|
|
25
|
+
// single transient write failure latches capture/commit until the next dsh
|
|
26
|
+
// restart. Started here so every session shares one single-flight drainer.
|
|
27
|
+
runtime.startDrainer();
|
|
28
|
+
ctx.effect(
|
|
29
|
+
() => () => runtime.stopDrainer(),
|
|
30
|
+
"openvikingMemory.stopDrainer()",
|
|
31
|
+
);
|
|
24
32
|
|
|
25
33
|
ctx.on("agent/session-start", ({ agent }) => {
|
|
26
34
|
if (skipMemory(agent.session)) return false;
|
|
@@ -33,13 +41,17 @@ export function apply(ctx, input = {}) {
|
|
|
33
41
|
|
|
34
42
|
// prepend: downstream waterfall listeners run first, so this plugin sees
|
|
35
43
|
// the final claimed batch and appends after every other contributor.
|
|
44
|
+
// Profile + recall are independent after `next()`; run them concurrently so
|
|
45
|
+
// the agent/pre-step waterfall (which currently gates user/message push in
|
|
46
|
+
// dsh-agent-loop) spends less wall time (#4515).
|
|
36
47
|
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
37
48
|
const decision = await next();
|
|
38
49
|
if (skipMemory(agent.session)) return decision;
|
|
39
50
|
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
40
|
-
const profile = await
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
const [profile, recall] = await Promise.all([
|
|
52
|
+
runtime.profileMessage(agent),
|
|
53
|
+
runtime.recallMessage(agent, decision.messages),
|
|
54
|
+
]);
|
|
43
55
|
if (signal.aborted) return decision;
|
|
44
56
|
const additions = [profile, recall].filter(Boolean);
|
|
45
57
|
return additions.length > 0
|
|
@@ -59,6 +71,7 @@ export function apply(ctx, input = {}) {
|
|
|
59
71
|
});
|
|
60
72
|
|
|
61
73
|
ctx.on("tools/pre-execute", guardVikingUri);
|
|
74
|
+
ctx.on("tools/post-execute", noticeVikingUri);
|
|
62
75
|
|
|
63
76
|
// Mounted last, and deliberately not awaited: the bridge's apply blocks on
|
|
64
77
|
// its first tools/list, so a server that accepts the connection but never
|