@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
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { getLcmDbFeatures } from "./features.js";
|
|
3
|
+
|
|
4
|
+
type SummaryColumnInfo = {
|
|
5
|
+
name?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SummaryDepthRow = {
|
|
9
|
+
summary_id: string;
|
|
10
|
+
conversation_id: number;
|
|
11
|
+
kind: "leaf" | "condensed";
|
|
12
|
+
depth: number;
|
|
13
|
+
token_count: number;
|
|
14
|
+
created_at: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SummaryMessageTimeRangeRow = {
|
|
18
|
+
summary_id: string;
|
|
19
|
+
earliest_at: string | null;
|
|
20
|
+
latest_at: string | null;
|
|
21
|
+
source_message_token_count: number | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SummaryParentEdgeRow = {
|
|
25
|
+
summary_id: string;
|
|
26
|
+
parent_summary_id: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function ensureSummaryDepthColumn(db: DatabaseSync): void {
|
|
30
|
+
const summaryColumns = db.prepare(`PRAGMA table_info(summaries)`).all() as SummaryColumnInfo[];
|
|
31
|
+
const hasDepth = summaryColumns.some((col) => col.name === "depth");
|
|
32
|
+
if (!hasDepth) {
|
|
33
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN depth INTEGER NOT NULL DEFAULT 0`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureSummaryMetadataColumns(db: DatabaseSync): void {
|
|
38
|
+
const summaryColumns = db.prepare(`PRAGMA table_info(summaries)`).all() as SummaryColumnInfo[];
|
|
39
|
+
const hasEarliestAt = summaryColumns.some((col) => col.name === "earliest_at");
|
|
40
|
+
const hasLatestAt = summaryColumns.some((col) => col.name === "latest_at");
|
|
41
|
+
const hasDescendantCount = summaryColumns.some((col) => col.name === "descendant_count");
|
|
42
|
+
const hasDescendantTokenCount = summaryColumns.some((col) => col.name === "descendant_token_count");
|
|
43
|
+
const hasSourceMessageTokenCount = summaryColumns.some(
|
|
44
|
+
(col) => col.name === "source_message_token_count",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!hasEarliestAt) {
|
|
48
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN earliest_at TEXT`);
|
|
49
|
+
}
|
|
50
|
+
if (!hasLatestAt) {
|
|
51
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN latest_at TEXT`);
|
|
52
|
+
}
|
|
53
|
+
if (!hasDescendantCount) {
|
|
54
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN descendant_count INTEGER NOT NULL DEFAULT 0`);
|
|
55
|
+
}
|
|
56
|
+
if (!hasDescendantTokenCount) {
|
|
57
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN descendant_token_count INTEGER NOT NULL DEFAULT 0`);
|
|
58
|
+
}
|
|
59
|
+
if (!hasSourceMessageTokenCount) {
|
|
60
|
+
db.exec(`ALTER TABLE summaries ADD COLUMN source_message_token_count INTEGER NOT NULL DEFAULT 0`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseTimestamp(value: string | null | undefined): Date | null {
|
|
65
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const direct = new Date(value);
|
|
70
|
+
if (!Number.isNaN(direct.getTime())) {
|
|
71
|
+
return direct;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalized = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
|
|
75
|
+
const parsed = new Date(normalized);
|
|
76
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isoStringOrNull(value: Date | null): string | null {
|
|
80
|
+
return value ? value.toISOString() : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function backfillSummaryDepths(db: DatabaseSync): void {
|
|
84
|
+
// Leaves are always depth 0, even if legacy rows had malformed values.
|
|
85
|
+
db.exec(`UPDATE summaries SET depth = 0 WHERE kind = 'leaf'`);
|
|
86
|
+
|
|
87
|
+
const conversationRows = db
|
|
88
|
+
.prepare(`SELECT DISTINCT conversation_id FROM summaries WHERE kind = 'condensed'`)
|
|
89
|
+
.all() as Array<{ conversation_id: number }>;
|
|
90
|
+
if (conversationRows.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const updateDepthStmt = db.prepare(`UPDATE summaries SET depth = ? WHERE summary_id = ?`);
|
|
95
|
+
|
|
96
|
+
for (const row of conversationRows) {
|
|
97
|
+
const conversationId = row.conversation_id;
|
|
98
|
+
const summaries = db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT summary_id, conversation_id, kind, depth, token_count, created_at
|
|
101
|
+
FROM summaries
|
|
102
|
+
WHERE conversation_id = ?`,
|
|
103
|
+
)
|
|
104
|
+
.all(conversationId) as SummaryDepthRow[];
|
|
105
|
+
|
|
106
|
+
const depthBySummaryId = new Map<string, number>();
|
|
107
|
+
const unresolvedCondensedIds = new Set<string>();
|
|
108
|
+
for (const summary of summaries) {
|
|
109
|
+
if (summary.kind === "leaf") {
|
|
110
|
+
depthBySummaryId.set(summary.summary_id, 0);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
unresolvedCondensedIds.add(summary.summary_id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const edges = db
|
|
117
|
+
.prepare(
|
|
118
|
+
`SELECT summary_id, parent_summary_id
|
|
119
|
+
FROM summary_parents
|
|
120
|
+
WHERE summary_id IN (
|
|
121
|
+
SELECT summary_id FROM summaries
|
|
122
|
+
WHERE conversation_id = ? AND kind = 'condensed'
|
|
123
|
+
)`,
|
|
124
|
+
)
|
|
125
|
+
.all(conversationId) as SummaryParentEdgeRow[];
|
|
126
|
+
const parentsBySummaryId = new Map<string, string[]>();
|
|
127
|
+
for (const edge of edges) {
|
|
128
|
+
const existing = parentsBySummaryId.get(edge.summary_id) ?? [];
|
|
129
|
+
existing.push(edge.parent_summary_id);
|
|
130
|
+
parentsBySummaryId.set(edge.summary_id, existing);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while (unresolvedCondensedIds.size > 0) {
|
|
134
|
+
let progressed = false;
|
|
135
|
+
|
|
136
|
+
for (const summaryId of [...unresolvedCondensedIds]) {
|
|
137
|
+
const parentIds = parentsBySummaryId.get(summaryId) ?? [];
|
|
138
|
+
if (parentIds.length === 0) {
|
|
139
|
+
depthBySummaryId.set(summaryId, 1);
|
|
140
|
+
unresolvedCondensedIds.delete(summaryId);
|
|
141
|
+
progressed = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let maxParentDepth = -1;
|
|
146
|
+
let allParentsResolved = true;
|
|
147
|
+
for (const parentId of parentIds) {
|
|
148
|
+
const parentDepth = depthBySummaryId.get(parentId);
|
|
149
|
+
if (parentDepth == null) {
|
|
150
|
+
allParentsResolved = false;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (parentDepth > maxParentDepth) {
|
|
154
|
+
maxParentDepth = parentDepth;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!allParentsResolved) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
depthBySummaryId.set(summaryId, maxParentDepth + 1);
|
|
163
|
+
unresolvedCondensedIds.delete(summaryId);
|
|
164
|
+
progressed = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Guard against malformed cycles/cross-conversation references.
|
|
168
|
+
if (!progressed) {
|
|
169
|
+
for (const summaryId of unresolvedCondensedIds) {
|
|
170
|
+
depthBySummaryId.set(summaryId, 1);
|
|
171
|
+
}
|
|
172
|
+
unresolvedCondensedIds.clear();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const summary of summaries) {
|
|
177
|
+
const depth = depthBySummaryId.get(summary.summary_id);
|
|
178
|
+
if (depth == null) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
updateDepthStmt.run(depth, summary.summary_id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function backfillSummaryMetadata(db: DatabaseSync): void {
|
|
187
|
+
const conversationRows = db
|
|
188
|
+
.prepare(`SELECT DISTINCT conversation_id FROM summaries`)
|
|
189
|
+
.all() as Array<{ conversation_id: number }>;
|
|
190
|
+
if (conversationRows.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const updateMetadataStmt = db.prepare(
|
|
195
|
+
`UPDATE summaries
|
|
196
|
+
SET earliest_at = ?, latest_at = ?, descendant_count = ?,
|
|
197
|
+
descendant_token_count = ?, source_message_token_count = ?
|
|
198
|
+
WHERE summary_id = ?`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
for (const conversationRow of conversationRows) {
|
|
202
|
+
const conversationId = conversationRow.conversation_id;
|
|
203
|
+
const summaries = db
|
|
204
|
+
.prepare(
|
|
205
|
+
`SELECT summary_id, conversation_id, kind, depth, token_count, created_at
|
|
206
|
+
FROM summaries
|
|
207
|
+
WHERE conversation_id = ?
|
|
208
|
+
ORDER BY depth ASC, created_at ASC`,
|
|
209
|
+
)
|
|
210
|
+
.all(conversationId) as SummaryDepthRow[];
|
|
211
|
+
if (summaries.length === 0) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const leafRanges = db
|
|
216
|
+
.prepare(
|
|
217
|
+
`SELECT
|
|
218
|
+
sm.summary_id,
|
|
219
|
+
MIN(m.created_at) AS earliest_at,
|
|
220
|
+
MAX(m.created_at) AS latest_at,
|
|
221
|
+
COALESCE(SUM(m.token_count), 0) AS source_message_token_count
|
|
222
|
+
FROM summary_messages sm
|
|
223
|
+
JOIN messages m ON m.message_id = sm.message_id
|
|
224
|
+
JOIN summaries s ON s.summary_id = sm.summary_id
|
|
225
|
+
WHERE s.conversation_id = ? AND s.kind = 'leaf'
|
|
226
|
+
GROUP BY sm.summary_id`,
|
|
227
|
+
)
|
|
228
|
+
.all(conversationId) as SummaryMessageTimeRangeRow[];
|
|
229
|
+
const leafRangeBySummaryId = new Map(
|
|
230
|
+
leafRanges.map((row) => [
|
|
231
|
+
row.summary_id,
|
|
232
|
+
{
|
|
233
|
+
earliestAt: row.earliest_at,
|
|
234
|
+
latestAt: row.latest_at,
|
|
235
|
+
sourceMessageTokenCount: row.source_message_token_count,
|
|
236
|
+
},
|
|
237
|
+
]),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const edges = db
|
|
241
|
+
.prepare(
|
|
242
|
+
`SELECT summary_id, parent_summary_id
|
|
243
|
+
FROM summary_parents
|
|
244
|
+
WHERE summary_id IN (
|
|
245
|
+
SELECT summary_id FROM summaries WHERE conversation_id = ?
|
|
246
|
+
)`,
|
|
247
|
+
)
|
|
248
|
+
.all(conversationId) as SummaryParentEdgeRow[];
|
|
249
|
+
const parentsBySummaryId = new Map<string, string[]>();
|
|
250
|
+
for (const edge of edges) {
|
|
251
|
+
const existing = parentsBySummaryId.get(edge.summary_id) ?? [];
|
|
252
|
+
existing.push(edge.parent_summary_id);
|
|
253
|
+
parentsBySummaryId.set(edge.summary_id, existing);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const metadataBySummaryId = new Map<
|
|
257
|
+
string,
|
|
258
|
+
{
|
|
259
|
+
earliestAt: Date | null;
|
|
260
|
+
latestAt: Date | null;
|
|
261
|
+
descendantCount: number;
|
|
262
|
+
descendantTokenCount: number;
|
|
263
|
+
sourceMessageTokenCount: number;
|
|
264
|
+
}
|
|
265
|
+
>();
|
|
266
|
+
const tokenCountBySummaryId = new Map(
|
|
267
|
+
summaries.map((summary) => [summary.summary_id, Math.max(0, Math.floor(summary.token_count ?? 0))]),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
for (const summary of summaries) {
|
|
271
|
+
const fallbackDate = parseTimestamp(summary.created_at);
|
|
272
|
+
if (summary.kind === "leaf") {
|
|
273
|
+
const range = leafRangeBySummaryId.get(summary.summary_id);
|
|
274
|
+
const earliestAt = parseTimestamp(range?.earliestAt ?? summary.created_at) ?? fallbackDate;
|
|
275
|
+
const latestAt = parseTimestamp(range?.latestAt ?? summary.created_at) ?? fallbackDate;
|
|
276
|
+
|
|
277
|
+
metadataBySummaryId.set(summary.summary_id, {
|
|
278
|
+
earliestAt,
|
|
279
|
+
latestAt,
|
|
280
|
+
descendantCount: 0,
|
|
281
|
+
descendantTokenCount: 0,
|
|
282
|
+
sourceMessageTokenCount: Math.max(
|
|
283
|
+
0,
|
|
284
|
+
Math.floor(range?.sourceMessageTokenCount ?? 0),
|
|
285
|
+
),
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const parentIds = parentsBySummaryId.get(summary.summary_id) ?? [];
|
|
291
|
+
if (parentIds.length === 0) {
|
|
292
|
+
metadataBySummaryId.set(summary.summary_id, {
|
|
293
|
+
earliestAt: fallbackDate,
|
|
294
|
+
latestAt: fallbackDate,
|
|
295
|
+
descendantCount: 0,
|
|
296
|
+
descendantTokenCount: 0,
|
|
297
|
+
sourceMessageTokenCount: 0,
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let earliestAt: Date | null = null;
|
|
303
|
+
let latestAt: Date | null = null;
|
|
304
|
+
let descendantCount = 0;
|
|
305
|
+
let descendantTokenCount = 0;
|
|
306
|
+
let sourceMessageTokenCount = 0;
|
|
307
|
+
|
|
308
|
+
for (const parentId of parentIds) {
|
|
309
|
+
const parentMetadata = metadataBySummaryId.get(parentId);
|
|
310
|
+
if (!parentMetadata) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const parentEarliest = parentMetadata.earliestAt;
|
|
315
|
+
if (parentEarliest && (!earliestAt || parentEarliest < earliestAt)) {
|
|
316
|
+
earliestAt = parentEarliest;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parentLatest = parentMetadata.latestAt;
|
|
320
|
+
if (parentLatest && (!latestAt || parentLatest > latestAt)) {
|
|
321
|
+
latestAt = parentLatest;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
descendantCount += Math.max(0, parentMetadata.descendantCount) + 1;
|
|
325
|
+
const parentTokenCount = tokenCountBySummaryId.get(parentId) ?? 0;
|
|
326
|
+
descendantTokenCount +=
|
|
327
|
+
Math.max(0, parentTokenCount) + Math.max(0, parentMetadata.descendantTokenCount);
|
|
328
|
+
sourceMessageTokenCount += Math.max(0, parentMetadata.sourceMessageTokenCount);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
metadataBySummaryId.set(summary.summary_id, {
|
|
332
|
+
earliestAt: earliestAt ?? fallbackDate,
|
|
333
|
+
latestAt: latestAt ?? fallbackDate,
|
|
334
|
+
descendantCount: Math.max(0, descendantCount),
|
|
335
|
+
descendantTokenCount: Math.max(0, descendantTokenCount),
|
|
336
|
+
sourceMessageTokenCount: Math.max(0, sourceMessageTokenCount),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const summary of summaries) {
|
|
341
|
+
const metadata = metadataBySummaryId.get(summary.summary_id);
|
|
342
|
+
if (!metadata) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
updateMetadataStmt.run(
|
|
347
|
+
isoStringOrNull(metadata.earliestAt),
|
|
348
|
+
isoStringOrNull(metadata.latestAt),
|
|
349
|
+
Math.max(0, metadata.descendantCount),
|
|
350
|
+
Math.max(0, metadata.descendantTokenCount),
|
|
351
|
+
Math.max(0, metadata.sourceMessageTokenCount),
|
|
352
|
+
summary.summary_id,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function runLcmMigrations(
|
|
359
|
+
db: DatabaseSync,
|
|
360
|
+
options?: { fts5Available?: boolean },
|
|
361
|
+
): void {
|
|
362
|
+
db.exec(`
|
|
363
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
364
|
+
conversation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
365
|
+
session_id TEXT NOT NULL,
|
|
366
|
+
title TEXT,
|
|
367
|
+
bootstrapped_at TEXT,
|
|
368
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
369
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
373
|
+
message_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
374
|
+
conversation_id INTEGER NOT NULL REFERENCES conversations(conversation_id) ON DELETE CASCADE,
|
|
375
|
+
seq INTEGER NOT NULL,
|
|
376
|
+
role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool')),
|
|
377
|
+
content TEXT NOT NULL,
|
|
378
|
+
token_count INTEGER NOT NULL,
|
|
379
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
380
|
+
UNIQUE (conversation_id, seq)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
CREATE TABLE IF NOT EXISTS summaries (
|
|
384
|
+
summary_id TEXT PRIMARY KEY,
|
|
385
|
+
conversation_id INTEGER NOT NULL REFERENCES conversations(conversation_id) ON DELETE CASCADE,
|
|
386
|
+
kind TEXT NOT NULL CHECK (kind IN ('leaf', 'condensed')),
|
|
387
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
388
|
+
content TEXT NOT NULL,
|
|
389
|
+
token_count INTEGER NOT NULL,
|
|
390
|
+
earliest_at TEXT,
|
|
391
|
+
latest_at TEXT,
|
|
392
|
+
descendant_count INTEGER NOT NULL DEFAULT 0,
|
|
393
|
+
descendant_token_count INTEGER NOT NULL DEFAULT 0,
|
|
394
|
+
source_message_token_count INTEGER NOT NULL DEFAULT 0,
|
|
395
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
396
|
+
file_ids TEXT NOT NULL DEFAULT '[]'
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
CREATE TABLE IF NOT EXISTS message_parts (
|
|
400
|
+
part_id TEXT PRIMARY KEY,
|
|
401
|
+
message_id INTEGER NOT NULL REFERENCES messages(message_id) ON DELETE CASCADE,
|
|
402
|
+
session_id TEXT NOT NULL,
|
|
403
|
+
part_type TEXT NOT NULL CHECK (part_type IN (
|
|
404
|
+
'text', 'reasoning', 'tool', 'patch', 'file',
|
|
405
|
+
'subtask', 'compaction', 'step_start', 'step_finish',
|
|
406
|
+
'snapshot', 'agent', 'retry'
|
|
407
|
+
)),
|
|
408
|
+
ordinal INTEGER NOT NULL,
|
|
409
|
+
text_content TEXT,
|
|
410
|
+
is_ignored INTEGER,
|
|
411
|
+
is_synthetic INTEGER,
|
|
412
|
+
tool_call_id TEXT,
|
|
413
|
+
tool_name TEXT,
|
|
414
|
+
tool_status TEXT,
|
|
415
|
+
tool_input TEXT,
|
|
416
|
+
tool_output TEXT,
|
|
417
|
+
tool_error TEXT,
|
|
418
|
+
tool_title TEXT,
|
|
419
|
+
patch_hash TEXT,
|
|
420
|
+
patch_files TEXT,
|
|
421
|
+
file_mime TEXT,
|
|
422
|
+
file_name TEXT,
|
|
423
|
+
file_url TEXT,
|
|
424
|
+
subtask_prompt TEXT,
|
|
425
|
+
subtask_desc TEXT,
|
|
426
|
+
subtask_agent TEXT,
|
|
427
|
+
step_reason TEXT,
|
|
428
|
+
step_cost REAL,
|
|
429
|
+
step_tokens_in INTEGER,
|
|
430
|
+
step_tokens_out INTEGER,
|
|
431
|
+
snapshot_hash TEXT,
|
|
432
|
+
compaction_auto INTEGER,
|
|
433
|
+
metadata TEXT,
|
|
434
|
+
UNIQUE (message_id, ordinal)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
CREATE TABLE IF NOT EXISTS summary_messages (
|
|
438
|
+
summary_id TEXT NOT NULL REFERENCES summaries(summary_id) ON DELETE CASCADE,
|
|
439
|
+
message_id INTEGER NOT NULL REFERENCES messages(message_id) ON DELETE RESTRICT,
|
|
440
|
+
ordinal INTEGER NOT NULL,
|
|
441
|
+
PRIMARY KEY (summary_id, message_id)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
CREATE TABLE IF NOT EXISTS summary_parents (
|
|
445
|
+
summary_id TEXT NOT NULL REFERENCES summaries(summary_id) ON DELETE CASCADE,
|
|
446
|
+
parent_summary_id TEXT NOT NULL REFERENCES summaries(summary_id) ON DELETE RESTRICT,
|
|
447
|
+
ordinal INTEGER NOT NULL,
|
|
448
|
+
PRIMARY KEY (summary_id, parent_summary_id)
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
CREATE TABLE IF NOT EXISTS context_items (
|
|
452
|
+
conversation_id INTEGER NOT NULL REFERENCES conversations(conversation_id) ON DELETE CASCADE,
|
|
453
|
+
ordinal INTEGER NOT NULL,
|
|
454
|
+
item_type TEXT NOT NULL CHECK (item_type IN ('message', 'summary')),
|
|
455
|
+
message_id INTEGER REFERENCES messages(message_id) ON DELETE RESTRICT,
|
|
456
|
+
summary_id TEXT REFERENCES summaries(summary_id) ON DELETE RESTRICT,
|
|
457
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
458
|
+
PRIMARY KEY (conversation_id, ordinal),
|
|
459
|
+
CHECK (
|
|
460
|
+
(item_type = 'message' AND message_id IS NOT NULL AND summary_id IS NULL) OR
|
|
461
|
+
(item_type = 'summary' AND summary_id IS NOT NULL AND message_id IS NULL)
|
|
462
|
+
)
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
CREATE TABLE IF NOT EXISTS large_files (
|
|
466
|
+
file_id TEXT PRIMARY KEY,
|
|
467
|
+
conversation_id INTEGER NOT NULL REFERENCES conversations(conversation_id) ON DELETE CASCADE,
|
|
468
|
+
file_name TEXT,
|
|
469
|
+
mime_type TEXT,
|
|
470
|
+
byte_size INTEGER,
|
|
471
|
+
storage_uri TEXT NOT NULL,
|
|
472
|
+
exploration_summary TEXT,
|
|
473
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
-- Indexes
|
|
477
|
+
CREATE INDEX IF NOT EXISTS messages_conv_seq_idx ON messages (conversation_id, seq);
|
|
478
|
+
CREATE INDEX IF NOT EXISTS summaries_conv_created_idx ON summaries (conversation_id, created_at);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS message_parts_message_idx ON message_parts (message_id);
|
|
480
|
+
CREATE INDEX IF NOT EXISTS message_parts_type_idx ON message_parts (part_type);
|
|
481
|
+
CREATE INDEX IF NOT EXISTS context_items_conv_idx ON context_items (conversation_id, ordinal);
|
|
482
|
+
CREATE INDEX IF NOT EXISTS large_files_conv_idx ON large_files (conversation_id, created_at);
|
|
483
|
+
`);
|
|
484
|
+
|
|
485
|
+
// Forward-compatible conversations migration for existing DBs.
|
|
486
|
+
const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all() as Array<{
|
|
487
|
+
name?: string;
|
|
488
|
+
}>;
|
|
489
|
+
const hasBootstrappedAt = conversationColumns.some((col) => col.name === "bootstrapped_at");
|
|
490
|
+
if (!hasBootstrappedAt) {
|
|
491
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN bootstrapped_at TEXT`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
ensureSummaryDepthColumn(db);
|
|
495
|
+
ensureSummaryMetadataColumns(db);
|
|
496
|
+
backfillSummaryDepths(db);
|
|
497
|
+
backfillSummaryMetadata(db);
|
|
498
|
+
|
|
499
|
+
const fts5Available = options?.fts5Available ?? getLcmDbFeatures(db).fts5Available;
|
|
500
|
+
if (!fts5Available) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// FTS5 virtual tables for full-text search (cannot use IF NOT EXISTS, so check manually)
|
|
505
|
+
const hasFts = db
|
|
506
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'")
|
|
507
|
+
.get();
|
|
508
|
+
|
|
509
|
+
if (hasFts) {
|
|
510
|
+
// Check for stale schema: external-content FTS tables with content_rowid cause errors.
|
|
511
|
+
// Drop and recreate as standalone FTS if the old schema is detected.
|
|
512
|
+
const ftsSchema = (
|
|
513
|
+
db
|
|
514
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='messages_fts'")
|
|
515
|
+
.get() as { sql: string } | undefined
|
|
516
|
+
)?.sql;
|
|
517
|
+
if (ftsSchema && ftsSchema.includes("content_rowid")) {
|
|
518
|
+
db.exec("DROP TABLE messages_fts");
|
|
519
|
+
db.exec(`
|
|
520
|
+
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
521
|
+
content,
|
|
522
|
+
tokenize='porter unicode61'
|
|
523
|
+
);
|
|
524
|
+
INSERT INTO messages_fts(rowid, content) SELECT message_id, content FROM messages;
|
|
525
|
+
`);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
db.exec(`
|
|
529
|
+
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
530
|
+
content,
|
|
531
|
+
tokenize='porter unicode61'
|
|
532
|
+
);
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const summariesFtsInfo = db
|
|
537
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='summaries_fts'")
|
|
538
|
+
.get() as { sql?: string } | undefined;
|
|
539
|
+
const summariesFtsSql = summariesFtsInfo?.sql ?? "";
|
|
540
|
+
const summariesFtsColumns = db.prepare(`PRAGMA table_info(summaries_fts)`).all() as Array<{
|
|
541
|
+
name?: string;
|
|
542
|
+
}>;
|
|
543
|
+
const hasSummaryIdColumn = summariesFtsColumns.some((col) => col.name === "summary_id");
|
|
544
|
+
const shouldRecreateSummariesFts =
|
|
545
|
+
!summariesFtsInfo ||
|
|
546
|
+
!hasSummaryIdColumn ||
|
|
547
|
+
summariesFtsSql.includes("content_rowid='summary_id'") ||
|
|
548
|
+
summariesFtsSql.includes('content_rowid="summary_id"');
|
|
549
|
+
if (shouldRecreateSummariesFts) {
|
|
550
|
+
db.exec(`
|
|
551
|
+
DROP TABLE IF EXISTS summaries_fts;
|
|
552
|
+
CREATE VIRTUAL TABLE summaries_fts USING fts5(
|
|
553
|
+
summary_id UNINDEXED,
|
|
554
|
+
content,
|
|
555
|
+
tokenize='porter unicode61'
|
|
556
|
+
);
|
|
557
|
+
INSERT INTO summaries_fts(summary_id, content)
|
|
558
|
+
SELECT summary_id, content FROM summaries;
|
|
559
|
+
`);
|
|
560
|
+
}
|
|
561
|
+
}
|