@remnic/plugin-openclaw 1.0.6 → 1.0.8
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 +36 -0
- package/dist/{calibration-3JHF25QT.js → calibration-BAC7KNKR.js} +2 -1
- package/dist/{causal-consolidation-EBLROS42.js → causal-consolidation-33R5JTPX.js} +7 -5
- package/dist/chunk-3A5ELHTT.js +61 -0
- package/dist/chunk-5ZW5XJQ6.js +125 -0
- package/dist/chunk-6OJAU466.js +148 -0
- package/dist/{chunk-QHMR3D7U.js → chunk-BQLPVRIU.js} +109 -2
- package/dist/chunk-DIZW6H5J.js +136 -0
- package/dist/{chunk-KPMXWORS.js → chunk-JJSNPSCD.js} +608 -354
- package/dist/{chunk-3SA5F4WT.js → chunk-NXLHSCLU.js} +125 -69
- package/dist/{chunk-GUKYM4XZ.js → chunk-PFH73PN6.js} +3 -3
- package/dist/consolidation-undo-5ZSX4MWO.js +426 -0
- package/dist/contradiction-review-SVGBS3V5.js +21 -0
- package/dist/contradiction-scan-U3QKHWQN.js +412 -0
- package/dist/{engine-BU6GNUJ5.js → engine-M5G6ZJU7.js} +3 -2
- package/dist/extraction-judge-telemetry-GHOTVYMP.js +14 -0
- package/dist/{fallback-llm-HJRCHKSA.js → fallback-llm-QEAPMDW7.js} +2 -1
- package/dist/index.js +6467 -1285
- package/dist/resolution-YITUVUTH.js +100 -0
- package/dist/{storage-BA6OBLMK.js → storage-DM4ZGOCN.js} +2 -1
- package/openclaw.plugin.json +280 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,6 +38,42 @@ Then restart the gateway:
|
|
|
38
38
|
launchctl kickstart -k gui/501/ai.openclaw.gateway
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Benchmarking The OpenClaw Chain
|
|
42
|
+
|
|
43
|
+
The benchmark CLI can now exercise the real OpenClaw-backed answer path instead
|
|
44
|
+
of only the stripped retrieval harness. Use the `openclaw-chain` runtime
|
|
45
|
+
profile to load the Remnic plugin config from `openclaw.json`, route answer
|
|
46
|
+
generation through the configured gateway chain, and optionally attach a
|
|
47
|
+
provider-backed judge:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
remnic bench run longmemeval \
|
|
51
|
+
--runtime-profile openclaw-chain \
|
|
52
|
+
--openclaw-config ~/.openclaw/openclaw.json \
|
|
53
|
+
--gateway-agent-id memory-primary
|
|
54
|
+
|
|
55
|
+
remnic bench run longmemeval \
|
|
56
|
+
--runtime-profile openclaw-chain \
|
|
57
|
+
--openclaw-config ~/.openclaw/openclaw.json \
|
|
58
|
+
--gateway-agent-id memory-primary \
|
|
59
|
+
--judge-provider openai \
|
|
60
|
+
--judge-model gpt-5.4-mini
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To compare the stripped harness, direct Remnic runtime, and OpenClaw chain in a
|
|
64
|
+
single pass, run a profile matrix:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
remnic bench run longmemeval \
|
|
68
|
+
--matrix baseline,real,openclaw-chain \
|
|
69
|
+
--openclaw-config ~/.openclaw/openclaw.json \
|
|
70
|
+
--remnic-config ~/.config/remnic/config.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Each stored result records its `runtimeProfile`, provider metadata, and the
|
|
74
|
+
resolved Remnic config so benchmark comparisons can distinguish retrieval-only
|
|
75
|
+
runs from real runtime and OpenClaw chain runs.
|
|
76
|
+
|
|
41
77
|
## What it does
|
|
42
78
|
|
|
43
79
|
This plugin hooks into the OpenClaw gateway lifecycle:
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildExtensionsBlockForConsolidation,
|
|
3
3
|
runPostConsolidationMaterialize
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import
|
|
4
|
+
} from "./chunk-BQLPVRIU.js";
|
|
5
|
+
import {
|
|
6
|
+
FallbackLlmClient
|
|
7
|
+
} from "./chunk-NXLHSCLU.js";
|
|
8
|
+
import "./chunk-3A5ELHTT.js";
|
|
9
|
+
import "./chunk-JJSNPSCD.js";
|
|
10
|
+
import "./chunk-6OJAU466.js";
|
|
6
11
|
import {
|
|
7
12
|
readChainIndex,
|
|
8
13
|
resolveChainsDir
|
|
@@ -10,9 +15,6 @@ import {
|
|
|
10
15
|
import {
|
|
11
16
|
isRecord
|
|
12
17
|
} from "./chunk-YHH3SXKD.js";
|
|
13
|
-
import {
|
|
14
|
-
FallbackLlmClient
|
|
15
|
-
} from "./chunk-3SA5F4WT.js";
|
|
16
18
|
import {
|
|
17
19
|
listJsonFiles,
|
|
18
20
|
readJsonFile
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ../remnic-core/src/json-extract.ts
|
|
2
|
+
function stripCodeFences(text) {
|
|
3
|
+
return text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, (_m, inner) => String(inner).trim());
|
|
4
|
+
}
|
|
5
|
+
function extractJsonCandidates(text) {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
const cleaned = stripCodeFences(trimmed);
|
|
8
|
+
const candidates = [];
|
|
9
|
+
if (cleaned.length > 0) candidates.push(cleaned);
|
|
10
|
+
candidates.push(...scanBalancedJsonBlocks(cleaned));
|
|
11
|
+
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
12
|
+
if (objMatch) candidates.push(objMatch[0]);
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
return candidates.map((c) => c.trim()).filter((c) => c.length > 0).filter((c) => {
|
|
15
|
+
if (seen.has(c)) return false;
|
|
16
|
+
seen.add(c);
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function scanBalancedJsonBlocks(text) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const opens = /* @__PURE__ */ new Set(["{", "["]);
|
|
23
|
+
const closes = { "{": "}", "[": "]" };
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
const start = text[i];
|
|
26
|
+
if (!opens.has(start)) continue;
|
|
27
|
+
const expectedClose = closes[start];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let inString = false;
|
|
30
|
+
let escape = false;
|
|
31
|
+
for (let j = i; j < text.length; j++) {
|
|
32
|
+
const ch = text[j];
|
|
33
|
+
if (inString) {
|
|
34
|
+
if (escape) {
|
|
35
|
+
escape = false;
|
|
36
|
+
} else if (ch === "\\") {
|
|
37
|
+
escape = true;
|
|
38
|
+
} else if (ch === '"') {
|
|
39
|
+
inString = false;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === '"') {
|
|
44
|
+
inString = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === start) depth++;
|
|
48
|
+
if (ch === expectedClose) depth--;
|
|
49
|
+
if (depth === 0) {
|
|
50
|
+
out.push(text.slice(i, j + 1).trim());
|
|
51
|
+
i = j;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
extractJsonCandidates
|
|
61
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-UFU5GGGA.js";
|
|
4
|
+
|
|
5
|
+
// ../remnic-core/src/extraction-judge-telemetry.ts
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
8
|
+
var EXTRACTION_JUDGE_VERDICT_CATEGORY = "EXTRACTION_JUDGE_VERDICT";
|
|
9
|
+
function judgeTelemetryPath(memoryDir) {
|
|
10
|
+
return path.join(
|
|
11
|
+
memoryDir,
|
|
12
|
+
"state",
|
|
13
|
+
"observation-ledger",
|
|
14
|
+
"extraction-judge-verdicts.jsonl"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
async function recordJudgeVerdict(event, options) {
|
|
18
|
+
if (!options.enabled) return;
|
|
19
|
+
const filePath = judgeTelemetryPath(options.memoryDir);
|
|
20
|
+
try {
|
|
21
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
22
|
+
await appendFile(filePath, `${JSON.stringify(event)}
|
|
23
|
+
`, "utf-8");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
log.debug(
|
|
26
|
+
`extraction-judge-telemetry: append failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function readJudgeVerdictStats(memoryDir, opts = {}) {
|
|
31
|
+
const filePath = judgeTelemetryPath(memoryDir);
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = await readFile(filePath, "utf-8");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const code = err.code;
|
|
37
|
+
if (code === "ENOENT") {
|
|
38
|
+
return {
|
|
39
|
+
total: 0,
|
|
40
|
+
accept: 0,
|
|
41
|
+
reject: 0,
|
|
42
|
+
defer: 0,
|
|
43
|
+
deferCapTriggered: 0,
|
|
44
|
+
meanElapsedMs: 0,
|
|
45
|
+
deferRate: 0,
|
|
46
|
+
malformed: 0
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
let total = 0;
|
|
52
|
+
let accept = 0;
|
|
53
|
+
let reject = 0;
|
|
54
|
+
let defer = 0;
|
|
55
|
+
let deferCapTriggered = 0;
|
|
56
|
+
let elapsedSum = 0;
|
|
57
|
+
let malformed = 0;
|
|
58
|
+
let firstTs;
|
|
59
|
+
let lastTs;
|
|
60
|
+
for (const line of raw.split("\n")) {
|
|
61
|
+
if (!line.trim()) continue;
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(line);
|
|
65
|
+
} catch {
|
|
66
|
+
malformed += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
|
+
malformed += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const p = parsed;
|
|
74
|
+
if (p.category !== EXTRACTION_JUDGE_VERDICT_CATEGORY) {
|
|
75
|
+
malformed += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const ts = typeof p.ts === "string" ? p.ts : null;
|
|
79
|
+
if (ts === null) {
|
|
80
|
+
malformed += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const tsMs = Date.parse(ts);
|
|
84
|
+
if (!Number.isFinite(tsMs)) {
|
|
85
|
+
malformed += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (typeof opts.sinceMs === "number" && tsMs < opts.sinceMs) continue;
|
|
89
|
+
if (typeof opts.untilMs === "number" && tsMs >= opts.untilMs) continue;
|
|
90
|
+
const kind = p.verdictKind;
|
|
91
|
+
if (kind === "accept") accept += 1;
|
|
92
|
+
else if (kind === "reject") reject += 1;
|
|
93
|
+
else if (kind === "defer") defer += 1;
|
|
94
|
+
else {
|
|
95
|
+
malformed += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (p.deferCapTriggered === true) deferCapTriggered += 1;
|
|
99
|
+
if (typeof p.elapsedMs === "number" && Number.isFinite(p.elapsedMs)) {
|
|
100
|
+
elapsedSum += p.elapsedMs;
|
|
101
|
+
}
|
|
102
|
+
total += 1;
|
|
103
|
+
if (firstTs === void 0 || ts < firstTs) firstTs = ts;
|
|
104
|
+
if (lastTs === void 0 || ts > lastTs) lastTs = ts;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
total,
|
|
108
|
+
accept,
|
|
109
|
+
reject,
|
|
110
|
+
defer,
|
|
111
|
+
deferCapTriggered,
|
|
112
|
+
meanElapsedMs: total > 0 ? elapsedSum / total : 0,
|
|
113
|
+
deferRate: total > 0 ? defer / total : 0,
|
|
114
|
+
firstTs,
|
|
115
|
+
lastTs,
|
|
116
|
+
malformed
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
EXTRACTION_JUDGE_VERDICT_CATEGORY,
|
|
122
|
+
judgeTelemetryPath,
|
|
123
|
+
recordJudgeVerdict,
|
|
124
|
+
readJudgeVerdictStats
|
|
125
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ../remnic-core/src/page-versioning.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
access,
|
|
6
|
+
mkdir,
|
|
7
|
+
readFile,
|
|
8
|
+
writeFile,
|
|
9
|
+
unlink
|
|
10
|
+
} from "fs/promises";
|
|
11
|
+
var NOOP_LOGGER = {
|
|
12
|
+
debug: () => {
|
|
13
|
+
},
|
|
14
|
+
warn: () => {
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var writeLocks = /* @__PURE__ */ new Map();
|
|
18
|
+
function withPageLock(pageKey, fn) {
|
|
19
|
+
const prev = writeLocks.get(pageKey) ?? Promise.resolve();
|
|
20
|
+
const next = prev.then(fn, fn);
|
|
21
|
+
writeLocks.set(pageKey, next.then(() => {
|
|
22
|
+
}, () => {
|
|
23
|
+
}));
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
function contentHash(content) {
|
|
27
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
function sidecarKey(pagePath) {
|
|
30
|
+
const withoutExt = pagePath.replace(/\.md$/i, "");
|
|
31
|
+
return withoutExt.replace(/[\\/]/g, "__");
|
|
32
|
+
}
|
|
33
|
+
function sidecarDir(memoryDir, sidecar, pagePath) {
|
|
34
|
+
return path.join(memoryDir, sidecar, sidecarKey(pagePath));
|
|
35
|
+
}
|
|
36
|
+
function manifestPath(memoryDir, sidecar, pagePath) {
|
|
37
|
+
return path.join(sidecarDir(memoryDir, sidecar, pagePath), "manifest.json");
|
|
38
|
+
}
|
|
39
|
+
async function fileExists(p) {
|
|
40
|
+
try {
|
|
41
|
+
await access(p);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function readManifest(memoryDir, sidecar, pagePath) {
|
|
48
|
+
const mp = manifestPath(memoryDir, sidecar, pagePath);
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(mp, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
53
|
+
return { pagePath, versions: [], currentVersion: "0" };
|
|
54
|
+
}
|
|
55
|
+
const obj = parsed;
|
|
56
|
+
const versions = Array.isArray(obj.versions) ? obj.versions : [];
|
|
57
|
+
const currentVersion = typeof obj.currentVersion === "string" ? obj.currentVersion : "0";
|
|
58
|
+
return { pagePath: typeof obj.pagePath === "string" ? obj.pagePath : pagePath, versions, currentVersion };
|
|
59
|
+
} catch {
|
|
60
|
+
return { pagePath, versions: [], currentVersion: "0" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function writeManifest(memoryDir, sidecar, pagePath, history) {
|
|
64
|
+
const dir = sidecarDir(memoryDir, sidecar, pagePath);
|
|
65
|
+
await mkdir(dir, { recursive: true });
|
|
66
|
+
const mp = manifestPath(memoryDir, sidecar, pagePath);
|
|
67
|
+
await writeFile(mp, JSON.stringify(history, null, 2) + "\n", "utf-8");
|
|
68
|
+
}
|
|
69
|
+
async function createVersion(pagePath, content, trigger, config, log = NOOP_LOGGER, note, memoryDir) {
|
|
70
|
+
const { sidecarDir: sidecar, maxVersionsPerPage } = config;
|
|
71
|
+
const resolvedMemoryDir = memoryDir ?? resolveMemoryDir(pagePath);
|
|
72
|
+
const mPath = manifestPath(resolvedMemoryDir, sidecar, relPath(pagePath, resolvedMemoryDir));
|
|
73
|
+
return withPageLock(mPath, async () => {
|
|
74
|
+
const history = await readManifest(resolvedMemoryDir, sidecar, relPath(pagePath, resolvedMemoryDir));
|
|
75
|
+
const nextId = String(history.versions.length > 0 ? Math.max(...history.versions.map((v) => Number(v.versionId))) + 1 : 1);
|
|
76
|
+
const hash = contentHash(content);
|
|
77
|
+
const version = {
|
|
78
|
+
versionId: nextId,
|
|
79
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
80
|
+
contentHash: hash,
|
|
81
|
+
sizeBytes: Buffer.byteLength(content, "utf-8"),
|
|
82
|
+
trigger,
|
|
83
|
+
...note !== void 0 ? { note } : {}
|
|
84
|
+
};
|
|
85
|
+
const dir = sidecarDir(resolvedMemoryDir, sidecar, relPath(pagePath, resolvedMemoryDir));
|
|
86
|
+
await mkdir(dir, { recursive: true });
|
|
87
|
+
const ext = path.extname(pagePath) || ".md";
|
|
88
|
+
const snapshotPath = path.join(dir, `${nextId}${ext}`);
|
|
89
|
+
await writeFile(snapshotPath, content, "utf-8");
|
|
90
|
+
history.versions.push(version);
|
|
91
|
+
history.currentVersion = nextId;
|
|
92
|
+
if (maxVersionsPerPage > 0 && history.versions.length > maxVersionsPerPage) {
|
|
93
|
+
const toRemove = history.versions.splice(0, history.versions.length - maxVersionsPerPage);
|
|
94
|
+
for (const old of toRemove) {
|
|
95
|
+
const oldPath = path.join(dir, `${old.versionId}${ext}`);
|
|
96
|
+
try {
|
|
97
|
+
await unlink(oldPath);
|
|
98
|
+
} catch {
|
|
99
|
+
log.debug(`page-versioning: could not remove old snapshot ${oldPath}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await writeManifest(resolvedMemoryDir, sidecar, relPath(pagePath, resolvedMemoryDir), history);
|
|
104
|
+
log.debug(`page-versioning: created version ${nextId} for ${pagePath} (trigger=${trigger})`);
|
|
105
|
+
return version;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function getVersion(pagePath, versionId, config, memoryDir) {
|
|
109
|
+
const resolvedMemoryDir = memoryDir ?? resolveMemoryDir(pagePath);
|
|
110
|
+
const rel = relPath(pagePath, resolvedMemoryDir);
|
|
111
|
+
const ext = path.extname(pagePath) || ".md";
|
|
112
|
+
const dir = sidecarDir(resolvedMemoryDir, config.sidecarDir, rel);
|
|
113
|
+
const snapshotPath = path.join(dir, `${versionId}${ext}`);
|
|
114
|
+
if (!await fileExists(snapshotPath)) {
|
|
115
|
+
throw new Error(`Version ${versionId} not found for ${pagePath}`);
|
|
116
|
+
}
|
|
117
|
+
return readFile(snapshotPath, "utf-8");
|
|
118
|
+
}
|
|
119
|
+
function resolveMemoryDir(pagePath) {
|
|
120
|
+
const knownSubdirs = /* @__PURE__ */ new Set([
|
|
121
|
+
"facts",
|
|
122
|
+
"corrections",
|
|
123
|
+
"entities",
|
|
124
|
+
"state",
|
|
125
|
+
"artifacts",
|
|
126
|
+
"questions",
|
|
127
|
+
"profiles"
|
|
128
|
+
]);
|
|
129
|
+
let dir = path.dirname(pagePath);
|
|
130
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
131
|
+
const base = path.basename(dir);
|
|
132
|
+
if (knownSubdirs.has(base) || /^\d{4}-\d{2}-\d{2}$/.test(base)) {
|
|
133
|
+
dir = path.dirname(dir);
|
|
134
|
+
} else {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return dir;
|
|
139
|
+
}
|
|
140
|
+
function relPath(pagePath, memoryDir) {
|
|
141
|
+
return path.relative(memoryDir, pagePath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
sidecarKey,
|
|
146
|
+
createVersion,
|
|
147
|
+
getVersion
|
|
148
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
StorageManager
|
|
3
|
-
|
|
2
|
+
StorageManager,
|
|
3
|
+
isConsolidationOperator
|
|
4
|
+
} from "./chunk-JJSNPSCD.js";
|
|
4
5
|
import {
|
|
5
6
|
countRecallTokenOverlap,
|
|
6
7
|
normalizeRecallTokens
|
|
@@ -936,6 +937,109 @@ Write ONLY the consolidated memory content (no metadata, no explanation, no prea
|
|
|
936
937
|
function parseConsolidationResponse(response) {
|
|
937
938
|
return response.trim();
|
|
938
939
|
}
|
|
940
|
+
function chooseConsolidationOperator(cluster) {
|
|
941
|
+
if (cluster.memories.length <= 1) return "update";
|
|
942
|
+
return "merge";
|
|
943
|
+
}
|
|
944
|
+
function buildOperatorAwareConsolidationPrompt(cluster) {
|
|
945
|
+
const memoryTexts = cluster.memories.map(
|
|
946
|
+
(m, i) => `Memory ${i + 1} (${m.frontmatter.id}, created ${m.frontmatter.created}):
|
|
947
|
+
${m.content}`
|
|
948
|
+
).join("\n\n");
|
|
949
|
+
return `You are a memory consolidation system. The following ${cluster.memories.length} memories in the "${cluster.category}" category contain overlapping information.
|
|
950
|
+
|
|
951
|
+
Pick exactly ONE consolidation operator for this cluster and return a JSON object.
|
|
952
|
+
|
|
953
|
+
Operator vocabulary:
|
|
954
|
+
- "merge" \u2014 multiple distinct source memories overlap and should be collapsed into one canonical memory (most common).
|
|
955
|
+
- "update" \u2014 one source memory carries a stale value that a newer source supersedes within the same logical fact.
|
|
956
|
+
- "split" \u2014 a single logical source really encodes multiple distinct facts that should be separated (rare; if you pick split, still emit ONE canonical body \u2014 the write path will chunk it later).
|
|
957
|
+
|
|
958
|
+
Output JSON ONLY, no prose before or after. The "operator" key MUST be set to exactly one of the three strings "merge", "update", or "split" \u2014 never a pipe-separated placeholder like "merge|update|split". Example shape:
|
|
959
|
+
{
|
|
960
|
+
"operator": "merge",
|
|
961
|
+
"output": "<the canonical memory text>"
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
The "output" value must:
|
|
965
|
+
1. Preserve ALL unique information from every source memory
|
|
966
|
+
2. Remove redundancy and repetition
|
|
967
|
+
3. Use clear, concise language
|
|
968
|
+
4. Match the "${cluster.category}" category and tone
|
|
969
|
+
5. NOT add information that isn't in the sources
|
|
970
|
+
|
|
971
|
+
${memoryTexts}
|
|
972
|
+
|
|
973
|
+
Return ONLY the JSON object:`;
|
|
974
|
+
}
|
|
975
|
+
function parseOperatorAwareConsolidationResponse(response, cluster) {
|
|
976
|
+
const fallback = {
|
|
977
|
+
operator: chooseConsolidationOperator(cluster),
|
|
978
|
+
output: response.trim()
|
|
979
|
+
};
|
|
980
|
+
const trimmed = response.trim();
|
|
981
|
+
if (trimmed.length === 0) return fallback;
|
|
982
|
+
const fenced = /^```(?:json)?\s*([\s\S]*?)```\s*$/u.exec(trimmed);
|
|
983
|
+
const payload = fenced ? fenced[1].trim() : trimmed;
|
|
984
|
+
const parsed = findLastJsonObjectWithOperator(payload);
|
|
985
|
+
if (parsed === void 0) return fallback;
|
|
986
|
+
if (typeof parsed !== "object" || parsed === null) return fallback;
|
|
987
|
+
const obj = parsed;
|
|
988
|
+
const rawOperator = typeof obj.operator === "string" ? obj.operator.trim().toLowerCase() : "";
|
|
989
|
+
const rawOutput = typeof obj.output === "string" ? obj.output : "";
|
|
990
|
+
const operator = isConsolidationOperator(rawOperator) ? rawOperator : chooseConsolidationOperator(cluster);
|
|
991
|
+
const output = rawOutput.trim().length > 0 ? rawOutput.trim() : response.trim();
|
|
992
|
+
return { operator, output };
|
|
993
|
+
}
|
|
994
|
+
function findLastJsonObjectWithOperator(text) {
|
|
995
|
+
let searchFrom = 0;
|
|
996
|
+
let last = void 0;
|
|
997
|
+
while (searchFrom < text.length) {
|
|
998
|
+
const start = text.indexOf("{", searchFrom);
|
|
999
|
+
if (start < 0) return last;
|
|
1000
|
+
let depth = 0;
|
|
1001
|
+
let inString = false;
|
|
1002
|
+
let escape = false;
|
|
1003
|
+
let closed = false;
|
|
1004
|
+
let endIdx = -1;
|
|
1005
|
+
for (let i = start; i < text.length; i++) {
|
|
1006
|
+
const ch = text[i];
|
|
1007
|
+
if (inString) {
|
|
1008
|
+
if (escape) {
|
|
1009
|
+
escape = false;
|
|
1010
|
+
} else if (ch === "\\") {
|
|
1011
|
+
escape = true;
|
|
1012
|
+
} else if (ch === '"') {
|
|
1013
|
+
inString = false;
|
|
1014
|
+
}
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (ch === '"') {
|
|
1018
|
+
inString = true;
|
|
1019
|
+
} else if (ch === "{") {
|
|
1020
|
+
depth += 1;
|
|
1021
|
+
} else if (ch === "}") {
|
|
1022
|
+
depth -= 1;
|
|
1023
|
+
if (depth === 0) {
|
|
1024
|
+
closed = true;
|
|
1025
|
+
endIdx = i;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (!closed) return last;
|
|
1031
|
+
const slice = text.slice(start, endIdx + 1);
|
|
1032
|
+
try {
|
|
1033
|
+
const parsed = JSON.parse(slice);
|
|
1034
|
+
if (typeof parsed === "object" && parsed !== null && "operator" in parsed) {
|
|
1035
|
+
last = parsed;
|
|
1036
|
+
}
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
searchFrom = endIdx + 1;
|
|
1040
|
+
}
|
|
1041
|
+
return last;
|
|
1042
|
+
}
|
|
939
1043
|
async function buildExtensionsBlockForConsolidation(config) {
|
|
940
1044
|
if (!config.memoryExtensionsEnabled) return "";
|
|
941
1045
|
const root = resolveExtensionsRoot(config);
|
|
@@ -955,6 +1059,9 @@ export {
|
|
|
955
1059
|
findSimilarClusters,
|
|
956
1060
|
buildConsolidationPrompt,
|
|
957
1061
|
parseConsolidationResponse,
|
|
1062
|
+
chooseConsolidationOperator,
|
|
1063
|
+
buildOperatorAwareConsolidationPrompt,
|
|
1064
|
+
parseOperatorAwareConsolidationResponse,
|
|
958
1065
|
buildExtensionsBlockForConsolidation,
|
|
959
1066
|
materializeAfterSemanticConsolidation
|
|
960
1067
|
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ../remnic-core/src/contradiction/contradiction-review.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
function computePairId(memoryIdA, memoryIdB) {
|
|
6
|
+
const sorted = [memoryIdA, memoryIdB].sort();
|
|
7
|
+
return createHash("sha256").update(sorted.join("::")).digest("hex").slice(0, 24);
|
|
8
|
+
}
|
|
9
|
+
function reviewDir(memoryDir) {
|
|
10
|
+
return path.join(memoryDir, ".review", "contradictions");
|
|
11
|
+
}
|
|
12
|
+
function pairPath(memoryDir, pairId) {
|
|
13
|
+
if (pairId.includes("/") || pairId.includes("\\") || pairId.includes("..")) {
|
|
14
|
+
throw new Error(`Invalid pairId: ${pairId}`);
|
|
15
|
+
}
|
|
16
|
+
return path.join(reviewDir(memoryDir), `${pairId}.json`);
|
|
17
|
+
}
|
|
18
|
+
function ensureDir(memoryDir) {
|
|
19
|
+
const dir = reviewDir(memoryDir);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writePair(memoryDir, pair) {
|
|
25
|
+
ensureDir(memoryDir);
|
|
26
|
+
const pairId = computePairId(pair.memoryIds[0], pair.memoryIds[1]);
|
|
27
|
+
const existing = readPair(memoryDir, pairId);
|
|
28
|
+
if (existing?.resolution) {
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
31
|
+
if (existing && existing.confidence >= pair.confidence) {
|
|
32
|
+
return existing;
|
|
33
|
+
}
|
|
34
|
+
const full = {
|
|
35
|
+
...pair,
|
|
36
|
+
pairId,
|
|
37
|
+
lastReviewedAt: existing?.lastReviewedAt,
|
|
38
|
+
resolution: existing?.resolution
|
|
39
|
+
};
|
|
40
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
41
|
+
const tmpPath = `${filePath}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmpPath, JSON.stringify(full, null, 2), "utf-8");
|
|
43
|
+
fs.renameSync(tmpPath, filePath);
|
|
44
|
+
return full;
|
|
45
|
+
}
|
|
46
|
+
function writePairs(memoryDir, pairs) {
|
|
47
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const pair of pairs) {
|
|
50
|
+
const key = computePairId(pair.memoryIds[0], pair.memoryIds[1]);
|
|
51
|
+
if (seen.has(key)) continue;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
results.push(writePair(memoryDir, pair));
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
function readPair(memoryDir, pairId) {
|
|
58
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
59
|
+
try {
|
|
60
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.memoryIds)) {
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function listPairs(memoryDir, options) {
|
|
71
|
+
const startTime = Date.now();
|
|
72
|
+
const dir = reviewDir(memoryDir);
|
|
73
|
+
const { filter = "all", namespace, limit = 50 } = options ?? {};
|
|
74
|
+
const pairs = [];
|
|
75
|
+
let total = 0;
|
|
76
|
+
if (!fs.existsSync(dir)) {
|
|
77
|
+
return { pairs: [], total: 0, durationMs: Date.now() - startTime };
|
|
78
|
+
}
|
|
79
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
80
|
+
if (!entry.endsWith(".json")) continue;
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(path.join(dir, entry), "utf-8");
|
|
83
|
+
const pair = JSON.parse(raw);
|
|
84
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
85
|
+
if (!Array.isArray(pair.memoryIds)) continue;
|
|
86
|
+
if (namespace && pair.namespace !== namespace) continue;
|
|
87
|
+
if (filter === "unresolved") {
|
|
88
|
+
if (pair.resolution) continue;
|
|
89
|
+
if (pair.verdict === "independent") continue;
|
|
90
|
+
} else if (filter !== "all" && pair.verdict !== filter) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
total++;
|
|
94
|
+
if (pairs.length < limit) pairs.push(pair);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { pairs, total, durationMs: Date.now() - startTime };
|
|
100
|
+
}
|
|
101
|
+
function isCoolingDown(pair, cooldownDays) {
|
|
102
|
+
if (cooldownDays <= 0) return false;
|
|
103
|
+
if (!pair.lastReviewedAt) return false;
|
|
104
|
+
const lastReviewed = new Date(pair.lastReviewedAt).getTime();
|
|
105
|
+
if (!Number.isFinite(lastReviewed)) return false;
|
|
106
|
+
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1e3;
|
|
107
|
+
return Date.now() < lastReviewed + cooldownMs;
|
|
108
|
+
}
|
|
109
|
+
function resolvePair(memoryDir, pairId, verb) {
|
|
110
|
+
const existing = readPair(memoryDir, pairId);
|
|
111
|
+
if (!existing) return null;
|
|
112
|
+
const updated = {
|
|
113
|
+
...existing,
|
|
114
|
+
lastReviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115
|
+
resolution: verb
|
|
116
|
+
};
|
|
117
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
118
|
+
const tmpPath = `${filePath}.tmp`;
|
|
119
|
+
fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2), "utf-8");
|
|
120
|
+
fs.renameSync(tmpPath, filePath);
|
|
121
|
+
return updated;
|
|
122
|
+
}
|
|
123
|
+
function memoryHashesChanged(_memoryDir, _pair, _getCurrentHash) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
computePairId,
|
|
129
|
+
writePair,
|
|
130
|
+
writePairs,
|
|
131
|
+
readPair,
|
|
132
|
+
listPairs,
|
|
133
|
+
isCoolingDown,
|
|
134
|
+
resolvePair,
|
|
135
|
+
memoryHashesChanged
|
|
136
|
+
};
|