@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,600 @@
1
+ import type { ConversationStore } from "./store/conversation-store.js";
2
+ import type {
3
+ SummaryStore,
4
+ SummaryRecord,
5
+ ContextItemRecord,
6
+ } from "./store/summary-store.js";
7
+
8
+ // ── Types ─────────────────────────────────────────────────────────────────────
9
+
10
+ export type IntegrityCheck = {
11
+ name: string;
12
+ status: "pass" | "fail" | "warn";
13
+ message: string;
14
+ details?: unknown;
15
+ };
16
+
17
+ export type IntegrityReport = {
18
+ conversationId: number;
19
+ checks: IntegrityCheck[];
20
+ passCount: number;
21
+ failCount: number;
22
+ warnCount: number;
23
+ scannedAt: Date;
24
+ };
25
+
26
+ export type LcmMetrics = {
27
+ conversationId: number;
28
+ contextTokens: number;
29
+ messageCount: number;
30
+ summaryCount: number;
31
+ contextItemCount: number;
32
+ leafSummaryCount: number;
33
+ condensedSummaryCount: number;
34
+ largeFileCount: number;
35
+ collectedAt: Date;
36
+ };
37
+
38
+ // ── IntegrityChecker ──────────────────────────────────────────────────────────
39
+
40
+ export class IntegrityChecker {
41
+ constructor(
42
+ private conversationStore: ConversationStore,
43
+ private summaryStore: SummaryStore,
44
+ ) {}
45
+
46
+ /**
47
+ * Run all integrity checks for a conversation and return a full report.
48
+ * Each check runs independently -- a failure in one does not short-circuit
49
+ * the remaining checks.
50
+ */
51
+ async scan(conversationId: number): Promise<IntegrityReport> {
52
+ const checks: IntegrityCheck[] = [];
53
+
54
+ // 1. conversation_exists
55
+ checks.push(await this.checkConversationExists(conversationId));
56
+
57
+ // If the conversation does not exist, the remaining checks will still
58
+ // execute (operating on empty result sets) so the report is complete.
59
+
60
+ // 2. context_items_contiguous
61
+ checks.push(await this.checkContextItemsContiguous(conversationId));
62
+
63
+ // 3. context_items_valid_refs
64
+ checks.push(await this.checkContextItemsValidRefs(conversationId));
65
+
66
+ // 4. summaries_have_lineage
67
+ checks.push(await this.checkSummariesHaveLineage(conversationId));
68
+
69
+ // 5. no_orphan_summaries
70
+ checks.push(await this.checkNoOrphanSummaries(conversationId));
71
+
72
+ // 6. context_token_consistency
73
+ checks.push(await this.checkContextTokenConsistency(conversationId));
74
+
75
+ // 7. message_seq_contiguous
76
+ checks.push(await this.checkMessageSeqContiguous(conversationId));
77
+
78
+ // 8. no_duplicate_context_refs
79
+ checks.push(await this.checkNoDuplicateContextRefs(conversationId));
80
+
81
+ const passCount = checks.filter((c) => c.status === "pass").length;
82
+ const failCount = checks.filter((c) => c.status === "fail").length;
83
+ const warnCount = checks.filter((c) => c.status === "warn").length;
84
+
85
+ return {
86
+ conversationId,
87
+ checks,
88
+ passCount,
89
+ failCount,
90
+ warnCount,
91
+ scannedAt: new Date(),
92
+ };
93
+ }
94
+
95
+ // ── Individual checks ───────────────────────────────────────────────────
96
+
97
+ private async checkConversationExists(
98
+ conversationId: number,
99
+ ): Promise<IntegrityCheck> {
100
+ const conversation =
101
+ await this.conversationStore.getConversation(conversationId);
102
+ if (conversation) {
103
+ return {
104
+ name: "conversation_exists",
105
+ status: "pass",
106
+ message: `Conversation ${conversationId} exists`,
107
+ };
108
+ }
109
+ return {
110
+ name: "conversation_exists",
111
+ status: "fail",
112
+ message: `Conversation ${conversationId} not found`,
113
+ };
114
+ }
115
+
116
+ private async checkContextItemsContiguous(
117
+ conversationId: number,
118
+ ): Promise<IntegrityCheck> {
119
+ const items = await this.summaryStore.getContextItems(conversationId);
120
+ if (items.length === 0) {
121
+ return {
122
+ name: "context_items_contiguous",
123
+ status: "pass",
124
+ message: "No context items to check",
125
+ };
126
+ }
127
+
128
+ const gaps: { expected: number; actual: number }[] = [];
129
+ for (let i = 0; i < items.length; i++) {
130
+ if (items[i].ordinal !== i) {
131
+ gaps.push({ expected: i, actual: items[i].ordinal });
132
+ }
133
+ }
134
+
135
+ if (gaps.length === 0) {
136
+ return {
137
+ name: "context_items_contiguous",
138
+ status: "pass",
139
+ message: `All ${items.length} context items have contiguous ordinals`,
140
+ };
141
+ }
142
+
143
+ return {
144
+ name: "context_items_contiguous",
145
+ status: "fail",
146
+ message: `Found ${gaps.length} ordinal gap(s) in context items`,
147
+ details: { gaps },
148
+ };
149
+ }
150
+
151
+ private async checkContextItemsValidRefs(
152
+ conversationId: number,
153
+ ): Promise<IntegrityCheck> {
154
+ const items = await this.summaryStore.getContextItems(conversationId);
155
+ const danglingRefs: {
156
+ ordinal: number;
157
+ itemType: string;
158
+ refId: number | string;
159
+ }[] = [];
160
+
161
+ for (const item of items) {
162
+ if (item.itemType === "message" && item.messageId != null) {
163
+ const msg = await this.conversationStore.getMessageById(item.messageId);
164
+ if (!msg) {
165
+ danglingRefs.push({
166
+ ordinal: item.ordinal,
167
+ itemType: "message",
168
+ refId: item.messageId,
169
+ });
170
+ }
171
+ } else if (item.itemType === "summary" && item.summaryId != null) {
172
+ const sum = await this.summaryStore.getSummary(item.summaryId);
173
+ if (!sum) {
174
+ danglingRefs.push({
175
+ ordinal: item.ordinal,
176
+ itemType: "summary",
177
+ refId: item.summaryId,
178
+ });
179
+ }
180
+ }
181
+ }
182
+
183
+ if (danglingRefs.length === 0) {
184
+ return {
185
+ name: "context_items_valid_refs",
186
+ status: "pass",
187
+ message: "All context item references are valid",
188
+ };
189
+ }
190
+
191
+ return {
192
+ name: "context_items_valid_refs",
193
+ status: "fail",
194
+ message: `Found ${danglingRefs.length} dangling reference(s) in context items`,
195
+ details: { danglingRefs },
196
+ };
197
+ }
198
+
199
+ private async checkSummariesHaveLineage(
200
+ conversationId: number,
201
+ ): Promise<IntegrityCheck> {
202
+ const summaries =
203
+ await this.summaryStore.getSummariesByConversation(conversationId);
204
+ const missingLineage: { summaryId: string; kind: string; issue: string }[] =
205
+ [];
206
+
207
+ for (const summary of summaries) {
208
+ if (summary.kind === "leaf") {
209
+ // Leaf summaries must link to at least one message
210
+ const messageIds = await this.summaryStore.getSummaryMessages(
211
+ summary.summaryId,
212
+ );
213
+ if (messageIds.length === 0) {
214
+ missingLineage.push({
215
+ summaryId: summary.summaryId,
216
+ kind: "leaf",
217
+ issue: "no linked messages in summary_messages",
218
+ });
219
+ }
220
+ } else if (summary.kind === "condensed") {
221
+ // Condensed summaries must link to at least one parent summary
222
+ const parents = await this.summaryStore.getSummaryParents(
223
+ summary.summaryId,
224
+ );
225
+ if (parents.length === 0) {
226
+ missingLineage.push({
227
+ summaryId: summary.summaryId,
228
+ kind: "condensed",
229
+ issue: "no linked parents in summary_parents",
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ if (missingLineage.length === 0) {
236
+ return {
237
+ name: "summaries_have_lineage",
238
+ status: "pass",
239
+ message: `All ${summaries.length} summaries have proper lineage`,
240
+ };
241
+ }
242
+
243
+ return {
244
+ name: "summaries_have_lineage",
245
+ status: "fail",
246
+ message: `Found ${missingLineage.length} summary/summaries missing lineage`,
247
+ details: { missingLineage },
248
+ };
249
+ }
250
+
251
+ private async checkNoOrphanSummaries(
252
+ conversationId: number,
253
+ ): Promise<IntegrityCheck> {
254
+ const summaries =
255
+ await this.summaryStore.getSummariesByConversation(conversationId);
256
+ const contextItems =
257
+ await this.summaryStore.getContextItems(conversationId);
258
+
259
+ // Build set of summary IDs that appear in context_items
260
+ const contextSummaryIds = new Set(
261
+ contextItems
262
+ .filter((ci) => ci.itemType === "summary" && ci.summaryId != null)
263
+ .map((ci) => ci.summaryId as string),
264
+ );
265
+
266
+ // Build set of summary IDs that are parents of other summaries
267
+ const parentSummaryIds = new Set<string>();
268
+ for (const summary of summaries) {
269
+ const children = await this.summaryStore.getSummaryChildren(
270
+ summary.summaryId,
271
+ );
272
+ if (children.length > 0) {
273
+ parentSummaryIds.add(summary.summaryId);
274
+ }
275
+ }
276
+
277
+ // Orphans are summaries in neither set
278
+ const orphans: string[] = [];
279
+ for (const summary of summaries) {
280
+ if (
281
+ !contextSummaryIds.has(summary.summaryId) &&
282
+ !parentSummaryIds.has(summary.summaryId)
283
+ ) {
284
+ orphans.push(summary.summaryId);
285
+ }
286
+ }
287
+
288
+ if (orphans.length === 0) {
289
+ return {
290
+ name: "no_orphan_summaries",
291
+ status: "pass",
292
+ message: "No orphaned summaries found",
293
+ };
294
+ }
295
+
296
+ return {
297
+ name: "no_orphan_summaries",
298
+ status: "warn",
299
+ message: `Found ${orphans.length} orphaned summary/summaries disconnected from the DAG`,
300
+ details: { orphanedSummaryIds: orphans },
301
+ };
302
+ }
303
+
304
+ private async checkContextTokenConsistency(
305
+ conversationId: number,
306
+ ): Promise<IntegrityCheck> {
307
+ const contextItems =
308
+ await this.summaryStore.getContextItems(conversationId);
309
+
310
+ // Manually sum token counts from referenced messages and summaries
311
+ let manualSum = 0;
312
+ for (const item of contextItems) {
313
+ if (item.itemType === "message" && item.messageId != null) {
314
+ const msg = await this.conversationStore.getMessageById(item.messageId);
315
+ if (msg) {
316
+ manualSum += msg.tokenCount;
317
+ }
318
+ } else if (item.itemType === "summary" && item.summaryId != null) {
319
+ const sum = await this.summaryStore.getSummary(item.summaryId);
320
+ if (sum) {
321
+ manualSum += sum.tokenCount;
322
+ }
323
+ }
324
+ }
325
+
326
+ // Compare with the aggregate query
327
+ const aggregateTotal =
328
+ await this.summaryStore.getContextTokenCount(conversationId);
329
+
330
+ if (manualSum === aggregateTotal) {
331
+ return {
332
+ name: "context_token_consistency",
333
+ status: "pass",
334
+ message: `Context token count is consistent (${aggregateTotal} tokens)`,
335
+ };
336
+ }
337
+
338
+ return {
339
+ name: "context_token_consistency",
340
+ status: "fail",
341
+ message: `Token count mismatch: item-level sum = ${manualSum}, aggregate query = ${aggregateTotal}`,
342
+ details: { manualSum, aggregateTotal, difference: manualSum - aggregateTotal },
343
+ };
344
+ }
345
+
346
+ private async checkMessageSeqContiguous(
347
+ conversationId: number,
348
+ ): Promise<IntegrityCheck> {
349
+ const messages = await this.conversationStore.getMessages(conversationId);
350
+ if (messages.length === 0) {
351
+ return {
352
+ name: "message_seq_contiguous",
353
+ status: "pass",
354
+ message: "No messages to check",
355
+ };
356
+ }
357
+
358
+ const gaps: { expected: number; actual: number }[] = [];
359
+ for (let i = 0; i < messages.length; i++) {
360
+ if (messages[i].seq !== i) {
361
+ gaps.push({ expected: i, actual: messages[i].seq });
362
+ }
363
+ }
364
+
365
+ if (gaps.length === 0) {
366
+ return {
367
+ name: "message_seq_contiguous",
368
+ status: "pass",
369
+ message: `All ${messages.length} messages have contiguous seq values`,
370
+ };
371
+ }
372
+
373
+ return {
374
+ name: "message_seq_contiguous",
375
+ status: "fail",
376
+ message: `Found ${gaps.length} seq gap(s) in messages`,
377
+ details: { gaps },
378
+ };
379
+ }
380
+
381
+ private async checkNoDuplicateContextRefs(
382
+ conversationId: number,
383
+ ): Promise<IntegrityCheck> {
384
+ const items = await this.summaryStore.getContextItems(conversationId);
385
+
386
+ const seenMessageIds = new Map<number, number[]>();
387
+ const seenSummaryIds = new Map<string, number[]>();
388
+ const duplicates: {
389
+ refType: string;
390
+ refId: number | string;
391
+ ordinals: number[];
392
+ }[] = [];
393
+
394
+ for (const item of items) {
395
+ if (item.itemType === "message" && item.messageId != null) {
396
+ const ordinals = seenMessageIds.get(item.messageId) ?? [];
397
+ ordinals.push(item.ordinal);
398
+ seenMessageIds.set(item.messageId, ordinals);
399
+ } else if (item.itemType === "summary" && item.summaryId != null) {
400
+ const ordinals = seenSummaryIds.get(item.summaryId) ?? [];
401
+ ordinals.push(item.ordinal);
402
+ seenSummaryIds.set(item.summaryId, ordinals);
403
+ }
404
+ }
405
+
406
+ for (const [messageId, ordinals] of seenMessageIds) {
407
+ if (ordinals.length > 1) {
408
+ duplicates.push({ refType: "message", refId: messageId, ordinals });
409
+ }
410
+ }
411
+ for (const [summaryId, ordinals] of seenSummaryIds) {
412
+ if (ordinals.length > 1) {
413
+ duplicates.push({ refType: "summary", refId: summaryId, ordinals });
414
+ }
415
+ }
416
+
417
+ if (duplicates.length === 0) {
418
+ return {
419
+ name: "no_duplicate_context_refs",
420
+ status: "pass",
421
+ message: "No duplicate references in context items",
422
+ };
423
+ }
424
+
425
+ return {
426
+ name: "no_duplicate_context_refs",
427
+ status: "fail",
428
+ message: `Found ${duplicates.length} duplicate reference(s) in context items`,
429
+ details: { duplicates },
430
+ };
431
+ }
432
+ }
433
+
434
+ // ── repairPlan ────────────────────────────────────────────────────────────────
435
+
436
+ /**
437
+ * Generate human-readable repair suggestions for each failing or warning check
438
+ * in an integrity report. Does not perform any actual repairs.
439
+ */
440
+ export function repairPlan(report: IntegrityReport): string[] {
441
+ const suggestions: string[] = [];
442
+
443
+ for (const check of report.checks) {
444
+ if (check.status === "pass") continue;
445
+
446
+ switch (check.name) {
447
+ case "conversation_exists":
448
+ suggestions.push(
449
+ `Create or restore conversation ${report.conversationId} in the conversations table`,
450
+ );
451
+ break;
452
+
453
+ case "context_items_contiguous":
454
+ suggestions.push(
455
+ "Resequence context items to fix ordinal gaps",
456
+ );
457
+ break;
458
+
459
+ case "context_items_valid_refs": {
460
+ const details = check.details as {
461
+ danglingRefs: { ordinal: number; itemType: string; refId: number | string }[];
462
+ } | undefined;
463
+ if (details?.danglingRefs) {
464
+ for (const ref of details.danglingRefs) {
465
+ suggestions.push(
466
+ `Remove context item at ordinal ${ref.ordinal} referencing missing ${ref.itemType} ${ref.refId}`,
467
+ );
468
+ }
469
+ } else {
470
+ suggestions.push(
471
+ "Remove context items with dangling references",
472
+ );
473
+ }
474
+ break;
475
+ }
476
+
477
+ case "summaries_have_lineage": {
478
+ const details = check.details as {
479
+ missingLineage: { summaryId: string; kind: string; issue: string }[];
480
+ } | undefined;
481
+ if (details?.missingLineage) {
482
+ for (const entry of details.missingLineage) {
483
+ if (entry.kind === "leaf") {
484
+ suggestions.push(
485
+ `Add missing lineage for leaf summary ${entry.summaryId} (link to source messages via summary_messages)`,
486
+ );
487
+ } else {
488
+ suggestions.push(
489
+ `Add missing lineage for condensed summary ${entry.summaryId} (link to parent summaries via summary_parents)`,
490
+ );
491
+ }
492
+ }
493
+ } else {
494
+ suggestions.push(
495
+ "Add missing lineage links for summaries",
496
+ );
497
+ }
498
+ break;
499
+ }
500
+
501
+ case "no_orphan_summaries": {
502
+ const details = check.details as {
503
+ orphanedSummaryIds: string[];
504
+ } | undefined;
505
+ if (details?.orphanedSummaryIds) {
506
+ for (const id of details.orphanedSummaryIds) {
507
+ suggestions.push(
508
+ `Remove orphaned summary ${id} from summaries table`,
509
+ );
510
+ }
511
+ } else {
512
+ suggestions.push(
513
+ "Remove orphaned summaries disconnected from the DAG",
514
+ );
515
+ }
516
+ break;
517
+ }
518
+
519
+ case "context_token_consistency":
520
+ suggestions.push(
521
+ "Recompute context token count to reconcile mismatch between item-level sum and aggregate query",
522
+ );
523
+ break;
524
+
525
+ case "message_seq_contiguous":
526
+ suggestions.push(
527
+ "Resequence message seq values to eliminate gaps (renumber starting from 0)",
528
+ );
529
+ break;
530
+
531
+ case "no_duplicate_context_refs": {
532
+ const details = check.details as {
533
+ duplicates: { refType: string; refId: number | string; ordinals: number[] }[];
534
+ } | undefined;
535
+ if (details?.duplicates) {
536
+ for (const dup of details.duplicates) {
537
+ const keepOrdinal = dup.ordinals[0];
538
+ const removeOrdinals = dup.ordinals.slice(1).join(", ");
539
+ suggestions.push(
540
+ `Deduplicate ${dup.refType} ${dup.refId}: keep ordinal ${keepOrdinal}, remove ordinals ${removeOrdinals}`,
541
+ );
542
+ }
543
+ } else {
544
+ suggestions.push(
545
+ "Remove duplicate message_id or summary_id references from context items",
546
+ );
547
+ }
548
+ break;
549
+ }
550
+
551
+ default:
552
+ suggestions.push(`Address failing check: ${check.name} -- ${check.message}`);
553
+ break;
554
+ }
555
+ }
556
+
557
+ return suggestions;
558
+ }
559
+
560
+ // ── Observability ─────────────────────────────────────────────────────────────
561
+
562
+ /**
563
+ * Collect LCM observability metrics for a conversation by querying the stores.
564
+ */
565
+ export async function collectMetrics(
566
+ conversationId: number,
567
+ conversationStore: ConversationStore,
568
+ summaryStore: SummaryStore,
569
+ ): Promise<LcmMetrics> {
570
+ const [
571
+ contextTokens,
572
+ messageCount,
573
+ summaries,
574
+ contextItems,
575
+ largeFiles,
576
+ ] = await Promise.all([
577
+ summaryStore.getContextTokenCount(conversationId),
578
+ conversationStore.getMessageCount(conversationId),
579
+ summaryStore.getSummariesByConversation(conversationId),
580
+ summaryStore.getContextItems(conversationId),
581
+ summaryStore.getLargeFilesByConversation(conversationId),
582
+ ]);
583
+
584
+ const leafSummaryCount = summaries.filter((s) => s.kind === "leaf").length;
585
+ const condensedSummaryCount = summaries.filter(
586
+ (s) => s.kind === "condensed",
587
+ ).length;
588
+
589
+ return {
590
+ conversationId,
591
+ contextTokens,
592
+ messageCount,
593
+ summaryCount: summaries.length,
594
+ contextItemCount: contextItems.length,
595
+ leafSummaryCount,
596
+ condensedSummaryCount,
597
+ largeFileCount: largeFiles.length,
598
+ collectedAt: new Date(),
599
+ };
600
+ }