@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,383 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { LcmConfig } from "./db/config.js";
3
+ import type { RetrievalEngine, ExpandResult, GrepResult } from "./retrieval.js";
4
+
5
+ // ── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ export type ExpansionRequest = {
8
+ /** Summary IDs to expand */
9
+ summaryIds: string[];
10
+ /** Max traversal depth per summary (default: 3) */
11
+ maxDepth?: number;
12
+ /** Max tokens across the entire expansion (default: config.maxExpandTokens) */
13
+ tokenCap?: number;
14
+ /** Whether to include raw source messages at leaf level */
15
+ includeMessages?: boolean;
16
+ /** Conversation ID scope */
17
+ conversationId: number;
18
+ };
19
+
20
+ export type ExpansionResult = {
21
+ /** Expanded summaries with their children/messages */
22
+ expansions: Array<{
23
+ summaryId: string;
24
+ children: Array<{
25
+ summaryId: string;
26
+ kind: string;
27
+ snippet: string;
28
+ tokenCount: number;
29
+ }>;
30
+ messages: Array<{
31
+ messageId: number;
32
+ role: string;
33
+ snippet: string;
34
+ tokenCount: number;
35
+ }>;
36
+ }>;
37
+ /** Cited IDs for follow-up traversal */
38
+ citedIds: string[];
39
+ /** Total tokens in the result */
40
+ totalTokens: number;
41
+ /** Whether any expansion was truncated */
42
+ truncated: boolean;
43
+ };
44
+
45
+ // ── Helpers ──────────────────────────────────────────────────────────────────
46
+
47
+ const SNIPPET_MAX_CHARS = 200;
48
+
49
+ /** Truncate content to a short snippet for display. */
50
+ function truncateSnippet(content: string, maxChars: number = SNIPPET_MAX_CHARS): string {
51
+ if (content.length <= maxChars) {
52
+ return content;
53
+ }
54
+ return content.slice(0, maxChars) + "...";
55
+ }
56
+
57
+ /**
58
+ * Resolve the effective expansion token cap by applying a configured default
59
+ * and an explicit upper bound.
60
+ */
61
+ export function resolveExpansionTokenCap(input: {
62
+ requestedTokenCap?: number;
63
+ maxExpandTokens: number;
64
+ }): number {
65
+ const maxExpandTokens = Math.max(1, Math.trunc(input.maxExpandTokens));
66
+ const requestedTokenCap = input.requestedTokenCap;
67
+ if (typeof requestedTokenCap !== "number" || !Number.isFinite(requestedTokenCap)) {
68
+ return maxExpandTokens;
69
+ }
70
+ return Math.min(Math.max(1, Math.trunc(requestedTokenCap)), maxExpandTokens);
71
+ }
72
+
73
+ /**
74
+ * Convert a single RetrievalEngine.expand() result into the ExpansionResult
75
+ * entry format, truncating content to short snippets.
76
+ */
77
+ function toExpansionEntry(
78
+ summaryId: string,
79
+ raw: ExpandResult,
80
+ ): ExpansionResult["expansions"][number] {
81
+ return {
82
+ summaryId,
83
+ children: raw.children.map((c) => ({
84
+ summaryId: c.summaryId,
85
+ kind: c.kind,
86
+ snippet: truncateSnippet(c.content),
87
+ tokenCount: c.tokenCount,
88
+ })),
89
+ messages: raw.messages.map((m) => ({
90
+ messageId: m.messageId,
91
+ role: m.role,
92
+ snippet: truncateSnippet(m.content),
93
+ tokenCount: m.tokenCount,
94
+ })),
95
+ };
96
+ }
97
+
98
+ /** Collect all referenced summary IDs from an expansion entry. */
99
+ function collectCitedIds(entry: ExpansionResult["expansions"][number]): string[] {
100
+ const ids: string[] = [entry.summaryId];
101
+ for (const child of entry.children) {
102
+ ids.push(child.summaryId);
103
+ }
104
+ return ids;
105
+ }
106
+
107
+ // ── ExpansionOrchestrator ────────────────────────────────────────────────────
108
+
109
+ export class ExpansionOrchestrator {
110
+ constructor(private retrieval: RetrievalEngine) {}
111
+
112
+ /**
113
+ * Expand each summary ID using the RetrievalEngine, collecting results and
114
+ * enforcing a global token cap across all expansions.
115
+ */
116
+ async expand(request: ExpansionRequest): Promise<ExpansionResult> {
117
+ const maxDepth = request.maxDepth ?? 3;
118
+ const tokenCap = request.tokenCap ?? Infinity;
119
+ const includeMessages = request.includeMessages ?? false;
120
+
121
+ const result: ExpansionResult = {
122
+ expansions: [],
123
+ citedIds: [],
124
+ totalTokens: 0,
125
+ truncated: false,
126
+ };
127
+
128
+ const citedSet = new Set<string>();
129
+
130
+ for (const summaryId of request.summaryIds) {
131
+ if (result.truncated) {
132
+ break;
133
+ }
134
+
135
+ // Calculate remaining budget for this expansion
136
+ const remainingBudget = tokenCap - result.totalTokens;
137
+ if (remainingBudget <= 0) {
138
+ result.truncated = true;
139
+ break;
140
+ }
141
+
142
+ const raw = await this.retrieval.expand({
143
+ summaryId,
144
+ depth: maxDepth,
145
+ includeMessages,
146
+ tokenCap: remainingBudget,
147
+ });
148
+
149
+ const entry = toExpansionEntry(summaryId, raw);
150
+ result.expansions.push(entry);
151
+ result.totalTokens += raw.estimatedTokens;
152
+
153
+ // Track cited IDs
154
+ for (const id of collectCitedIds(entry)) {
155
+ citedSet.add(id);
156
+ }
157
+
158
+ if (raw.truncated) {
159
+ result.truncated = true;
160
+ }
161
+ }
162
+
163
+ result.citedIds = [...citedSet];
164
+ return result;
165
+ }
166
+
167
+ /**
168
+ * Convenience method: grep for matching summaries, then expand the top results.
169
+ * Combines the routing pass (grep) with the deep expansion pass.
170
+ */
171
+ async describeAndExpand(input: {
172
+ query: string;
173
+ mode: "regex" | "full_text";
174
+ conversationId?: number;
175
+ maxDepth?: number;
176
+ tokenCap?: number;
177
+ }): Promise<ExpansionResult> {
178
+ const grepResult: GrepResult = await this.retrieval.grep({
179
+ query: input.query,
180
+ mode: input.mode,
181
+ scope: "summaries",
182
+ conversationId: input.conversationId,
183
+ });
184
+
185
+ const summaryIds = [...grepResult.summaries]
186
+ .sort((a, b) => {
187
+ const recencyDelta = b.createdAt.getTime() - a.createdAt.getTime();
188
+ if (recencyDelta !== 0) {
189
+ return recencyDelta;
190
+ }
191
+ const aRank = a.rank ?? Number.POSITIVE_INFINITY;
192
+ const bRank = b.rank ?? Number.POSITIVE_INFINITY;
193
+ return aRank - bRank;
194
+ })
195
+ .map((s) => s.summaryId);
196
+ if (summaryIds.length === 0) {
197
+ return {
198
+ expansions: [],
199
+ citedIds: [],
200
+ totalTokens: 0,
201
+ truncated: false,
202
+ };
203
+ }
204
+
205
+ return this.expand({
206
+ summaryIds,
207
+ maxDepth: input.maxDepth,
208
+ tokenCap: input.tokenCap,
209
+ includeMessages: false,
210
+ conversationId: input.conversationId ?? 0,
211
+ });
212
+ }
213
+ }
214
+
215
+ // ── Distill for subagent ─────────────────────────────────────────────────────
216
+
217
+ /**
218
+ * Format an ExpansionResult into a compact text payload suitable for passing
219
+ * to a subagent or returning to the main agent.
220
+ */
221
+ export function distillForSubagent(result: ExpansionResult): string {
222
+ const lines: string[] = [];
223
+
224
+ lines.push(
225
+ `## Expansion Results (${result.expansions.length} summaries, ${result.totalTokens} total tokens)`,
226
+ );
227
+ lines.push("");
228
+
229
+ for (const entry of result.expansions) {
230
+ // Determine kind from children presence: if it has children it was a condensed node
231
+ const kind = entry.children.length > 0 ? "condensed" : "leaf";
232
+ const tokenSum =
233
+ entry.children.reduce((sum, c) => sum + c.tokenCount, 0) +
234
+ entry.messages.reduce((sum, m) => sum + m.tokenCount, 0);
235
+
236
+ lines.push(`### ${entry.summaryId} (${kind}, ${tokenSum} tokens)`);
237
+
238
+ if (entry.children.length > 0) {
239
+ lines.push(`Children: ${entry.children.map((c) => c.summaryId).join(", ")}`);
240
+ }
241
+
242
+ if (entry.messages.length > 0) {
243
+ const msgParts = entry.messages.map(
244
+ (m) => `msg#${m.messageId} (${m.role}, ${m.tokenCount} tokens)`,
245
+ );
246
+ lines.push(`Messages: ${msgParts.join(", ")}`);
247
+ }
248
+
249
+ // Show a snippet for children that have content
250
+ for (const child of entry.children) {
251
+ if (child.snippet) {
252
+ lines.push(`[Snippet: ${truncateSnippet(child.snippet)}]`);
253
+ break; // Only show one snippet per entry to keep it compact
254
+ }
255
+ }
256
+
257
+ lines.push("");
258
+ }
259
+
260
+ if (result.citedIds.length > 0) {
261
+ lines.push(`Cited IDs for follow-up: ${result.citedIds.join(", ")}`);
262
+ }
263
+
264
+ lines.push(`[Truncated: ${result.truncated ? "yes" : "no"}]`);
265
+
266
+ return lines.join("\n");
267
+ }
268
+
269
+ // ── Tool definition ──────────────────────────────────────────────────────────
270
+
271
+ const LcmExpansionSchema = Type.Object({
272
+ summaryIds: Type.Optional(
273
+ Type.Array(Type.String(), {
274
+ description: "Summary IDs to expand (e.g. sum_abc123). Required if query is not provided.",
275
+ }),
276
+ ),
277
+ query: Type.Optional(
278
+ Type.String({
279
+ description:
280
+ "Text query to grep for matching summaries before expanding. " +
281
+ "If provided, summaryIds is ignored and the top grep results are expanded instead.",
282
+ }),
283
+ ),
284
+ maxDepth: Type.Optional(
285
+ Type.Number({
286
+ description: "Max traversal depth per summary (default: 3).",
287
+ minimum: 1,
288
+ maximum: 10,
289
+ }),
290
+ ),
291
+ tokenCap: Type.Optional(
292
+ Type.Number({
293
+ description: "Max tokens across the entire expansion result.",
294
+ minimum: 1,
295
+ }),
296
+ ),
297
+ includeMessages: Type.Optional(
298
+ Type.Boolean({
299
+ description: "Whether to include raw source messages at leaf level (default: false).",
300
+ }),
301
+ ),
302
+ });
303
+
304
+ /**
305
+ * Build a tool definition object for LCM expansion that can be registered as
306
+ * an agent tool. Follows the pattern used in `src/agents/tools/`.
307
+ *
308
+ * Requires an already-initialised ExpansionOrchestrator and an LcmConfig
309
+ * (for the default tokenCap).
310
+ */
311
+ export function buildExpansionToolDefinition(options: {
312
+ orchestrator: ExpansionOrchestrator;
313
+ config: LcmConfig;
314
+ conversationId: number;
315
+ }) {
316
+ const { orchestrator, config, conversationId } = options;
317
+
318
+ return {
319
+ name: "lcm_expand",
320
+ description:
321
+ "Expand compacted conversation summaries from LCM (Lossless Context Management). " +
322
+ "Traverses the summary DAG to retrieve children and source messages. " +
323
+ "Use this to drill into previously-compacted context when you need detail " +
324
+ "that was summarised away. Returns a compact text payload with cited IDs for follow-up.",
325
+ parameters: LcmExpansionSchema,
326
+ execute: async (
327
+ _toolCallId: string,
328
+ params: Record<string, unknown>,
329
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: unknown }> => {
330
+ const summaryIds = params.summaryIds as string[] | undefined;
331
+ const query = typeof params.query === "string" ? params.query.trim() : undefined;
332
+ const maxDepth =
333
+ typeof params.maxDepth === "number" ? Math.trunc(params.maxDepth) : undefined;
334
+ const requestedTokenCap =
335
+ typeof params.tokenCap === "number" ? Math.trunc(params.tokenCap) : undefined;
336
+ const tokenCap = resolveExpansionTokenCap({
337
+ requestedTokenCap,
338
+ maxExpandTokens: config.maxExpandTokens,
339
+ });
340
+ const includeMessages =
341
+ typeof params.includeMessages === "boolean" ? params.includeMessages : false;
342
+
343
+ let result: ExpansionResult;
344
+
345
+ if (query) {
346
+ // Grep-first path: find summaries matching the query, then expand
347
+ result = await orchestrator.describeAndExpand({
348
+ query,
349
+ mode: "full_text",
350
+ conversationId,
351
+ maxDepth,
352
+ tokenCap,
353
+ });
354
+ } else if (summaryIds && summaryIds.length > 0) {
355
+ // Direct expansion of specific summary IDs
356
+ result = await orchestrator.expand({
357
+ summaryIds,
358
+ maxDepth,
359
+ tokenCap,
360
+ includeMessages,
361
+ conversationId,
362
+ });
363
+ } else {
364
+ const text = "Error: either summaryIds or query must be provided.";
365
+ return {
366
+ content: [{ type: "text", text }],
367
+ details: { error: text },
368
+ };
369
+ }
370
+
371
+ const distilled = distillForSubagent(result);
372
+ return {
373
+ content: [{ type: "text", text: distilled }],
374
+ details: {
375
+ expansionCount: result.expansions.length,
376
+ citedIds: result.citedIds,
377
+ totalTokens: result.totalTokens,
378
+ truncated: result.truncated,
379
+ },
380
+ };
381
+ },
382
+ };
383
+ }