@remnic/plugin-openclaw 1.0.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/dist/calibration-KQXCC77L.js +235 -0
- package/dist/causal-chain-LA3IQNL6.js +22 -0
- package/dist/causal-consolidation-WINYJQJ4.js +205 -0
- package/dist/causal-retrieval-ITNQBUQM.js +182 -0
- package/dist/causal-trajectory-graph-7Z5DD66L.js +59 -0
- package/dist/chunk-5LE4HTVL.js +46 -0
- package/dist/chunk-BZ27H3BL.js +158 -0
- package/dist/chunk-DMGIUDBO.js +41 -0
- package/dist/chunk-GUSMRW4H.js +12 -0
- package/dist/chunk-H3SKMYPU.js +340 -0
- package/dist/chunk-HSBPDYF4.js +278 -0
- package/dist/chunk-JGEKL3WH.js +434 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-TJZ7KBCC.js +577 -0
- package/dist/chunk-WLR4WL6B.js +5893 -0
- package/dist/chunk-Y7JG2Q3V.js +4242 -0
- package/dist/engine-QHRKR53Q.js +11 -0
- package/dist/fallback-llm-2VMRPBHR.js +8 -0
- package/dist/index.js +61705 -0
- package/dist/legacy-hook-compat-XQ7FP6FV.js +35 -0
- package/dist/logger-NZE2OBVA.js +9 -0
- package/dist/storage-AGYBIME4.js +16 -0
- package/openclaw.plugin.json +3779 -0
- package/package.json +54 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendEdge
|
|
3
|
+
} from "./chunk-H3SKMYPU.js";
|
|
4
|
+
import "./chunk-MLKGABMK.js";
|
|
5
|
+
|
|
6
|
+
// ../remnic-core/src/causal-trajectory-graph.ts
|
|
7
|
+
function causalTrajectoryGraphNodeId(trajectoryId, stage) {
|
|
8
|
+
return `causal-trajectory/${trajectoryId}#${stage}`;
|
|
9
|
+
}
|
|
10
|
+
function buildCausalTrajectoryGraphEdges(record) {
|
|
11
|
+
const edges = [
|
|
12
|
+
{
|
|
13
|
+
from: causalTrajectoryGraphNodeId(record.trajectoryId, "goal"),
|
|
14
|
+
to: causalTrajectoryGraphNodeId(record.trajectoryId, "action"),
|
|
15
|
+
type: "causal",
|
|
16
|
+
weight: 1,
|
|
17
|
+
label: "goal_to_action",
|
|
18
|
+
ts: record.recordedAt
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
from: causalTrajectoryGraphNodeId(record.trajectoryId, "action"),
|
|
22
|
+
to: causalTrajectoryGraphNodeId(record.trajectoryId, "observation"),
|
|
23
|
+
type: "causal",
|
|
24
|
+
weight: 1,
|
|
25
|
+
label: "action_to_observation",
|
|
26
|
+
ts: record.recordedAt
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
from: causalTrajectoryGraphNodeId(record.trajectoryId, "observation"),
|
|
30
|
+
to: causalTrajectoryGraphNodeId(record.trajectoryId, "outcome"),
|
|
31
|
+
type: "causal",
|
|
32
|
+
weight: 1,
|
|
33
|
+
label: `observation_to_outcome:${record.outcomeKind}`,
|
|
34
|
+
ts: record.recordedAt
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
if (record.followUpSummary) {
|
|
38
|
+
edges.push({
|
|
39
|
+
from: causalTrajectoryGraphNodeId(record.trajectoryId, "outcome"),
|
|
40
|
+
to: causalTrajectoryGraphNodeId(record.trajectoryId, "follow_up"),
|
|
41
|
+
type: "causal",
|
|
42
|
+
weight: 1,
|
|
43
|
+
label: "outcome_to_follow_up",
|
|
44
|
+
ts: record.recordedAt
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return edges;
|
|
48
|
+
}
|
|
49
|
+
async function appendCausalTrajectoryGraphEdges(options) {
|
|
50
|
+
const edges = buildCausalTrajectoryGraphEdges(options.record);
|
|
51
|
+
for (const edge of edges) {
|
|
52
|
+
await appendEdge(options.memoryDir, edge);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
appendCausalTrajectoryGraphEdges,
|
|
57
|
+
buildCausalTrajectoryGraphEdges,
|
|
58
|
+
causalTrajectoryGraphNodeId
|
|
59
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ../remnic-core/src/json-store.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { readFile, readdir } from "fs/promises";
|
|
4
|
+
async function listJsonFiles(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const fullPath = path.join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
out.push(...await listJsonFiles(fullPath));
|
|
12
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
13
|
+
out.push(fullPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out.sort();
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function listNamedFiles(dir, fileName) {
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
out.push(...await listNamedFiles(fullPath, fileName));
|
|
29
|
+
} else if (entry.isFile() && entry.name === fileName) {
|
|
30
|
+
out.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out.sort();
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function readJsonFile(filePath) {
|
|
39
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
listJsonFiles,
|
|
44
|
+
listNamedFiles,
|
|
45
|
+
readJsonFile
|
|
46
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertIsoRecordedAt,
|
|
3
|
+
assertSafePathSegment,
|
|
4
|
+
assertString,
|
|
5
|
+
countRecallTokenOverlap,
|
|
6
|
+
isRecord,
|
|
7
|
+
normalizeRecallTokens,
|
|
8
|
+
optionalString,
|
|
9
|
+
optionalStringArray,
|
|
10
|
+
validateStringRecord
|
|
11
|
+
} from "./chunk-JGEKL3WH.js";
|
|
12
|
+
import {
|
|
13
|
+
listJsonFiles,
|
|
14
|
+
readJsonFile
|
|
15
|
+
} from "./chunk-5LE4HTVL.js";
|
|
16
|
+
|
|
17
|
+
// ../remnic-core/src/causal-trajectory.ts
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
20
|
+
function validateMetadata(raw) {
|
|
21
|
+
return validateStringRecord(raw, "metadata");
|
|
22
|
+
}
|
|
23
|
+
function resolveCausalTrajectoryStoreDir(memoryDir, overrideDir) {
|
|
24
|
+
if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
|
|
25
|
+
return overrideDir.trim();
|
|
26
|
+
}
|
|
27
|
+
return path.join(memoryDir, "state", "causal-trajectories");
|
|
28
|
+
}
|
|
29
|
+
function validateCausalTrajectoryRecord(raw) {
|
|
30
|
+
if (!isRecord(raw)) throw new Error("causal trajectory record must be an object");
|
|
31
|
+
if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
|
|
32
|
+
const outcomeKind = assertString(raw.outcomeKind, "outcomeKind");
|
|
33
|
+
if (!["success", "failure", "partial", "unknown"].includes(outcomeKind)) {
|
|
34
|
+
throw new Error("outcomeKind must be one of success|failure|partial|unknown");
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
trajectoryId: assertSafePathSegment(assertString(raw.trajectoryId, "trajectoryId"), "trajectoryId"),
|
|
39
|
+
recordedAt: assertIsoRecordedAt(assertString(raw.recordedAt, "recordedAt")),
|
|
40
|
+
sessionKey: assertString(raw.sessionKey, "sessionKey"),
|
|
41
|
+
goal: assertString(raw.goal, "goal"),
|
|
42
|
+
actionSummary: assertString(raw.actionSummary, "actionSummary"),
|
|
43
|
+
observationSummary: assertString(raw.observationSummary, "observationSummary"),
|
|
44
|
+
outcomeKind,
|
|
45
|
+
outcomeSummary: assertString(raw.outcomeSummary, "outcomeSummary"),
|
|
46
|
+
followUpSummary: optionalString(raw.followUpSummary),
|
|
47
|
+
objectiveStateSnapshotRefs: optionalStringArray(raw.objectiveStateSnapshotRefs, "objectiveStateSnapshotRefs"),
|
|
48
|
+
entityRefs: optionalStringArray(raw.entityRefs, "entityRefs"),
|
|
49
|
+
tags: optionalStringArray(raw.tags, "tags"),
|
|
50
|
+
metadata: validateMetadata(raw.metadata)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function readCausalTrajectoryRecords(options) {
|
|
54
|
+
const rootDir = resolveCausalTrajectoryStoreDir(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
55
|
+
const files = await listJsonFiles(path.join(rootDir, "trajectories"));
|
|
56
|
+
const trajectories = [];
|
|
57
|
+
const invalidTrajectories = [];
|
|
58
|
+
for (const filePath of files) {
|
|
59
|
+
try {
|
|
60
|
+
trajectories.push(validateCausalTrajectoryRecord(await readJsonFile(filePath)));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
invalidTrajectories.push({
|
|
63
|
+
path: filePath,
|
|
64
|
+
error: error instanceof Error ? error.message : String(error)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { files, trajectories, invalidTrajectories };
|
|
69
|
+
}
|
|
70
|
+
async function getCausalTrajectoryStoreStatus(options) {
|
|
71
|
+
const rootDir = resolveCausalTrajectoryStoreDir(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
72
|
+
const trajectoriesDir = path.join(rootDir, "trajectories");
|
|
73
|
+
const { files, trajectories, invalidTrajectories } = await readCausalTrajectoryRecords(options);
|
|
74
|
+
trajectories.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
|
|
75
|
+
const byOutcome = {};
|
|
76
|
+
for (const trajectory of trajectories) {
|
|
77
|
+
byOutcome[trajectory.outcomeKind] = (byOutcome[trajectory.outcomeKind] ?? 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
enabled: options.enabled,
|
|
81
|
+
rootDir,
|
|
82
|
+
trajectoriesDir,
|
|
83
|
+
trajectories: {
|
|
84
|
+
total: files.length,
|
|
85
|
+
valid: trajectories.length,
|
|
86
|
+
invalid: invalidTrajectories.length,
|
|
87
|
+
byOutcome,
|
|
88
|
+
latestTrajectoryId: trajectories[0]?.trajectoryId,
|
|
89
|
+
latestRecordedAt: trajectories[0]?.recordedAt,
|
|
90
|
+
latestSessionKey: trajectories[0]?.sessionKey
|
|
91
|
+
},
|
|
92
|
+
latestTrajectory: trajectories[0],
|
|
93
|
+
invalidTrajectories
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function lexicalScoreCausalTrajectoryRecord(record, queryTokens) {
|
|
97
|
+
const weightedFields = [
|
|
98
|
+
["goal", record.goal, 4],
|
|
99
|
+
["action", record.actionSummary, 3],
|
|
100
|
+
["observation", record.observationSummary, 3],
|
|
101
|
+
["outcome", record.outcomeSummary, 3],
|
|
102
|
+
["follow_up", record.followUpSummary, 2],
|
|
103
|
+
["outcome_kind", record.outcomeKind, 1],
|
|
104
|
+
["tags", record.tags?.join(" "), 2],
|
|
105
|
+
["entity_refs", record.entityRefs?.join(" "), 2],
|
|
106
|
+
["objective_state_refs", record.objectiveStateSnapshotRefs?.join(" "), 1]
|
|
107
|
+
];
|
|
108
|
+
let score = 0;
|
|
109
|
+
const matchedFields = [];
|
|
110
|
+
for (const [field, value, weight] of weightedFields) {
|
|
111
|
+
const matches = countRecallTokenOverlap(queryTokens, value, ["make"]);
|
|
112
|
+
if (matches > 0) matchedFields.push(field);
|
|
113
|
+
score += matches * weight;
|
|
114
|
+
}
|
|
115
|
+
return { score, matchedFields };
|
|
116
|
+
}
|
|
117
|
+
function scoreCausalTrajectoryRecord(record, lexicalScore, sessionKey) {
|
|
118
|
+
let score = lexicalScore;
|
|
119
|
+
if (sessionKey && record.sessionKey === sessionKey) score += 1.5;
|
|
120
|
+
const recordedAtMs = Date.parse(record.recordedAt);
|
|
121
|
+
if (Number.isFinite(recordedAtMs)) {
|
|
122
|
+
const ageHours = Math.max(0, (Date.now() - recordedAtMs) / 36e5);
|
|
123
|
+
score += 1 / (1 + ageHours);
|
|
124
|
+
}
|
|
125
|
+
return score;
|
|
126
|
+
}
|
|
127
|
+
async function searchCausalTrajectories(options) {
|
|
128
|
+
const maxResults = Math.max(0, Math.floor(options.maxResults));
|
|
129
|
+
if (maxResults === 0) return [];
|
|
130
|
+
const { trajectories } = await readCausalTrajectoryRecords(options);
|
|
131
|
+
if (trajectories.length === 0) return [];
|
|
132
|
+
const queryTokens = new Set(normalizeRecallTokens(options.query, ["make"]));
|
|
133
|
+
if (queryTokens.size === 0) return [];
|
|
134
|
+
const scored = trajectories.map((record) => {
|
|
135
|
+
const lexical = lexicalScoreCausalTrajectoryRecord(record, queryTokens);
|
|
136
|
+
return {
|
|
137
|
+
record,
|
|
138
|
+
matchedFields: lexical.matchedFields,
|
|
139
|
+
lexicalScore: lexical.score,
|
|
140
|
+
score: scoreCausalTrajectoryRecord(record, lexical.score, options.sessionKey)
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
const filtered = scored.filter((result) => result.lexicalScore > 0);
|
|
144
|
+
filtered.sort((left, right) => {
|
|
145
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
146
|
+
return right.record.recordedAt.localeCompare(left.record.recordedAt);
|
|
147
|
+
});
|
|
148
|
+
return filtered.slice(0, maxResults).map(({ record, score, matchedFields }) => ({
|
|
149
|
+
record,
|
|
150
|
+
score,
|
|
151
|
+
matchedFields
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
getCausalTrajectoryStoreStatus,
|
|
157
|
+
searchCausalTrajectories
|
|
158
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ../remnic-core/src/logger.ts
|
|
2
|
+
var NOOP_LOGGER = {
|
|
3
|
+
info() {
|
|
4
|
+
},
|
|
5
|
+
warn() {
|
|
6
|
+
},
|
|
7
|
+
error() {
|
|
8
|
+
},
|
|
9
|
+
debug() {
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var _backend = NOOP_LOGGER;
|
|
13
|
+
var _debug = false;
|
|
14
|
+
function initLogger(backend, debug) {
|
|
15
|
+
_backend = backend;
|
|
16
|
+
_debug = debug;
|
|
17
|
+
}
|
|
18
|
+
var log = {
|
|
19
|
+
info(msg, ...args) {
|
|
20
|
+
_backend.info(`remnic: ${msg}`, ...args);
|
|
21
|
+
},
|
|
22
|
+
warn(msg, ...args) {
|
|
23
|
+
_backend.warn(`remnic: ${msg}`, ...args);
|
|
24
|
+
},
|
|
25
|
+
error(msg, err) {
|
|
26
|
+
const detail = err instanceof Error ? err.message : err ? String(err) : "";
|
|
27
|
+
_backend.error(
|
|
28
|
+
`remnic: ${msg}${detail ? ` \u2014 ${detail}` : ""}`
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
debug(msg, ...args) {
|
|
32
|
+
if (!_debug) return;
|
|
33
|
+
const fn = _backend.debug ?? _backend.info;
|
|
34
|
+
fn(`remnic [debug]: ${msg}`, ...args);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
initLogger,
|
|
40
|
+
log
|
|
41
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// ../remnic-core/src/graph.ts
|
|
2
|
+
import { mkdir, appendFile, readFile } from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var CAUSAL_PHRASES = [
|
|
5
|
+
"as a result",
|
|
6
|
+
"led to",
|
|
7
|
+
"because of",
|
|
8
|
+
"therefore",
|
|
9
|
+
"caused",
|
|
10
|
+
"because"
|
|
11
|
+
];
|
|
12
|
+
function graphsDir(memoryDir) {
|
|
13
|
+
return path.join(memoryDir, "state", "graphs");
|
|
14
|
+
}
|
|
15
|
+
function graphFilePath(memoryDir, type) {
|
|
16
|
+
return path.join(graphsDir(memoryDir), `${type}.jsonl`);
|
|
17
|
+
}
|
|
18
|
+
async function ensureGraphsDir(memoryDir) {
|
|
19
|
+
await mkdir(graphsDir(memoryDir), { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
async function appendEdge(memoryDir, edge) {
|
|
22
|
+
await ensureGraphsDir(memoryDir);
|
|
23
|
+
const line = JSON.stringify(edge) + "\n";
|
|
24
|
+
await appendFile(graphFilePath(memoryDir, edge.type), line, "utf8");
|
|
25
|
+
}
|
|
26
|
+
async function readEdges(memoryDir, type) {
|
|
27
|
+
const filePath = graphFilePath(memoryDir, type);
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(filePath, "utf8");
|
|
30
|
+
const edges = [];
|
|
31
|
+
for (const line of raw.split("\n")) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed) continue;
|
|
34
|
+
try {
|
|
35
|
+
edges.push(JSON.parse(trimmed));
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return edges;
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function readAllEdges(memoryDir, config) {
|
|
45
|
+
const parts = await Promise.all([
|
|
46
|
+
config.entityGraphEnabled ? readEdges(memoryDir, "entity") : Promise.resolve([]),
|
|
47
|
+
config.timeGraphEnabled ? readEdges(memoryDir, "time") : Promise.resolve([]),
|
|
48
|
+
config.causalGraphEnabled ? readEdges(memoryDir, "causal") : Promise.resolve([])
|
|
49
|
+
]);
|
|
50
|
+
return parts.flat();
|
|
51
|
+
}
|
|
52
|
+
function isValidGraphEdge(raw, expectedType) {
|
|
53
|
+
if (!raw || typeof raw !== "object") return false;
|
|
54
|
+
const edge = raw;
|
|
55
|
+
return edge.type === expectedType && typeof edge.from === "string" && edge.from.length > 0 && typeof edge.to === "string" && edge.to.length > 0 && typeof edge.weight === "number" && Number.isFinite(edge.weight) && typeof edge.label === "string" && typeof edge.ts === "string";
|
|
56
|
+
}
|
|
57
|
+
async function analyzeGraphHealth(memoryDir, options) {
|
|
58
|
+
const enabledTypes = [];
|
|
59
|
+
if (options?.entityGraphEnabled !== false) enabledTypes.push("entity");
|
|
60
|
+
if (options?.timeGraphEnabled !== false) enabledTypes.push("time");
|
|
61
|
+
if (options?.causalGraphEnabled !== false) enabledTypes.push("causal");
|
|
62
|
+
const files = [];
|
|
63
|
+
const globalNodes = /* @__PURE__ */ new Set();
|
|
64
|
+
for (const type of enabledTypes) {
|
|
65
|
+
const filePath = graphFilePath(memoryDir, type);
|
|
66
|
+
let exists = true;
|
|
67
|
+
let totalLines = 0;
|
|
68
|
+
let validEdges = 0;
|
|
69
|
+
let corruptLines = 0;
|
|
70
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
71
|
+
try {
|
|
72
|
+
const raw = await readFile(filePath, "utf8");
|
|
73
|
+
for (const line of raw.split("\n")) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed) continue;
|
|
76
|
+
totalLines += 1;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(trimmed);
|
|
79
|
+
if (!isValidGraphEdge(parsed, type)) {
|
|
80
|
+
corruptLines += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
validEdges += 1;
|
|
84
|
+
nodes.add(parsed.from);
|
|
85
|
+
nodes.add(parsed.to);
|
|
86
|
+
globalNodes.add(parsed.from);
|
|
87
|
+
globalNodes.add(parsed.to);
|
|
88
|
+
} catch {
|
|
89
|
+
corruptLines += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
exists = false;
|
|
94
|
+
}
|
|
95
|
+
files.push({
|
|
96
|
+
type,
|
|
97
|
+
filePath,
|
|
98
|
+
exists,
|
|
99
|
+
totalLines,
|
|
100
|
+
validEdges,
|
|
101
|
+
corruptLines,
|
|
102
|
+
uniqueNodes: nodes.size
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const totals = files.reduce(
|
|
106
|
+
(acc, item) => {
|
|
107
|
+
acc.totalLines += item.totalLines;
|
|
108
|
+
acc.validEdges += item.validEdges;
|
|
109
|
+
acc.corruptLines += item.corruptLines;
|
|
110
|
+
return acc;
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
totalLines: 0,
|
|
114
|
+
validEdges: 0,
|
|
115
|
+
corruptLines: 0,
|
|
116
|
+
uniqueNodes: globalNodes.size
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
totals.uniqueNodes = globalNodes.size;
|
|
120
|
+
const report = {
|
|
121
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
|
+
enabledTypes,
|
|
123
|
+
totals,
|
|
124
|
+
files
|
|
125
|
+
};
|
|
126
|
+
if (options?.includeRepairGuidance === true) {
|
|
127
|
+
const guidance = [];
|
|
128
|
+
if (totals.corruptLines > 0) {
|
|
129
|
+
guidance.push("Corrupt graph lines detected: back up memory/state/graphs, then rebuild graphs from clean memory replay/extraction runs.");
|
|
130
|
+
}
|
|
131
|
+
if (totals.validEdges === 0) {
|
|
132
|
+
guidance.push("No valid edges detected yet: run normal extraction traffic (or replay ingestion) to seed graph files.");
|
|
133
|
+
}
|
|
134
|
+
if (guidance.length > 0) report.repairGuidance = guidance;
|
|
135
|
+
}
|
|
136
|
+
return report;
|
|
137
|
+
}
|
|
138
|
+
function detectCausalPhrase(text) {
|
|
139
|
+
const lower = text.toLowerCase();
|
|
140
|
+
for (const phrase of CAUSAL_PHRASES) {
|
|
141
|
+
if (lower.includes(phrase)) return phrase;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
var GraphIndex = class _GraphIndex {
|
|
146
|
+
memoryDir;
|
|
147
|
+
cfg;
|
|
148
|
+
// Cache for readAllEdges() result. With 30k+ entity edges (6 MB JSONL) the
|
|
149
|
+
// file read + JSON parse takes 2-4 s per call. This instance-level cache
|
|
150
|
+
// eliminates that overhead on every spreadingActivation() call; it is
|
|
151
|
+
// invalidated (set to null) in onMemoryWritten() so new edges appear promptly.
|
|
152
|
+
edgeCache = null;
|
|
153
|
+
static EDGE_CACHE_TTL_MS = 3e5;
|
|
154
|
+
// 5 minutes
|
|
155
|
+
constructor(memoryDir, cfg) {
|
|
156
|
+
this.memoryDir = memoryDir;
|
|
157
|
+
this.cfg = cfg;
|
|
158
|
+
}
|
|
159
|
+
/** Clear the edge cache so the next spreadingActivation() re-reads from disk.
|
|
160
|
+
* Call after any code path that appends edges outside of onMemoryWritten(). */
|
|
161
|
+
invalidateEdgeCache() {
|
|
162
|
+
this.edgeCache = null;
|
|
163
|
+
}
|
|
164
|
+
async loadEdgesCached() {
|
|
165
|
+
if (this.edgeCache && Date.now() - this.edgeCache.loadedAt < _GraphIndex.EDGE_CACHE_TTL_MS) {
|
|
166
|
+
return this.edgeCache.allEdges;
|
|
167
|
+
}
|
|
168
|
+
const allEdges = await readAllEdges(this.memoryDir, {
|
|
169
|
+
entityGraphEnabled: this.cfg.entityGraphEnabled,
|
|
170
|
+
timeGraphEnabled: this.cfg.timeGraphEnabled,
|
|
171
|
+
causalGraphEnabled: this.cfg.causalGraphEnabled
|
|
172
|
+
});
|
|
173
|
+
this.edgeCache = { allEdges, loadedAt: Date.now() };
|
|
174
|
+
return allEdges;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Called after a memory is written to disk.
|
|
178
|
+
*
|
|
179
|
+
* @param memoryPath - relative path from memoryDir (e.g. "facts/2026-02-22/abc.md")
|
|
180
|
+
* @param entityRef - entityRef frontmatter field (if any)
|
|
181
|
+
* @param content - full memory text (for causal detection)
|
|
182
|
+
* @param created - ISO timestamp of this memory
|
|
183
|
+
* @param threadId - current thread ID (for time graph)
|
|
184
|
+
* @param recentInThread - paths of the N most-recent memories in this thread (for time graph)
|
|
185
|
+
* @param entitySiblings - paths of other memories that share the same entityRef (for entity graph)
|
|
186
|
+
*/
|
|
187
|
+
async onMemoryWritten(opts) {
|
|
188
|
+
if (!this.cfg.multiGraphMemoryEnabled) return;
|
|
189
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
190
|
+
try {
|
|
191
|
+
if (this.cfg.entityGraphEnabled && opts.entityRef && opts.entitySiblings?.length) {
|
|
192
|
+
const siblings = opts.entitySiblings.slice(0, this.cfg.maxEntityGraphEdgesPerMemory);
|
|
193
|
+
for (const sibling of siblings) {
|
|
194
|
+
await appendEdge(this.memoryDir, {
|
|
195
|
+
from: opts.memoryPath,
|
|
196
|
+
to: sibling,
|
|
197
|
+
type: "entity",
|
|
198
|
+
weight: 1,
|
|
199
|
+
label: opts.entityRef,
|
|
200
|
+
ts
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (this.cfg.timeGraphEnabled && opts.threadId && opts.recentInThread?.length) {
|
|
205
|
+
const predecessor = opts.recentInThread[opts.recentInThread.length - 1];
|
|
206
|
+
if (predecessor && predecessor !== opts.memoryPath) {
|
|
207
|
+
await appendEdge(this.memoryDir, {
|
|
208
|
+
from: predecessor,
|
|
209
|
+
to: opts.memoryPath,
|
|
210
|
+
type: "time",
|
|
211
|
+
weight: 1,
|
|
212
|
+
label: opts.threadId,
|
|
213
|
+
ts
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (this.cfg.causalGraphEnabled && opts.causalPredecessor) {
|
|
218
|
+
const phrase = detectCausalPhrase(opts.content);
|
|
219
|
+
if (phrase) {
|
|
220
|
+
await appendEdge(this.memoryDir, {
|
|
221
|
+
from: opts.causalPredecessor,
|
|
222
|
+
to: opts.memoryPath,
|
|
223
|
+
type: "causal",
|
|
224
|
+
weight: 1,
|
|
225
|
+
label: phrase,
|
|
226
|
+
ts
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const { log } = await import("./logger-NZE2OBVA.js");
|
|
232
|
+
log.warn(`[graph] onMemoryWritten error: ${err}`);
|
|
233
|
+
} finally {
|
|
234
|
+
this.edgeCache = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Spreading activation BFS (SYNAPSE-inspired).
|
|
239
|
+
*
|
|
240
|
+
* Starting from `seeds`, traverse the combined graph for up to `maxSteps` hops.
|
|
241
|
+
* Each candidate gets an activation score = edge.weight × decay^hop.
|
|
242
|
+
* Returns top-N candidate paths sorted by descending activation score.
|
|
243
|
+
*
|
|
244
|
+
* @param seeds - initial memory paths to expand from (e.g. QMD top results)
|
|
245
|
+
* @param maxSteps - max BFS hops (from config: maxGraphTraversalSteps)
|
|
246
|
+
* @returns Array of {path, score} sorted descending, not including seed paths
|
|
247
|
+
*/
|
|
248
|
+
async spreadingActivation(seeds, maxSteps) {
|
|
249
|
+
if (!this.cfg.multiGraphMemoryEnabled) return [];
|
|
250
|
+
const steps = maxSteps ?? this.cfg.maxGraphTraversalSteps;
|
|
251
|
+
const decay = this.cfg.graphActivationDecay;
|
|
252
|
+
try {
|
|
253
|
+
const allEdges = await this.loadEdgesCached();
|
|
254
|
+
const adj = /* @__PURE__ */ new Map();
|
|
255
|
+
for (const edge of allEdges) {
|
|
256
|
+
if (!adj.has(edge.from)) adj.set(edge.from, []);
|
|
257
|
+
adj.get(edge.from).push(edge);
|
|
258
|
+
if (edge.type !== "causal") {
|
|
259
|
+
if (!adj.has(edge.to)) adj.set(edge.to, []);
|
|
260
|
+
adj.get(edge.to).push({ ...edge, from: edge.to, to: edge.from });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const seedSet = new Set(seeds);
|
|
264
|
+
const scores = /* @__PURE__ */ new Map();
|
|
265
|
+
const provenance = /* @__PURE__ */ new Map();
|
|
266
|
+
const visited = new Set(seeds);
|
|
267
|
+
const queue = seeds.map((s) => [s, 0, s]);
|
|
268
|
+
while (queue.length > 0) {
|
|
269
|
+
const [node, hop, sourceSeed] = queue.shift();
|
|
270
|
+
if (hop >= steps) continue;
|
|
271
|
+
const edges = adj.get(node) ?? [];
|
|
272
|
+
for (const edge of edges) {
|
|
273
|
+
const neighbor = edge.to === node ? edge.from : edge.to;
|
|
274
|
+
const score = edge.weight * Math.pow(decay, hop + 1);
|
|
275
|
+
if (!seedSet.has(neighbor)) {
|
|
276
|
+
const existing = scores.get(neighbor) ?? 0;
|
|
277
|
+
scores.set(neighbor, existing + score);
|
|
278
|
+
const prev = provenance.get(neighbor);
|
|
279
|
+
if (!prev || hop + 1 < prev.hopDepth || hop + 1 === prev.hopDepth && score > prev.decayedWeight) {
|
|
280
|
+
provenance.set(neighbor, {
|
|
281
|
+
seed: sourceSeed,
|
|
282
|
+
hopDepth: hop + 1,
|
|
283
|
+
decayedWeight: score,
|
|
284
|
+
graphType: edge.type
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!visited.has(neighbor)) {
|
|
289
|
+
visited.add(neighbor);
|
|
290
|
+
queue.push([neighbor, hop + 1, sourceSeed]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (this.cfg.graphLateralInhibitionEnabled && scores.size > 1) {
|
|
295
|
+
const inhibited = applyLateralInhibition(scores, {
|
|
296
|
+
beta: this.cfg.graphLateralInhibitionBeta,
|
|
297
|
+
topM: this.cfg.graphLateralInhibitionTopM
|
|
298
|
+
});
|
|
299
|
+
for (const [k, v] of inhibited) {
|
|
300
|
+
scores.set(k, v);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return Array.from(scores.entries()).map(([p, score]) => ({
|
|
304
|
+
path: p,
|
|
305
|
+
score,
|
|
306
|
+
seed: provenance.get(p)?.seed ?? "",
|
|
307
|
+
hopDepth: provenance.get(p)?.hopDepth ?? 0,
|
|
308
|
+
decayedWeight: provenance.get(p)?.decayedWeight ?? 0,
|
|
309
|
+
graphType: provenance.get(p)?.graphType ?? "entity"
|
|
310
|
+
})).sort((a, b) => b.score - a.score);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
const { log } = await import("./logger-NZE2OBVA.js");
|
|
313
|
+
log.warn(`[graph] spreadingActivation error: ${err}`);
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function applyLateralInhibition(scores, opts) {
|
|
319
|
+
const { beta, topM } = opts;
|
|
320
|
+
if (beta === 0 || topM === 0) return new Map(scores);
|
|
321
|
+
const sorted = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
|
|
322
|
+
const topCompetitors = sorted.slice(0, topM);
|
|
323
|
+
const result = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const [node, u] of scores) {
|
|
325
|
+
let inhibition = 0;
|
|
326
|
+
for (const [, uK] of topCompetitors) {
|
|
327
|
+
if (uK > u) {
|
|
328
|
+
inhibition += uK - u;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
result.set(node, Math.max(0, u - beta * inhibition));
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export {
|
|
337
|
+
appendEdge,
|
|
338
|
+
analyzeGraphHealth,
|
|
339
|
+
GraphIndex
|
|
340
|
+
};
|