@martian-engineering/lossless-claw 0.8.0 → 0.8.1
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/README.md +8 -0
- package/dist/index.js +19240 -0
- package/docs/configuration.md +15 -5
- package/openclaw.plugin.json +27 -3
- package/package.json +7 -6
- package/skills/lossless-claw/references/config.md +37 -0
- package/index.ts +0 -2
- package/src/assembler.ts +0 -1196
- package/src/compaction.ts +0 -1753
- package/src/db/config.ts +0 -345
- package/src/db/connection.ts +0 -151
- package/src/db/features.ts +0 -61
- package/src/db/migration.ts +0 -868
- package/src/engine.ts +0 -4486
- package/src/estimate-tokens.ts +0 -80
- package/src/expansion-auth.ts +0 -365
- package/src/expansion-policy.ts +0 -303
- package/src/expansion.ts +0 -383
- package/src/integrity.ts +0 -600
- package/src/large-files.ts +0 -546
- package/src/lcm-log.ts +0 -37
- package/src/openclaw-bridge.ts +0 -22
- package/src/plugin/index.ts +0 -2037
- package/src/plugin/lcm-command.ts +0 -1040
- package/src/plugin/lcm-doctor-apply.ts +0 -540
- package/src/plugin/lcm-doctor-cleaners.ts +0 -655
- package/src/plugin/lcm-doctor-shared.ts +0 -210
- package/src/plugin/shared-init.ts +0 -59
- package/src/prune.ts +0 -391
- package/src/retrieval.ts +0 -360
- package/src/session-patterns.ts +0 -23
- package/src/startup-banner-log.ts +0 -49
- package/src/store/compaction-telemetry-store.ts +0 -156
- package/src/store/conversation-store.ts +0 -929
- package/src/store/fts5-sanitize.ts +0 -50
- package/src/store/full-text-fallback.ts +0 -83
- package/src/store/full-text-sort.ts +0 -21
- package/src/store/index.ts +0 -39
- package/src/store/parse-utc-timestamp.ts +0 -25
- package/src/store/summary-store.ts +0 -1519
- package/src/summarize.ts +0 -1508
- package/src/tools/common.ts +0 -53
- package/src/tools/lcm-conversation-scope.ts +0 -127
- package/src/tools/lcm-describe-tool.ts +0 -245
- package/src/tools/lcm-expand-query-tool.ts +0 -1235
- package/src/tools/lcm-expand-tool.delegation.ts +0 -580
- package/src/tools/lcm-expand-tool.ts +0 -453
- package/src/tools/lcm-expansion-recursion-guard.ts +0 -373
- package/src/tools/lcm-grep-tool.ts +0 -228
- package/src/transaction-mutex.ts +0 -136
- package/src/transcript-repair.ts +0 -301
- package/src/types.ts +0 -165
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { withDatabaseTransaction } from "../transaction-mutex.js";
|
|
3
|
-
import { formatTimestamp } from "../compaction.js";
|
|
4
|
-
import type { LcmConfig } from "../db/config.js";
|
|
5
|
-
import type { LcmSummarizeFn } from "../summarize.js";
|
|
6
|
-
import { createLcmSummarizeFromLegacyParams } from "../summarize.js";
|
|
7
|
-
import type { LcmDependencies } from "../types.js";
|
|
8
|
-
import { detectDoctorMarker, loadDoctorTargets, type DoctorTargetRecord } from "./lcm-doctor-shared.js";
|
|
9
|
-
import { estimateTokens } from "../estimate-tokens.js";
|
|
10
|
-
|
|
11
|
-
type SummaryOverride = {
|
|
12
|
-
content: string;
|
|
13
|
-
tokenCount: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type SummaryTimeRange = {
|
|
17
|
-
earliestAt: Date | null;
|
|
18
|
-
latestAt: Date | null;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type DoctorApplySkip = {
|
|
22
|
-
summaryId: string;
|
|
23
|
-
reason: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type DoctorApplyResult =
|
|
27
|
-
| {
|
|
28
|
-
kind: "applied";
|
|
29
|
-
detected: number;
|
|
30
|
-
repaired: number;
|
|
31
|
-
unchanged: number;
|
|
32
|
-
skipped: DoctorApplySkip[];
|
|
33
|
-
repairedSummaryIds: string[];
|
|
34
|
-
}
|
|
35
|
-
| {
|
|
36
|
-
kind: "unavailable";
|
|
37
|
-
reason: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type DoctorApplyRow = {
|
|
41
|
-
summary_id: string;
|
|
42
|
-
ordinal: number;
|
|
43
|
-
content?: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Repair broken summaries for a single resolved conversation.
|
|
48
|
-
*/
|
|
49
|
-
export async function applyScopedDoctorRepair(params: {
|
|
50
|
-
db: DatabaseSync;
|
|
51
|
-
config: LcmConfig;
|
|
52
|
-
conversationId: number;
|
|
53
|
-
deps?: LcmDependencies;
|
|
54
|
-
summarize?: LcmSummarizeFn;
|
|
55
|
-
runtimeConfig?: unknown;
|
|
56
|
-
}): Promise<DoctorApplyResult> {
|
|
57
|
-
const targets = loadDoctorTargets(params.db, params.conversationId);
|
|
58
|
-
if (targets.length === 0) {
|
|
59
|
-
return {
|
|
60
|
-
kind: "applied",
|
|
61
|
-
detected: 0,
|
|
62
|
-
repaired: 0,
|
|
63
|
-
unchanged: 0,
|
|
64
|
-
skipped: [],
|
|
65
|
-
repairedSummaryIds: [],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const summarize = await resolveDoctorApplySummarize(params);
|
|
70
|
-
if (!summarize) {
|
|
71
|
-
return {
|
|
72
|
-
kind: "unavailable",
|
|
73
|
-
reason: "Lossless Claw could not resolve a summarizer for native doctor apply through the normal model/auth chain.",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const ordered = orderDoctorTargets(params.db, params.conversationId, targets);
|
|
78
|
-
const overrides = new Map<string, SummaryOverride>();
|
|
79
|
-
const skipped: DoctorApplySkip[] = [];
|
|
80
|
-
const repairedSummaryIds: string[] = [];
|
|
81
|
-
let unchanged = 0;
|
|
82
|
-
|
|
83
|
-
for (const target of ordered) {
|
|
84
|
-
try {
|
|
85
|
-
const sourceText = buildSummarySourceText({
|
|
86
|
-
db: params.db,
|
|
87
|
-
target,
|
|
88
|
-
timezone: params.config.timezone,
|
|
89
|
-
overrides,
|
|
90
|
-
});
|
|
91
|
-
if (!sourceText.trim()) {
|
|
92
|
-
skipped.push({
|
|
93
|
-
summaryId: target.summaryId,
|
|
94
|
-
reason: "source text resolved empty",
|
|
95
|
-
});
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const previousSummary = resolvePreviousSummaryContext({
|
|
100
|
-
db: params.db,
|
|
101
|
-
target,
|
|
102
|
-
overrides,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const rewritten = (await summarize(sourceText, false, {
|
|
106
|
-
previousSummary,
|
|
107
|
-
isCondensed: isCondensedTarget(target),
|
|
108
|
-
...(isCondensedTarget(target) ? { depth: target.depth } : {}),
|
|
109
|
-
})).trim();
|
|
110
|
-
if (!rewritten) {
|
|
111
|
-
skipped.push({
|
|
112
|
-
summaryId: target.summaryId,
|
|
113
|
-
reason: "summarizer returned empty output",
|
|
114
|
-
});
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (detectDoctorMarker(rewritten)) {
|
|
118
|
-
skipped.push({
|
|
119
|
-
summaryId: target.summaryId,
|
|
120
|
-
reason: "rewritten content still contains a doctor marker",
|
|
121
|
-
});
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (rewritten === target.content.trim()) {
|
|
125
|
-
unchanged += 1;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const tokenCount = estimateTokens(rewritten);
|
|
130
|
-
overrides.set(target.summaryId, {
|
|
131
|
-
content: rewritten,
|
|
132
|
-
tokenCount,
|
|
133
|
-
});
|
|
134
|
-
repairedSummaryIds.push(target.summaryId);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
skipped.push({
|
|
137
|
-
summaryId: target.summaryId,
|
|
138
|
-
reason: error instanceof Error ? error.message : "unknown repair failure",
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (repairedSummaryIds.length > 0) {
|
|
144
|
-
await withDatabaseTransaction(params.db, "BEGIN IMMEDIATE", async () => {
|
|
145
|
-
for (const summaryId of repairedSummaryIds) {
|
|
146
|
-
const override = overrides.get(summaryId);
|
|
147
|
-
if (!override) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
params.db
|
|
151
|
-
.prepare(
|
|
152
|
-
`UPDATE summaries
|
|
153
|
-
SET content = ?, token_count = ?
|
|
154
|
-
WHERE summary_id = ?`,
|
|
155
|
-
)
|
|
156
|
-
.run(override.content, override.tokenCount, summaryId);
|
|
157
|
-
updateSummaryFts(params.db, summaryId, override.content);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
kind: "applied",
|
|
164
|
-
detected: targets.length,
|
|
165
|
-
repaired: repairedSummaryIds.length,
|
|
166
|
-
unchanged,
|
|
167
|
-
skipped,
|
|
168
|
-
repairedSummaryIds,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function resolveDoctorApplySummarize(params: {
|
|
173
|
-
config: LcmConfig;
|
|
174
|
-
deps?: LcmDependencies;
|
|
175
|
-
summarize?: LcmSummarizeFn;
|
|
176
|
-
runtimeConfig?: unknown;
|
|
177
|
-
}): Promise<LcmSummarizeFn | undefined> {
|
|
178
|
-
if (typeof params.summarize === "function") {
|
|
179
|
-
return params.summarize;
|
|
180
|
-
}
|
|
181
|
-
if (!params.deps) {
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const runtimeSummarizer = await createLcmSummarizeFromLegacyParams({
|
|
186
|
-
deps: params.deps,
|
|
187
|
-
legacyParams: {
|
|
188
|
-
config: params.runtimeConfig,
|
|
189
|
-
agentDir: params.deps.resolveAgentDir(),
|
|
190
|
-
},
|
|
191
|
-
customInstructions: params.config.customInstructions || undefined,
|
|
192
|
-
});
|
|
193
|
-
return runtimeSummarizer?.fn;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function isCondensedTarget(target: DoctorTargetRecord): boolean {
|
|
197
|
-
return !(target.depth === 0 || target.kind === "leaf");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function orderDoctorTargets(
|
|
201
|
-
db: DatabaseSync,
|
|
202
|
-
conversationId: number,
|
|
203
|
-
targets: DoctorTargetRecord[],
|
|
204
|
-
): DoctorTargetRecord[] {
|
|
205
|
-
const leafOrdinals = loadDoctorLeafOrdinals(db, conversationId);
|
|
206
|
-
const activeLeaves: Array<DoctorTargetRecord & { contextOrdinal: number }> = [];
|
|
207
|
-
const orphanLeaves: DoctorTargetRecord[] = [];
|
|
208
|
-
const condensed: DoctorTargetRecord[] = [];
|
|
209
|
-
|
|
210
|
-
for (const target of targets) {
|
|
211
|
-
if (!isCondensedTarget(target)) {
|
|
212
|
-
const contextOrdinal = leafOrdinals.get(target.summaryId);
|
|
213
|
-
if (typeof contextOrdinal === "number") {
|
|
214
|
-
activeLeaves.push({ ...target, contextOrdinal });
|
|
215
|
-
} else {
|
|
216
|
-
orphanLeaves.push(target);
|
|
217
|
-
}
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
condensed.push(target);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
activeLeaves.sort((left, right) => left.contextOrdinal - right.contextOrdinal);
|
|
224
|
-
orphanLeaves.sort(compareDoctorTargets);
|
|
225
|
-
condensed.sort(compareDoctorTargets);
|
|
226
|
-
|
|
227
|
-
return [...activeLeaves, ...orphanLeaves, ...condensed];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function compareDoctorTargets(left: DoctorTargetRecord, right: DoctorTargetRecord): number {
|
|
231
|
-
if (left.depth !== right.depth) {
|
|
232
|
-
return left.depth - right.depth;
|
|
233
|
-
}
|
|
234
|
-
if (left.createdAt !== right.createdAt) {
|
|
235
|
-
return left.createdAt.localeCompare(right.createdAt);
|
|
236
|
-
}
|
|
237
|
-
return left.summaryId.localeCompare(right.summaryId);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function loadDoctorLeafOrdinals(db: DatabaseSync, conversationId: number): Map<string, number> {
|
|
241
|
-
const rows = db
|
|
242
|
-
.prepare(
|
|
243
|
-
`SELECT ci.summary_id, ci.ordinal, COALESCE(s.content, '') AS content
|
|
244
|
-
FROM context_items ci
|
|
245
|
-
JOIN summaries s ON s.summary_id = ci.summary_id
|
|
246
|
-
WHERE ci.conversation_id = ?
|
|
247
|
-
AND ci.item_type = 'summary'
|
|
248
|
-
AND COALESCE(s.depth, 0) = 0
|
|
249
|
-
ORDER BY ci.ordinal ASC`,
|
|
250
|
-
)
|
|
251
|
-
.all(conversationId) as DoctorApplyRow[];
|
|
252
|
-
|
|
253
|
-
const ordinals = new Map<string, number>();
|
|
254
|
-
for (const row of rows) {
|
|
255
|
-
if (!detectDoctorMarker(row.content ?? "")) {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
ordinals.set(row.summary_id, row.ordinal);
|
|
259
|
-
}
|
|
260
|
-
return ordinals;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function buildSummarySourceText(params: {
|
|
264
|
-
db: DatabaseSync;
|
|
265
|
-
target: DoctorTargetRecord;
|
|
266
|
-
timezone: string;
|
|
267
|
-
overrides: Map<string, SummaryOverride>;
|
|
268
|
-
}): string {
|
|
269
|
-
return isCondensedTarget(params.target)
|
|
270
|
-
? buildCondensedSourceText(params)
|
|
271
|
-
: buildLeafSourceText(params);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function buildLeafSourceText(params: {
|
|
275
|
-
db: DatabaseSync;
|
|
276
|
-
target: DoctorTargetRecord;
|
|
277
|
-
timezone: string;
|
|
278
|
-
}): string {
|
|
279
|
-
const rows = params.db
|
|
280
|
-
.prepare(
|
|
281
|
-
`SELECT m.created_at, COALESCE(m.content, '') AS content
|
|
282
|
-
FROM summary_messages sm
|
|
283
|
-
JOIN messages m ON m.message_id = sm.message_id
|
|
284
|
-
WHERE sm.summary_id = ?
|
|
285
|
-
ORDER BY sm.ordinal ASC`,
|
|
286
|
-
)
|
|
287
|
-
.all(params.target.summaryId) as Array<{ created_at: string; content: string }>;
|
|
288
|
-
if (rows.length === 0) {
|
|
289
|
-
throw new Error("no messages linked to summary");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return rows
|
|
293
|
-
.map((row) => `[${formatSqliteTimestamp(row.created_at, params.timezone)}]\n${row.content}`)
|
|
294
|
-
.join("\n\n");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function buildCondensedSourceText(params: {
|
|
298
|
-
db: DatabaseSync;
|
|
299
|
-
target: DoctorTargetRecord;
|
|
300
|
-
timezone: string;
|
|
301
|
-
overrides: Map<string, SummaryOverride>;
|
|
302
|
-
}): string {
|
|
303
|
-
const rows = params.db
|
|
304
|
-
.prepare(
|
|
305
|
-
`SELECT
|
|
306
|
-
sp.parent_summary_id AS summary_id,
|
|
307
|
-
COALESCE(s.content, '') AS content,
|
|
308
|
-
s.earliest_at,
|
|
309
|
-
s.latest_at,
|
|
310
|
-
s.created_at
|
|
311
|
-
FROM summary_parents sp
|
|
312
|
-
JOIN summaries s ON s.summary_id = sp.parent_summary_id
|
|
313
|
-
WHERE sp.summary_id = ?
|
|
314
|
-
ORDER BY sp.ordinal ASC`,
|
|
315
|
-
)
|
|
316
|
-
.all(params.target.summaryId) as Array<{
|
|
317
|
-
summary_id: string;
|
|
318
|
-
content: string;
|
|
319
|
-
earliest_at: string | null;
|
|
320
|
-
latest_at: string | null;
|
|
321
|
-
created_at: string;
|
|
322
|
-
}>;
|
|
323
|
-
if (rows.length === 0) {
|
|
324
|
-
throw new Error("no child summaries linked to summary");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const parts = rows
|
|
328
|
-
.map((row) => {
|
|
329
|
-
const override = params.overrides.get(row.summary_id);
|
|
330
|
-
const content = (override?.content ?? row.content).trim();
|
|
331
|
-
if (!content) {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
const timeRange = resolveSummaryTimeRange({
|
|
335
|
-
earliestAt: row.earliest_at,
|
|
336
|
-
latestAt: row.latest_at,
|
|
337
|
-
createdAt: row.created_at,
|
|
338
|
-
});
|
|
339
|
-
const header = formatSummaryTimeRange(timeRange, params.timezone);
|
|
340
|
-
return header ? `${header}\n${content}` : content;
|
|
341
|
-
})
|
|
342
|
-
.filter((value): value is string => typeof value === "string");
|
|
343
|
-
|
|
344
|
-
if (parts.length === 0) {
|
|
345
|
-
throw new Error("child summaries resolved empty");
|
|
346
|
-
}
|
|
347
|
-
return parts.join("\n\n");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function resolvePreviousSummaryContext(params: {
|
|
351
|
-
db: DatabaseSync;
|
|
352
|
-
target: DoctorTargetRecord;
|
|
353
|
-
overrides: Map<string, SummaryOverride>;
|
|
354
|
-
}): string | undefined {
|
|
355
|
-
return (
|
|
356
|
-
previousViaContextItems(params) ??
|
|
357
|
-
previousViaSummaryParents(params) ??
|
|
358
|
-
previousViaTimestamp(params)
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function previousViaContextItems(params: {
|
|
363
|
-
db: DatabaseSync;
|
|
364
|
-
target: DoctorTargetRecord;
|
|
365
|
-
overrides: Map<string, SummaryOverride>;
|
|
366
|
-
}): string | undefined {
|
|
367
|
-
const targetRow = params.db
|
|
368
|
-
.prepare(
|
|
369
|
-
`SELECT ordinal
|
|
370
|
-
FROM context_items
|
|
371
|
-
WHERE conversation_id = ?
|
|
372
|
-
AND item_type = 'summary'
|
|
373
|
-
AND summary_id = ?
|
|
374
|
-
LIMIT 1`,
|
|
375
|
-
)
|
|
376
|
-
.get(params.target.conversationId, params.target.summaryId) as { ordinal: number } | undefined;
|
|
377
|
-
if (!targetRow) {
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const previousRow = params.db
|
|
382
|
-
.prepare(
|
|
383
|
-
`SELECT s.summary_id
|
|
384
|
-
FROM context_items ci
|
|
385
|
-
JOIN summaries s ON s.summary_id = ci.summary_id
|
|
386
|
-
WHERE ci.conversation_id = ?
|
|
387
|
-
AND ci.item_type = 'summary'
|
|
388
|
-
AND COALESCE(s.depth, 0) = ?
|
|
389
|
-
AND ci.ordinal < ?
|
|
390
|
-
ORDER BY ci.ordinal DESC
|
|
391
|
-
LIMIT 1`,
|
|
392
|
-
)
|
|
393
|
-
.get(
|
|
394
|
-
params.target.conversationId,
|
|
395
|
-
params.target.depth,
|
|
396
|
-
targetRow.ordinal,
|
|
397
|
-
) as { summary_id: string } | undefined;
|
|
398
|
-
return resolveSummaryContent(params.db, previousRow?.summary_id, params.overrides);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function previousViaSummaryParents(params: {
|
|
402
|
-
db: DatabaseSync;
|
|
403
|
-
target: DoctorTargetRecord;
|
|
404
|
-
overrides: Map<string, SummaryOverride>;
|
|
405
|
-
}): string | undefined {
|
|
406
|
-
const parentRow = params.db
|
|
407
|
-
.prepare(
|
|
408
|
-
`SELECT summary_id, ordinal
|
|
409
|
-
FROM summary_parents
|
|
410
|
-
WHERE parent_summary_id = ?
|
|
411
|
-
LIMIT 1`,
|
|
412
|
-
)
|
|
413
|
-
.get(params.target.summaryId) as { summary_id: string; ordinal: number } | undefined;
|
|
414
|
-
if (!parentRow) {
|
|
415
|
-
return undefined;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const previousRow = params.db
|
|
419
|
-
.prepare(
|
|
420
|
-
`SELECT parent_summary_id AS summary_id
|
|
421
|
-
FROM summary_parents
|
|
422
|
-
WHERE summary_id = ?
|
|
423
|
-
AND ordinal < ?
|
|
424
|
-
ORDER BY ordinal DESC
|
|
425
|
-
LIMIT 1`,
|
|
426
|
-
)
|
|
427
|
-
.get(parentRow.summary_id, parentRow.ordinal) as { summary_id: string } | undefined;
|
|
428
|
-
return resolveSummaryContent(params.db, previousRow?.summary_id, params.overrides);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function previousViaTimestamp(params: {
|
|
432
|
-
db: DatabaseSync;
|
|
433
|
-
target: DoctorTargetRecord;
|
|
434
|
-
overrides: Map<string, SummaryOverride>;
|
|
435
|
-
}): string | undefined {
|
|
436
|
-
if (!params.target.createdAt.trim()) {
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const previousRow = params.db
|
|
441
|
-
.prepare(
|
|
442
|
-
`SELECT summary_id
|
|
443
|
-
FROM summaries
|
|
444
|
-
WHERE conversation_id = ?
|
|
445
|
-
AND COALESCE(depth, 0) = ?
|
|
446
|
-
AND (created_at < ? OR (created_at = ? AND summary_id < ?))
|
|
447
|
-
ORDER BY created_at DESC, summary_id DESC
|
|
448
|
-
LIMIT 1`,
|
|
449
|
-
)
|
|
450
|
-
.get(
|
|
451
|
-
params.target.conversationId,
|
|
452
|
-
params.target.depth,
|
|
453
|
-
params.target.createdAt,
|
|
454
|
-
params.target.createdAt,
|
|
455
|
-
params.target.summaryId,
|
|
456
|
-
) as { summary_id: string } | undefined;
|
|
457
|
-
return resolveSummaryContent(params.db, previousRow?.summary_id, params.overrides);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function resolveSummaryContent(
|
|
461
|
-
db: DatabaseSync,
|
|
462
|
-
summaryId: string | undefined,
|
|
463
|
-
overrides: Map<string, SummaryOverride>,
|
|
464
|
-
): string | undefined {
|
|
465
|
-
if (!summaryId) {
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const override = overrides.get(summaryId);
|
|
470
|
-
if (override?.content.trim()) {
|
|
471
|
-
return override.content.trim();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const row = db
|
|
475
|
-
.prepare(`SELECT COALESCE(content, '') AS content FROM summaries WHERE summary_id = ?`)
|
|
476
|
-
.get(summaryId) as { content: string } | undefined;
|
|
477
|
-
const content = row?.content.trim();
|
|
478
|
-
return content ? content : undefined;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function resolveSummaryTimeRange(params: {
|
|
482
|
-
earliestAt: string | null;
|
|
483
|
-
latestAt: string | null;
|
|
484
|
-
createdAt: string;
|
|
485
|
-
}): SummaryTimeRange {
|
|
486
|
-
const earliestAt = parseSqliteTimestamp(params.earliestAt) ?? parseSqliteTimestamp(params.createdAt);
|
|
487
|
-
const latestAt = parseSqliteTimestamp(params.latestAt) ?? parseSqliteTimestamp(params.createdAt);
|
|
488
|
-
return {
|
|
489
|
-
earliestAt,
|
|
490
|
-
latestAt,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function formatSummaryTimeRange(range: SummaryTimeRange, timezone: string): string {
|
|
495
|
-
if (!range.earliestAt || !range.latestAt) {
|
|
496
|
-
return "";
|
|
497
|
-
}
|
|
498
|
-
return `[${formatTimestamp(range.earliestAt, timezone)} - ${formatTimestamp(range.latestAt, timezone)}]`;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function formatSqliteTimestamp(value: string, timezone: string): string {
|
|
502
|
-
const date = parseSqliteTimestamp(value);
|
|
503
|
-
if (date) {
|
|
504
|
-
return formatTimestamp(date, timezone);
|
|
505
|
-
}
|
|
506
|
-
const fallback = value.trim();
|
|
507
|
-
return fallback || "unknown";
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function parseSqliteTimestamp(value: string | null | undefined): Date | null {
|
|
511
|
-
const normalized = value?.trim();
|
|
512
|
-
if (!normalized) {
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const direct = new Date(normalized);
|
|
517
|
-
if (!Number.isNaN(direct.getTime())) {
|
|
518
|
-
return direct;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const sqlite = new Date(normalized.replace(" ", "T") + "Z");
|
|
522
|
-
if (!Number.isNaN(sqlite.getTime())) {
|
|
523
|
-
return sqlite;
|
|
524
|
-
}
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
function updateSummaryFts(db: DatabaseSync, summaryId: string, content: string): void {
|
|
530
|
-
try {
|
|
531
|
-
const update = db
|
|
532
|
-
.prepare(`UPDATE summaries_fts SET content = ? WHERE summary_id = ?`)
|
|
533
|
-
.run(content, summaryId);
|
|
534
|
-
if (Number(update.changes ?? 0) === 0) {
|
|
535
|
-
db.prepare(`INSERT INTO summaries_fts(summary_id, content) VALUES (?, ?)`).run(summaryId, content);
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
// FTS repair is best-effort; the primary source of truth is summaries.
|
|
539
|
-
}
|
|
540
|
-
}
|