@openviking/dsh-memory-plugin 0.1.0

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 ADDED
@@ -0,0 +1,129 @@
1
+ # OpenViking Memory for DeepSeek Harness
2
+
3
+ An installable DeepSeek Harness bundle that adds OpenViking auto-recall, session capture, `viking://` URI protection, and model-invocable memory tools.
4
+
5
+ ## Requirements
6
+
7
+ - `@deepseek-ai/dsh` `0.1.0-rc.6`
8
+ - Node.js `^22.19.0` or `>=24`
9
+ - A reachable OpenViking server
10
+
11
+ The bundle has no runtime npm dependencies. Its tool and message structures
12
+ come from the DSH constructors (`defineTool` from `@deepseek-ai/dsh-tools`,
13
+ `createUserMessage` from `@deepseek-ai/dsh-llm`) behind exact-pinned
14
+ peerDependencies that DSH exposes through its profile fallback at boot time, so
15
+ the definitions track DSH's contracts instead of hand-built object shapes.
16
+ It is tested against `0.1.0-rc.6`; install that exact DSH release because
17
+ prerelease package dist-tags are not synchronized across the package family.
18
+
19
+ ## Why injection uses pre-step user messages, not the system prompt
20
+
21
+ Recall and profile context enter through the `agent/pre-step` waterfall as
22
+ durable, source-attributed user messages (`source: { kind: 'plugin', … }`).
23
+ They are deliberately **not** added to the system prompt: a DSH preset whose
24
+ persona declares `complete: true` (the stock `minimal` preset does) restores
25
+ that persona as the sole prompt section after assembly, silently discarding
26
+ every other contribution — a system-prompt-based memory plugin loses its
27
+ context under such presets with no error. Pre-step injection also makes each
28
+ injection a session event that replays, is visible to compaction, and never
29
+ reaches `request/header`.
30
+
31
+ ## Install
32
+
33
+ From the OpenViking repository:
34
+
35
+ ```bash
36
+ dsh plugin --profile default add ./examples/dsh-memory-plugin
37
+ ```
38
+
39
+ Or install the published package:
40
+
41
+ ```bash
42
+ dsh plugin --profile default add @openviking/dsh-memory-plugin
43
+ ```
44
+
45
+ Confirm that the profile includes the bundle:
46
+
47
+ ```bash
48
+ dsh --profile default --dump-config
49
+ ```
50
+
51
+ The package patch mounts the runtime inside a Cordis group with an isolated `openvikingMemory` service.
52
+
53
+ ## Configuration
54
+
55
+ OpenViking credentials use the same resolution order as the other memory plugins:
56
+
57
+ 1. `OPENVIKING_*` environment variables
58
+ 2. `~/.openviking/ovcli.conf`
59
+ 3. `~/.openviking/ov.conf`
60
+
61
+ Common environment variables:
62
+
63
+ | Variable | Purpose |
64
+ | --- | --- |
65
+ | `OPENVIKING_URL` / `OPENVIKING_BASE_URL` | OpenViking server endpoint |
66
+ | `OPENVIKING_API_KEY` / `OPENVIKING_BEARER_TOKEN` | Bearer credential |
67
+ | `OPENVIKING_ACCOUNT` | Trusted-mode account |
68
+ | `OPENVIKING_USER` | Trusted-mode user |
69
+ | `OPENVIKING_PEER_ID` | Explicit actor peer |
70
+ | `OPENVIKING_WORKSPACE_PEER` | Derive a peer from each DSH session workspace by default |
71
+ | `OPENVIKING_RECALL_PEER_SCOPE` | `all` for cross-workspace recall or `actor` for isolation |
72
+
73
+ The patch can also carry plugin config:
74
+
75
+ ```yaml
76
+ - insert:
77
+ - id: openviking-memory
78
+ name: '@deepseek-ai/cordis-plugin-group'
79
+ group: true
80
+ isolate:
81
+ openvikingMemory: true
82
+ config:
83
+ - id: openviking-memory-runtime
84
+ name: '@openviking/dsh-memory-plugin'
85
+ config:
86
+ endpoint: http://127.0.0.1:1933
87
+ recallTokenBudget: 2000
88
+ scoreThreshold: 0.35
89
+ captureToolResults: false
90
+ commitTokenThreshold: 20000
91
+ ```
92
+
93
+ ## Behavior
94
+
95
+ - `agent/session-start` injects the OpenViking profile and available-memory index through `agent.inject()`.
96
+ - `agent/pre-step` retrieves with the current step input and appends a durable plugin message to that same step.
97
+ - `session/event` captures user, assistant, and optionally tool-result messages without scraping a transcript.
98
+ - `turn/end` checks the OpenViking pending-token threshold and commits when required.
99
+ - Failed writes enter the shared OpenViking pending queue for replay at the next session start.
100
+ - `tools/pre-execute` blocks DSH filesystem and shell tools from treating `viking://` URIs as local paths.
101
+
102
+ 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.
103
+
104
+ ## Tools
105
+
106
+ The bundle registers:
107
+
108
+ - `viking_search`
109
+ - `viking_read`
110
+ - `viking_browse`
111
+ - `viking_remember`
112
+ - `viking_forget`
113
+ - `viking_add_resource`
114
+ - `viking_archive_expand`
115
+
116
+ `viking_forget` performs permanent deletion. The calling model should use it only when the user explicitly requests deletion.
117
+
118
+ ## Testing
119
+
120
+ ```bash
121
+ npm ci # installs the exact-pinned dsh devDependencies the tests exercise
122
+ npm test # node --test *.test.mjs — runs in the repo's PR workflow
123
+ ```
124
+
125
+ `live-recall.test.mjs` is an opt-in end-to-end gate against a real OpenViking
126
+ server: it stores a sentinel memory through a session commit, waits for
127
+ extraction, and asserts recall returns that sentinel — the property no stub
128
+ can certify. Enable it with `OPENVIKING_E2E=1` plus the normal credential
129
+ chain; it skips otherwise (including in CI until a server secret exists).
package/capture.mjs ADDED
@@ -0,0 +1,104 @@
1
+ import {
2
+ extractPartsFromPayload,
3
+ extractTextFromPayload,
4
+ shouldCaptureText,
5
+ } from "./shared/capture-utils.mjs";
6
+
7
+ export const OPENVIKING_PLUGIN_SOURCE = "openviking-memory";
8
+
9
+ export function captureEvent(event, config, toolNames = new Map()) {
10
+ if (!event || typeof event !== "object") return null;
11
+ if (event.type === "tool/call") {
12
+ if (config.captureToolResults === true) {
13
+ toolNames.set(String(event.data.callId), event.data.name);
14
+ }
15
+ return null;
16
+ }
17
+
18
+ const message = eventMessage(event);
19
+ if (!message) return null;
20
+ const toolCallId = event.type === "tool/result"
21
+ ? String(message.source?.callId || message.content?.[0]?.toolCallId || "")
22
+ : "";
23
+ try {
24
+ return captureMessage(event, message, config, toolNames);
25
+ } finally {
26
+ if (toolCallId) toolNames.delete(toolCallId);
27
+ }
28
+ }
29
+
30
+ function captureMessage(event, message, config, toolNames) {
31
+ // Whitelist by source: plugin-injected user messages (this plugin's recall
32
+ // blocks, time-context snapshots, any other plugin's context) are model
33
+ // input, not human input — mirroring them would launder synthetic text
34
+ // into memory as if a person said it.
35
+ if (message.source?.kind === "plugin") return null;
36
+ if (message.role === "assistant" && config.captureAssistantTurns === false) {
37
+ return null;
38
+ }
39
+ if (message.source?.kind === "tool" && config.captureToolResults !== true) {
40
+ return null;
41
+ }
42
+
43
+ const role = message.role === "assistant" ? "assistant" : "user";
44
+ const toolNameById = Object.fromEntries(toolNames);
45
+ const rawText = extractTextFromPayload(message, {
46
+ toolMaxChars: config.captureToolMaxChars,
47
+ });
48
+ const parts = extractPartsFromPayload(message, {
49
+ toolMaxChars: config.captureToolMaxChars,
50
+ toolNameById,
51
+ });
52
+ const decision = shouldCaptureText(rawText, role, config);
53
+ const structuredParts = parts.filter(part => part?.type !== "text");
54
+ if (!decision.shouldCapture && structuredParts.length === 0) return null;
55
+
56
+ const hasTextPart = parts.some(part => part?.type === "text");
57
+ const bodyParts = [
58
+ ...(hasTextPart && decision.shouldCapture && decision.text
59
+ ? [{ type: "text", text: decision.text }]
60
+ : []),
61
+ ...structuredParts,
62
+ ];
63
+ const payload = bodyParts.length > 0
64
+ ? { role, parts: bodyParts }
65
+ : { role, content: decision.text };
66
+ const createdAt = eventCreatedAt(event);
67
+ if (createdAt) payload.created_at = createdAt;
68
+ if (config.peerId) payload.peer_id = config.peerId;
69
+ return payload;
70
+ }
71
+
72
+ export function promptText(messages) {
73
+ return (messages || [])
74
+ .filter(message => !(
75
+ message?.source?.kind === "plugin"
76
+ && message.source.plugin === OPENVIKING_PLUGIN_SOURCE
77
+ ))
78
+ .map(message => extractTextFromPayload(message))
79
+ .filter(Boolean)
80
+ .join("\n\n")
81
+ .trim();
82
+ }
83
+
84
+ function eventMessage(event) {
85
+ switch (event.type) {
86
+ case "user/message":
87
+ return event.data;
88
+ case "assistant/message":
89
+ case "tool/result":
90
+ return event.data?.message;
91
+ default:
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function eventCreatedAt(event) {
97
+ const time = Number(event?.time);
98
+ if (!Number.isFinite(time) || time < 0) return "";
99
+ try {
100
+ return new Date(time).toISOString();
101
+ } catch {
102
+ return "";
103
+ }
104
+ }
package/client.mjs ADDED
@@ -0,0 +1,206 @@
1
+ export class OpenVikingClient {
2
+ constructor(config) {
3
+ this.config = config;
4
+ 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;
65
+ }
66
+
67
+ async healthResult() {
68
+ const response = await this.fetchJSON("/health", {}, { timeoutMs: 5000 });
69
+ this.connected = response.ok;
70
+ return response;
71
+ }
72
+
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
+ async ensureSessionResult(sessionId, actorPeerId) {
80
+ const response = await this.fetchJSON("/api/v1/sessions", {
81
+ method: "POST",
82
+ body: JSON.stringify({ session_id: sessionId }),
83
+ }, { actorPeerId });
84
+ return response;
85
+ }
86
+
87
+ async getSession(sessionId, actorPeerId) {
88
+ const response = await this.fetchJSON(
89
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}`,
90
+ {},
91
+ { timeoutMs: 5000, actorPeerId },
92
+ );
93
+ return response.ok ? response.result : null;
94
+ }
95
+
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
+ async addMessage(sessionId, payload, actorPeerId) {
106
+ return this.fetchJSON(
107
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
108
+ {
109
+ method: "POST",
110
+ body: JSON.stringify(payload),
111
+ },
112
+ { actorPeerId },
113
+ );
114
+ }
115
+
116
+ async commitSession(sessionId, actorPeerId, options = {}) {
117
+ return this.fetchJSON(
118
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
119
+ {
120
+ method: "POST",
121
+ body: JSON.stringify({ keep_recent_count: this.config.commitKeepRecentCount }),
122
+ },
123
+ { timeoutMs: options.timeoutMs ?? 30000, actorPeerId },
124
+ );
125
+ }
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
+ }
package/config.mjs ADDED
@@ -0,0 +1,161 @@
1
+ import { buildUserAgent, resolveOpenVikingCredentials } from "./shared/credentials.mjs";
2
+ import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs";
3
+
4
+ export const PLUGIN_VERSION = "0.1.0";
5
+
6
+ const DEFAULT_CONFIG = Object.freeze({
7
+ endpoint: "http://127.0.0.1:1933",
8
+ apiKey: "",
9
+ account: "",
10
+ user: "",
11
+ peerId: "",
12
+ userAgent: "",
13
+ workspacePeer: true,
14
+ recallPeerScope: "all",
15
+ recallQueryExpansion: "auto",
16
+ recallQueryExpansionConfigured: false,
17
+ syncTurns: true,
18
+ recallTokenBudget: 2000,
19
+ recallMaxContentChars: 500,
20
+ recallPreferAbstract: true,
21
+ recallLimit: 10,
22
+ recallLimitConfigured: false,
23
+ scoreThreshold: 0.35,
24
+ minQueryLength: 3,
25
+ profileTokenBudget: 10000,
26
+ commitTokenThreshold: 20000,
27
+ commitKeepRecentCount: 10,
28
+ captureToolResults: false,
29
+ captureMode: "semantic",
30
+ captureMaxLength: 24000,
31
+ captureToolMaxChars: 1000000,
32
+ captureAssistantTurns: true,
33
+ requestTimeoutMs: 10000,
34
+ });
35
+
36
+ export function resolveConfig(input = {}, env = process.env, cwd = process.cwd()) {
37
+ const credentials = resolveOpenVikingCredentials(env);
38
+ const explicitPeerId = input.peerId || credentials.peerId;
39
+ const config = {
40
+ ...DEFAULT_CONFIG,
41
+ ...input,
42
+ endpoint: input.endpoint || credentials.baseUrl || DEFAULT_CONFIG.endpoint,
43
+ apiKey: input.apiKey || credentials.apiKey,
44
+ account: input.account || credentials.account,
45
+ user: input.user || credentials.user,
46
+ peerId: explicitPeerId,
47
+ explicitPeerId,
48
+ userAgent: buildUserAgent("dsh", PLUGIN_VERSION),
49
+ recallLimitConfigured: Object.prototype.hasOwnProperty.call(input, "recallLimit"),
50
+ recallQueryExpansionConfigured: Object.prototype.hasOwnProperty.call(input, "recallQueryExpansion"),
51
+ };
52
+
53
+ if (env.OPENVIKING_WORKSPACE_PEER !== undefined) {
54
+ config.workspacePeer = environmentBoolean(
55
+ env.OPENVIKING_WORKSPACE_PEER,
56
+ config.workspacePeer,
57
+ );
58
+ }
59
+ if (env.OPENVIKING_RECALL_PEER_SCOPE) {
60
+ config.recallPeerScope = env.OPENVIKING_RECALL_PEER_SCOPE;
61
+ }
62
+ if (env.OPENVIKING_RECALL_QUERY_EXPANSION) {
63
+ config.recallQueryExpansion = env.OPENVIKING_RECALL_QUERY_EXPANSION;
64
+ config.recallQueryExpansionConfigured = true;
65
+ }
66
+ if (env.OPENVIKING_RECALL_LIMIT) {
67
+ config.recallLimit = env.OPENVIKING_RECALL_LIMIT;
68
+ config.recallLimitConfigured = true;
69
+ }
70
+
71
+ config.endpoint = String(config.endpoint || DEFAULT_CONFIG.endpoint).replace(/\/+$/, "");
72
+ config.workspacePeer = config.workspacePeer !== false;
73
+ config.peerId = resolveEffectivePeerId({ cfg: config, cwd }).peerId;
74
+ config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all";
75
+ config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto";
76
+ config.recallLimit = clampInteger(config.recallLimit, 1, 50, DEFAULT_CONFIG.recallLimit);
77
+ config.recallMaxContentChars = clampInteger(
78
+ config.recallMaxContentChars,
79
+ 100,
80
+ 5000,
81
+ DEFAULT_CONFIG.recallMaxContentChars,
82
+ );
83
+ config.recallTokenBudget = clampInteger(
84
+ config.recallTokenBudget,
85
+ 200,
86
+ 50000,
87
+ DEFAULT_CONFIG.recallTokenBudget,
88
+ );
89
+ config.scoreThreshold = clampNumber(
90
+ config.scoreThreshold,
91
+ 0,
92
+ 1,
93
+ DEFAULT_CONFIG.scoreThreshold,
94
+ );
95
+ config.minQueryLength = clampInteger(
96
+ config.minQueryLength,
97
+ 1,
98
+ 64,
99
+ DEFAULT_CONFIG.minQueryLength,
100
+ );
101
+ config.profileTokenBudget = clampInteger(
102
+ config.profileTokenBudget,
103
+ 500,
104
+ 50000,
105
+ DEFAULT_CONFIG.profileTokenBudget,
106
+ );
107
+ config.commitTokenThreshold = clampInteger(
108
+ config.commitTokenThreshold,
109
+ 1000,
110
+ 1000000,
111
+ DEFAULT_CONFIG.commitTokenThreshold,
112
+ );
113
+ config.commitKeepRecentCount = clampInteger(
114
+ config.commitKeepRecentCount,
115
+ 0,
116
+ 1000,
117
+ DEFAULT_CONFIG.commitKeepRecentCount,
118
+ );
119
+ config.captureMaxLength = clampInteger(
120
+ config.captureMaxLength,
121
+ 200,
122
+ 100000,
123
+ DEFAULT_CONFIG.captureMaxLength,
124
+ );
125
+ config.captureToolMaxChars = clampInteger(
126
+ config.captureToolMaxChars,
127
+ 200,
128
+ 1000000,
129
+ DEFAULT_CONFIG.captureToolMaxChars,
130
+ );
131
+ config.requestTimeoutMs = clampInteger(
132
+ config.requestTimeoutMs,
133
+ 1000,
134
+ 120000,
135
+ DEFAULT_CONFIG.requestTimeoutMs,
136
+ );
137
+ config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic";
138
+ config.syncTurns = config.syncTurns !== false;
139
+ config.captureAssistantTurns = config.captureAssistantTurns !== false;
140
+ config.captureToolResults = config.captureToolResults === true;
141
+ return config;
142
+ }
143
+
144
+ function environmentBoolean(value, fallback) {
145
+ const normalized = String(value || "").trim().toLowerCase();
146
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
147
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
148
+ return fallback;
149
+ }
150
+
151
+ function clampInteger(value, minimum, maximum, fallback) {
152
+ const number = Math.round(Number(value));
153
+ if (!Number.isFinite(number)) return fallback;
154
+ return Math.max(minimum, Math.min(maximum, number));
155
+ }
156
+
157
+ function clampNumber(value, minimum, maximum, fallback) {
158
+ const number = Number(value);
159
+ if (!Number.isFinite(number)) return fallback;
160
+ return Math.max(minimum, Math.min(maximum, number));
161
+ }
@@ -0,0 +1,9 @@
1
+ - insert:
2
+ - id: openviking-memory
3
+ name: '@deepseek-ai/cordis-plugin-group'
4
+ group: true
5
+ isolate:
6
+ openvikingMemory: true
7
+ config:
8
+ - id: openviking-memory-runtime
9
+ name: '@openviking/dsh-memory-plugin'
package/index.mjs ADDED
@@ -0,0 +1,56 @@
1
+ import { OpenVikingClient } from "./client.mjs";
2
+ import { resolveConfig } from "./config.mjs";
3
+ import { injectStartupProfile } from "./lifecycle.mjs";
4
+ import { OpenVikingRuntime } from "./runtime.mjs";
5
+ import { registerOpenVikingTools } from "./tools.mjs";
6
+ import { guardVikingUri } from "./uri-guard.mjs";
7
+
8
+ export const name = "openviking-memory";
9
+ export const inject = ["agents", "sessions", "tools"];
10
+
11
+ export function apply(ctx, input = {}) {
12
+ const config = resolveConfig(input);
13
+ const client = new OpenVikingClient(config);
14
+ const runtime = new OpenVikingRuntime(client, config, ctx.logger);
15
+ ctx.provide("openvikingMemory", runtime);
16
+ ctx.effect(
17
+ () => () => runtime.disposeAll(),
18
+ "openvikingMemory.disposeAll()",
19
+ );
20
+
21
+ registerOpenVikingTools(ctx, client, runtime);
22
+
23
+ ctx.on("agent/session-start", ({ agent }) => {
24
+ agent.ctx.effect(
25
+ () => () => runtime.dispose(agent.session),
26
+ "openvikingMemory.disposeSession()",
27
+ );
28
+ return injectStartupProfile(agent, runtime);
29
+ });
30
+
31
+ // prepend: downstream waterfall listeners run first, so this plugin sees
32
+ // the final claimed batch and appends after every other contributor.
33
+ ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
34
+ const decision = await next();
35
+ if (decision.kind !== "enter" || signal.aborted) return decision;
36
+ const profile = await runtime.profileMessage(agent);
37
+ if (signal.aborted) return decision;
38
+ const recall = await runtime.recallMessage(agent, decision.messages);
39
+ if (signal.aborted) return decision;
40
+ const additions = [profile, recall].filter(Boolean);
41
+ return additions.length > 0
42
+ ? { kind: "enter", messages: [...decision.messages, ...additions] }
43
+ : decision;
44
+ }, { prepend: true });
45
+
46
+ ctx.on("session/event", (session, event) => {
47
+ runtime.capture(session, event);
48
+ runtime.maybeCommit(session, event);
49
+ });
50
+
51
+ ctx.on("session/flush", async session => {
52
+ await runtime.flush(session);
53
+ });
54
+
55
+ ctx.on("tools/pre-execute", guardVikingUri);
56
+ }
package/lifecycle.mjs ADDED
@@ -0,0 +1,8 @@
1
+ export async function injectStartupProfile(agent, runtime) {
2
+ await runtime.initialize(agent);
3
+ if (agent.status !== "idle") return false;
4
+ const profile = await runtime.profileMessage(agent);
5
+ if (!profile) return false;
6
+ agent.inject(profile);
7
+ return true;
8
+ }