@jonathangu/openclawbrain 0.3.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/LICENSE +21 -0
- package/README.md +412 -0
- package/bin/openclawbrain.js +15 -0
- package/docs/END_STATE.md +244 -0
- package/docs/EVIDENCE.md +128 -0
- package/docs/RELEASE_CONTRACT.md +91 -0
- package/docs/agent-tools.md +106 -0
- package/docs/architecture.md +224 -0
- package/docs/configuration.md +178 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
- package/docs/evidence/README.md +16 -0
- package/docs/fts5.md +161 -0
- package/docs/tui.md +506 -0
- package/index.ts +1372 -0
- package/openclaw.plugin.json +136 -0
- package/package.json +66 -0
- package/src/assembler.ts +804 -0
- package/src/brain-cli.ts +316 -0
- package/src/brain-core/decay.ts +35 -0
- package/src/brain-core/episode.ts +82 -0
- package/src/brain-core/graph.ts +321 -0
- package/src/brain-core/health.ts +116 -0
- package/src/brain-core/mutator.ts +281 -0
- package/src/brain-core/pack.ts +117 -0
- package/src/brain-core/policy.ts +153 -0
- package/src/brain-core/replay.ts +1 -0
- package/src/brain-core/teacher.ts +105 -0
- package/src/brain-core/trace.ts +40 -0
- package/src/brain-core/traverse.ts +230 -0
- package/src/brain-core/types.ts +405 -0
- package/src/brain-core/update.ts +123 -0
- package/src/brain-harvest/human.ts +46 -0
- package/src/brain-harvest/scanner.ts +98 -0
- package/src/brain-harvest/self.ts +147 -0
- package/src/brain-runtime/assembler-extension.ts +230 -0
- package/src/brain-runtime/evidence-detectors.ts +68 -0
- package/src/brain-runtime/graph-io.ts +72 -0
- package/src/brain-runtime/harvester-extension.ts +98 -0
- package/src/brain-runtime/service.ts +659 -0
- package/src/brain-runtime/tools.ts +109 -0
- package/src/brain-runtime/worker-state.ts +106 -0
- package/src/brain-runtime/worker-supervisor.ts +169 -0
- package/src/brain-store/embedding.ts +179 -0
- package/src/brain-store/init.ts +347 -0
- package/src/brain-store/migrations.ts +188 -0
- package/src/brain-store/store.ts +816 -0
- package/src/brain-worker/child-runner.ts +321 -0
- package/src/brain-worker/jobs.ts +12 -0
- package/src/brain-worker/mutation-job.ts +5 -0
- package/src/brain-worker/promotion-job.ts +5 -0
- package/src/brain-worker/protocol.ts +79 -0
- package/src/brain-worker/teacher-job.ts +5 -0
- package/src/brain-worker/update-job.ts +5 -0
- package/src/brain-worker/worker.ts +422 -0
- package/src/compaction.ts +1332 -0
- package/src/db/config.ts +265 -0
- package/src/db/connection.ts +72 -0
- package/src/db/features.ts +42 -0
- package/src/db/migration.ts +561 -0
- package/src/engine.ts +1995 -0
- package/src/expansion-auth.ts +351 -0
- package/src/expansion-policy.ts +303 -0
- package/src/expansion.ts +383 -0
- package/src/integrity.ts +600 -0
- package/src/large-files.ts +527 -0
- package/src/openclaw-bridge.ts +22 -0
- package/src/retrieval.ts +357 -0
- package/src/store/conversation-store.ts +748 -0
- package/src/store/fts5-sanitize.ts +29 -0
- package/src/store/full-text-fallback.ts +74 -0
- package/src/store/index.ts +29 -0
- package/src/store/summary-store.ts +918 -0
- package/src/summarize.ts +847 -0
- package/src/tools/common.ts +53 -0
- package/src/tools/lcm-conversation-scope.ts +76 -0
- package/src/tools/lcm-describe-tool.ts +234 -0
- package/src/tools/lcm-expand-query-tool.ts +594 -0
- package/src/tools/lcm-expand-tool.delegation.ts +556 -0
- package/src/tools/lcm-expand-tool.ts +448 -0
- package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
- package/src/tools/lcm-grep-tool.ts +200 -0
- package/src/transcript-repair.ts +301 -0
- package/src/types.ts +149 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite persistence for the brain's learned retrieval graph.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same patterns as LCM's ConversationStore and SummaryStore:
|
|
5
|
+
* - Constructor takes DatabaseSync
|
|
6
|
+
* - Snake_case DB rows → camelCase TypeScript objects
|
|
7
|
+
* - Prepared statements for performance
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type {
|
|
15
|
+
BrainNode,
|
|
16
|
+
BrainEdge,
|
|
17
|
+
EdgeKind,
|
|
18
|
+
Episode,
|
|
19
|
+
Label,
|
|
20
|
+
RewardSource,
|
|
21
|
+
Pack,
|
|
22
|
+
MutationProposal,
|
|
23
|
+
MutationStatus,
|
|
24
|
+
DecisionTrace,
|
|
25
|
+
BrainEvidence,
|
|
26
|
+
BrainEvidenceKind,
|
|
27
|
+
BrainEvidenceResolution,
|
|
28
|
+
ResolvedLabel,
|
|
29
|
+
SeedWeight,
|
|
30
|
+
} from "../brain-core/types.js";
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════
|
|
33
|
+
// Embedding serialization
|
|
34
|
+
// ═══════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
function serializeEmbedding(emb: Float32Array | null): Buffer | null {
|
|
37
|
+
if (!emb) return null;
|
|
38
|
+
return Buffer.from(emb.buffer, emb.byteOffset, emb.byteLength);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deserializeEmbedding(blob: Buffer | Uint8Array | null): Float32Array | null {
|
|
42
|
+
if (!blob) return null;
|
|
43
|
+
const buf = blob instanceof Buffer ? blob : Buffer.from(blob);
|
|
44
|
+
return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════
|
|
48
|
+
// BrainStore
|
|
49
|
+
// ═══════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
export class BrainStore {
|
|
52
|
+
constructor(
|
|
53
|
+
private db: DatabaseSync,
|
|
54
|
+
private options: { brainRoot?: string } = {},
|
|
55
|
+
) {
|
|
56
|
+
if (this.options.brainRoot) {
|
|
57
|
+
mkdirSync(this.options.brainRoot, { recursive: true });
|
|
58
|
+
mkdirSync(this.getPacksDir(), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Nodes ───
|
|
63
|
+
|
|
64
|
+
insertNode(node: BrainNode): void {
|
|
65
|
+
this.db.prepare(`
|
|
66
|
+
INSERT INTO brain_nodes (id, kind, content, embedding, source_uri, trust, tags, token_count, metadata, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
`).run(
|
|
69
|
+
node.id, node.kind, node.content,
|
|
70
|
+
serializeEmbedding(node.embedding),
|
|
71
|
+
node.sourceUri, node.trust,
|
|
72
|
+
JSON.stringify(node.tags), node.tokenCount,
|
|
73
|
+
JSON.stringify(node.metadata),
|
|
74
|
+
node.createdAt, node.updatedAt,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getNode(id: string): BrainNode | null {
|
|
79
|
+
const row = this.db.prepare(`SELECT * FROM brain_nodes WHERE id = ?`).get(id) as Record<string, unknown> | undefined;
|
|
80
|
+
return row ? this.toNode(row) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getAllNodes(): BrainNode[] {
|
|
84
|
+
const rows = this.db.prepare(`SELECT * FROM brain_nodes`).all() as Record<string, unknown>[];
|
|
85
|
+
return rows.map((r) => this.toNode(r));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
updateNodeEmbedding(id: string, embedding: Float32Array): void {
|
|
89
|
+
this.db.prepare(`UPDATE brain_nodes SET embedding = ?, updated_at = ? WHERE id = ?`)
|
|
90
|
+
.run(serializeEmbedding(embedding), Date.now(), id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clearGraph(): void {
|
|
94
|
+
this.db.exec(`
|
|
95
|
+
DELETE FROM brain_seed_weights;
|
|
96
|
+
DELETE FROM brain_edges;
|
|
97
|
+
DELETE FROM brain_nodes;
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deleteNode(id: string): void {
|
|
102
|
+
this.db.prepare(`DELETE FROM brain_seed_weights WHERE node_id = ?`).run(id);
|
|
103
|
+
this.db.prepare(`DELETE FROM brain_edges WHERE source = ? OR target = ?`).run(id, id);
|
|
104
|
+
this.db.prepare(`DELETE FROM brain_nodes WHERE id = ?`).run(id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private toNode(row: Record<string, unknown>): BrainNode {
|
|
108
|
+
return {
|
|
109
|
+
id: row.id as string,
|
|
110
|
+
kind: row.kind as BrainNode["kind"],
|
|
111
|
+
content: row.content as string,
|
|
112
|
+
embedding: deserializeEmbedding(row.embedding as Buffer | null),
|
|
113
|
+
sourceUri: (row.source_uri as string) || null,
|
|
114
|
+
trust: row.trust as BrainNode["trust"],
|
|
115
|
+
tags: JSON.parse((row.tags as string) || "[]"),
|
|
116
|
+
tokenCount: (row.token_count as number) || 0,
|
|
117
|
+
metadata: JSON.parse((row.metadata as string) || "{}"),
|
|
118
|
+
createdAt: row.created_at as number,
|
|
119
|
+
updatedAt: row.updated_at as number,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Edges ───
|
|
124
|
+
|
|
125
|
+
insertEdge(edge: BrainEdge): void {
|
|
126
|
+
this.db.prepare(`
|
|
127
|
+
INSERT OR REPLACE INTO brain_edges (source, target, kind, weight, prior, metadata, decayed_at, created_at)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
+
`).run(
|
|
130
|
+
edge.source, edge.target, edge.kind,
|
|
131
|
+
edge.weight, edge.prior,
|
|
132
|
+
JSON.stringify(edge.metadata),
|
|
133
|
+
edge.decayedAt, edge.createdAt,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getOutgoingEdges(source: string): BrainEdge[] {
|
|
138
|
+
const rows = this.db.prepare(`SELECT * FROM brain_edges WHERE source = ?`).all(source) as Record<string, unknown>[];
|
|
139
|
+
return rows.map((r) => this.toEdge(r));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
updateEdgeWeight(source: string, target: string, kind: EdgeKind, weight: number): void {
|
|
143
|
+
this.db.prepare(`UPDATE brain_edges SET weight = ? WHERE source = ? AND target = ? AND kind = ?`)
|
|
144
|
+
.run(weight, source, target, kind);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
deleteEdge(source: string, target: string, kind: EdgeKind): void {
|
|
148
|
+
this.db.prepare(`DELETE FROM brain_edges WHERE source = ? AND target = ? AND kind = ?`)
|
|
149
|
+
.run(source, target, kind);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
decayAllWeights(rate: number): void {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
this.db.prepare(`
|
|
155
|
+
UPDATE brain_edges SET weight = weight * ? + prior * (1.0 - ?), decayed_at = ?
|
|
156
|
+
`).run(rate, rate, now);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private toEdge(row: Record<string, unknown>): BrainEdge {
|
|
160
|
+
return {
|
|
161
|
+
source: row.source as string,
|
|
162
|
+
target: row.target as string,
|
|
163
|
+
kind: row.kind as EdgeKind,
|
|
164
|
+
weight: row.weight as number,
|
|
165
|
+
prior: row.prior as number,
|
|
166
|
+
metadata: JSON.parse((row.metadata as string) || "{}"),
|
|
167
|
+
decayedAt: row.decayed_at as number,
|
|
168
|
+
createdAt: row.created_at as number,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Seed Weights ───
|
|
173
|
+
|
|
174
|
+
setSeedWeight(nodeId: string, weight: number): void {
|
|
175
|
+
this.db.prepare(`
|
|
176
|
+
INSERT INTO brain_seed_weights (node_id, weight, updated_at)
|
|
177
|
+
VALUES (?, ?, ?)
|
|
178
|
+
ON CONFLICT(node_id) DO UPDATE SET weight = excluded.weight, updated_at = excluded.updated_at
|
|
179
|
+
`).run(nodeId, weight, Date.now());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getSeedWeight(nodeId: string): SeedWeight | null {
|
|
183
|
+
const row = this.db.prepare(`SELECT * FROM brain_seed_weights WHERE node_id = ?`).get(nodeId) as Record<string, unknown> | undefined;
|
|
184
|
+
if (!row) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
nodeId: row.node_id as string,
|
|
189
|
+
weight: row.weight as number,
|
|
190
|
+
updatedAt: row.updated_at as number,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getSeedWeights(nodeIds: string[]): Record<string, number> {
|
|
195
|
+
if (nodeIds.length === 0) {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
const placeholders = nodeIds.map(() => "?").join(", ");
|
|
199
|
+
const rows = this.db.prepare(`SELECT node_id, weight FROM brain_seed_weights WHERE node_id IN (${placeholders})`).all(...nodeIds) as Array<{ node_id: string; weight: number }>;
|
|
200
|
+
const weights: Record<string, number> = {};
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
weights[row.node_id] = row.weight;
|
|
203
|
+
}
|
|
204
|
+
return weights;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getAllSeedWeights(): SeedWeight[] {
|
|
208
|
+
const rows = this.db.prepare(`SELECT * FROM brain_seed_weights`).all() as Record<string, unknown>[];
|
|
209
|
+
return rows.map((row) => ({
|
|
210
|
+
nodeId: row.node_id as string,
|
|
211
|
+
weight: row.weight as number,
|
|
212
|
+
updatedAt: row.updated_at as number,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Episodes ───
|
|
217
|
+
|
|
218
|
+
insertEpisode(episode: Episode): void {
|
|
219
|
+
this.db.prepare(`
|
|
220
|
+
INSERT INTO brain_episodes (id, conversation_id, query_text, query_embedding, trajectory, fired_nodes, vetoed_nodes, context_chars, reward, reward_source, pack_version, created_at)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
222
|
+
`).run(
|
|
223
|
+
episode.id, episode.conversationId, episode.queryText,
|
|
224
|
+
serializeEmbedding(episode.queryEmbedding),
|
|
225
|
+
JSON.stringify(episode.trajectory),
|
|
226
|
+
JSON.stringify(episode.firedNodes),
|
|
227
|
+
JSON.stringify(episode.vetoedNodes),
|
|
228
|
+
episode.contextChars, episode.reward, episode.rewardSource,
|
|
229
|
+
episode.packVersion, episode.createdAt,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getEpisode(id: string): Episode | null {
|
|
234
|
+
const row = this.db.prepare(`SELECT * FROM brain_episodes WHERE id = ?`).get(id) as Record<string, unknown> | undefined;
|
|
235
|
+
return row ? this.toEpisode(row) : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getRecentEpisodes(limit: number): Episode[] {
|
|
239
|
+
const rows = this.db.prepare(`SELECT * FROM brain_episodes ORDER BY created_at DESC LIMIT ?`).all(limit) as Record<string, unknown>[];
|
|
240
|
+
return rows.map((r) => this.toEpisode(r));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getRecentEpisodesForConversation(conversationId: number, limit: number): Episode[] {
|
|
244
|
+
const rows = this.db.prepare(`
|
|
245
|
+
SELECT * FROM brain_episodes
|
|
246
|
+
WHERE conversation_id = ?
|
|
247
|
+
ORDER BY created_at DESC
|
|
248
|
+
LIMIT ?
|
|
249
|
+
`).all(conversationId, limit) as Record<string, unknown>[];
|
|
250
|
+
return rows.map((r) => this.toEpisode(r));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getEpisodesForUpdate(limit: number): Episode[] {
|
|
254
|
+
const rows = this.db.prepare(`
|
|
255
|
+
SELECT * FROM brain_episodes WHERE reward IS NOT NULL AND updated = 0 ORDER BY created_at ASC LIMIT ?
|
|
256
|
+
`).all(limit) as Record<string, unknown>[];
|
|
257
|
+
return rows.map((r) => this.toEpisode(r));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getUnlabeledEpisodes(limit: number): Episode[] {
|
|
261
|
+
const rows = this.db.prepare(`
|
|
262
|
+
SELECT * FROM brain_episodes WHERE reward IS NULL ORDER BY created_at ASC LIMIT ?
|
|
263
|
+
`).all(limit) as Record<string, unknown>[];
|
|
264
|
+
return rows.map((r) => this.toEpisode(r));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setEpisodeReward(id: string, reward: number, source: RewardSource): void {
|
|
268
|
+
this.db.prepare(`UPDATE brain_episodes SET reward = ?, reward_source = ? WHERE id = ?`)
|
|
269
|
+
.run(reward, source, id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
markEpisodeUpdated(id: string): void {
|
|
273
|
+
this.db.prepare(`UPDATE brain_episodes SET updated = 1 WHERE id = ?`).run(id);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private toEpisode(row: Record<string, unknown>): Episode {
|
|
277
|
+
return {
|
|
278
|
+
id: row.id as string,
|
|
279
|
+
conversationId: (row.conversation_id as number) ?? null,
|
|
280
|
+
queryText: (row.query_text as string) || "",
|
|
281
|
+
queryEmbedding: deserializeEmbedding(row.query_embedding as Buffer | null),
|
|
282
|
+
trajectory: JSON.parse((row.trajectory as string) || "[]"),
|
|
283
|
+
firedNodes: JSON.parse((row.fired_nodes as string) || "[]"),
|
|
284
|
+
vetoedNodes: JSON.parse((row.vetoed_nodes as string) || "[]"),
|
|
285
|
+
contextChars: (row.context_chars as number) || 0,
|
|
286
|
+
reward: (row.reward as number) ?? null,
|
|
287
|
+
rewardSource: (row.reward_source as RewardSource) ?? null,
|
|
288
|
+
packVersion: (row.pack_version as number) ?? null,
|
|
289
|
+
createdAt: row.created_at as number,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Labels ───
|
|
294
|
+
|
|
295
|
+
insertLabel(params: { episodeId: string; source: RewardSource; value: number; confidence?: number; reason?: string }): Label {
|
|
296
|
+
const id = `bl_${randomUUID().slice(0, 8)}`;
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
this.db.prepare(`
|
|
299
|
+
INSERT INTO brain_labels (id, episode_id, source, value, confidence, reason, applied, created_at)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?)
|
|
301
|
+
`).run(id, params.episodeId, params.source, params.value, params.confidence ?? 1.0, params.reason ?? null, now);
|
|
302
|
+
return {
|
|
303
|
+
id, episodeId: params.episodeId, source: params.source,
|
|
304
|
+
value: params.value, confidence: params.confidence ?? 1.0,
|
|
305
|
+
reason: params.reason ?? null, applied: false, createdAt: now,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getPendingLabels(): Label[] {
|
|
310
|
+
const rows = this.db.prepare(`SELECT * FROM brain_labels WHERE applied = 0 ORDER BY created_at ASC`).all() as Record<string, unknown>[];
|
|
311
|
+
return rows.map((r) => ({
|
|
312
|
+
id: r.id as string,
|
|
313
|
+
episodeId: r.episode_id as string,
|
|
314
|
+
source: r.source as RewardSource,
|
|
315
|
+
value: r.value as number,
|
|
316
|
+
confidence: (r.confidence as number) ?? 1.0,
|
|
317
|
+
reason: (r.reason as string) ?? null,
|
|
318
|
+
applied: false,
|
|
319
|
+
createdAt: r.created_at as number,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
countPendingLabelsBySource(): Record<RewardSource, number> {
|
|
324
|
+
const rows = this.db.prepare(`
|
|
325
|
+
SELECT source, COUNT(*) as count
|
|
326
|
+
FROM brain_labels
|
|
327
|
+
WHERE applied = 0
|
|
328
|
+
GROUP BY source
|
|
329
|
+
`).all() as Array<{ source: RewardSource; count: number }>;
|
|
330
|
+
|
|
331
|
+
const counts: Record<RewardSource, number> = {
|
|
332
|
+
human: 0,
|
|
333
|
+
self: 0,
|
|
334
|
+
scanner: 0,
|
|
335
|
+
teacher: 0,
|
|
336
|
+
};
|
|
337
|
+
for (const row of rows) {
|
|
338
|
+
counts[row.source] = row.count;
|
|
339
|
+
}
|
|
340
|
+
return counts;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
markLabelApplied(id: string): void {
|
|
344
|
+
this.db.prepare(`UPDATE brain_labels SET applied = 1 WHERE id = ?`).run(id);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── Raw Evidence + Resolved Labels ───
|
|
348
|
+
|
|
349
|
+
insertEvidence(params: {
|
|
350
|
+
episodeId: string;
|
|
351
|
+
conversationId?: number | null;
|
|
352
|
+
source: RewardSource;
|
|
353
|
+
kind: BrainEvidenceKind;
|
|
354
|
+
value: number;
|
|
355
|
+
confidence?: number;
|
|
356
|
+
reason?: string;
|
|
357
|
+
contentSnippet?: string;
|
|
358
|
+
metadata?: Record<string, unknown>;
|
|
359
|
+
}): BrainEvidence {
|
|
360
|
+
const id = `be_${randomUUID().slice(0, 8)}`;
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
this.db.prepare(`
|
|
363
|
+
INSERT INTO brain_evidence (id, episode_id, conversation_id, source, kind, value, confidence, reason, content_snippet, metadata, resolved, created_at)
|
|
364
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)
|
|
365
|
+
`).run(
|
|
366
|
+
id,
|
|
367
|
+
params.episodeId,
|
|
368
|
+
params.conversationId ?? null,
|
|
369
|
+
params.source,
|
|
370
|
+
params.kind,
|
|
371
|
+
params.value,
|
|
372
|
+
params.confidence ?? 1.0,
|
|
373
|
+
params.reason ?? null,
|
|
374
|
+
params.contentSnippet ?? null,
|
|
375
|
+
JSON.stringify(params.metadata ?? {}),
|
|
376
|
+
now,
|
|
377
|
+
);
|
|
378
|
+
return {
|
|
379
|
+
id,
|
|
380
|
+
episodeId: params.episodeId,
|
|
381
|
+
conversationId: params.conversationId ?? null,
|
|
382
|
+
source: params.source,
|
|
383
|
+
kind: params.kind,
|
|
384
|
+
value: params.value,
|
|
385
|
+
confidence: params.confidence ?? 1.0,
|
|
386
|
+
reason: params.reason ?? null,
|
|
387
|
+
contentSnippet: params.contentSnippet ?? null,
|
|
388
|
+
metadata: params.metadata ?? {},
|
|
389
|
+
resolved: false,
|
|
390
|
+
createdAt: now,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getPendingEvidence(limit = 100): BrainEvidence[] {
|
|
395
|
+
const rows = this.db.prepare(`
|
|
396
|
+
SELECT *
|
|
397
|
+
FROM brain_evidence
|
|
398
|
+
WHERE resolved = 0
|
|
399
|
+
ORDER BY created_at ASC
|
|
400
|
+
LIMIT ?
|
|
401
|
+
`).all(limit) as Record<string, unknown>[];
|
|
402
|
+
return rows.map((row) => this.toEvidence(row));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
countPendingEvidenceBySource(): Record<RewardSource, number> {
|
|
406
|
+
const rows = this.db.prepare(`
|
|
407
|
+
SELECT source, COUNT(*) as count
|
|
408
|
+
FROM brain_evidence
|
|
409
|
+
WHERE resolved = 0
|
|
410
|
+
GROUP BY source
|
|
411
|
+
`).all() as Array<{ source: RewardSource; count: number }>;
|
|
412
|
+
|
|
413
|
+
const counts: Record<RewardSource, number> = {
|
|
414
|
+
human: 0,
|
|
415
|
+
self: 0,
|
|
416
|
+
scanner: 0,
|
|
417
|
+
teacher: 0,
|
|
418
|
+
};
|
|
419
|
+
for (const row of rows) {
|
|
420
|
+
counts[row.source] = row.count;
|
|
421
|
+
}
|
|
422
|
+
return counts;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
resolveEvidence(params: {
|
|
426
|
+
evidenceId: string;
|
|
427
|
+
episodeId: string;
|
|
428
|
+
source: RewardSource;
|
|
429
|
+
value: number;
|
|
430
|
+
confidence: number;
|
|
431
|
+
resolution: BrainEvidenceResolution;
|
|
432
|
+
labelId?: string | null;
|
|
433
|
+
note?: string | null;
|
|
434
|
+
}): ResolvedLabel {
|
|
435
|
+
const id = `br_${randomUUID().slice(0, 8)}`;
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
this.db.prepare(`UPDATE brain_evidence SET resolved = 1 WHERE id = ?`).run(params.evidenceId);
|
|
438
|
+
this.db.prepare(`
|
|
439
|
+
INSERT INTO brain_resolved_labels (id, evidence_id, episode_id, source, value, confidence, resolution, label_id, note, created_at)
|
|
440
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
441
|
+
`).run(
|
|
442
|
+
id,
|
|
443
|
+
params.evidenceId,
|
|
444
|
+
params.episodeId,
|
|
445
|
+
params.source,
|
|
446
|
+
params.value,
|
|
447
|
+
params.confidence,
|
|
448
|
+
params.resolution,
|
|
449
|
+
params.labelId ?? null,
|
|
450
|
+
params.note ?? null,
|
|
451
|
+
now,
|
|
452
|
+
);
|
|
453
|
+
return {
|
|
454
|
+
id,
|
|
455
|
+
evidenceId: params.evidenceId,
|
|
456
|
+
episodeId: params.episodeId,
|
|
457
|
+
source: params.source,
|
|
458
|
+
value: params.value,
|
|
459
|
+
confidence: params.confidence,
|
|
460
|
+
resolution: params.resolution,
|
|
461
|
+
labelId: params.labelId ?? null,
|
|
462
|
+
note: params.note ?? null,
|
|
463
|
+
createdAt: now,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
getResolvedLabelsForEpisode(episodeId: string, limit = 20): ResolvedLabel[] {
|
|
468
|
+
const rows = this.db.prepare(`
|
|
469
|
+
SELECT *
|
|
470
|
+
FROM brain_resolved_labels
|
|
471
|
+
WHERE episode_id = ?
|
|
472
|
+
ORDER BY created_at DESC
|
|
473
|
+
LIMIT ?
|
|
474
|
+
`).all(episodeId, limit) as Record<string, unknown>[];
|
|
475
|
+
return rows.map((row) => ({
|
|
476
|
+
id: row.id as string,
|
|
477
|
+
evidenceId: row.evidence_id as string,
|
|
478
|
+
episodeId: row.episode_id as string,
|
|
479
|
+
source: row.source as RewardSource,
|
|
480
|
+
value: row.value as number,
|
|
481
|
+
confidence: (row.confidence as number) ?? 1.0,
|
|
482
|
+
resolution: row.resolution as BrainEvidenceResolution,
|
|
483
|
+
labelId: (row.label_id as string) ?? null,
|
|
484
|
+
note: (row.note as string) ?? null,
|
|
485
|
+
createdAt: row.created_at as number,
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private toEvidence(row: Record<string, unknown>): BrainEvidence {
|
|
490
|
+
return {
|
|
491
|
+
id: row.id as string,
|
|
492
|
+
episodeId: row.episode_id as string,
|
|
493
|
+
conversationId: (row.conversation_id as number) ?? null,
|
|
494
|
+
source: row.source as RewardSource,
|
|
495
|
+
kind: row.kind as BrainEvidenceKind,
|
|
496
|
+
value: row.value as number,
|
|
497
|
+
confidence: (row.confidence as number) ?? 1.0,
|
|
498
|
+
reason: (row.reason as string) ?? null,
|
|
499
|
+
contentSnippet: (row.content_snippet as string) ?? null,
|
|
500
|
+
metadata: JSON.parse((row.metadata as string) || "{}"),
|
|
501
|
+
resolved: !!(row.resolved as number),
|
|
502
|
+
createdAt: row.created_at as number,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ─── Packs ───
|
|
507
|
+
|
|
508
|
+
insertPack(params: { nodeCount: number; edgeCount: number; healthJson: string }): Pack {
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
this.db.prepare(`
|
|
511
|
+
INSERT INTO brain_packs (node_count, edge_count, health_json, created_at) VALUES (?, ?, ?, ?)
|
|
512
|
+
`).run(params.nodeCount, params.edgeCount, params.healthJson, now);
|
|
513
|
+
|
|
514
|
+
const row = this.db.prepare(`SELECT last_insert_rowid() as version`).get() as { version: number };
|
|
515
|
+
return {
|
|
516
|
+
version: row.version, nodeCount: params.nodeCount, edgeCount: params.edgeCount,
|
|
517
|
+
healthJson: params.healthJson, promotedAt: null, rolledBack: false, createdAt: now,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
getCurrentPack(): Pack | null {
|
|
522
|
+
const row = this.db.prepare(`
|
|
523
|
+
SELECT * FROM brain_packs WHERE promoted_at IS NOT NULL AND rolled_back = 0 ORDER BY version DESC LIMIT 1
|
|
524
|
+
`).get() as Record<string, unknown> | undefined;
|
|
525
|
+
if (!row) return null;
|
|
526
|
+
return {
|
|
527
|
+
version: row.version as number, nodeCount: row.node_count as number,
|
|
528
|
+
edgeCount: row.edge_count as number, healthJson: row.health_json as string,
|
|
529
|
+
promotedAt: (row.promoted_at as number) ?? null, rolledBack: !!(row.rolled_back as number),
|
|
530
|
+
createdAt: row.created_at as number,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
promotePack(version: number): void {
|
|
535
|
+
this.db.prepare(`UPDATE brain_packs SET promoted_at = ? WHERE version = ?`).run(Date.now(), version);
|
|
536
|
+
if (this.options.brainRoot) {
|
|
537
|
+
writeFileSync(this.getCurrentPackFile(), String(version), "utf8");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
rollbackPack(version: number): void {
|
|
542
|
+
this.db.prepare(`UPDATE brain_packs SET rolled_back = 1 WHERE version = ?`).run(version);
|
|
543
|
+
if (!this.options.brainRoot) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const row = this.db.prepare(`
|
|
548
|
+
SELECT version
|
|
549
|
+
FROM brain_packs
|
|
550
|
+
WHERE promoted_at IS NOT NULL AND rolled_back = 0 AND version < ?
|
|
551
|
+
ORDER BY version DESC
|
|
552
|
+
LIMIT 1
|
|
553
|
+
`).get(version) as { version?: number } | undefined;
|
|
554
|
+
|
|
555
|
+
if (typeof row?.version === "number") {
|
|
556
|
+
writeFileSync(this.getCurrentPackFile(), String(row.version), "utf8");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
writeFileSync(this.getCurrentPackFile(), "", "utf8");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ─── Mutations ───
|
|
564
|
+
|
|
565
|
+
insertMutation(mutation: MutationProposal): void {
|
|
566
|
+
this.db.prepare(`
|
|
567
|
+
INSERT INTO brain_mutations (id, kind, proposal, evidence, expected_gain, status, created_at)
|
|
568
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
569
|
+
`).run(
|
|
570
|
+
mutation.id, mutation.kind, JSON.stringify(mutation.proposal),
|
|
571
|
+
mutation.evidence ? JSON.stringify(mutation.evidence) : null,
|
|
572
|
+
mutation.expectedGain, mutation.status, mutation.createdAt,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
resolveMutation(id: string, status: MutationStatus): void {
|
|
577
|
+
this.db.prepare(`UPDATE brain_mutations SET status = ?, resolved_at = ? WHERE id = ?`)
|
|
578
|
+
.run(status, Date.now(), id);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
getMutationsByStatus(status: MutationStatus, limit = 50): MutationProposal[] {
|
|
582
|
+
const rows = this.db.prepare(`
|
|
583
|
+
SELECT * FROM brain_mutations
|
|
584
|
+
WHERE status = ?
|
|
585
|
+
ORDER BY created_at ASC
|
|
586
|
+
LIMIT ?
|
|
587
|
+
`).all(status, limit) as Record<string, unknown>[];
|
|
588
|
+
return rows.map((row) => ({
|
|
589
|
+
id: row.id as string,
|
|
590
|
+
kind: row.kind as MutationProposal["kind"],
|
|
591
|
+
proposal: JSON.parse((row.proposal as string) || "{}"),
|
|
592
|
+
evidence: row.evidence ? JSON.parse(row.evidence as string) : null,
|
|
593
|
+
expectedGain: (row.expected_gain as number) ?? null,
|
|
594
|
+
status: row.status as MutationStatus,
|
|
595
|
+
createdAt: row.created_at as number,
|
|
596
|
+
resolvedAt: (row.resolved_at as number) ?? null,
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
countMutationsByStatus(): Record<MutationStatus, number> {
|
|
601
|
+
const rows = this.db.prepare(`
|
|
602
|
+
SELECT status, COUNT(*) as count
|
|
603
|
+
FROM brain_mutations
|
|
604
|
+
GROUP BY status
|
|
605
|
+
`).all() as Array<{ status: MutationStatus; count: number }>;
|
|
606
|
+
|
|
607
|
+
const counts: Record<MutationStatus, number> = {
|
|
608
|
+
pending: 0,
|
|
609
|
+
validated: 0,
|
|
610
|
+
promoted: 0,
|
|
611
|
+
rejected: 0,
|
|
612
|
+
};
|
|
613
|
+
for (const row of rows) {
|
|
614
|
+
counts[row.status] = row.count;
|
|
615
|
+
}
|
|
616
|
+
return counts;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
countOrphanedTraceRows(): number {
|
|
620
|
+
const row = this.db.prepare(`
|
|
621
|
+
SELECT COUNT(*) as count
|
|
622
|
+
FROM brain_traces t
|
|
623
|
+
LEFT JOIN brain_episodes e ON e.id = t.episode_id
|
|
624
|
+
WHERE t.episode_id IS NOT NULL AND e.id IS NULL
|
|
625
|
+
`).get() as { count: number };
|
|
626
|
+
return row.count ?? 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ─── Traces ───
|
|
630
|
+
|
|
631
|
+
insertTrace(trace: DecisionTrace): void {
|
|
632
|
+
this.db.prepare(`
|
|
633
|
+
INSERT INTO brain_traces (id, episode_id, pack_version, query_text, seed_scores, trajectory, fired_nodes, vetoed_nodes, context_chars, footer, created_at)
|
|
634
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
635
|
+
`).run(
|
|
636
|
+
trace.id, trace.episodeId, trace.packVersion, trace.queryText,
|
|
637
|
+
JSON.stringify(trace.seedScores), JSON.stringify(trace.trajectory),
|
|
638
|
+
JSON.stringify(trace.firedNodes), JSON.stringify(trace.vetoedNodes),
|
|
639
|
+
trace.contextChars, trace.footer, trace.createdAt,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
getRecentTraces(limit: number): DecisionTrace[] {
|
|
644
|
+
const rows = this.db.prepare(`SELECT * FROM brain_traces ORDER BY created_at DESC LIMIT ?`).all(limit) as Record<string, unknown>[];
|
|
645
|
+
return rows.map((r) => this.toTrace(r));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getTrace(id: string): DecisionTrace | null {
|
|
649
|
+
const row = this.db.prepare(`SELECT * FROM brain_traces WHERE id = ?`).get(id) as Record<string, unknown> | undefined;
|
|
650
|
+
return row ? this.toTrace(row) : null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private toTrace(row: Record<string, unknown>): DecisionTrace {
|
|
654
|
+
return {
|
|
655
|
+
id: row.id as string,
|
|
656
|
+
episodeId: (row.episode_id as string) ?? null,
|
|
657
|
+
packVersion: (row.pack_version as number) ?? null,
|
|
658
|
+
queryText: (row.query_text as string) || "",
|
|
659
|
+
seedScores: JSON.parse((row.seed_scores as string) || "[]"),
|
|
660
|
+
trajectory: JSON.parse((row.trajectory as string) || "[]"),
|
|
661
|
+
firedNodes: JSON.parse((row.fired_nodes as string) || "[]"),
|
|
662
|
+
vetoedNodes: JSON.parse((row.vetoed_nodes as string) || "[]"),
|
|
663
|
+
contextChars: (row.context_chars as number) || 0,
|
|
664
|
+
footer: (row.footer as string) || "",
|
|
665
|
+
createdAt: row.created_at as number,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ─── Training State ───
|
|
670
|
+
|
|
671
|
+
getTrainingState(key: string): string | null {
|
|
672
|
+
const row = this.db.prepare(`SELECT value FROM brain_training_state WHERE key = ?`).get(key) as { value: string } | undefined;
|
|
673
|
+
return row?.value ?? null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
setTrainingState(key: string, value: string | number): void {
|
|
677
|
+
this.db.prepare(`INSERT OR REPLACE INTO brain_training_state (key, value) VALUES (?, ?)`)
|
|
678
|
+
.run(key, String(value));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ─── Bulk load into BrainGraph ───
|
|
682
|
+
|
|
683
|
+
loadAllEdges(): BrainEdge[] {
|
|
684
|
+
const rows = this.db.prepare(`SELECT * FROM brain_edges`).all() as Record<string, unknown>[];
|
|
685
|
+
return rows.map((r) => this.toEdge(r));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
loadAllSeedWeights(): SeedWeight[] {
|
|
689
|
+
return this.getAllSeedWeights();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
getCurrentPackVersion(): number | null {
|
|
693
|
+
if (!this.options.brainRoot) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const currentFile = this.getCurrentPackFile();
|
|
697
|
+
if (!existsSync(currentFile)) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
const value = readFileSync(currentFile, "utf8").trim();
|
|
701
|
+
if (!value) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const parsed = Number.parseInt(value, 10);
|
|
705
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
writePackSnapshot(params: {
|
|
709
|
+
version: number;
|
|
710
|
+
nodes: BrainNode[];
|
|
711
|
+
edges: BrainEdge[];
|
|
712
|
+
seedWeights?: SeedWeight[];
|
|
713
|
+
metadata: Record<string, unknown>;
|
|
714
|
+
}): string {
|
|
715
|
+
if (!this.options.brainRoot) {
|
|
716
|
+
throw new Error("brainRoot is required to write pack snapshots");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const packDir = this.getPackDir(params.version);
|
|
720
|
+
mkdirSync(packDir, { recursive: true });
|
|
721
|
+
|
|
722
|
+
const manifest = {
|
|
723
|
+
version: params.version,
|
|
724
|
+
nodeCount: params.nodes.length,
|
|
725
|
+
edgeCount: params.edges.length,
|
|
726
|
+
createdAt: Date.now(),
|
|
727
|
+
};
|
|
728
|
+
writeFileSync(join(packDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
729
|
+
writeFileSync(join(packDir, "metadata.json"), JSON.stringify(params.metadata, null, 2), "utf8");
|
|
730
|
+
writeFileSync(
|
|
731
|
+
join(packDir, "nodes.jsonl"),
|
|
732
|
+
`${params.nodes.map((node) => JSON.stringify({
|
|
733
|
+
...node,
|
|
734
|
+
embedding: node.embedding ? Array.from(node.embedding) : null,
|
|
735
|
+
})).join("\n")}\n`,
|
|
736
|
+
"utf8",
|
|
737
|
+
);
|
|
738
|
+
writeFileSync(
|
|
739
|
+
join(packDir, "edges.jsonl"),
|
|
740
|
+
`${params.edges.map((edge) => JSON.stringify(edge)).join("\n")}\n`,
|
|
741
|
+
"utf8",
|
|
742
|
+
);
|
|
743
|
+
writeFileSync(
|
|
744
|
+
join(packDir, "seed-weights.jsonl"),
|
|
745
|
+
`${(params.seedWeights ?? []).map((seedWeight) => JSON.stringify(seedWeight)).join("\n")}${(params.seedWeights ?? []).length > 0 ? "\n" : ""}`,
|
|
746
|
+
"utf8",
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
return packDir;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
readPackSnapshot(version: number): {
|
|
753
|
+
nodes: BrainNode[];
|
|
754
|
+
edges: BrainEdge[];
|
|
755
|
+
seedWeights: SeedWeight[];
|
|
756
|
+
metadata: Record<string, unknown>;
|
|
757
|
+
} | null {
|
|
758
|
+
if (!this.options.brainRoot) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const packDir = this.getPackDir(version);
|
|
763
|
+
const nodesFile = join(packDir, "nodes.jsonl");
|
|
764
|
+
const edgesFile = join(packDir, "edges.jsonl");
|
|
765
|
+
const seedWeightsFile = join(packDir, "seed-weights.jsonl");
|
|
766
|
+
const metadataFile = join(packDir, "metadata.json");
|
|
767
|
+
if (!existsSync(nodesFile) || !existsSync(edgesFile)) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const parseJsonl = (value: string): Record<string, unknown>[] =>
|
|
772
|
+
value
|
|
773
|
+
.split(/\r?\n/)
|
|
774
|
+
.map((line) => line.trim())
|
|
775
|
+
.filter((line) => line.length > 0)
|
|
776
|
+
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
777
|
+
|
|
778
|
+
const nodes = parseJsonl(readFileSync(nodesFile, "utf8")).map((row) => ({
|
|
779
|
+
...(row as unknown as BrainNode),
|
|
780
|
+
embedding: Array.isArray(row.embedding)
|
|
781
|
+
? new Float32Array((row.embedding as number[]).map((value) => Number(value)))
|
|
782
|
+
: null,
|
|
783
|
+
}));
|
|
784
|
+
const edges = parseJsonl(readFileSync(edgesFile, "utf8")) as unknown as BrainEdge[];
|
|
785
|
+
const seedWeights = existsSync(seedWeightsFile)
|
|
786
|
+
? parseJsonl(readFileSync(seedWeightsFile, "utf8")).map((row) => ({
|
|
787
|
+
nodeId: row.nodeId as string,
|
|
788
|
+
weight: Number(row.weight ?? 0),
|
|
789
|
+
updatedAt: Number(row.updatedAt ?? 0),
|
|
790
|
+
}))
|
|
791
|
+
: [];
|
|
792
|
+
const metadata = existsSync(metadataFile)
|
|
793
|
+
? JSON.parse(readFileSync(metadataFile, "utf8")) as Record<string, unknown>
|
|
794
|
+
: {};
|
|
795
|
+
|
|
796
|
+
return { nodes, edges, seedWeights, metadata };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private getPacksDir(): string {
|
|
800
|
+
if (!this.options.brainRoot) {
|
|
801
|
+
throw new Error("brainRoot is not configured");
|
|
802
|
+
}
|
|
803
|
+
return join(this.options.brainRoot, "packs");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private getCurrentPackFile(): string {
|
|
807
|
+
if (!this.options.brainRoot) {
|
|
808
|
+
throw new Error("brainRoot is not configured");
|
|
809
|
+
}
|
|
810
|
+
return join(this.options.brainRoot, "current");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private getPackDir(version: number): string {
|
|
814
|
+
return join(this.getPacksDir(), `v${version.toString().padStart(6, "0")}`);
|
|
815
|
+
}
|
|
816
|
+
}
|