@openviking/opencode-plugin 2026.10.14 → 2026.10.15-2
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/lib/memory-session.mjs +29 -2
- package/lib/shared/capture-utils.mjs +35 -6
- package/package.json +2 -2
package/lib/memory-session.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
safeStringify,
|
|
26
26
|
} from "./utils.mjs"
|
|
27
27
|
|
|
28
|
+
const STALE_SESSION_STATE_MS = 7 * 24 * 60 * 60 * 1000
|
|
29
|
+
|
|
28
30
|
export function createMemorySessionManager({ config, pluginRoot }) {
|
|
29
31
|
const sessions = new Map()
|
|
30
32
|
const statePath = path.join(pluginRoot, "openviking-session-state.json")
|
|
@@ -77,10 +79,17 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
77
79
|
log("ERROR", "persistence", "Unsupported session map version", { version: data.version })
|
|
78
80
|
return
|
|
79
81
|
}
|
|
82
|
+
const staleBefore = Date.now() - STALE_SESSION_STATE_MS
|
|
83
|
+
let pruned = 0
|
|
80
84
|
for (const [opencodeSessionId, persisted] of Object.entries(data.sessions ?? {})) {
|
|
85
|
+
if (isPrunable(persisted, staleBefore)) {
|
|
86
|
+
pruned += 1
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
81
89
|
sessions.set(opencodeSessionId, deserializeSessionState(persisted))
|
|
82
90
|
}
|
|
83
|
-
log("INFO", "persistence", "Session state loaded", { count: sessions.size })
|
|
91
|
+
log("INFO", "persistence", "Session state loaded", { count: sessions.size, pruned })
|
|
92
|
+
if (pruned > 0) await enqueueSave()
|
|
84
93
|
} catch (error) {
|
|
85
94
|
log("ERROR", "persistence", "Failed to load session state", { error: error?.message })
|
|
86
95
|
if (fs.existsSync(statePath)) {
|
|
@@ -133,6 +142,12 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
144
|
|
|
145
|
+
// A stale session whose messages are all captured holds nothing left to send.
|
|
146
|
+
function isPrunable(persisted, staleBefore) {
|
|
147
|
+
if ((persisted.lastActivityAt ?? 0) >= staleBefore) return false
|
|
148
|
+
return (persisted.messages ?? []).every(([, message]) => message.captured)
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
function deserializeSessionState(persisted) {
|
|
137
152
|
return {
|
|
138
153
|
ovSessionId: persisted.ovSessionId,
|
|
@@ -301,7 +316,11 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
301
316
|
const run = (state.flushChain ?? Promise.resolve()).then(async () => {
|
|
302
317
|
const added = await flushPendingMessages(opencodeSessionId, state)
|
|
303
318
|
if (commit && isCaptureEnabled(config)) {
|
|
304
|
-
await commitOvSession(state.ovSessionId, { force: true, reason })
|
|
319
|
+
const result = await commitOvSession(state.ovSessionId, { force: true, reason })
|
|
320
|
+
// Server lost the session (reset/reinstall); drop the stale mapping.
|
|
321
|
+
if (result.status === "not_found" && sessions.get(opencodeSessionId) === state) {
|
|
322
|
+
sessions.delete(opencodeSessionId)
|
|
323
|
+
}
|
|
305
324
|
} else if (added > 0) {
|
|
306
325
|
await maybeCommitByThreshold(state)
|
|
307
326
|
}
|
|
@@ -500,6 +519,14 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
500
519
|
})
|
|
501
520
|
return { status: "queued" }
|
|
502
521
|
}
|
|
522
|
+
if (res.status === 404) {
|
|
523
|
+
log("WARN", "session", "OpenViking session not found", {
|
|
524
|
+
openviking_session: ovSessionId,
|
|
525
|
+
reason,
|
|
526
|
+
trace_id: res.traceId,
|
|
527
|
+
})
|
|
528
|
+
return { status: "not_found" }
|
|
529
|
+
}
|
|
503
530
|
log("ERROR", "session", "Failed to commit OpenViking session", {
|
|
504
531
|
openviking_session: ovSessionId,
|
|
505
532
|
reason,
|
|
@@ -362,14 +362,41 @@ export function extractPartsFromPayload(payload, options = {}) {
|
|
|
362
362
|
return parts;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
function substituteValue(value, rules, role) {
|
|
366
|
+
if (typeof value === "string") {
|
|
367
|
+
let current = value;
|
|
368
|
+
for (const rule of rules) {
|
|
369
|
+
if (rule.op !== "s" || (rule.scope && rule.scope !== role)) continue;
|
|
370
|
+
current = current.replace(rule.re, rule.replacement);
|
|
371
|
+
}
|
|
372
|
+
return current;
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(value)) return value.map((item) => substituteValue(item, rules, role));
|
|
375
|
+
if (value && typeof value === "object") {
|
|
376
|
+
return Object.fromEntries(
|
|
377
|
+
Object.entries(value).map(([key, item]) => [key, substituteValue(item, rules, role)]),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function substituteToolPart(part, rules, role) {
|
|
384
|
+
if (part.type !== "tool") return part;
|
|
385
|
+
const out = { ...part };
|
|
386
|
+
if (part.tool_input !== undefined) out.tool_input = substituteValue(part.tool_input, rules, role);
|
|
387
|
+
if (part.tool_output !== undefined) out.tool_output = substituteValue(part.tool_output, rules, role);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
365
391
|
/**
|
|
366
392
|
* Prune blank text parts and run `cfg.captureFilters` over the ones that remain.
|
|
367
393
|
*
|
|
368
394
|
* One drop verdict is taken per turn, on the aggregate of its text parts; the
|
|
369
395
|
* individual parts are then rewritten with the substitutions only, so a rule can
|
|
370
|
-
* never both keep and drop the same turn. Tool call/result parts
|
|
371
|
-
*
|
|
372
|
-
*
|
|
396
|
+
* never both keep and drop the same turn. Tool call/result parts get the
|
|
397
|
+
* substitutions too, on their input values and output, so a redaction rule also
|
|
398
|
+
* covers a secret that reached a command or its output; only text parts decide
|
|
399
|
+
* the drop verdict, and a turn dropped on its text takes its tool parts with it.
|
|
373
400
|
*/
|
|
374
401
|
export function filterCaptureParts(parts, role, cfg = {}) {
|
|
375
402
|
const kept = [];
|
|
@@ -389,7 +416,9 @@ export function filterCaptureParts(parts, role, cfg = {}) {
|
|
|
389
416
|
if (!compiled.rules.length || kept.length === 0) return blank;
|
|
390
417
|
|
|
391
418
|
const textParts = kept.filter((part) => part.type === "text");
|
|
392
|
-
if (textParts.length === 0)
|
|
419
|
+
if (textParts.length === 0) {
|
|
420
|
+
return { parts: kept.map((part) => substituteToolPart(part, compiled.rules, role)), dropped: false };
|
|
421
|
+
}
|
|
393
422
|
|
|
394
423
|
const verdict = applyInputFilters(textParts.map((part) => part.text).join("\n\n"), compiled.rules, {
|
|
395
424
|
role,
|
|
@@ -399,7 +428,7 @@ export function filterCaptureParts(parts, role, cfg = {}) {
|
|
|
399
428
|
const out = [];
|
|
400
429
|
for (const part of kept) {
|
|
401
430
|
if (part.type !== "text") {
|
|
402
|
-
out.push(part);
|
|
431
|
+
out.push(substituteToolPart(part, compiled.rules, role));
|
|
403
432
|
continue;
|
|
404
433
|
}
|
|
405
434
|
const shaped = applyInputFilters(part.text, compiled.rules, { role, substituteOnly: true });
|
|
@@ -431,7 +460,7 @@ function faithfulCaptureDecision(text, cfg) {
|
|
|
431
460
|
/**
|
|
432
461
|
* Shape one host message before an adapter builds its session payload.
|
|
433
462
|
* Sanitize injected context before evaluating keep/drop rules, so text that
|
|
434
|
-
* will not be sent cannot satisfy a keep rule. Tool parts
|
|
463
|
+
* will not be sent cannot satisfy a keep rule. Tool parts never decide keep or drop.
|
|
435
464
|
*/
|
|
436
465
|
export function shapeCapturePayload(payload, role, cfg = {}, { toolNameById = {}, faithful = false } = {}) {
|
|
437
466
|
const options = { toolMaxChars: cfg.captureToolMaxChars, toolNameById };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/opencode-plugin",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.15-2",
|
|
4
4
|
"author": "@openviking",
|
|
5
5
|
"description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
|
|
6
6
|
"type": "module",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"code-search"
|
|
47
47
|
],
|
|
48
48
|
"license": "Apache-2.0",
|
|
49
|
-
"openvikingSourceCommit": "
|
|
49
|
+
"openvikingSourceCommit": "621b1ddd9945c362464d18482adb1a48df5ee554"
|
|
50
50
|
}
|