@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 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 newer in the `0.1.x` release line
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, so there is nothing extra to add to a profile. The supported peer
44
- range starts at `0.1.0-rc.6` and stops before `0.2.0`, allowing current DSH
45
- releases whose core packages resolve to newer release candidates. The local
46
- devDependencies and `overrides` stay pinned to rc.6 so CI continues to exercise
47
- the minimum supported contract.
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 credentials travel to the proxy through the child
81
- environment, because DSH scrubs credential-shaped names out of the inherited env
82
- and a subprocess cannot see the Cordis patch; the proxy then applies the usual
83
- `OPENVIKING_*` → `ovcli.conf` → `ov.conf` chain and reloads when those files
84
- change.
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`, serving only `skills/`. DSH already mounts a
104
- `filesystem` provider over the project and user skill roots; an isolated
105
- provider adds the bundle's skill without shadowing or duplicating that catalog.
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
- - Failed writes enter the shared OpenViking pending queue for replay at the next session start.
157
- - `tools/pre-execute` blocks DSH filesystem and shell tools from treating `viking://` URIs as local paths, pointing the model at the bridged `mcp__openviking__*` tools instead.
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
- headers(options = {}) {
8
- const headers = { "Content-Type": "application/json" };
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 { buildUserAgent, resolveOpenVikingCredentials } from "./shared/credentials.mjs";
2
- import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs";
1
+ import { buildPluginConfig } from "./shared/plugin-config.mjs";
2
+ import { loadCredentialFiles } from "./shared/credentials.mjs";
3
3
 
4
- export const PLUGIN_VERSION = "0.3.0";
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
- const DEFAULT_CONFIG = Object.freeze({
14
- endpoint: "http://127.0.0.1:1933",
15
- apiKey: "",
16
- account: "",
17
- user: "",
18
- peerId: "",
19
- userAgent: "",
20
- workspacePeer: true,
21
- recallPeerScope: "all",
22
- recallQueryExpansion: "auto",
23
- recallQueryExpansionConfigured: false,
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
- const credentials = resolveOpenVikingCredentials(env);
47
- const explicitPeerId = input.peerId || credentials.peerId;
48
- const config = {
49
- ...DEFAULT_CONFIG,
50
- ...input,
51
- endpoint: input.endpoint || credentials.baseUrl || DEFAULT_CONFIG.endpoint,
52
- apiKey: input.apiKey || credentials.apiKey,
53
- account: input.account || credentials.account,
54
- user: input.user || credentials.user,
55
- peerId: explicitPeerId,
56
- explicitPeerId,
57
- userAgent: buildUserAgent("dsh", PLUGIN_VERSION),
58
- recallLimitConfigured: Object.prototype.hasOwnProperty.call(input, "recallLimit"),
59
- recallQueryExpansionConfigured: Object.prototype.hasOwnProperty.call(input, "recallQueryExpansion"),
60
- };
61
-
62
- if (env.OPENVIKING_WORKSPACE_PEER !== undefined) {
63
- config.workspacePeer = environmentBoolean(
64
- env.OPENVIKING_WORKSPACE_PEER,
65
- config.workspacePeer,
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 runtime.profileMessage(agent);
41
- if (signal.aborted) return decision;
42
- const recall = await runtime.recallMessage(agent, decision.messages);
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