@jonathangu/openclawbrain 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/bin/openclawbrain.js +15 -0
- package/docs/END_STATE.md +244 -0
- package/docs/EVIDENCE.md +128 -0
- package/docs/RELEASE_CONTRACT.md +91 -0
- package/docs/agent-tools.md +106 -0
- package/docs/architecture.md +224 -0
- package/docs/configuration.md +178 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
- package/docs/evidence/README.md +16 -0
- package/docs/fts5.md +161 -0
- package/docs/tui.md +506 -0
- package/index.ts +1372 -0
- package/openclaw.plugin.json +136 -0
- package/package.json +66 -0
- package/src/assembler.ts +804 -0
- package/src/brain-cli.ts +316 -0
- package/src/brain-core/decay.ts +35 -0
- package/src/brain-core/episode.ts +82 -0
- package/src/brain-core/graph.ts +321 -0
- package/src/brain-core/health.ts +116 -0
- package/src/brain-core/mutator.ts +281 -0
- package/src/brain-core/pack.ts +117 -0
- package/src/brain-core/policy.ts +153 -0
- package/src/brain-core/replay.ts +1 -0
- package/src/brain-core/teacher.ts +105 -0
- package/src/brain-core/trace.ts +40 -0
- package/src/brain-core/traverse.ts +230 -0
- package/src/brain-core/types.ts +405 -0
- package/src/brain-core/update.ts +123 -0
- package/src/brain-harvest/human.ts +46 -0
- package/src/brain-harvest/scanner.ts +98 -0
- package/src/brain-harvest/self.ts +147 -0
- package/src/brain-runtime/assembler-extension.ts +230 -0
- package/src/brain-runtime/evidence-detectors.ts +68 -0
- package/src/brain-runtime/graph-io.ts +72 -0
- package/src/brain-runtime/harvester-extension.ts +98 -0
- package/src/brain-runtime/service.ts +659 -0
- package/src/brain-runtime/tools.ts +109 -0
- package/src/brain-runtime/worker-state.ts +106 -0
- package/src/brain-runtime/worker-supervisor.ts +169 -0
- package/src/brain-store/embedding.ts +179 -0
- package/src/brain-store/init.ts +347 -0
- package/src/brain-store/migrations.ts +188 -0
- package/src/brain-store/store.ts +816 -0
- package/src/brain-worker/child-runner.ts +321 -0
- package/src/brain-worker/jobs.ts +12 -0
- package/src/brain-worker/mutation-job.ts +5 -0
- package/src/brain-worker/promotion-job.ts +5 -0
- package/src/brain-worker/protocol.ts +79 -0
- package/src/brain-worker/teacher-job.ts +5 -0
- package/src/brain-worker/update-job.ts +5 -0
- package/src/brain-worker/worker.ts +422 -0
- package/src/compaction.ts +1332 -0
- package/src/db/config.ts +265 -0
- package/src/db/connection.ts +72 -0
- package/src/db/features.ts +42 -0
- package/src/db/migration.ts +561 -0
- package/src/engine.ts +1995 -0
- package/src/expansion-auth.ts +351 -0
- package/src/expansion-policy.ts +303 -0
- package/src/expansion.ts +383 -0
- package/src/integrity.ts +600 -0
- package/src/large-files.ts +527 -0
- package/src/openclaw-bridge.ts +22 -0
- package/src/retrieval.ts +357 -0
- package/src/store/conversation-store.ts +748 -0
- package/src/store/fts5-sanitize.ts +29 -0
- package/src/store/full-text-fallback.ts +74 -0
- package/src/store/index.ts +29 -0
- package/src/store/summary-store.ts +918 -0
- package/src/summarize.ts +847 -0
- package/src/tools/common.ts +53 -0
- package/src/tools/lcm-conversation-scope.ts +76 -0
- package/src/tools/lcm-describe-tool.ts +234 -0
- package/src/tools/lcm-expand-query-tool.ts +594 -0
- package/src/tools/lcm-expand-tool.delegation.ts +556 -0
- package/src/tools/lcm-expand-tool.ts +448 -0
- package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
- package/src/tools/lcm-grep-tool.ts +200 -0
- package/src/transcript-repair.ts +301 -0
- package/src/types.ts +149 -0
package/src/integrity.ts
ADDED
|
@@ -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
|
+
}
|