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