@martian-engineering/lossless-claw 0.8.0 → 0.8.2

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 (52) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +971 -0
  3. package/docs/configuration.md +15 -5
  4. package/openclaw.plugin.json +27 -3
  5. package/package.json +7 -6
  6. package/skills/lossless-claw/references/config.md +37 -0
  7. package/index.ts +0 -2
  8. package/src/assembler.ts +0 -1196
  9. package/src/compaction.ts +0 -1753
  10. package/src/db/config.ts +0 -345
  11. package/src/db/connection.ts +0 -151
  12. package/src/db/features.ts +0 -61
  13. package/src/db/migration.ts +0 -868
  14. package/src/engine.ts +0 -4486
  15. package/src/estimate-tokens.ts +0 -80
  16. package/src/expansion-auth.ts +0 -365
  17. package/src/expansion-policy.ts +0 -303
  18. package/src/expansion.ts +0 -383
  19. package/src/integrity.ts +0 -600
  20. package/src/large-files.ts +0 -546
  21. package/src/lcm-log.ts +0 -37
  22. package/src/openclaw-bridge.ts +0 -22
  23. package/src/plugin/index.ts +0 -2037
  24. package/src/plugin/lcm-command.ts +0 -1040
  25. package/src/plugin/lcm-doctor-apply.ts +0 -540
  26. package/src/plugin/lcm-doctor-cleaners.ts +0 -655
  27. package/src/plugin/lcm-doctor-shared.ts +0 -210
  28. package/src/plugin/shared-init.ts +0 -59
  29. package/src/prune.ts +0 -391
  30. package/src/retrieval.ts +0 -360
  31. package/src/session-patterns.ts +0 -23
  32. package/src/startup-banner-log.ts +0 -49
  33. package/src/store/compaction-telemetry-store.ts +0 -156
  34. package/src/store/conversation-store.ts +0 -929
  35. package/src/store/fts5-sanitize.ts +0 -50
  36. package/src/store/full-text-fallback.ts +0 -83
  37. package/src/store/full-text-sort.ts +0 -21
  38. package/src/store/index.ts +0 -39
  39. package/src/store/parse-utc-timestamp.ts +0 -25
  40. package/src/store/summary-store.ts +0 -1519
  41. package/src/summarize.ts +0 -1508
  42. package/src/tools/common.ts +0 -53
  43. package/src/tools/lcm-conversation-scope.ts +0 -127
  44. package/src/tools/lcm-describe-tool.ts +0 -245
  45. package/src/tools/lcm-expand-query-tool.ts +0 -1235
  46. package/src/tools/lcm-expand-tool.delegation.ts +0 -580
  47. package/src/tools/lcm-expand-tool.ts +0 -453
  48. package/src/tools/lcm-expansion-recursion-guard.ts +0 -373
  49. package/src/tools/lcm-grep-tool.ts +0 -228
  50. package/src/transaction-mutex.ts +0 -136
  51. package/src/transcript-repair.ts +0 -301
  52. 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
- }