@openviking/openclaw-plugin 2026.9.1 → 2026.9.4
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 +3 -10
- package/auto-recall.ts +126 -135
- package/client.ts +95 -0
- package/config.ts +8 -6
- package/dist/auto-recall.js +113 -114
- package/dist/client.js +54 -0
- package/dist/config.js +6 -4
- package/dist/shared/recall-compress-core.mjs +188 -0
- package/dist/shared/recall-core.mjs +645 -0
- package/install-manifest.json +1 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -1
- package/shared/recall-compress-core.mjs +195 -0
- package/shared/recall-core.d.mts +39 -0
- package/shared/recall-core.mjs +678 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const NO_RELEVANT_MEMORY = "NO_RELEVANT_MEMORY";
|
|
7
|
+
export const DIGEST_HEADER = "OpenViking memory digest:";
|
|
8
|
+
const COMPRESS_OK = "ok";
|
|
9
|
+
const COMPRESS_EMPTY = "empty";
|
|
10
|
+
const COMPRESS_FAILED = "failed";
|
|
11
|
+
|
|
12
|
+
// Medium-constraint prompt: state the goal and two structural floors, but no hard
|
|
13
|
+
// bullet-length contract. Hard per-bullet limits pin the digest to headline
|
|
14
|
+
// density; leaving it unconstrained lets small models rewrite long URIs into dead
|
|
15
|
+
// links, which the URI repair below cleans up.
|
|
16
|
+
export function buildRecallCompressionPrompt({ query, rendered, maxBullets = 6 }) {
|
|
17
|
+
return `You are a memory relevance compressor utility.
|
|
18
|
+
Do not use any tools. Do not investigate. Only transform the given text.
|
|
19
|
+
|
|
20
|
+
User query:
|
|
21
|
+
${query}
|
|
22
|
+
|
|
23
|
+
Retrieved OpenViking context fragments:
|
|
24
|
+
${rendered}
|
|
25
|
+
|
|
26
|
+
Write a memory digest for a coding agent about to answer that query. Keep the
|
|
27
|
+
concrete facts (paths, identifiers, decisions, constraints); drop pleasantries
|
|
28
|
+
and conversational filler.
|
|
29
|
+
|
|
30
|
+
Format rules:
|
|
31
|
+
- Group related facts by topic, one bullet per topic, at most ${maxBullets} bullets.
|
|
32
|
+
- Start every bullet with "- ".
|
|
33
|
+
- End every bullet with its source, copied verbatim from the fragments above:
|
|
34
|
+
"来源:viking://..." or "source: viking://...". Never edit, shorten, or invent a URI.
|
|
35
|
+
- Output the digest body only. No preamble, no closing remark.
|
|
36
|
+
|
|
37
|
+
If nothing above is relevant to the query, output exactly: ${NO_RELEVANT_MEMORY}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function editDistance(a, b) {
|
|
41
|
+
if (a === b) return 0;
|
|
42
|
+
const rows = a.length + 1;
|
|
43
|
+
const cols = b.length + 1;
|
|
44
|
+
let prev = Array.from({ length: cols }, (_, i) => i);
|
|
45
|
+
for (let i = 1; i < rows; i += 1) {
|
|
46
|
+
const cur = [i];
|
|
47
|
+
for (let j = 1; j < cols; j += 1) {
|
|
48
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
49
|
+
cur[j] = Math.min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
50
|
+
}
|
|
51
|
+
prev = cur;
|
|
52
|
+
}
|
|
53
|
+
return prev[cols - 1];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function nearestUri(candidate, validUris) {
|
|
57
|
+
let best = "";
|
|
58
|
+
let bestDistance = Infinity;
|
|
59
|
+
for (const uri of validUris) {
|
|
60
|
+
const distance = editDistance(candidate, uri);
|
|
61
|
+
if (distance < bestDistance) {
|
|
62
|
+
best = uri;
|
|
63
|
+
bestDistance = distance;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Only repair near-misses; an unrelated hallucination is dropped instead.
|
|
67
|
+
const tolerance = Math.max(4, Math.floor(candidate.length * 0.25));
|
|
68
|
+
return bestDistance <= tolerance ? best : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Small models occasionally mangle long URIs. Snap every cited URI back onto the
|
|
73
|
+
* set the server actually returned, and drop bullets whose citation cannot be
|
|
74
|
+
* recovered so the digest never carries a dead link.
|
|
75
|
+
*/
|
|
76
|
+
export function repairDigestUris(digest, validUris = []) {
|
|
77
|
+
const text = String(digest || "");
|
|
78
|
+
if (!text) return "";
|
|
79
|
+
const valid = validUris.map((uri) => String(uri || "").trim()).filter(Boolean);
|
|
80
|
+
if (!valid.length) return text;
|
|
81
|
+
const validSet = new Set(valid);
|
|
82
|
+
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const line of text.split(/\r?\n/)) {
|
|
85
|
+
if (!line.trim().startsWith("- ")) {
|
|
86
|
+
lines.push(line);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let dropped = false;
|
|
90
|
+
const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
|
|
91
|
+
if (validSet.has(uri)) return uri;
|
|
92
|
+
const nearest = nearestUri(uri, valid);
|
|
93
|
+
if (nearest) return nearest;
|
|
94
|
+
dropped = true;
|
|
95
|
+
return uri;
|
|
96
|
+
});
|
|
97
|
+
if (!dropped) lines.push(repaired);
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n").trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeCompressedContext(raw, maxChars = 4000, maxBullets = 6) {
|
|
103
|
+
const text = String(raw || "").trim();
|
|
104
|
+
if (!text) return null;
|
|
105
|
+
if (text.toUpperCase() === NO_RELEVANT_MEMORY) return "";
|
|
106
|
+
const bullets = text.split(/\r?\n/)
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => /^[-*]\s+/.test(line) && line.includes("viking://"))
|
|
109
|
+
.slice(0, Math.max(1, maxBullets))
|
|
110
|
+
.map((line) => `- ${line.replace(/^[-*]\s+/, "").slice(0, 500).trim()}`);
|
|
111
|
+
if (!bullets.length) return null;
|
|
112
|
+
return (`${DIGEST_HEADER}\n${bullets.join("\n")}`).slice(0, Math.max(100, maxChars));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function recallDigestCacheKey({
|
|
116
|
+
query = "",
|
|
117
|
+
rendered = "",
|
|
118
|
+
entries = [],
|
|
119
|
+
maxInputChars = 18000,
|
|
120
|
+
maxBullets = 6,
|
|
121
|
+
} = {}) {
|
|
122
|
+
const uris = entries.map((entry) => String(entry?.uri || "").trim()).filter(Boolean).sort();
|
|
123
|
+
const source = JSON.stringify({
|
|
124
|
+
version: 2,
|
|
125
|
+
query: String(query),
|
|
126
|
+
rendered: String(rendered).slice(0, maxInputChars),
|
|
127
|
+
uris,
|
|
128
|
+
maxInputChars,
|
|
129
|
+
maxBullets,
|
|
130
|
+
});
|
|
131
|
+
return createHash("sha256").update(source).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readCache(path) {
|
|
135
|
+
if (!path) return null;
|
|
136
|
+
try { return JSON.parse(await readFile(path, "utf8")); } catch { return null; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeCache(path, value) {
|
|
140
|
+
if (!path) return;
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(dirname(path), { recursive: true });
|
|
143
|
+
const tmp = `${path}.tmp`;
|
|
144
|
+
await writeFile(tmp, JSON.stringify(value));
|
|
145
|
+
await rename(tmp, path);
|
|
146
|
+
} catch { /* best effort */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function compressRecallContext({
|
|
150
|
+
query,
|
|
151
|
+
rendered,
|
|
152
|
+
entries = [],
|
|
153
|
+
cfg = {},
|
|
154
|
+
runCompressor,
|
|
155
|
+
cachePath = "",
|
|
156
|
+
now = 0,
|
|
157
|
+
}) {
|
|
158
|
+
const input = String(rendered || "").trim();
|
|
159
|
+
if (!input) return { status: COMPRESS_EMPTY, context: "" };
|
|
160
|
+
const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
|
|
161
|
+
if (input.length < minChars) return { status: COMPRESS_OK, context: input };
|
|
162
|
+
|
|
163
|
+
const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
|
|
164
|
+
const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
|
|
165
|
+
const key = recallDigestCacheKey({
|
|
166
|
+
query,
|
|
167
|
+
rendered: input,
|
|
168
|
+
entries,
|
|
169
|
+
maxInputChars,
|
|
170
|
+
maxBullets,
|
|
171
|
+
});
|
|
172
|
+
const cached = await readCache(cachePath);
|
|
173
|
+
if (cached?.key === key && typeof cached.digest === "string") {
|
|
174
|
+
return { status: COMPRESS_OK, context: cached.digest };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const prompt = buildRecallCompressionPrompt({
|
|
178
|
+
query,
|
|
179
|
+
rendered: input.slice(0, maxInputChars),
|
|
180
|
+
maxBullets,
|
|
181
|
+
});
|
|
182
|
+
const raw = await runCompressor(prompt);
|
|
183
|
+
const normalized = normalizeCompressedContext(raw, 4000, maxBullets);
|
|
184
|
+
if (normalized === null) return { status: COMPRESS_FAILED, context: "" };
|
|
185
|
+
if (!normalized) return { status: COMPRESS_EMPTY, context: "" };
|
|
186
|
+
|
|
187
|
+
const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
|
|
188
|
+
const digest = repairDigestUris(normalized, validUris.length
|
|
189
|
+
? validUris
|
|
190
|
+
: (input.match(/viking:\/\/[^\s<>"']+/g) || []));
|
|
191
|
+
if (!digest) return { status: COMPRESS_FAILED, context: "" };
|
|
192
|
+
|
|
193
|
+
await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
|
|
194
|
+
return { status: COMPRESS_OK, context: digest };
|
|
195
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type ContextSearchBody = {
|
|
2
|
+
query: string;
|
|
3
|
+
mode: "context";
|
|
4
|
+
purpose: "coding";
|
|
5
|
+
score_threshold: number;
|
|
6
|
+
quotas?: Record<string, number>;
|
|
7
|
+
max_tokens?: number;
|
|
8
|
+
peer_scope?: "actor";
|
|
9
|
+
session_id?: string;
|
|
10
|
+
query_expansion?: "off" | "auto";
|
|
11
|
+
dedup_turns?: number;
|
|
12
|
+
exclude_uris?: string[];
|
|
13
|
+
rewrite?: boolean | "auto";
|
|
14
|
+
rewrite_max_bullets?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type NormalizedContextEntry = {
|
|
18
|
+
uri: string;
|
|
19
|
+
category: string;
|
|
20
|
+
detail: string;
|
|
21
|
+
score: number;
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function buildContextSearchBody(
|
|
26
|
+
cfg?: Record<string, unknown>,
|
|
27
|
+
options?: {
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
excludeUris?: string[];
|
|
30
|
+
localCompressorAvailable?: boolean;
|
|
31
|
+
},
|
|
32
|
+
): ContextSearchBody;
|
|
33
|
+
|
|
34
|
+
export function contextRequestTimeoutMs(
|
|
35
|
+
cfg?: Record<string, unknown>,
|
|
36
|
+
body?: Record<string, unknown>,
|
|
37
|
+
): number | undefined;
|
|
38
|
+
|
|
39
|
+
export function normalizeContextEntry(entry?: unknown): NormalizedContextEntry;
|