@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,316 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { BrainGraph } from "./brain-core/graph.js";
6
+ import { computeHealth } from "./brain-core/health.js";
7
+ import { PackManager } from "./brain-core/pack.js";
8
+ import { BrainStore } from "./brain-store/store.js";
9
+ import { runBrainMigrations } from "./brain-store/migrations.js";
10
+ import { initBrain } from "./brain-store/init.js";
11
+ import {
12
+ createEmbeddingClient,
13
+ describeEmbeddingConfig,
14
+ } from "./brain-store/embedding.js";
15
+ import { resolveLcmConfig } from "./db/config.js";
16
+ import { flattenSeedWeights } from "./brain-runtime/graph-io.js";
17
+ import { readWorkerRuntimeState } from "./brain-runtime/worker-state.js";
18
+
19
+ function printJson(payload: unknown): void {
20
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
21
+ }
22
+
23
+ function usage(): never {
24
+ process.stderr.write(
25
+ "Usage: openclawbrain <init|status|trace|replay|promote|rollback|disable|enable|doctor> [args]\n",
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ function loadStore() {
31
+ const config = resolveLcmConfig(process.env, {});
32
+ const brainConfig = config.brain;
33
+ if (!brainConfig) {
34
+ throw new Error("OpenClawBrain configuration is unavailable");
35
+ }
36
+
37
+ mkdirSync(brainConfig.root, { recursive: true });
38
+ const dbPath = join(brainConfig.root, "state.db");
39
+ const db = new DatabaseSync(dbPath);
40
+ db.exec("PRAGMA journal_mode = WAL");
41
+ db.exec("PRAGMA foreign_keys = ON");
42
+ runBrainMigrations(db);
43
+ const store = new BrainStore(db, { brainRoot: brainConfig.root });
44
+ const graph = new BrainGraph();
45
+ for (const node of store.getAllNodes()) {
46
+ graph.addNode(node);
47
+ }
48
+ for (const edge of store.loadAllEdges()) {
49
+ graph.addEdge(edge);
50
+ }
51
+
52
+ return { config, brainConfig, store, graph };
53
+ }
54
+
55
+ function flattenEdges(graph: BrainGraph) {
56
+ return graph.getAllEdges();
57
+ }
58
+
59
+ async function commandInit(workspaceArg?: string): Promise<void> {
60
+ const { brainConfig, store, graph } = loadStore();
61
+ const embedFn = createEmbeddingClient({ config: brainConfig });
62
+ if (!embedFn) {
63
+ throw new Error("OPENCLAWBRAIN_EMBEDDING_MODEL is required for init");
64
+ }
65
+
66
+ const workspaceRoot = resolve(workspaceArg ?? process.cwd());
67
+ const result = await initBrain({
68
+ workspaceRoot,
69
+ embedFn,
70
+ semanticThreshold: brainConfig.semanticThreshold,
71
+ log: { info: () => {}, warn: () => {} },
72
+ });
73
+
74
+ store.clearGraph();
75
+ graph.clear();
76
+ for (const node of result.nodes) {
77
+ graph.addNode(node);
78
+ store.insertNode(node);
79
+ }
80
+ for (const edge of result.edges) {
81
+ graph.addEdge(edge);
82
+ store.insertEdge(edge);
83
+ }
84
+
85
+ const health = computeHealth(graph, [], 0);
86
+ const pack = store.insertPack({
87
+ nodeCount: health.nodeCount,
88
+ edgeCount: health.edgeCount,
89
+ healthJson: JSON.stringify(health),
90
+ });
91
+ store.writePackSnapshot({
92
+ version: pack.version,
93
+ nodes: graph.getAllNodes(),
94
+ edges: flattenEdges(graph),
95
+ seedWeights: flattenSeedWeights(graph),
96
+ metadata: { reason: "cli-init", workspaceRoot, summary: result.summary },
97
+ });
98
+ store.promotePack(pack.version);
99
+
100
+ printJson({
101
+ command: "init",
102
+ workspaceRoot,
103
+ summary: result.summary,
104
+ packVersion: pack.version,
105
+ });
106
+ }
107
+
108
+ function commandStatus(): void {
109
+ const { store, graph, brainConfig } = loadStore();
110
+ const recentEpisodes = store.getRecentEpisodes(100);
111
+ const currentPack = store.getCurrentPackVersion();
112
+ const health = computeHealth(graph, recentEpisodes, currentPack ?? 0);
113
+ const currentSnapshot = currentPack !== null ? store.readPackSnapshot(currentPack) : null;
114
+ const embeddingConfig = describeEmbeddingConfig(brainConfig);
115
+ const workerState = readWorkerRuntimeState(store, brainConfig);
116
+
117
+ printJson({
118
+ command: "status",
119
+ brainRoot: brainConfig.root,
120
+ disabled: existsSync(join(brainConfig.root, "DISABLED")),
121
+ shadowMode: brainConfig.shadowMode,
122
+ embeddingProvider: brainConfig.embeddingProvider,
123
+ embeddingModel: brainConfig.embeddingModel,
124
+ embeddingBaseUrl: brainConfig.embeddingModel ? embeddingConfig.baseUrl : "",
125
+ embeddingAuthMode: embeddingConfig.authMode,
126
+ embeddingConfigError: embeddingConfig.error,
127
+ ...workerState,
128
+ currentPackVersion: currentPack,
129
+ currentPackMetadata: currentSnapshot?.metadata ?? null,
130
+ pendingEvidence: store.getPendingEvidence(100).length,
131
+ pendingEvidenceBySource: store.countPendingEvidenceBySource(),
132
+ pendingLabels: store.getPendingLabels().length,
133
+ pendingLabelsBySource: store.countPendingLabelsBySource(),
134
+ mutationBacklog: store.countMutationsByStatus(),
135
+ lastPromotionReason: store.getTrainingState("last_promotion_reason"),
136
+ lastReplayFailureReason: store.getTrainingState("last_replay_failure_reason"),
137
+ lastAssemblyDecision: {
138
+ mode: store.getTrainingState("last_assembly_mode"),
139
+ footer: store.getTrainingState("last_assembly_footer"),
140
+ episodeId: store.getTrainingState("last_assembly_episode_id"),
141
+ traceId: store.getTrainingState("last_assembly_trace_id"),
142
+ },
143
+ seedLearningEnabled: graph.hasSeedWeights(),
144
+ recentTraceCount: store.getRecentTraces(5).length,
145
+ ...health,
146
+ });
147
+ }
148
+
149
+ function commandTrace(traceId?: string): void {
150
+ const { store } = loadStore();
151
+ const trace = traceId ? store.getTrace(traceId) : store.getRecentTraces(1)[0] ?? null;
152
+ const chosenSeed = trace?.seedScores.find((seed) => seed.chosen) ?? null;
153
+ printJson({
154
+ command: "trace",
155
+ trace,
156
+ chosenSeed,
157
+ finalSectionOrder: [
158
+ "correction_cards",
159
+ "route_selected_evidence",
160
+ "toolcards_and_workflows",
161
+ "transcript_support",
162
+ ],
163
+ });
164
+ }
165
+
166
+ function commandReplay(): void {
167
+ const { store, graph, brainConfig } = loadStore();
168
+ const gate = new PackManager(
169
+ {
170
+ insertPack: (params) => store.insertPack(params),
171
+ promotePack: (version) => store.promotePack(version),
172
+ rollbackPack: (version) => store.rollbackPack(version),
173
+ },
174
+ graph,
175
+ { info: () => {}, warn: () => {} },
176
+ ).replayGate(store.getRecentEpisodes(brainConfig.replayEpisodeCount), {
177
+ minFiredPerQuery: brainConfig.minFiredPerQuery,
178
+ maxDormantPercent: brainConfig.maxDormantPercent,
179
+ maxOrphanCount: brainConfig.maxOrphanCount,
180
+ });
181
+ printJson({
182
+ command: "replay",
183
+ passed: gate.passed,
184
+ reason: gate.reason,
185
+ health: gate.health,
186
+ });
187
+ }
188
+
189
+ function commandPromote(): void {
190
+ const { store, graph } = loadStore();
191
+ const health = computeHealth(graph, store.getRecentEpisodes(100), store.getCurrentPackVersion() ?? 0);
192
+ const pack = store.insertPack({
193
+ nodeCount: health.nodeCount,
194
+ edgeCount: health.edgeCount,
195
+ healthJson: JSON.stringify(health),
196
+ });
197
+ store.writePackSnapshot({
198
+ version: pack.version,
199
+ nodes: graph.getAllNodes(),
200
+ edges: flattenEdges(graph),
201
+ seedWeights: flattenSeedWeights(graph),
202
+ metadata: { reason: "cli-promote" },
203
+ });
204
+ store.promotePack(pack.version);
205
+ printJson({
206
+ command: "promote",
207
+ version: pack.version,
208
+ });
209
+ }
210
+
211
+ function commandRollback(versionArg?: string): void {
212
+ const { store } = loadStore();
213
+ const version = versionArg ? Number.parseInt(versionArg, 10) : store.getCurrentPackVersion();
214
+ if (!version) {
215
+ throw new Error("No pack version available to roll back");
216
+ }
217
+ store.rollbackPack(version);
218
+ printJson({
219
+ command: "rollback",
220
+ version,
221
+ currentPackVersion: store.getCurrentPackVersion(),
222
+ });
223
+ }
224
+
225
+ function commandDisable(): void {
226
+ const { brainConfig } = loadStore();
227
+ const disabledFile = join(brainConfig.root, "DISABLED");
228
+ writeFileSync(disabledFile, "disabled\n", "utf8");
229
+ printJson({
230
+ command: "disable",
231
+ disabledFile,
232
+ });
233
+ }
234
+
235
+ function commandEnable(): void {
236
+ const { brainConfig } = loadStore();
237
+ const disabledFile = join(brainConfig.root, "DISABLED");
238
+ if (existsSync(disabledFile)) {
239
+ rmSync(disabledFile);
240
+ }
241
+ printJson({
242
+ command: "enable",
243
+ disabledFile,
244
+ enabled: true,
245
+ });
246
+ }
247
+
248
+ function commandDoctor(): void {
249
+ const { brainConfig, store, graph } = loadStore();
250
+ const currentPackVersion = store.getCurrentPackVersion();
251
+ const snapshot = currentPackVersion !== null ? store.readPackSnapshot(currentPackVersion) : null;
252
+ const embeddingConfig = describeEmbeddingConfig(brainConfig);
253
+ const workerState = readWorkerRuntimeState(store, brainConfig);
254
+ printJson({
255
+ command: "doctor",
256
+ brainRoot: brainConfig.root,
257
+ stateDbExists: existsSync(join(brainConfig.root, "state.db")),
258
+ currentPackVersion,
259
+ currentPackSnapshotExists: snapshot !== null,
260
+ embeddingConfigured: brainConfig.embeddingModel.trim().length > 0,
261
+ embeddingProvider: brainConfig.embeddingProvider,
262
+ embeddingModel: brainConfig.embeddingModel,
263
+ embeddingBaseUrl: brainConfig.embeddingModel.trim().length > 0 ? embeddingConfig.baseUrl : "",
264
+ embeddingAuthMode: embeddingConfig.authMode,
265
+ embeddingConfigError: embeddingConfig.error,
266
+ shadowMode: brainConfig.shadowMode,
267
+ ...workerState,
268
+ disabled: existsSync(join(brainConfig.root, "DISABLED")),
269
+ pendingEvidence: store.getPendingEvidence(100).length,
270
+ mutationBacklog: store.countMutationsByStatus(),
271
+ orphanedTraceRows: store.countOrphanedTraceRows(),
272
+ nodeCount: graph.nodeCount(),
273
+ edgeCount: graph.edgeCount(),
274
+ lastTraceId: store.getRecentTraces(1)[0]?.id ?? null,
275
+ });
276
+ }
277
+
278
+ async function main(): Promise<void> {
279
+ const [command, arg] = process.argv.slice(2);
280
+ switch (command) {
281
+ case "init":
282
+ await commandInit(arg);
283
+ return;
284
+ case "status":
285
+ commandStatus();
286
+ return;
287
+ case "trace":
288
+ commandTrace(arg);
289
+ return;
290
+ case "replay":
291
+ commandReplay();
292
+ return;
293
+ case "promote":
294
+ commandPromote();
295
+ return;
296
+ case "rollback":
297
+ commandRollback(arg);
298
+ return;
299
+ case "disable":
300
+ commandDisable();
301
+ return;
302
+ case "enable":
303
+ commandEnable();
304
+ return;
305
+ case "doctor":
306
+ commandDoctor();
307
+ return;
308
+ default:
309
+ usage();
310
+ }
311
+ }
312
+
313
+ void main().catch((error) => {
314
+ process.stderr.write(`${(error as Error).message}\n`);
315
+ process.exit(1);
316
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Weight decay toward structural priors.
3
+ *
4
+ * Without evidence reinforcing them, learned edge weights
5
+ * should converge back to their structural/semantic priors.
6
+ *
7
+ * Decay formula: w_new = w * rate + prior * (1 - rate)
8
+ *
9
+ * rate = 1.0 → no decay (weight unchanged)
10
+ * rate = 0.0 → instant snap to prior
11
+ * rate = 0.995 → slow decay (default)
12
+ */
13
+
14
+ import type { BrainEdge } from "./types.js";
15
+ import type { BrainGraph } from "./graph.js";
16
+
17
+ /**
18
+ * Decay a single weight toward its prior.
19
+ */
20
+ export function decayWeight(weight: number, prior: number, rate: number): number {
21
+ return weight * rate + prior * (1 - rate);
22
+ }
23
+
24
+ /**
25
+ * Batch decay all edge weights in the graph toward their priors.
26
+ */
27
+ export function decayAllWeights(graph: BrainGraph, rate: number, now: number): void {
28
+ for (const node of graph.getAllNodes()) {
29
+ const edges = graph.getOutgoingEdges(node.id);
30
+ for (const edge of edges) {
31
+ edge.weight = decayWeight(edge.weight, edge.prior, rate);
32
+ edge.decayedAt = now;
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Episode recording and replay.
3
+ */
4
+
5
+ import { randomUUID } from "node:crypto";
6
+ import type { Episode, TrajectoryStep, PolicyParams } from "./types.js";
7
+ import { DEFAULT_POLICY_PARAMS } from "./types.js";
8
+ import type { BrainGraph } from "./graph.js";
9
+ import type { TraverseResult } from "./traverse.js";
10
+ import { softmaxPolicy } from "./policy.js";
11
+
12
+ export function recordEpisode(params: {
13
+ traversalResult: TraverseResult;
14
+ queryText: string;
15
+ queryEmbedding: Float32Array | null;
16
+ conversationId: number | null;
17
+ packVersion: number | null;
18
+ }): Episode {
19
+ return {
20
+ id: `be_${randomUUID().slice(0, 12)}`,
21
+ conversationId: params.conversationId,
22
+ queryText: params.queryText,
23
+ queryEmbedding: params.queryEmbedding,
24
+ trajectory: params.traversalResult.trajectory,
25
+ firedNodes: params.traversalResult.firedNodes.map((n) => n.nodeId),
26
+ vetoedNodes: params.traversalResult.vetoedNodes.map((v) => v.nodeId),
27
+ contextChars: params.traversalResult.contextChars,
28
+ reward: null,
29
+ rewardSource: null,
30
+ packVersion: params.packVersion,
31
+ createdAt: Date.now(),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Replay an episode against a (possibly mutated) graph.
37
+ * Returns what the policy WOULD produce with updated weights.
38
+ * Used for replay-gate validation before pack promotion.
39
+ */
40
+ export function replayEpisode(
41
+ episode: Episode,
42
+ graph: BrainGraph,
43
+ policyParams: PolicyParams = DEFAULT_POLICY_PARAMS,
44
+ ): { firedNodes: string[]; wouldChange: boolean } {
45
+ if (!episode.queryEmbedding || episode.trajectory.length === 0) {
46
+ return { firedNodes: [], wouldChange: false };
47
+ }
48
+
49
+ const fired: string[] = [];
50
+ let changed = false;
51
+
52
+ for (const step of episode.trajectory) {
53
+ if (step.chosenAction.type !== "traverse") continue;
54
+
55
+ const actions = step.candidates.map((c) => c.action);
56
+ const state = {
57
+ currentNodeId: step.stateSnapshot.currentNodeId,
58
+ queryEmbedding: episode.queryEmbedding,
59
+ visited: new Set<string>(),
60
+ fired,
61
+ budgetRemaining: step.stateSnapshot.budgetRemaining,
62
+ hopCount: step.stateSnapshot.hopCount,
63
+ maxHops: 8,
64
+ };
65
+
66
+ const newDist = softmaxPolicy(actions, state, graph, policyParams);
67
+ const originalChoice = step.chosenAction;
68
+ const newTopAction = newDist.reduce((best, d) => d.probability > best.probability ? d : best, newDist[0]);
69
+
70
+ if (newTopAction.action.type !== originalChoice.type ||
71
+ (newTopAction.action.type === "traverse" && originalChoice.type === "traverse" &&
72
+ newTopAction.action.targetNodeId !== originalChoice.targetNodeId)) {
73
+ changed = true;
74
+ }
75
+
76
+ if (step.chosenAction.type === "traverse") {
77
+ fired.push(step.chosenAction.targetNodeId);
78
+ }
79
+ }
80
+
81
+ return { firedNodes: fired, wouldChange: changed };
82
+ }