@remnic/plugin-openclaw 1.0.7 → 1.0.9
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/dist/{calibration-BAC7KNKR.js → calibration-674TDQNV.js} +1 -1
- package/dist/{causal-consolidation-S6M7UTZG.js → causal-consolidation-5BEXLQV5.js} +7 -6
- package/dist/chunk-5ZW5XJQ6.js +125 -0
- package/dist/chunk-6OJAU466.js +148 -0
- package/dist/{chunk-NXLHSCLU.js → chunk-7TENHBV2.js} +22 -8
- package/dist/{chunk-SVGN3ACY.js → chunk-HCFFXBLV.js} +3 -3
- package/dist/{chunk-KPMXWORS.js → chunk-JJSNPSCD.js} +608 -354
- package/dist/{chunk-QHMR3D7U.js → chunk-S2ISS4AH.js} +115 -5
- package/dist/consolidation-undo-5ZSX4MWO.js +426 -0
- package/dist/{contradiction-scan-LRRLWUOS.js → contradiction-scan-U3QKHWQN.js} +42 -6
- package/dist/{engine-WGNTTFYE.js → engine-65C2J63X.js} +3 -2
- package/dist/extraction-judge-telemetry-GHOTVYMP.js +14 -0
- package/dist/{fallback-llm-QEAPMDW7.js → fallback-llm-LVK5PDIM.js} +1 -1
- package/dist/index.js +6134 -1298
- package/dist/{storage-BA6OBLMK.js → storage-DM4ZGOCN.js} +2 -1
- package/openclaw.plugin.json +259 -14
- package/package.json +2 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
readEnvVar,
|
|
3
|
+
resolveHomeDir
|
|
4
|
+
} from "./chunk-7TENHBV2.js";
|
|
5
|
+
import {
|
|
6
|
+
StorageManager,
|
|
7
|
+
isConsolidationOperator
|
|
8
|
+
} from "./chunk-JJSNPSCD.js";
|
|
4
9
|
import {
|
|
5
10
|
countRecallTokenOverlap,
|
|
6
11
|
normalizeRecallTokens
|
|
@@ -422,10 +427,9 @@ function validateMemoryMd(content) {
|
|
|
422
427
|
}
|
|
423
428
|
function resolveCodexHome(override) {
|
|
424
429
|
if (override && override.trim().length > 0) return override;
|
|
425
|
-
const fromEnv =
|
|
430
|
+
const fromEnv = readEnvVar("CODEX_HOME");
|
|
426
431
|
if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
|
|
427
|
-
|
|
428
|
-
return path.join(home, ".codex");
|
|
432
|
+
return path.join(resolveHomeDir(), ".codex");
|
|
429
433
|
}
|
|
430
434
|
function readSentinel(sentinelPath) {
|
|
431
435
|
if (!existsSync(sentinelPath)) return null;
|
|
@@ -936,6 +940,109 @@ Write ONLY the consolidated memory content (no metadata, no explanation, no prea
|
|
|
936
940
|
function parseConsolidationResponse(response) {
|
|
937
941
|
return response.trim();
|
|
938
942
|
}
|
|
943
|
+
function chooseConsolidationOperator(cluster) {
|
|
944
|
+
if (cluster.memories.length <= 1) return "update";
|
|
945
|
+
return "merge";
|
|
946
|
+
}
|
|
947
|
+
function buildOperatorAwareConsolidationPrompt(cluster) {
|
|
948
|
+
const memoryTexts = cluster.memories.map(
|
|
949
|
+
(m, i) => `Memory ${i + 1} (${m.frontmatter.id}, created ${m.frontmatter.created}):
|
|
950
|
+
${m.content}`
|
|
951
|
+
).join("\n\n");
|
|
952
|
+
return `You are a memory consolidation system. The following ${cluster.memories.length} memories in the "${cluster.category}" category contain overlapping information.
|
|
953
|
+
|
|
954
|
+
Pick exactly ONE consolidation operator for this cluster and return a JSON object.
|
|
955
|
+
|
|
956
|
+
Operator vocabulary:
|
|
957
|
+
- "merge" \u2014 multiple distinct source memories overlap and should be collapsed into one canonical memory (most common).
|
|
958
|
+
- "update" \u2014 one source memory carries a stale value that a newer source supersedes within the same logical fact.
|
|
959
|
+
- "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).
|
|
960
|
+
|
|
961
|
+
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:
|
|
962
|
+
{
|
|
963
|
+
"operator": "merge",
|
|
964
|
+
"output": "<the canonical memory text>"
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
The "output" value must:
|
|
968
|
+
1. Preserve ALL unique information from every source memory
|
|
969
|
+
2. Remove redundancy and repetition
|
|
970
|
+
3. Use clear, concise language
|
|
971
|
+
4. Match the "${cluster.category}" category and tone
|
|
972
|
+
5. NOT add information that isn't in the sources
|
|
973
|
+
|
|
974
|
+
${memoryTexts}
|
|
975
|
+
|
|
976
|
+
Return ONLY the JSON object:`;
|
|
977
|
+
}
|
|
978
|
+
function parseOperatorAwareConsolidationResponse(response, cluster) {
|
|
979
|
+
const fallback = {
|
|
980
|
+
operator: chooseConsolidationOperator(cluster),
|
|
981
|
+
output: response.trim()
|
|
982
|
+
};
|
|
983
|
+
const trimmed = response.trim();
|
|
984
|
+
if (trimmed.length === 0) return fallback;
|
|
985
|
+
const fenced = /^```(?:json)?\s*([\s\S]*?)```\s*$/u.exec(trimmed);
|
|
986
|
+
const payload = fenced ? fenced[1].trim() : trimmed;
|
|
987
|
+
const parsed = findLastJsonObjectWithOperator(payload);
|
|
988
|
+
if (parsed === void 0) return fallback;
|
|
989
|
+
if (typeof parsed !== "object" || parsed === null) return fallback;
|
|
990
|
+
const obj = parsed;
|
|
991
|
+
const rawOperator = typeof obj.operator === "string" ? obj.operator.trim().toLowerCase() : "";
|
|
992
|
+
const rawOutput = typeof obj.output === "string" ? obj.output : "";
|
|
993
|
+
const operator = isConsolidationOperator(rawOperator) ? rawOperator : chooseConsolidationOperator(cluster);
|
|
994
|
+
const output = rawOutput.trim().length > 0 ? rawOutput.trim() : response.trim();
|
|
995
|
+
return { operator, output };
|
|
996
|
+
}
|
|
997
|
+
function findLastJsonObjectWithOperator(text) {
|
|
998
|
+
let searchFrom = 0;
|
|
999
|
+
let last = void 0;
|
|
1000
|
+
while (searchFrom < text.length) {
|
|
1001
|
+
const start = text.indexOf("{", searchFrom);
|
|
1002
|
+
if (start < 0) return last;
|
|
1003
|
+
let depth = 0;
|
|
1004
|
+
let inString = false;
|
|
1005
|
+
let escape = false;
|
|
1006
|
+
let closed = false;
|
|
1007
|
+
let endIdx = -1;
|
|
1008
|
+
for (let i = start; i < text.length; i++) {
|
|
1009
|
+
const ch = text[i];
|
|
1010
|
+
if (inString) {
|
|
1011
|
+
if (escape) {
|
|
1012
|
+
escape = false;
|
|
1013
|
+
} else if (ch === "\\") {
|
|
1014
|
+
escape = true;
|
|
1015
|
+
} else if (ch === '"') {
|
|
1016
|
+
inString = false;
|
|
1017
|
+
}
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (ch === '"') {
|
|
1021
|
+
inString = true;
|
|
1022
|
+
} else if (ch === "{") {
|
|
1023
|
+
depth += 1;
|
|
1024
|
+
} else if (ch === "}") {
|
|
1025
|
+
depth -= 1;
|
|
1026
|
+
if (depth === 0) {
|
|
1027
|
+
closed = true;
|
|
1028
|
+
endIdx = i;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (!closed) return last;
|
|
1034
|
+
const slice = text.slice(start, endIdx + 1);
|
|
1035
|
+
try {
|
|
1036
|
+
const parsed = JSON.parse(slice);
|
|
1037
|
+
if (typeof parsed === "object" && parsed !== null && "operator" in parsed) {
|
|
1038
|
+
last = parsed;
|
|
1039
|
+
}
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
searchFrom = endIdx + 1;
|
|
1043
|
+
}
|
|
1044
|
+
return last;
|
|
1045
|
+
}
|
|
939
1046
|
async function buildExtensionsBlockForConsolidation(config) {
|
|
940
1047
|
if (!config.memoryExtensionsEnabled) return "";
|
|
941
1048
|
const root = resolveExtensionsRoot(config);
|
|
@@ -955,6 +1062,9 @@ export {
|
|
|
955
1062
|
findSimilarClusters,
|
|
956
1063
|
buildConsolidationPrompt,
|
|
957
1064
|
parseConsolidationResponse,
|
|
1065
|
+
chooseConsolidationOperator,
|
|
1066
|
+
buildOperatorAwareConsolidationPrompt,
|
|
1067
|
+
parseOperatorAwareConsolidationResponse,
|
|
958
1068
|
buildExtensionsBlockForConsolidation,
|
|
959
1069
|
materializeAfterSemanticConsolidation
|
|
960
1070
|
};
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getVersion
|
|
3
|
+
} from "./chunk-6OJAU466.js";
|
|
4
|
+
import "./chunk-MLKGABMK.js";
|
|
5
|
+
|
|
6
|
+
// ../remnic-core/src/consolidation-undo.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { mkdir, writeFile, access, realpath, lstat } from "fs/promises";
|
|
9
|
+
import { constants as fsConstants } from "fs";
|
|
10
|
+
var DERIVED_FROM_ENTRY_RE = /^(.+):(\d+)$/;
|
|
11
|
+
function parseEntry(entry) {
|
|
12
|
+
if (typeof entry !== "string") return null;
|
|
13
|
+
const match = entry.match(DERIVED_FROM_ENTRY_RE);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
return { pagePath: match[1], versionId: match[2] };
|
|
16
|
+
}
|
|
17
|
+
function isInsideDirectory(candidate, root) {
|
|
18
|
+
const normRoot = path.resolve(root);
|
|
19
|
+
const normCandidate = path.resolve(candidate);
|
|
20
|
+
const rel = path.relative(normRoot, normCandidate);
|
|
21
|
+
if (rel.length === 0) return true;
|
|
22
|
+
if (rel.startsWith("..")) return false;
|
|
23
|
+
if (path.isAbsolute(rel)) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
async function isInsideDirectoryRealpath(candidate, root) {
|
|
27
|
+
if (!isInsideDirectory(candidate, root)) return false;
|
|
28
|
+
const rawSegments = candidate.replace(/\\/g, "/").split("/");
|
|
29
|
+
if (rawSegments.some((s) => s === "..")) return false;
|
|
30
|
+
let resolvedRoot;
|
|
31
|
+
try {
|
|
32
|
+
resolvedRoot = await realpath(path.resolve(root));
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const normCandidate = path.resolve(candidate);
|
|
37
|
+
const normRoot = path.resolve(root);
|
|
38
|
+
const relFromRoot = path.relative(normRoot, normCandidate);
|
|
39
|
+
const segments = relFromRoot.length > 0 ? relFromRoot.split(path.sep) : [];
|
|
40
|
+
for (let i = 0; i <= segments.length; i++) {
|
|
41
|
+
const probe = i === 0 ? normRoot : path.join(normRoot, ...segments.slice(0, i));
|
|
42
|
+
try {
|
|
43
|
+
const st = await lstat(probe);
|
|
44
|
+
if (st.isSymbolicLink() && probe !== normRoot) {
|
|
45
|
+
let target;
|
|
46
|
+
try {
|
|
47
|
+
target = await realpath(probe);
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const rel = path.relative(resolvedRoot, target);
|
|
52
|
+
if (rel.length === 0) continue;
|
|
53
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const parts = normCandidate.split(path.sep);
|
|
59
|
+
for (let i = parts.length; i > 0; i--) {
|
|
60
|
+
const probe = parts.slice(0, i).join(path.sep) || path.sep;
|
|
61
|
+
try {
|
|
62
|
+
const resolved = await realpath(probe);
|
|
63
|
+
const trailing = parts.slice(i).join(path.sep);
|
|
64
|
+
const final = trailing.length > 0 ? path.join(resolved, trailing) : resolved;
|
|
65
|
+
const rel = path.relative(resolvedRoot, final);
|
|
66
|
+
if (rel.length === 0) return true;
|
|
67
|
+
if (rel.startsWith("..")) return false;
|
|
68
|
+
if (path.isAbsolute(rel)) return false;
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
var NON_ACTIVE_PREFIXES = ["archive/", "state/"];
|
|
77
|
+
function normalizeRelativePath(p) {
|
|
78
|
+
const parts = p.replace(/\\/g, "/").split("/");
|
|
79
|
+
const resolved = [];
|
|
80
|
+
for (const seg of parts) {
|
|
81
|
+
if (seg === "" || seg === ".") continue;
|
|
82
|
+
if (seg === "..") {
|
|
83
|
+
if (resolved.length > 0) resolved.pop();
|
|
84
|
+
} else {
|
|
85
|
+
resolved.push(seg);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return resolved.join("/");
|
|
89
|
+
}
|
|
90
|
+
function isActiveMemoryRelativePath(pagePath, sidecarDir) {
|
|
91
|
+
const normalized = normalizeRelativePath(pagePath);
|
|
92
|
+
const prefixes = [...NON_ACTIVE_PREFIXES];
|
|
93
|
+
if (sidecarDir) {
|
|
94
|
+
const normSidecar = normalizeRelativePath(sidecarDir);
|
|
95
|
+
prefixes.push(normSidecar + "/");
|
|
96
|
+
}
|
|
97
|
+
for (const prefix of prefixes) {
|
|
98
|
+
if (normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
async function isRegularFile(p) {
|
|
105
|
+
try {
|
|
106
|
+
const st = await lstat(p);
|
|
107
|
+
return st.isFile();
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function fileExists(p) {
|
|
113
|
+
try {
|
|
114
|
+
await access(p, fsConstants.F_OK);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function runConsolidationUndo(options) {
|
|
121
|
+
const { storage, memoryDir, targetPath, versioning } = options;
|
|
122
|
+
const dryRun = options.dryRun === true;
|
|
123
|
+
const result = {
|
|
124
|
+
targetPath,
|
|
125
|
+
targetArchived: false,
|
|
126
|
+
restores: [],
|
|
127
|
+
dryRun
|
|
128
|
+
};
|
|
129
|
+
if (!await isInsideDirectoryRealpath(targetPath, memoryDir)) {
|
|
130
|
+
result.error = `target path ${targetPath} is outside memory directory ${memoryDir}`;
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
const targetRel = path.relative(memoryDir, targetPath);
|
|
134
|
+
if (!isActiveMemoryRelativePath(targetRel, versioning.sidecarDir)) {
|
|
135
|
+
result.error = `target path "${targetRel}" is inside a non-active directory \u2014 refusing to operate`;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
const target = await storage.readMemoryByPath(targetPath);
|
|
139
|
+
if (!target) {
|
|
140
|
+
result.error = `could not load target memory at ${targetPath}`;
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
const derivedFrom = target.frontmatter.derived_from;
|
|
144
|
+
if (!Array.isArray(derivedFrom) || derivedFrom.length === 0) {
|
|
145
|
+
result.error = "target memory has no derived_from entries \u2014 nothing to undo";
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
const plans = [];
|
|
149
|
+
for (const rawEntry of derivedFrom) {
|
|
150
|
+
const entry = typeof rawEntry === "string" ? rawEntry : String(rawEntry);
|
|
151
|
+
const parsed = parseEntry(rawEntry);
|
|
152
|
+
if (!parsed) {
|
|
153
|
+
plans.push({
|
|
154
|
+
kind: "skip",
|
|
155
|
+
restore: {
|
|
156
|
+
entry,
|
|
157
|
+
sourcePath: "",
|
|
158
|
+
outcome: "skipped_malformed_entry",
|
|
159
|
+
detail: `expected "<path>:<version>" shape`
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (path.isAbsolute(parsed.pagePath)) {
|
|
165
|
+
plans.push({
|
|
166
|
+
kind: "skip",
|
|
167
|
+
restore: {
|
|
168
|
+
entry,
|
|
169
|
+
sourcePath: parsed.pagePath,
|
|
170
|
+
outcome: "skipped_malformed_entry",
|
|
171
|
+
detail: `derived_from path must be relative, got absolute: "${parsed.pagePath}"`
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const sourcePath = path.join(memoryDir, parsed.pagePath);
|
|
177
|
+
if (!await isInsideDirectoryRealpath(sourcePath, memoryDir)) {
|
|
178
|
+
plans.push({
|
|
179
|
+
kind: "skip",
|
|
180
|
+
restore: {
|
|
181
|
+
entry,
|
|
182
|
+
sourcePath,
|
|
183
|
+
outcome: "skipped_outside_memory_dir",
|
|
184
|
+
detail: `resolved path escapes memory directory ${memoryDir}`
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
let resolvedRelative = parsed.pagePath;
|
|
190
|
+
try {
|
|
191
|
+
const realBase = await realpath(memoryDir);
|
|
192
|
+
try {
|
|
193
|
+
const realSource = await realpath(sourcePath);
|
|
194
|
+
const rel = path.relative(realBase, realSource);
|
|
195
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
196
|
+
resolvedRelative = rel.replace(/\\/g, "/");
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
const parentDir = path.dirname(sourcePath);
|
|
200
|
+
try {
|
|
201
|
+
const realParent = await realpath(parentDir);
|
|
202
|
+
const parentRel = path.relative(realBase, realParent);
|
|
203
|
+
if (!parentRel.startsWith("..") && !path.isAbsolute(parentRel)) {
|
|
204
|
+
const leafName = path.basename(sourcePath);
|
|
205
|
+
resolvedRelative = path.join(parentRel, leafName).replace(/\\/g, "/");
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
if (!isActiveMemoryRelativePath(parsed.pagePath, versioning.sidecarDir) || !isActiveMemoryRelativePath(resolvedRelative, versioning.sidecarDir)) {
|
|
213
|
+
plans.push({
|
|
214
|
+
kind: "skip",
|
|
215
|
+
restore: {
|
|
216
|
+
entry,
|
|
217
|
+
sourcePath,
|
|
218
|
+
outcome: "skipped_non_active_path",
|
|
219
|
+
detail: `source path "${parsed.pagePath}" is inside a non-active directory (archive/state/versions)`
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (path.resolve(sourcePath) === path.resolve(targetPath)) {
|
|
225
|
+
plans.push({
|
|
226
|
+
kind: "skip",
|
|
227
|
+
restore: {
|
|
228
|
+
entry,
|
|
229
|
+
sourcePath,
|
|
230
|
+
outcome: "skipped_self_referential",
|
|
231
|
+
detail: `derived_from entry "${entry}" resolves to the same file as the target \u2014 refusing to count as recovered`
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (await isRegularFile(sourcePath)) {
|
|
237
|
+
plans.push({ kind: "recovered_existing", entry, sourcePath });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (await fileExists(sourcePath)) {
|
|
241
|
+
plans.push({
|
|
242
|
+
kind: "skip",
|
|
243
|
+
restore: {
|
|
244
|
+
entry,
|
|
245
|
+
sourcePath,
|
|
246
|
+
outcome: "skipped_non_regular_file",
|
|
247
|
+
detail: "source path is occupied by a non-regular-file; refusing to proceed"
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
let snapshotContent;
|
|
253
|
+
try {
|
|
254
|
+
snapshotContent = await getVersion(
|
|
255
|
+
sourcePath,
|
|
256
|
+
parsed.versionId,
|
|
257
|
+
versioning,
|
|
258
|
+
memoryDir
|
|
259
|
+
);
|
|
260
|
+
} catch {
|
|
261
|
+
plans.push({
|
|
262
|
+
kind: "skip",
|
|
263
|
+
restore: {
|
|
264
|
+
entry,
|
|
265
|
+
sourcePath,
|
|
266
|
+
outcome: "skipped_snapshot_missing",
|
|
267
|
+
detail: `no snapshot for version ${parsed.versionId}`
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
plans.push({ kind: "write", entry, sourcePath, content: snapshotContent });
|
|
273
|
+
}
|
|
274
|
+
const skipped = plans.filter((p) => p.kind === "skip");
|
|
275
|
+
if (skipped.length > 0) {
|
|
276
|
+
for (const p of plans) {
|
|
277
|
+
if (p.kind === "skip") {
|
|
278
|
+
result.restores.push(p.restore);
|
|
279
|
+
} else if (p.kind === "write") {
|
|
280
|
+
result.restores.push({
|
|
281
|
+
entry: p.entry,
|
|
282
|
+
sourcePath: p.sourcePath,
|
|
283
|
+
outcome: dryRun ? "skipped_dry_run" : "skipped_blocked_by_other_failures",
|
|
284
|
+
detail: dryRun ? "would restore from snapshot (blocked by other failures)" : "snapshot available but undo aborted due to other failures"
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
result.restores.push({
|
|
288
|
+
entry: p.entry,
|
|
289
|
+
sourcePath: p.sourcePath,
|
|
290
|
+
outcome: "skipped_file_exists",
|
|
291
|
+
detail: "source file already exists; no restore needed"
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const recovered = result.restores.filter(
|
|
296
|
+
(r) => r.outcome === "restored" || r.outcome === "skipped_file_exists"
|
|
297
|
+
).length;
|
|
298
|
+
if (recovered === 0) {
|
|
299
|
+
result.error = "no sources could be recovered (all snapshots missing or paths unsafe); target not archived to preserve data";
|
|
300
|
+
} else {
|
|
301
|
+
result.error = `${skipped.length} of ${plans.length} sources could not be recovered; target not archived (undo is all-or-nothing)`;
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
const seenSourcePaths = /* @__PURE__ */ new Set();
|
|
306
|
+
const dedupedPlans = [];
|
|
307
|
+
for (const p of plans) {
|
|
308
|
+
if (p.kind === "write" || p.kind === "recovered_existing") {
|
|
309
|
+
if (seenSourcePaths.has(p.sourcePath)) {
|
|
310
|
+
dedupedPlans.push({
|
|
311
|
+
kind: "skip",
|
|
312
|
+
restore: {
|
|
313
|
+
entry: p.kind === "write" ? p.entry : p.entry,
|
|
314
|
+
sourcePath: p.sourcePath,
|
|
315
|
+
outcome: "skipped_file_exists",
|
|
316
|
+
detail: "duplicate derived_from entry \u2014 source already processed"
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
seenSourcePaths.add(p.sourcePath);
|
|
322
|
+
}
|
|
323
|
+
dedupedPlans.push(p);
|
|
324
|
+
}
|
|
325
|
+
if (dryRun) {
|
|
326
|
+
for (const p of dedupedPlans) {
|
|
327
|
+
if (p.kind === "write") {
|
|
328
|
+
result.restores.push({
|
|
329
|
+
entry: p.entry,
|
|
330
|
+
sourcePath: p.sourcePath,
|
|
331
|
+
outcome: "skipped_dry_run",
|
|
332
|
+
detail: "would restore from snapshot"
|
|
333
|
+
});
|
|
334
|
+
} else if (p.kind === "recovered_existing") {
|
|
335
|
+
result.restores.push({
|
|
336
|
+
entry: p.entry,
|
|
337
|
+
sourcePath: p.sourcePath,
|
|
338
|
+
outcome: "skipped_file_exists",
|
|
339
|
+
detail: "source file already exists; no restore needed"
|
|
340
|
+
});
|
|
341
|
+
} else if (p.kind === "skip" && p.restore) {
|
|
342
|
+
result.restores.push(p.restore);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
let writeFailed = false;
|
|
348
|
+
for (const p of dedupedPlans) {
|
|
349
|
+
if (p.kind === "skip") {
|
|
350
|
+
if (p.restore) result.restores.push(p.restore);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (p.kind === "recovered_existing") {
|
|
354
|
+
result.restores.push({
|
|
355
|
+
entry: p.entry,
|
|
356
|
+
sourcePath: p.sourcePath,
|
|
357
|
+
outcome: "skipped_file_exists",
|
|
358
|
+
detail: "source file already exists; no restore needed"
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (p.kind === "write") {
|
|
363
|
+
if (writeFailed) {
|
|
364
|
+
result.restores.push({
|
|
365
|
+
entry: p.entry,
|
|
366
|
+
sourcePath: p.sourcePath,
|
|
367
|
+
outcome: "skipped_blocked_by_other_failures",
|
|
368
|
+
detail: "a prior source write failed; skipping remaining writes to honor all-or-nothing contract"
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await mkdir(path.dirname(p.sourcePath), { recursive: true });
|
|
374
|
+
await writeFile(p.sourcePath, p.content, { encoding: "utf-8", flag: "wx" });
|
|
375
|
+
result.restores.push({
|
|
376
|
+
entry: p.entry,
|
|
377
|
+
sourcePath: p.sourcePath,
|
|
378
|
+
outcome: "restored"
|
|
379
|
+
});
|
|
380
|
+
} catch (err) {
|
|
381
|
+
writeFailed = true;
|
|
382
|
+
result.restores.push({
|
|
383
|
+
entry: p.entry,
|
|
384
|
+
sourcePath: p.sourcePath,
|
|
385
|
+
outcome: "skipped_write_failed",
|
|
386
|
+
detail: `write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (writeFailed) {
|
|
392
|
+
result.error = "one or more source writes failed mid-restore; target not archived to preserve data";
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
const archivedAt = await storage.archiveMemory(target, {
|
|
396
|
+
actor: "consolidate-undo",
|
|
397
|
+
reasonCode: "consolidation-undo"
|
|
398
|
+
});
|
|
399
|
+
result.targetArchived = archivedAt !== null;
|
|
400
|
+
if (!result.targetArchived) {
|
|
401
|
+
result.error = "sources restored successfully but archiving the consolidated target failed; inspect storage for manual cleanup";
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
function formatConsolidationUndoResult(result) {
|
|
406
|
+
const lines = [];
|
|
407
|
+
lines.push(`consolidate undo ${result.dryRun ? "(dry run) " : ""}\u2192 ${result.targetPath}`);
|
|
408
|
+
for (const r of result.restores) {
|
|
409
|
+
lines.push(` - ${r.entry} \u2192 ${r.outcome}${r.detail ? ` (${r.detail})` : ""}`);
|
|
410
|
+
}
|
|
411
|
+
if (result.error) {
|
|
412
|
+
lines.push(` ERROR: ${result.error}`);
|
|
413
|
+
return lines.join("\n");
|
|
414
|
+
}
|
|
415
|
+
lines.push(
|
|
416
|
+
result.dryRun ? " (dry run \u2014 no files were modified, target not archived)" : ` target archived: ${result.targetArchived ? "yes" : "no"}`
|
|
417
|
+
);
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
420
|
+
export {
|
|
421
|
+
formatConsolidationUndoResult,
|
|
422
|
+
isActiveMemoryRelativePath,
|
|
423
|
+
isInsideDirectory,
|
|
424
|
+
isInsideDirectoryRealpath,
|
|
425
|
+
runConsolidationUndo
|
|
426
|
+
};
|
|
@@ -33,7 +33,7 @@ IMPORTANT:
|
|
|
33
33
|
- Two memories about the same entity/topic are NOT necessarily contradictory.
|
|
34
34
|
- Temporal changes ("Joshua uses pnpm" vs "Joshua switched to npm") ARE contradictions.
|
|
35
35
|
- Different aspects of the same entity ("Joshua uses pnpm" vs "Joshua works on Remnic") are "independent".`;
|
|
36
|
-
var
|
|
36
|
+
var defaultVerdictCache = /* @__PURE__ */ new Map();
|
|
37
37
|
var CACHE_MAX = 1e4;
|
|
38
38
|
function pairKey(idA, idB) {
|
|
39
39
|
const sorted = [idA, idB].sort();
|
|
@@ -50,7 +50,7 @@ function contentHash(a) {
|
|
|
50
50
|
async function judgeContradictionPairs(pairs, config, localLlm, fallbackLlm, cache) {
|
|
51
51
|
const startTime = Date.now();
|
|
52
52
|
const results = /* @__PURE__ */ new Map();
|
|
53
|
-
const activeCache = cache ??
|
|
53
|
+
const activeCache = cache ?? defaultVerdictCache;
|
|
54
54
|
let cached = 0;
|
|
55
55
|
let judged = 0;
|
|
56
56
|
const toJudge = [];
|
|
@@ -208,8 +208,9 @@ var SCAN_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
208
208
|
]);
|
|
209
209
|
async function runContradictionScan(deps) {
|
|
210
210
|
const startTime = Date.now();
|
|
211
|
-
const { storage, config, memoryDir, embeddingLookup, localLlm, fallbackLlm, namespace } = deps;
|
|
211
|
+
const { storage, config, memoryDir, embeddingLookup, embeddingLookupFactory, localLlm, fallbackLlm, namespace } = deps;
|
|
212
212
|
const scanConfig = config.contradictionScan;
|
|
213
|
+
const scopedEmbeddingLookup = embeddingLookupFactory ? embeddingLookupFactory(storage) : embeddingLookup;
|
|
213
214
|
if (!scanConfig.enabled) {
|
|
214
215
|
log.info("[contradiction-scan] disabled by config");
|
|
215
216
|
return { scanned: 0, candidates: 0, judged: 0, queued: 0, cooledDown: 0, elapsedMs: 0 };
|
|
@@ -224,7 +225,7 @@ async function runContradictionScan(deps) {
|
|
|
224
225
|
for (const p of existingPairs) {
|
|
225
226
|
existingMap.set(p.pairId, p);
|
|
226
227
|
}
|
|
227
|
-
const candidates = generatePairs(memories, existingMap, scanConfig,
|
|
228
|
+
const candidates = await generatePairs(memories, existingMap, scanConfig, scopedEmbeddingLookup);
|
|
228
229
|
const cooledDown = candidates.skipped;
|
|
229
230
|
log.info("[contradiction-scan] generated %d candidates (%d cooled down)", candidates.pairs.length, cooledDown);
|
|
230
231
|
if (candidates.pairs.length === 0) {
|
|
@@ -246,7 +247,8 @@ async function runContradictionScan(deps) {
|
|
|
246
247
|
categoryA: pair.categoryA,
|
|
247
248
|
categoryB: pair.categoryB
|
|
248
249
|
}));
|
|
249
|
-
const
|
|
250
|
+
const scanCache = /* @__PURE__ */ new Map();
|
|
251
|
+
const judgeResult = await judgeContradictionPairs(judgeInputs, config, localLlm, fallbackLlm, scanCache);
|
|
250
252
|
log.info("[contradiction-scan] judge completed: %d judged, %d cached in %dms", judgeResult.judged, judgeResult.cached, judgeResult.elapsed);
|
|
251
253
|
const queueEntries = [];
|
|
252
254
|
for (const [key, result] of judgeResult.results) {
|
|
@@ -273,8 +275,9 @@ async function runContradictionScan(deps) {
|
|
|
273
275
|
elapsedMs: elapsed
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
|
-
function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
278
|
+
async function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
277
279
|
const pairs = [];
|
|
280
|
+
const embeddingPairs = [];
|
|
278
281
|
let skipped = 0;
|
|
279
282
|
const seen = /* @__PURE__ */ new Set();
|
|
280
283
|
const byEntity = /* @__PURE__ */ new Map();
|
|
@@ -339,6 +342,39 @@ function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
|
339
342
|
});
|
|
340
343
|
}
|
|
341
344
|
}
|
|
345
|
+
if (embeddingLookup) {
|
|
346
|
+
const memoryById = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
347
|
+
for (const mem of memories) {
|
|
348
|
+
const id = mem.frontmatter.id;
|
|
349
|
+
try {
|
|
350
|
+
const hits = await embeddingLookup(mem.content, 20);
|
|
351
|
+
for (const hit of hits) {
|
|
352
|
+
if (hit.score < scanConfig.similarityFloor) continue;
|
|
353
|
+
if (hit.id === id) continue;
|
|
354
|
+
const peer = memoryById.get(hit.id);
|
|
355
|
+
if (!peer) continue;
|
|
356
|
+
const pairId = computePairId(id, hit.id);
|
|
357
|
+
if (seen.has(pairId)) continue;
|
|
358
|
+
seen.add(pairId);
|
|
359
|
+
const existing = existingPairs.get(pairId);
|
|
360
|
+
if (existing && isCoolingDown(existing, scanConfig.cooldownDays)) {
|
|
361
|
+
skipped++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
embeddingPairs.push({
|
|
365
|
+
idA: id,
|
|
366
|
+
idB: hit.id,
|
|
367
|
+
textA: mem.content,
|
|
368
|
+
textB: peer.content,
|
|
369
|
+
categoryA: mem.frontmatter.category,
|
|
370
|
+
categoryB: peer.frontmatter.category
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
pairs.push(...embeddingPairs);
|
|
342
378
|
return { pairs, skipped };
|
|
343
379
|
}
|
|
344
380
|
async function loadEligibleMemories(storage, namespace) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CompoundingEngine,
|
|
3
3
|
defaultTierMigrationCycleBudget
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-HCFFXBLV.js";
|
|
5
|
+
import "./chunk-JJSNPSCD.js";
|
|
6
|
+
import "./chunk-6OJAU466.js";
|
|
6
7
|
import "./chunk-UFU5GGGA.js";
|
|
7
8
|
import "./chunk-MLKGABMK.js";
|
|
8
9
|
export {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXTRACTION_JUDGE_VERDICT_CATEGORY,
|
|
3
|
+
judgeTelemetryPath,
|
|
4
|
+
readJudgeVerdictStats,
|
|
5
|
+
recordJudgeVerdict
|
|
6
|
+
} from "./chunk-5ZW5XJQ6.js";
|
|
7
|
+
import "./chunk-UFU5GGGA.js";
|
|
8
|
+
import "./chunk-MLKGABMK.js";
|
|
9
|
+
export {
|
|
10
|
+
EXTRACTION_JUDGE_VERDICT_CATEGORY,
|
|
11
|
+
judgeTelemetryPath,
|
|
12
|
+
readJudgeVerdictStats,
|
|
13
|
+
recordJudgeVerdict
|
|
14
|
+
};
|