@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/bin/openclawbrain.js +15 -0
  4. package/docs/END_STATE.md +244 -0
  5. package/docs/EVIDENCE.md +128 -0
  6. package/docs/RELEASE_CONTRACT.md +91 -0
  7. package/docs/agent-tools.md +106 -0
  8. package/docs/architecture.md +224 -0
  9. package/docs/configuration.md +178 -0
  10. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
  11. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
  12. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
  13. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
  14. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
  15. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
  16. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
  17. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
  18. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
  19. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
  20. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
  21. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
  22. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
  23. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
  24. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
  25. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
  26. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
  27. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
  28. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
  29. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
  30. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
  31. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
  32. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
  33. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
  34. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
  35. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
  36. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
  37. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
  38. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
  39. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
  40. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
  41. package/docs/evidence/README.md +16 -0
  42. package/docs/fts5.md +161 -0
  43. package/docs/tui.md +506 -0
  44. package/index.ts +1372 -0
  45. package/openclaw.plugin.json +136 -0
  46. package/package.json +66 -0
  47. package/src/assembler.ts +804 -0
  48. package/src/brain-cli.ts +316 -0
  49. package/src/brain-core/decay.ts +35 -0
  50. package/src/brain-core/episode.ts +82 -0
  51. package/src/brain-core/graph.ts +321 -0
  52. package/src/brain-core/health.ts +116 -0
  53. package/src/brain-core/mutator.ts +281 -0
  54. package/src/brain-core/pack.ts +117 -0
  55. package/src/brain-core/policy.ts +153 -0
  56. package/src/brain-core/replay.ts +1 -0
  57. package/src/brain-core/teacher.ts +105 -0
  58. package/src/brain-core/trace.ts +40 -0
  59. package/src/brain-core/traverse.ts +230 -0
  60. package/src/brain-core/types.ts +405 -0
  61. package/src/brain-core/update.ts +123 -0
  62. package/src/brain-harvest/human.ts +46 -0
  63. package/src/brain-harvest/scanner.ts +98 -0
  64. package/src/brain-harvest/self.ts +147 -0
  65. package/src/brain-runtime/assembler-extension.ts +230 -0
  66. package/src/brain-runtime/evidence-detectors.ts +68 -0
  67. package/src/brain-runtime/graph-io.ts +72 -0
  68. package/src/brain-runtime/harvester-extension.ts +98 -0
  69. package/src/brain-runtime/service.ts +659 -0
  70. package/src/brain-runtime/tools.ts +109 -0
  71. package/src/brain-runtime/worker-state.ts +106 -0
  72. package/src/brain-runtime/worker-supervisor.ts +169 -0
  73. package/src/brain-store/embedding.ts +179 -0
  74. package/src/brain-store/init.ts +347 -0
  75. package/src/brain-store/migrations.ts +188 -0
  76. package/src/brain-store/store.ts +816 -0
  77. package/src/brain-worker/child-runner.ts +321 -0
  78. package/src/brain-worker/jobs.ts +12 -0
  79. package/src/brain-worker/mutation-job.ts +5 -0
  80. package/src/brain-worker/promotion-job.ts +5 -0
  81. package/src/brain-worker/protocol.ts +79 -0
  82. package/src/brain-worker/teacher-job.ts +5 -0
  83. package/src/brain-worker/update-job.ts +5 -0
  84. package/src/brain-worker/worker.ts +422 -0
  85. package/src/compaction.ts +1332 -0
  86. package/src/db/config.ts +265 -0
  87. package/src/db/connection.ts +72 -0
  88. package/src/db/features.ts +42 -0
  89. package/src/db/migration.ts +561 -0
  90. package/src/engine.ts +1995 -0
  91. package/src/expansion-auth.ts +351 -0
  92. package/src/expansion-policy.ts +303 -0
  93. package/src/expansion.ts +383 -0
  94. package/src/integrity.ts +600 -0
  95. package/src/large-files.ts +527 -0
  96. package/src/openclaw-bridge.ts +22 -0
  97. package/src/retrieval.ts +357 -0
  98. package/src/store/conversation-store.ts +748 -0
  99. package/src/store/fts5-sanitize.ts +29 -0
  100. package/src/store/full-text-fallback.ts +74 -0
  101. package/src/store/index.ts +29 -0
  102. package/src/store/summary-store.ts +918 -0
  103. package/src/summarize.ts +847 -0
  104. package/src/tools/common.ts +53 -0
  105. package/src/tools/lcm-conversation-scope.ts +76 -0
  106. package/src/tools/lcm-describe-tool.ts +234 -0
  107. package/src/tools/lcm-expand-query-tool.ts +594 -0
  108. package/src/tools/lcm-expand-tool.delegation.ts +556 -0
  109. package/src/tools/lcm-expand-tool.ts +448 -0
  110. package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
  111. package/src/tools/lcm-grep-tool.ts +200 -0
  112. package/src/transcript-repair.ts +301 -0
  113. package/src/types.ts +149 -0
@@ -0,0 +1,659 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import type { OpenClawBrainRuntimeConfig } from "../db/config.js";
6
+ import type {
7
+ BrainConfig,
8
+ BrainNode,
9
+ DecisionTrace,
10
+ NodeKind,
11
+ TraversalResult,
12
+ } from "../brain-core/types.js";
13
+ import { DEFAULT_BRAIN_CONFIG } from "../brain-core/types.js";
14
+ import { BrainGraph } from "../brain-core/graph.js";
15
+ import { traverse } from "../brain-core/traverse.js";
16
+ import { recordEpisode } from "../brain-core/episode.js";
17
+ import { recordTrace } from "../brain-core/trace.js";
18
+ import { computeHealth } from "../brain-core/health.js";
19
+ import { BrainTeacher } from "../brain-core/teacher.js";
20
+ import { BrainMutator } from "../brain-core/mutator.js";
21
+ import { PackManager } from "../brain-core/pack.js";
22
+ import { BrainStore } from "../brain-store/store.js";
23
+ import { runBrainMigrations } from "../brain-store/migrations.js";
24
+ import { initBrain as runInit } from "../brain-store/init.js";
25
+ import {
26
+ createEmbeddingClient,
27
+ describeEmbeddingConfig,
28
+ type BrainEmbeddingFn,
29
+ } from "../brain-store/embedding.js";
30
+ import { LabelHarvester } from "./harvester-extension.js";
31
+ import { BrainWorker } from "../brain-worker/worker.js";
32
+ import type { LcmDependencies } from "../types.js";
33
+ import type { WorkerTeacherCompleteRequestMessage } from "../brain-worker/protocol.js";
34
+ import { flattenEdges, populateGraph, promoteGraphSnapshot, reloadGraphFromStore } from "./graph-io.js";
35
+ import { readWorkerRuntimeState } from "./worker-state.js";
36
+ import { WorkerSupervisor } from "./worker-supervisor.js";
37
+
38
+ function buildBrainConfig(
39
+ runtimeConfig: OpenClawBrainRuntimeConfig,
40
+ overrides?: Partial<BrainConfig>,
41
+ ): BrainConfig {
42
+ return {
43
+ ...DEFAULT_BRAIN_CONFIG,
44
+ ...runtimeConfig,
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ export class BrainService {
50
+ private deps: LcmDependencies;
51
+ private store: BrainStore;
52
+ private mutableGraph = new BrainGraph();
53
+ private servingGraph = new BrainGraph();
54
+ private worker: BrainWorker | null;
55
+ private childSupervisor: WorkerSupervisor | null = null;
56
+ private harvesterImpl: LabelHarvester;
57
+ private packManager: PackManager;
58
+ private embeddingClient: BrainEmbeddingFn | null;
59
+ private config: BrainConfig;
60
+ private resolvedTeacherModel: { provider: string; model: string } | null;
61
+ private teacherConfigError: string | null = null;
62
+ private initialized = false;
63
+ private latestEpisodeByConversation = new Map<number, string>();
64
+ private lastAssemblyDecision:
65
+ | {
66
+ mode: "use_brain" | "shadow" | "skip_no_query" | "skip_short_static_lookup" | "skip_no_embedding" | "skip_uninitialized" | "skip_budget_too_small";
67
+ conversationId?: number;
68
+ episodeId?: string | null;
69
+ traceId?: string | null;
70
+ footer?: string | null;
71
+ }
72
+ | null = null;
73
+
74
+ constructor(params: {
75
+ deps: LcmDependencies;
76
+ config?: Partial<BrainConfig>;
77
+ runtimeConfig?: OpenClawBrainRuntimeConfig;
78
+ }) {
79
+ this.deps = params.deps;
80
+ const runtimeConfig = params.runtimeConfig ?? params.deps.config.brain;
81
+ if (!runtimeConfig) {
82
+ throw new Error("OpenClawBrain runtime configuration is missing");
83
+ }
84
+
85
+ this.config = buildBrainConfig(runtimeConfig, params.config);
86
+ if (this.config.teacherEnabled) {
87
+ try {
88
+ this.resolvedTeacherModel = params.deps.resolveModel(
89
+ this.config.teacherModel || undefined,
90
+ this.config.teacherProvider || undefined,
91
+ );
92
+ } catch (error) {
93
+ this.resolvedTeacherModel = null;
94
+ this.teacherConfigError = (error as Error).message;
95
+ params.deps.log.warn(
96
+ `[brain] Teacher disabled: ${this.teacherConfigError}`,
97
+ );
98
+ }
99
+ } else {
100
+ this.resolvedTeacherModel = null;
101
+ }
102
+ mkdirSync(this.config.root, { recursive: true });
103
+
104
+ const db = new DatabaseSync(join(this.config.root, "state.db"));
105
+ db.exec("PRAGMA journal_mode = WAL");
106
+ db.exec("PRAGMA foreign_keys = ON");
107
+ runBrainMigrations(db);
108
+
109
+ this.store = new BrainStore(db, { brainRoot: this.config.root });
110
+ this.harvesterImpl = new LabelHarvester(
111
+ this.store,
112
+ params.deps.log,
113
+ (conversationId) => this.latestEpisodeByConversation.get(conversationId) ?? null,
114
+ );
115
+ this.embeddingClient = createEmbeddingClient({
116
+ config: runtimeConfig,
117
+ getApiKey: (provider, model) => params.deps.getApiKey(provider, model),
118
+ log: params.deps.log,
119
+ });
120
+
121
+ populateGraph(this.mutableGraph, this.store.getAllNodes(), this.store.loadAllEdges(), this.store.loadAllSeedWeights());
122
+ this.reloadServingGraph();
123
+
124
+ const persistence = {
125
+ insertNode: (node: Parameters<BrainStore["insertNode"]>[0]) => this.store.insertNode(node),
126
+ insertEdge: (edge: Parameters<BrainStore["insertEdge"]>[0]) => this.store.insertEdge(edge),
127
+ deleteNode: (id: string) => this.store.deleteNode(id),
128
+ deleteEdge: (source: string, target: string, kind: string) =>
129
+ this.store.deleteEdge(source, target, kind as never),
130
+ resolveMutation: (id: string, status: "promoted" | "rejected") =>
131
+ this.store.resolveMutation(id, status),
132
+ };
133
+ const mutator = new BrainMutator(persistence, this.mutableGraph, params.deps.log);
134
+ this.packManager = new PackManager(
135
+ {
136
+ insertPack: (pack) => this.store.insertPack(pack),
137
+ promotePack: (version) => this.store.promotePack(version),
138
+ rollbackPack: (version) => this.store.rollbackPack(version),
139
+ },
140
+ this.mutableGraph,
141
+ params.deps.log,
142
+ );
143
+
144
+ if (this.config.workerMode === "in_process") {
145
+ this.deps.log.warn("[brain] in_process worker mode is dev-only; use child mode for production operator truth");
146
+ const teacher =
147
+ this.config.teacherEnabled && this.resolvedTeacherModel
148
+ ? new BrainTeacher(
149
+ async (request) =>
150
+ params.deps.complete({
151
+ provider: request.provider,
152
+ model: request.model,
153
+ apiKey: request.apiKey,
154
+ messages: request.messages,
155
+ system: request.system,
156
+ maxTokens: request.maxTokens,
157
+ temperature: request.temperature,
158
+ }),
159
+ () => this.resolvedTeacherModel as { provider: string; model: string },
160
+ (provider, model) => params.deps.getApiKey(provider, model),
161
+ this.mutableGraph,
162
+ params.deps.log,
163
+ )
164
+ : null;
165
+
166
+ this.worker = new BrainWorker(
167
+ this.store,
168
+ this.mutableGraph,
169
+ teacher,
170
+ mutator,
171
+ this.packManager,
172
+ this.config,
173
+ params.deps.log,
174
+ {
175
+ isEnabled: () => this.isEnabled(),
176
+ onPromotionReady: async ({ healthJson }) => {
177
+ await this.promoteMutableGraph("worker", { healthJson });
178
+ },
179
+ },
180
+ );
181
+ } else {
182
+ this.worker = null;
183
+ this.childSupervisor = new WorkerSupervisor({
184
+ config: this.config,
185
+ store: this.store,
186
+ log: params.deps.log,
187
+ teacherModel: this.resolvedTeacherModel,
188
+ isEnabled: () => this.isEnabled(),
189
+ onPackPromoted: () => {
190
+ this.reloadMutableGraphFromStore();
191
+ this.reloadServingGraph();
192
+ },
193
+ onTeacherComplete: async (
194
+ message: WorkerTeacherCompleteRequestMessage,
195
+ teacherModel,
196
+ ) => {
197
+ const provider = typeof message.provider === "string"
198
+ ? message.provider
199
+ : teacherModel?.provider;
200
+ const model = typeof message.model === "string"
201
+ ? message.model
202
+ : teacherModel?.model;
203
+ const requestId = String(message.requestId ?? "");
204
+ if (!provider || !model || !requestId) {
205
+ return {
206
+ type: "teacher-complete-result",
207
+ requestId,
208
+ ok: false,
209
+ error: "teacher completion request missing provider/model/requestId",
210
+ };
211
+ }
212
+ try {
213
+ const apiKey = await this.deps.getApiKey(provider, model);
214
+ const result = await this.deps.complete({
215
+ provider,
216
+ model,
217
+ apiKey,
218
+ messages: Array.isArray(message.messages)
219
+ ? message.messages as Array<{ role: string; content: unknown }>
220
+ : [],
221
+ system: typeof message.system === "string" ? message.system : undefined,
222
+ maxTokens: Number(message.maxTokens ?? 200),
223
+ temperature: typeof message.temperature === "number" ? message.temperature : undefined,
224
+ });
225
+ return {
226
+ type: "teacher-complete-result",
227
+ requestId,
228
+ ok: true,
229
+ content: result.content ?? [],
230
+ };
231
+ } catch (error) {
232
+ return {
233
+ type: "teacher-complete-result",
234
+ requestId,
235
+ ok: false,
236
+ error: (error as Error).message,
237
+ };
238
+ }
239
+ },
240
+ });
241
+ }
242
+ }
243
+
244
+ startWorker(): void {
245
+ if (!this.isEnabled()) {
246
+ return;
247
+ }
248
+ if (this.config.workerMode === "in_process") {
249
+ this.store.setTrainingState("worker_mode", "in_process");
250
+ this.store.setTrainingState("worker_status", "running");
251
+ this.worker?.start();
252
+ return;
253
+ }
254
+ this.childSupervisor?.start();
255
+ }
256
+
257
+ stopWorker(): void {
258
+ if (this.config.workerMode === "in_process") {
259
+ this.store.setTrainingState("worker_status", "stopped");
260
+ this.worker?.stop();
261
+ return;
262
+ }
263
+ this.childSupervisor?.stop();
264
+ }
265
+
266
+ private notifyWorkerGraphReload(): void {
267
+ this.childSupervisor?.requestGraphReload();
268
+ }
269
+
270
+ private reloadMutableGraphFromStore(): void {
271
+ reloadGraphFromStore(this.store, this.mutableGraph);
272
+ }
273
+
274
+ get harvester(): LabelHarvester {
275
+ return this.harvesterImpl;
276
+ }
277
+
278
+ isEnabled(): boolean {
279
+ return this.config.enabled && !existsSync(join(this.config.root, "DISABLED"));
280
+ }
281
+
282
+ isInitialized(): boolean {
283
+ return this.initialized;
284
+ }
285
+
286
+ isEmbeddingConfigured(): boolean {
287
+ return Boolean(this.embeddingClient);
288
+ }
289
+
290
+ isShadowMode(): boolean {
291
+ return this.config.shadowMode;
292
+ }
293
+
294
+ noteAssemblyDecision(decision: NonNullable<BrainService["lastAssemblyDecision"]>): void {
295
+ this.lastAssemblyDecision = decision;
296
+ this.store.setTrainingState("last_assembly_mode", decision.mode);
297
+ this.store.setTrainingState("last_assembly_footer", decision.footer ?? "");
298
+ this.store.setTrainingState("last_assembly_episode_id", decision.episodeId ?? "");
299
+ this.store.setTrainingState("last_assembly_trace_id", decision.traceId ?? "");
300
+ }
301
+
302
+ async query(params: {
303
+ conversationId: number;
304
+ queryText: string;
305
+ budgetChars: number;
306
+ queryEmbedding?: Float32Array;
307
+ }): Promise<TraversalResult | null> {
308
+ if (!this.isEnabled() || this.servingGraph.nodeCount() === 0) {
309
+ return null;
310
+ }
311
+
312
+ const embedding =
313
+ params.queryEmbedding
314
+ ?? (this.embeddingClient ? await this.embeddingClient(params.queryText) : null);
315
+ if (!embedding || embedding.length === 0) {
316
+ return null;
317
+ }
318
+
319
+ const traversalResult = traverse({
320
+ graph: this.servingGraph,
321
+ queryEmbedding: embedding,
322
+ queryText: params.queryText,
323
+ maxHops: this.config.maxHops,
324
+ budgetChars: params.budgetChars,
325
+ temperature: this.config.servingTemperature,
326
+ maxSeeds: this.config.maxSeeds,
327
+ semanticThreshold: this.config.semanticThreshold,
328
+ });
329
+ if (traversalResult.firedNodes.length === 0) {
330
+ return null;
331
+ }
332
+
333
+ const episode = recordEpisode({
334
+ traversalResult,
335
+ queryText: params.queryText,
336
+ queryEmbedding: embedding,
337
+ conversationId: params.conversationId,
338
+ packVersion: this.store.getCurrentPackVersion(),
339
+ });
340
+ this.store.insertEpisode(episode);
341
+ this.latestEpisodeByConversation.set(params.conversationId, episode.id);
342
+
343
+ const trace = recordTrace({
344
+ traversalResult,
345
+ queryText: params.queryText,
346
+ episodeId: episode.id,
347
+ packVersion: episode.packVersion,
348
+ });
349
+ this.store.insertTrace(trace);
350
+
351
+ return {
352
+ fired: traversalResult.firedNodes,
353
+ vetoed: traversalResult.vetoedNodes,
354
+ episode,
355
+ trace,
356
+ };
357
+ }
358
+
359
+ async teach(params: {
360
+ instruction: string;
361
+ conversationId?: number;
362
+ kind?: string;
363
+ tags?: string[];
364
+ }): Promise<{ nodeId: string; packVersion: number | null }> {
365
+ this.reloadMutableGraphFromStore();
366
+ if (!this.embeddingClient) {
367
+ throw new Error("Embedding model is required before brain_teach can make knowledge retrievable");
368
+ }
369
+
370
+ const nodeKind = (params.kind ?? "correction") as NodeKind;
371
+ const now = Date.now();
372
+ const node: BrainNode = {
373
+ id: `bn_${randomUUID().slice(0, 12)}`,
374
+ kind: nodeKind,
375
+ content: params.instruction,
376
+ embedding: await this.embeddingClient(params.instruction),
377
+ sourceUri: null,
378
+ trust: "human",
379
+ tags: params.tags ?? [],
380
+ tokenCount: Math.ceil(params.instruction.length / 4),
381
+ metadata: { taught: true },
382
+ createdAt: now,
383
+ updatedAt: now,
384
+ };
385
+
386
+ this.mutableGraph.addNode(node);
387
+ this.store.insertNode(node);
388
+
389
+ const recentEpisodes = this.store
390
+ .getRecentEpisodes(10)
391
+ .filter((episode) => (
392
+ params.conversationId === undefined
393
+ ? true
394
+ : episode.conversationId === params.conversationId
395
+ ));
396
+ const exactEpisode =
397
+ typeof params.conversationId === "number"
398
+ ? this.store.getEpisode(this.latestEpisodeByConversation.get(params.conversationId) ?? "")
399
+ : null;
400
+ const recentEpisode = exactEpisode ?? recentEpisodes[0] ?? null;
401
+ const connectedNodes = new Set<string>();
402
+ const firstTraversalStep = recentEpisode?.trajectory.find(
403
+ (step) => step.chosenAction.type === "traverse",
404
+ );
405
+ const chosenSeedNodeId =
406
+ firstTraversalStep?.chosenAction.type === "traverse"
407
+ ? firstTraversalStep.chosenAction.targetNodeId
408
+ : null;
409
+ for (const firedNodeId of recentEpisode?.firedNodes ?? []) {
410
+ if (connectedNodes.has(firedNodeId)) {
411
+ continue;
412
+ }
413
+ connectedNodes.add(firedNodeId);
414
+ const edge = {
415
+ source: firedNodeId,
416
+ target: node.id,
417
+ kind: "learned" as const,
418
+ weight: 1.0,
419
+ prior: 1.0,
420
+ metadata: { taught: true, conversationId: params.conversationId ?? null },
421
+ decayedAt: now,
422
+ createdAt: now,
423
+ };
424
+ const reverse = {
425
+ ...edge,
426
+ source: node.id,
427
+ target: firedNodeId,
428
+ };
429
+ this.mutableGraph.addEdge(edge);
430
+ this.mutableGraph.addEdge(reverse);
431
+ this.store.insertEdge(edge);
432
+ this.store.insertEdge(reverse);
433
+ }
434
+ if (chosenSeedNodeId && !connectedNodes.has(chosenSeedNodeId)) {
435
+ const now = Date.now();
436
+ const seedEdge = {
437
+ source: chosenSeedNodeId,
438
+ target: node.id,
439
+ kind: "learned" as const,
440
+ weight: 1.0,
441
+ prior: 1.0,
442
+ metadata: { taught: true, seedRegion: true, conversationId: params.conversationId ?? null },
443
+ decayedAt: now,
444
+ createdAt: now,
445
+ };
446
+ const reverseSeedEdge = {
447
+ ...seedEdge,
448
+ source: node.id,
449
+ target: chosenSeedNodeId,
450
+ };
451
+ this.mutableGraph.addEdge(seedEdge);
452
+ this.mutableGraph.addEdge(reverseSeedEdge);
453
+ this.store.insertEdge(seedEdge);
454
+ this.store.insertEdge(reverseSeedEdge);
455
+ }
456
+
457
+ const misroutedTargetId = recentEpisode?.firedNodes.at(-1) ?? null;
458
+ if (chosenSeedNodeId && misroutedTargetId && misroutedTargetId !== node.id) {
459
+ const inhibitoryEdge = {
460
+ source: chosenSeedNodeId,
461
+ target: misroutedTargetId,
462
+ kind: "inhibitory" as const,
463
+ weight: -1.0,
464
+ prior: -1.0,
465
+ metadata: { taught: true, reason: "human correction", conversationId: params.conversationId ?? null },
466
+ decayedAt: Date.now(),
467
+ createdAt: Date.now(),
468
+ };
469
+ this.mutableGraph.addEdge(inhibitoryEdge);
470
+ this.store.insertEdge(inhibitoryEdge);
471
+ }
472
+
473
+ const targetEpisodes = exactEpisode ? [exactEpisode] : recentEpisodes.slice(0, 1);
474
+ for (const episode of targetEpisodes) {
475
+ if (episode && episode.reward === null) {
476
+ const reason = `correction taught: "${params.instruction.slice(0, 80)}"`;
477
+ this.store.insertEvidence({
478
+ episodeId: episode.id,
479
+ conversationId: episode.conversationId,
480
+ source: "human",
481
+ kind: "teach_correction",
482
+ value: -0.5,
483
+ confidence: 1.0,
484
+ reason,
485
+ contentSnippet: params.instruction.slice(0, 240),
486
+ metadata: {
487
+ taughtNodeId: node.id,
488
+ correctedEpisodeId: episode.id,
489
+ extractor: "brain_teach",
490
+ via: "brain_teach",
491
+ },
492
+ });
493
+ this.store.insertLabel({
494
+ episodeId: episode.id,
495
+ source: "human",
496
+ value: -0.5,
497
+ reason,
498
+ });
499
+ }
500
+ }
501
+
502
+ const packVersion = await this.promoteMutableGraph("teach", {
503
+ taughtNodeId: node.id,
504
+ conversationId: params.conversationId ?? null,
505
+ });
506
+ this.notifyWorkerGraphReload();
507
+ return { nodeId: node.id, packVersion };
508
+ }
509
+
510
+ async status(): Promise<Record<string, unknown>> {
511
+ this.reloadMutableGraphFromStore();
512
+ const recentEpisodes = this.store.getRecentEpisodes(100);
513
+ const currentPack = this.store.getCurrentPack();
514
+ const health = computeHealth(
515
+ this.mutableGraph,
516
+ recentEpisodes,
517
+ currentPack?.version ?? this.store.getCurrentPackVersion() ?? 0,
518
+ );
519
+ const recentTraces = this.store.getRecentTraces(5);
520
+ const workerState = readWorkerRuntimeState(this.store, this.config);
521
+
522
+ const embeddingConfig = describeEmbeddingConfig(this.config);
523
+
524
+ return {
525
+ initialized: this.initialized,
526
+ enabled: this.isEnabled(),
527
+ embeddingConfigured: Boolean(this.embeddingClient),
528
+ embeddingProvider: this.config.embeddingProvider,
529
+ embeddingModel: this.config.embeddingModel,
530
+ embeddingBaseUrl: this.config.embeddingModel ? embeddingConfig.baseUrl : "",
531
+ embeddingAuthMode: embeddingConfig.authMode,
532
+ embeddingConfigError: embeddingConfig.error,
533
+ currentPackVersion: this.store.getCurrentPackVersion(),
534
+ currentPackPromotedAt: currentPack?.promotedAt ?? null,
535
+ shadowMode: this.config.shadowMode,
536
+ teacherEnabled: this.config.teacherEnabled,
537
+ teacherConfigured: Boolean(this.resolvedTeacherModel),
538
+ teacherProvider: this.resolvedTeacherModel?.provider ?? this.config.teacherProvider,
539
+ teacherModel: this.resolvedTeacherModel?.model ?? this.config.teacherModel,
540
+ teacherConfigError: this.teacherConfigError,
541
+ ...workerState,
542
+ pendingEvidence: this.store.getPendingEvidence(100).length,
543
+ pendingEvidenceBySource: this.store.countPendingEvidenceBySource(),
544
+ pendingLabels: this.store.getPendingLabels().length,
545
+ pendingLabelsBySource: this.store.countPendingLabelsBySource(),
546
+ mutationBacklog: this.store.countMutationsByStatus(),
547
+ seedLearningEnabled: this.mutableGraph.hasSeedWeights(),
548
+ recentTraceCount: recentTraces.length,
549
+ lastTraceFooter: recentTraces[0]?.footer ?? null,
550
+ lastAssemblyDecision: this.lastAssemblyDecision,
551
+ lastPromotionReason: this.store.getTrainingState("last_promotion_reason"),
552
+ lastReplayFailureReason: this.store.getTrainingState("last_replay_failure_reason"),
553
+ brainRoot: this.config.root,
554
+ ...health,
555
+ };
556
+ }
557
+
558
+ async getTrace(traceId?: string): Promise<DecisionTrace | null> {
559
+ if (traceId) {
560
+ return this.store.getTrace(traceId);
561
+ }
562
+ return this.store.getRecentTraces(1)[0] ?? null;
563
+ }
564
+
565
+ async init(params: {
566
+ workspaceRoot: string;
567
+ embedFn?: BrainEmbeddingFn;
568
+ }): Promise<string> {
569
+ const embedFn = params.embedFn ?? this.embeddingClient;
570
+ if (!embedFn) {
571
+ throw new Error("OpenClawBrain init requires OPENCLAWBRAIN_EMBEDDING_MODEL or an explicit embedFn");
572
+ }
573
+
574
+ const result = await runInit({
575
+ workspaceRoot: params.workspaceRoot,
576
+ embedFn,
577
+ semanticThreshold: this.config.semanticThreshold,
578
+ log: { info: () => {}, warn: () => {} },
579
+ });
580
+
581
+ this.store.clearGraph();
582
+ this.mutableGraph.clear();
583
+ for (const node of result.nodes) {
584
+ this.mutableGraph.addNode(node);
585
+ this.store.insertNode(node);
586
+ }
587
+ for (const edge of result.edges) {
588
+ this.mutableGraph.addEdge(edge);
589
+ this.store.insertEdge(edge);
590
+ }
591
+
592
+ await this.promoteMutableGraph("init", {
593
+ workspaceRoot: params.workspaceRoot,
594
+ summary: result.summary,
595
+ });
596
+ this.notifyWorkerGraphReload();
597
+ return result.summary;
598
+ }
599
+
600
+ async harvestFromMessage(params: {
601
+ conversationId: number;
602
+ episodeId?: string;
603
+ role: string;
604
+ content: string;
605
+ messageParts?: Array<{
606
+ partType: string;
607
+ textContent?: string | null;
608
+ toolCallId?: string | null;
609
+ toolName?: string | null;
610
+ toolInput?: string | null;
611
+ toolOutput?: string | null;
612
+ metadata?: string | null;
613
+ }>;
614
+ }): Promise<void> {
615
+ await this.harvesterImpl.harvestFromMessage(params);
616
+ }
617
+
618
+ async promoteLatestCandidate(): Promise<number | null> {
619
+ this.reloadMutableGraphFromStore();
620
+ const version = await this.promoteMutableGraph("manual-promote", {});
621
+ this.notifyWorkerGraphReload();
622
+ return version;
623
+ }
624
+
625
+ rollback(version: number): void {
626
+ this.packManager.rollback(version);
627
+ this.reloadServingGraph();
628
+ }
629
+
630
+ private reloadServingGraph(): void {
631
+ const currentVersion = this.store.getCurrentPackVersion();
632
+ const snapshot = currentVersion !== null ? this.store.readPackSnapshot(currentVersion) : null;
633
+ if (!snapshot) {
634
+ this.servingGraph.clear();
635
+ this.initialized = false;
636
+ return;
637
+ }
638
+
639
+ populateGraph(this.servingGraph, snapshot.nodes, snapshot.edges, snapshot.seedWeights);
640
+ this.initialized = true;
641
+ }
642
+
643
+ private async promoteMutableGraph(
644
+ reason: string,
645
+ metadata: Record<string, unknown>,
646
+ ): Promise<number | null> {
647
+ this.reloadMutableGraphFromStore();
648
+ const version = promoteGraphSnapshot({
649
+ store: this.store,
650
+ graph: this.mutableGraph,
651
+ packManager: this.packManager,
652
+ config: this.config,
653
+ reason,
654
+ metadata,
655
+ });
656
+ this.reloadServingGraph();
657
+ return version;
658
+ }
659
+ }