@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,1040 +0,0 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
3
|
-
import packageJson from "../../package.json" with { type: "json" };
|
|
4
|
-
import type { LcmConfig } from "../db/config.js";
|
|
5
|
-
import type { LcmSummarizeFn } from "../summarize.js";
|
|
6
|
-
import type { LcmDependencies } from "../types.js";
|
|
7
|
-
import type { OpenClawPluginCommandDefinition, PluginCommandContext } from "openclaw/plugin-sdk";
|
|
8
|
-
import { applyScopedDoctorRepair } from "./lcm-doctor-apply.js";
|
|
9
|
-
import {
|
|
10
|
-
applyDoctorCleaners,
|
|
11
|
-
getDoctorCleanerApplyUnavailableReason,
|
|
12
|
-
getDoctorCleanerFilterIds,
|
|
13
|
-
scanDoctorCleaners,
|
|
14
|
-
type DoctorCleanerId,
|
|
15
|
-
} from "./lcm-doctor-cleaners.js";
|
|
16
|
-
import {
|
|
17
|
-
detectDoctorMarker,
|
|
18
|
-
getDoctorSummaryStats,
|
|
19
|
-
type DoctorSummaryStats,
|
|
20
|
-
} from "./lcm-doctor-shared.js";
|
|
21
|
-
|
|
22
|
-
const VISIBLE_COMMAND = "/lossless";
|
|
23
|
-
const HIDDEN_ALIAS = "/lcm";
|
|
24
|
-
|
|
25
|
-
type LcmStatusStats = {
|
|
26
|
-
conversationCount: number;
|
|
27
|
-
summaryCount: number;
|
|
28
|
-
storedSummaryTokens: number;
|
|
29
|
-
summarizedSourceTokens: number;
|
|
30
|
-
leafSummaryCount: number;
|
|
31
|
-
condensedSummaryCount: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type LcmConversationStatusStats = {
|
|
35
|
-
conversationId: number;
|
|
36
|
-
sessionId: string;
|
|
37
|
-
sessionKey: string | null;
|
|
38
|
-
messageCount: number;
|
|
39
|
-
summaryCount: number;
|
|
40
|
-
storedSummaryTokens: number;
|
|
41
|
-
summarizedSourceTokens: number;
|
|
42
|
-
contextTokenCount: number;
|
|
43
|
-
compressedTokenCount: number;
|
|
44
|
-
leafSummaryCount: number;
|
|
45
|
-
condensedSummaryCount: number;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type CurrentConversationResolution =
|
|
49
|
-
| {
|
|
50
|
-
kind: "resolved";
|
|
51
|
-
source: "session_key" | "session_key_via_session_id" | "session_id";
|
|
52
|
-
stats: LcmConversationStatusStats;
|
|
53
|
-
}
|
|
54
|
-
| {
|
|
55
|
-
kind: "unavailable";
|
|
56
|
-
reason: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
type ParsedLcmCommand =
|
|
60
|
-
| { kind: "status" }
|
|
61
|
-
| { kind: "doctor"; apply: boolean }
|
|
62
|
-
| { kind: "doctor_cleaners"; apply: boolean; filterId?: DoctorCleanerId; vacuum: boolean }
|
|
63
|
-
| { kind: "help"; error?: string };
|
|
64
|
-
|
|
65
|
-
const DOCTOR_CLEANER_IDS = new Set<DoctorCleanerId>(getDoctorCleanerFilterIds());
|
|
66
|
-
|
|
67
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
68
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
69
|
-
? (value as Record<string, unknown>)
|
|
70
|
-
: undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function formatBoolean(value: boolean): string {
|
|
74
|
-
return value ? "yes" : "no";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function formatNumber(value: number): string {
|
|
78
|
-
return new Intl.NumberFormat("en-US").format(value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function formatBytes(bytes: number): string {
|
|
82
|
-
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
83
|
-
return "unknown";
|
|
84
|
-
}
|
|
85
|
-
if (bytes < 1024) {
|
|
86
|
-
return `${bytes} B`;
|
|
87
|
-
}
|
|
88
|
-
const units = ["KB", "MB", "GB", "TB"];
|
|
89
|
-
let value = bytes / 1024;
|
|
90
|
-
let unitIndex = 0;
|
|
91
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
92
|
-
value /= 1024;
|
|
93
|
-
unitIndex += 1;
|
|
94
|
-
}
|
|
95
|
-
const precision = value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
96
|
-
return `${value.toFixed(precision)} ${units[unitIndex]}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function formatCommand(command: string): string {
|
|
100
|
-
return `\`${command}\``;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function buildHeaderLines(): string[] {
|
|
104
|
-
return [
|
|
105
|
-
`**๐ฆ Lossless Claw v${packageJson.version}**`,
|
|
106
|
-
`Help: ${formatCommand(`${VISIBLE_COMMAND} help`)} ยท Alias: ${formatCommand(HIDDEN_ALIAS)}`,
|
|
107
|
-
];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function buildSection(title: string, lines: string[]): string {
|
|
111
|
-
return [`**${title}**`, ...lines.map((line) => ` ${line}`)].join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function buildStatLine(label: string, value: string): string {
|
|
115
|
-
return `${label}: ${value}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function formatCompressionRatio(contextTokens: number, compressedTokens: number): string {
|
|
119
|
-
if (
|
|
120
|
-
!Number.isFinite(contextTokens) ||
|
|
121
|
-
contextTokens <= 0 ||
|
|
122
|
-
!Number.isFinite(compressedTokens) ||
|
|
123
|
-
compressedTokens <= 0
|
|
124
|
-
) {
|
|
125
|
-
return "n/a";
|
|
126
|
-
}
|
|
127
|
-
const ratio = Math.max(1, Math.round(compressedTokens / contextTokens));
|
|
128
|
-
return `1:${formatNumber(ratio)}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function truncateMiddle(value: string, maxChars: number): string {
|
|
132
|
-
if (value.length <= maxChars) {
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
if (maxChars <= 3) {
|
|
136
|
-
return value.slice(0, maxChars);
|
|
137
|
-
}
|
|
138
|
-
const head = Math.ceil((maxChars - 1) / 2);
|
|
139
|
-
const tail = Math.floor((maxChars - 1) / 2);
|
|
140
|
-
return `${value.slice(0, head)}โฆ${value.slice(value.length - tail)}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function splitArgs(rawArgs: string | undefined): string[] {
|
|
144
|
-
return (rawArgs ?? "")
|
|
145
|
-
.trim()
|
|
146
|
-
.split(/\s+/)
|
|
147
|
-
.map((token) => token.trim())
|
|
148
|
-
.filter(Boolean);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function parseDoctorCleanerApplyArgs(tokens: string[]):
|
|
152
|
-
| { ok: true; filterId?: DoctorCleanerId; vacuum: boolean }
|
|
153
|
-
| { ok: false; error: string } {
|
|
154
|
-
let filterId: DoctorCleanerId | undefined;
|
|
155
|
-
let vacuum = false;
|
|
156
|
-
|
|
157
|
-
for (const token of tokens) {
|
|
158
|
-
const normalized = token.toLowerCase();
|
|
159
|
-
if (normalized === "vacuum") {
|
|
160
|
-
vacuum = true;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (DOCTOR_CLEANER_IDS.has(normalized as DoctorCleanerId) && !filterId) {
|
|
164
|
-
filterId = normalized as DoctorCleanerId;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
ok: false,
|
|
169
|
-
error:
|
|
170
|
-
`\`${VISIBLE_COMMAND} doctor clean apply\` accepts at most one filter id (\`${getDoctorCleanerFilterIds().join("`, `")}\`) plus optional \`vacuum\`.`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return { ok: true, filterId, vacuum };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function parseLcmCommand(rawArgs: string | undefined): ParsedLcmCommand {
|
|
178
|
-
const tokens = splitArgs(rawArgs);
|
|
179
|
-
if (tokens.length === 0) {
|
|
180
|
-
return { kind: "status" };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const [head, ...rest] = tokens;
|
|
184
|
-
switch (head.toLowerCase()) {
|
|
185
|
-
case "status":
|
|
186
|
-
return rest.length === 0
|
|
187
|
-
? { kind: "status" }
|
|
188
|
-
: { kind: "help", error: "`/lcm status` does not accept extra arguments." };
|
|
189
|
-
case "doctor":
|
|
190
|
-
if (rest.length === 0) {
|
|
191
|
-
return { kind: "doctor", apply: false };
|
|
192
|
-
}
|
|
193
|
-
if (rest.length === 1 && rest[0]?.toLowerCase() === "clean") {
|
|
194
|
-
return { kind: "doctor_cleaners", apply: false, vacuum: false };
|
|
195
|
-
}
|
|
196
|
-
if (rest[0]?.toLowerCase() === "clean" && rest[1]?.toLowerCase() === "apply") {
|
|
197
|
-
const parsedApply = parseDoctorCleanerApplyArgs(rest.slice(2));
|
|
198
|
-
return parsedApply.ok
|
|
199
|
-
? {
|
|
200
|
-
kind: "doctor_cleaners",
|
|
201
|
-
apply: true,
|
|
202
|
-
filterId: parsedApply.filterId,
|
|
203
|
-
vacuum: parsedApply.vacuum,
|
|
204
|
-
}
|
|
205
|
-
: { kind: "help", error: parsedApply.error };
|
|
206
|
-
}
|
|
207
|
-
if (rest.length === 1 && rest[0]?.toLowerCase() === "apply") {
|
|
208
|
-
return { kind: "doctor", apply: true };
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
kind: "help",
|
|
212
|
-
error:
|
|
213
|
-
`\`${VISIBLE_COMMAND} doctor\` accepts no arguments, \`clean\` for global high-confidence junk diagnostics, \`clean apply [filter-id] [vacuum]\` for cleanup, or \`apply\` for the scoped summary repair path.`,
|
|
214
|
-
};
|
|
215
|
-
case "help":
|
|
216
|
-
return { kind: "help" };
|
|
217
|
-
default:
|
|
218
|
-
return {
|
|
219
|
-
kind: "help",
|
|
220
|
-
error: `Unknown subcommand \`${head}\`. Supported: status, doctor, doctor clean, doctor apply, help.`,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function getLcmStatusStats(db: DatabaseSync): LcmStatusStats {
|
|
226
|
-
const row = db
|
|
227
|
-
.prepare(
|
|
228
|
-
`SELECT
|
|
229
|
-
COALESCE((SELECT COUNT(*) FROM conversations), 0) AS conversation_count,
|
|
230
|
-
COALESCE(COUNT(*), 0) AS summary_count,
|
|
231
|
-
COALESCE(SUM(token_count), 0) AS stored_summary_tokens,
|
|
232
|
-
COALESCE(SUM(CASE WHEN kind = 'leaf' THEN source_message_token_count ELSE 0 END), 0) AS summarized_source_tokens,
|
|
233
|
-
COALESCE(SUM(CASE WHEN kind = 'leaf' THEN 1 ELSE 0 END), 0) AS leaf_summary_count,
|
|
234
|
-
COALESCE(SUM(CASE WHEN kind = 'condensed' THEN 1 ELSE 0 END), 0) AS condensed_summary_count
|
|
235
|
-
FROM summaries`,
|
|
236
|
-
)
|
|
237
|
-
.get() as
|
|
238
|
-
| {
|
|
239
|
-
conversation_count: number;
|
|
240
|
-
summary_count: number;
|
|
241
|
-
stored_summary_tokens: number;
|
|
242
|
-
summarized_source_tokens: number;
|
|
243
|
-
leaf_summary_count: number;
|
|
244
|
-
condensed_summary_count: number;
|
|
245
|
-
}
|
|
246
|
-
| undefined;
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
conversationCount: row?.conversation_count ?? 0,
|
|
250
|
-
summaryCount: row?.summary_count ?? 0,
|
|
251
|
-
storedSummaryTokens: row?.stored_summary_tokens ?? 0,
|
|
252
|
-
summarizedSourceTokens: row?.summarized_source_tokens ?? 0,
|
|
253
|
-
leafSummaryCount: row?.leaf_summary_count ?? 0,
|
|
254
|
-
condensedSummaryCount: row?.condensed_summary_count ?? 0,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getConversationStatusStats(
|
|
259
|
-
db: DatabaseSync,
|
|
260
|
-
conversationId: number,
|
|
261
|
-
): LcmConversationStatusStats | null {
|
|
262
|
-
const row = db
|
|
263
|
-
.prepare(
|
|
264
|
-
`SELECT
|
|
265
|
-
c.conversation_id,
|
|
266
|
-
c.session_id,
|
|
267
|
-
c.session_key,
|
|
268
|
-
COALESCE((SELECT COUNT(*) FROM messages WHERE conversation_id = c.conversation_id), 0) AS message_count,
|
|
269
|
-
COALESCE((SELECT COUNT(*) FROM summaries WHERE conversation_id = c.conversation_id), 0) AS summary_count,
|
|
270
|
-
COALESCE((SELECT SUM(token_count) FROM summaries WHERE conversation_id = c.conversation_id), 0) AS stored_summary_tokens,
|
|
271
|
-
COALESCE((SELECT SUM(CASE WHEN kind = 'leaf' THEN source_message_token_count ELSE 0 END) FROM summaries WHERE conversation_id = c.conversation_id), 0) AS summarized_source_tokens,
|
|
272
|
-
COALESCE((
|
|
273
|
-
SELECT SUM(token_count)
|
|
274
|
-
FROM (
|
|
275
|
-
SELECT m.token_count AS token_count
|
|
276
|
-
FROM context_items ci
|
|
277
|
-
JOIN messages m ON m.message_id = ci.message_id
|
|
278
|
-
WHERE ci.conversation_id = c.conversation_id
|
|
279
|
-
AND ci.item_type = 'message'
|
|
280
|
-
UNION ALL
|
|
281
|
-
SELECT s.token_count AS token_count
|
|
282
|
-
FROM context_items ci
|
|
283
|
-
JOIN summaries s ON s.summary_id = ci.summary_id
|
|
284
|
-
WHERE ci.conversation_id = c.conversation_id
|
|
285
|
-
AND ci.item_type = 'summary'
|
|
286
|
-
) context_token_rows
|
|
287
|
-
), 0) AS context_token_count,
|
|
288
|
-
COALESCE((
|
|
289
|
-
SELECT SUM(COALESCE(s.source_message_token_count, 0) + COALESCE(s.descendant_token_count, 0))
|
|
290
|
-
FROM context_items ci
|
|
291
|
-
JOIN summaries s ON s.summary_id = ci.summary_id
|
|
292
|
-
WHERE ci.conversation_id = c.conversation_id
|
|
293
|
-
AND ci.item_type = 'summary'
|
|
294
|
-
), 0) AS compressed_token_count,
|
|
295
|
-
COALESCE((SELECT SUM(CASE WHEN kind = 'leaf' THEN 1 ELSE 0 END) FROM summaries WHERE conversation_id = c.conversation_id), 0) AS leaf_summary_count,
|
|
296
|
-
COALESCE((SELECT SUM(CASE WHEN kind = 'condensed' THEN 1 ELSE 0 END) FROM summaries WHERE conversation_id = c.conversation_id), 0) AS condensed_summary_count
|
|
297
|
-
FROM conversations c
|
|
298
|
-
WHERE c.conversation_id = ?`,
|
|
299
|
-
)
|
|
300
|
-
.get(conversationId) as
|
|
301
|
-
| {
|
|
302
|
-
conversation_id: number;
|
|
303
|
-
session_id: string;
|
|
304
|
-
session_key: string | null;
|
|
305
|
-
message_count: number;
|
|
306
|
-
summary_count: number;
|
|
307
|
-
stored_summary_tokens: number;
|
|
308
|
-
summarized_source_tokens: number;
|
|
309
|
-
context_token_count: number;
|
|
310
|
-
compressed_token_count: number;
|
|
311
|
-
leaf_summary_count: number;
|
|
312
|
-
condensed_summary_count: number;
|
|
313
|
-
}
|
|
314
|
-
| undefined;
|
|
315
|
-
|
|
316
|
-
if (!row) {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
conversationId: row.conversation_id,
|
|
322
|
-
sessionId: row.session_id,
|
|
323
|
-
sessionKey: row.session_key,
|
|
324
|
-
messageCount: row.message_count,
|
|
325
|
-
summaryCount: row.summary_count,
|
|
326
|
-
storedSummaryTokens: row.stored_summary_tokens,
|
|
327
|
-
summarizedSourceTokens: row.summarized_source_tokens,
|
|
328
|
-
contextTokenCount: row.context_token_count,
|
|
329
|
-
compressedTokenCount: row.compressed_token_count,
|
|
330
|
-
leafSummaryCount: row.leaf_summary_count,
|
|
331
|
-
condensedSummaryCount: row.condensed_summary_count,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function normalizeIdentity(value: string | undefined): string | undefined {
|
|
336
|
-
const normalized = value?.trim();
|
|
337
|
-
return normalized ? normalized : undefined;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function getConversationStatusBySessionKey(
|
|
341
|
-
db: DatabaseSync,
|
|
342
|
-
sessionKey: string,
|
|
343
|
-
): LcmConversationStatusStats | null {
|
|
344
|
-
const row = db
|
|
345
|
-
.prepare(`SELECT conversation_id FROM conversations WHERE session_key = ? LIMIT 1`)
|
|
346
|
-
.get(sessionKey) as { conversation_id: number } | undefined;
|
|
347
|
-
|
|
348
|
-
if (!row) {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return getConversationStatusStats(db, row.conversation_id);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function getConversationStatusBySessionId(
|
|
356
|
-
db: DatabaseSync,
|
|
357
|
-
sessionId: string,
|
|
358
|
-
): LcmConversationStatusStats | null {
|
|
359
|
-
const row = db
|
|
360
|
-
.prepare(
|
|
361
|
-
`SELECT conversation_id
|
|
362
|
-
FROM conversations
|
|
363
|
-
WHERE session_id = ?
|
|
364
|
-
ORDER BY created_at DESC
|
|
365
|
-
LIMIT 1`,
|
|
366
|
-
)
|
|
367
|
-
.get(sessionId) as { conversation_id: number } | undefined;
|
|
368
|
-
|
|
369
|
-
if (!row) {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return getConversationStatusStats(db, row.conversation_id);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function resolveCurrentConversation(params: {
|
|
377
|
-
ctx: PluginCommandContext;
|
|
378
|
-
db: DatabaseSync;
|
|
379
|
-
}): Promise<CurrentConversationResolution> {
|
|
380
|
-
const sessionKey = normalizeIdentity(params.ctx.sessionKey);
|
|
381
|
-
const sessionId = normalizeIdentity(params.ctx.sessionId);
|
|
382
|
-
|
|
383
|
-
if (sessionKey) {
|
|
384
|
-
const bySessionKey = getConversationStatusBySessionKey(params.db, sessionKey);
|
|
385
|
-
if (bySessionKey) {
|
|
386
|
-
return { kind: "resolved", source: "session_key", stats: bySessionKey };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (sessionId) {
|
|
390
|
-
const bySessionId = getConversationStatusBySessionId(params.db, sessionId);
|
|
391
|
-
if (bySessionId) {
|
|
392
|
-
if (!bySessionId.sessionKey || bySessionId.sessionKey === sessionKey) {
|
|
393
|
-
return {
|
|
394
|
-
kind: "resolved",
|
|
395
|
-
source: "session_key_via_session_id",
|
|
396
|
-
stats: bySessionId,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
kind: "unavailable",
|
|
402
|
-
reason: `Active session key ${formatCommand(sessionKey)} is not stored in LCM yet. Session id fallback found conversation #${formatNumber(bySessionId.conversationId)}, but it is bound to ${formatCommand(bySessionId.sessionKey)}, so Global stats are safer.`,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
kind: "unavailable",
|
|
409
|
-
reason: sessionId
|
|
410
|
-
? `No LCM conversation is stored yet for active session key ${formatCommand(sessionKey)} or active session id ${formatCommand(sessionId)}.`
|
|
411
|
-
: `No LCM conversation is stored yet for active session key ${formatCommand(sessionKey)}.`,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (sessionId) {
|
|
416
|
-
const bySessionId = getConversationStatusBySessionId(params.db, sessionId);
|
|
417
|
-
if (bySessionId) {
|
|
418
|
-
return { kind: "resolved", source: "session_id", stats: bySessionId };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
kind: "unavailable",
|
|
423
|
-
reason: `OpenClaw did not expose an active session key here. Tried active session id ${formatCommand(sessionId)}, but no stored LCM conversation matched it.`,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
kind: "unavailable",
|
|
429
|
-
reason: "OpenClaw did not expose an active session key or session id here, so only GLOBAL stats are available.",
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function resolvePluginEnabled(config: unknown): boolean {
|
|
434
|
-
const root = asRecord(config);
|
|
435
|
-
const plugins = asRecord(root?.plugins);
|
|
436
|
-
const entries = asRecord(plugins?.entries);
|
|
437
|
-
const entry = asRecord(entries?.["lossless-claw"]);
|
|
438
|
-
if (typeof entry?.enabled === "boolean") {
|
|
439
|
-
return entry.enabled;
|
|
440
|
-
}
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function resolveContextEngineSlot(config: unknown): string {
|
|
445
|
-
const root = asRecord(config);
|
|
446
|
-
const plugins = asRecord(root?.plugins);
|
|
447
|
-
const slots = asRecord(plugins?.slots);
|
|
448
|
-
return typeof slots?.contextEngine === "string" ? slots.contextEngine.trim() : "";
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function resolvePluginSelected(config: unknown): boolean {
|
|
452
|
-
const slot = resolveContextEngineSlot(config);
|
|
453
|
-
return slot === "" || slot === "lossless-claw" || slot === "default";
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function resolveDbSizeLabel(dbPath: string): string {
|
|
457
|
-
const trimmed = dbPath.trim();
|
|
458
|
-
if (!trimmed || trimmed === ":memory:" || trimmed.startsWith("file::memory:")) {
|
|
459
|
-
return "in-memory";
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
return formatBytes(statSync(trimmed).size);
|
|
463
|
-
} catch {
|
|
464
|
-
return "missing";
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function buildHelpText(error?: string): string {
|
|
469
|
-
const lines = [
|
|
470
|
-
...(error ? [`โ ๏ธ ${error}`, ""] : []),
|
|
471
|
-
...buildHeaderLines(),
|
|
472
|
-
"",
|
|
473
|
-
buildSection("๐ Commands", [
|
|
474
|
-
buildStatLine(formatCommand(VISIBLE_COMMAND), "Show compact status output."),
|
|
475
|
-
buildStatLine(formatCommand(`${VISIBLE_COMMAND} status`), "Show plugin, Global, and current-conversation status."),
|
|
476
|
-
buildStatLine(formatCommand(`${VISIBLE_COMMAND} doctor`), "Scan for broken or truncated summaries."),
|
|
477
|
-
buildStatLine(
|
|
478
|
-
formatCommand(`${VISIBLE_COMMAND} doctor clean`),
|
|
479
|
-
"Report global high-confidence junk candidates without deleting anything.",
|
|
480
|
-
),
|
|
481
|
-
buildStatLine(
|
|
482
|
-
formatCommand(`${VISIBLE_COMMAND} doctor clean apply`),
|
|
483
|
-
"Delete approved high-confidence cleaner matches after creating a DB backup.",
|
|
484
|
-
),
|
|
485
|
-
buildStatLine(formatCommand(`${VISIBLE_COMMAND} doctor apply`), "Repair broken summaries in the current conversation."),
|
|
486
|
-
]),
|
|
487
|
-
"",
|
|
488
|
-
buildSection("๐งญ Notes", [
|
|
489
|
-
buildStatLine("subcommands", `Discover them with ${formatCommand(`${VISIBLE_COMMAND} help`)}.`),
|
|
490
|
-
buildStatLine("alias", `${formatCommand(HIDDEN_ALIAS)} is accepted as a shorter alias.`),
|
|
491
|
-
buildStatLine("current conversation", "Uses the active LCM session when the host exposes session identity."),
|
|
492
|
-
]),
|
|
493
|
-
];
|
|
494
|
-
return lines.join("\n");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function buildDoctorCleanerExampleLine(params: {
|
|
498
|
-
conversationId: number;
|
|
499
|
-
sessionKey: string | null;
|
|
500
|
-
messageCount: number;
|
|
501
|
-
firstMessagePreview: string | null;
|
|
502
|
-
}): string {
|
|
503
|
-
const sessionKey = params.sessionKey ? formatCommand(truncateMiddle(params.sessionKey, 44)) : "missing";
|
|
504
|
-
const preview = params.firstMessagePreview ? ` ยท first: ${JSON.stringify(params.firstMessagePreview)}` : "";
|
|
505
|
-
return `conv ${formatNumber(params.conversationId)} ยท session key ${sessionKey} ยท messages ${formatNumber(params.messageCount)}${preview}`;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async function buildStatusText(params: {
|
|
509
|
-
ctx: PluginCommandContext;
|
|
510
|
-
db: DatabaseSync;
|
|
511
|
-
config: LcmConfig;
|
|
512
|
-
}): Promise<string> {
|
|
513
|
-
const status = getLcmStatusStats(params.db);
|
|
514
|
-
const doctor = getDoctorSummaryStats(params.db);
|
|
515
|
-
const enabled = resolvePluginEnabled(params.ctx.config);
|
|
516
|
-
const selected = resolvePluginSelected(params.ctx.config);
|
|
517
|
-
const slot = resolveContextEngineSlot(params.ctx.config);
|
|
518
|
-
const dbSize = resolveDbSizeLabel(params.config.databasePath);
|
|
519
|
-
const current = await resolveCurrentConversation({
|
|
520
|
-
ctx: params.ctx,
|
|
521
|
-
db: params.db,
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const lines = [
|
|
525
|
-
...buildHeaderLines(),
|
|
526
|
-
"",
|
|
527
|
-
buildSection("๐งฉ Plugin", [
|
|
528
|
-
buildStatLine("enabled", formatBoolean(enabled)),
|
|
529
|
-
buildStatLine("selected", `${formatBoolean(selected)}${slot ? ` (slot=${slot})` : " (slot=unset)"}`),
|
|
530
|
-
buildStatLine("db path", params.config.databasePath),
|
|
531
|
-
buildStatLine("db size", dbSize),
|
|
532
|
-
]),
|
|
533
|
-
"",
|
|
534
|
-
buildSection("๐ Global", [
|
|
535
|
-
buildStatLine("conversations", formatNumber(status.conversationCount)),
|
|
536
|
-
buildStatLine(
|
|
537
|
-
"summaries",
|
|
538
|
-
`${formatNumber(status.summaryCount)} (${formatNumber(status.leafSummaryCount)} leaf, ${formatNumber(status.condensedSummaryCount)} condensed)`,
|
|
539
|
-
),
|
|
540
|
-
buildStatLine("stored summary tokens", formatNumber(status.storedSummaryTokens)),
|
|
541
|
-
buildStatLine("summarized source tokens", formatNumber(status.summarizedSourceTokens)),
|
|
542
|
-
]),
|
|
543
|
-
"",
|
|
544
|
-
];
|
|
545
|
-
|
|
546
|
-
if (current.kind === "resolved") {
|
|
547
|
-
const conversationDoctor =
|
|
548
|
-
doctor.byConversation.get(current.stats.conversationId) ?? {
|
|
549
|
-
total: 0,
|
|
550
|
-
old: 0,
|
|
551
|
-
truncated: 0,
|
|
552
|
-
fallback: 0,
|
|
553
|
-
};
|
|
554
|
-
lines.push(
|
|
555
|
-
buildSection("๐ Current conversation", [
|
|
556
|
-
buildStatLine("conversation id", formatNumber(current.stats.conversationId)),
|
|
557
|
-
buildStatLine(
|
|
558
|
-
"session key",
|
|
559
|
-
current.stats.sessionKey ? formatCommand(truncateMiddle(current.stats.sessionKey, 44)) : "missing",
|
|
560
|
-
),
|
|
561
|
-
buildStatLine("messages", formatNumber(current.stats.messageCount)),
|
|
562
|
-
buildStatLine(
|
|
563
|
-
"summaries",
|
|
564
|
-
`${formatNumber(current.stats.summaryCount)} (${formatNumber(current.stats.leafSummaryCount)} leaf, ${formatNumber(current.stats.condensedSummaryCount)} condensed)`,
|
|
565
|
-
),
|
|
566
|
-
buildStatLine("stored summary tokens", formatNumber(current.stats.storedSummaryTokens)),
|
|
567
|
-
buildStatLine("summarized source tokens", formatNumber(current.stats.summarizedSourceTokens)),
|
|
568
|
-
buildStatLine("tokens in context", formatNumber(current.stats.contextTokenCount)),
|
|
569
|
-
buildStatLine(
|
|
570
|
-
"compression ratio",
|
|
571
|
-
formatCompressionRatio(current.stats.contextTokenCount, current.stats.compressedTokenCount),
|
|
572
|
-
),
|
|
573
|
-
buildStatLine(
|
|
574
|
-
"doctor",
|
|
575
|
-
conversationDoctor.total > 0
|
|
576
|
-
? `${formatNumber(conversationDoctor.total)} issue(s) in this conversation`
|
|
577
|
-
: "clean",
|
|
578
|
-
),
|
|
579
|
-
]),
|
|
580
|
-
);
|
|
581
|
-
} else {
|
|
582
|
-
lines.push(
|
|
583
|
-
buildSection("๐ Current conversation", [
|
|
584
|
-
buildStatLine("status", "unavailable"),
|
|
585
|
-
buildStatLine("reason", current.reason),
|
|
586
|
-
buildStatLine("fallback", "Showing Global stats only."),
|
|
587
|
-
]),
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return lines.join("\n");
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function buildDoctorText(params: {
|
|
595
|
-
ctx: PluginCommandContext;
|
|
596
|
-
db: DatabaseSync;
|
|
597
|
-
}): Promise<string> {
|
|
598
|
-
const current = await resolveCurrentConversation(params);
|
|
599
|
-
|
|
600
|
-
if (current.kind === "unavailable") {
|
|
601
|
-
return [
|
|
602
|
-
...buildHeaderLines(),
|
|
603
|
-
"",
|
|
604
|
-
"๐ฉบ Lossless Claw Doctor",
|
|
605
|
-
"",
|
|
606
|
-
buildSection("๐ Current conversation", [
|
|
607
|
-
buildStatLine("status", "unavailable"),
|
|
608
|
-
buildStatLine("reason", current.reason),
|
|
609
|
-
buildStatLine("fallback", "Doctor is conversation-scoped, so no global scan ran."),
|
|
610
|
-
]),
|
|
611
|
-
].join("\n");
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const stats = getDoctorSummaryStats(params.db, current.stats.conversationId);
|
|
615
|
-
const lines = [
|
|
616
|
-
...buildHeaderLines(),
|
|
617
|
-
"",
|
|
618
|
-
"๐ฉบ Lossless Claw Doctor",
|
|
619
|
-
"",
|
|
620
|
-
buildSection("๐ Current conversation", [
|
|
621
|
-
buildStatLine("conversation id", formatNumber(current.stats.conversationId)),
|
|
622
|
-
buildStatLine(
|
|
623
|
-
"session key",
|
|
624
|
-
current.stats.sessionKey ? formatCommand(truncateMiddle(current.stats.sessionKey, 44)) : "missing",
|
|
625
|
-
),
|
|
626
|
-
buildStatLine("scope", "this conversation only"),
|
|
627
|
-
]),
|
|
628
|
-
"",
|
|
629
|
-
buildSection("๐งช Scan", [
|
|
630
|
-
buildStatLine("detected summaries", formatNumber(stats.total)),
|
|
631
|
-
buildStatLine("old-marker summaries", formatNumber(stats.old)),
|
|
632
|
-
buildStatLine("truncated-marker summaries", formatNumber(stats.truncated)),
|
|
633
|
-
buildStatLine("fallback-marker summaries", formatNumber(stats.fallback)),
|
|
634
|
-
buildStatLine("result", stats.total === 0 ? "clean" : "issues found"),
|
|
635
|
-
]),
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
if (stats.total > 0) {
|
|
639
|
-
const summaryList = stats.candidates
|
|
640
|
-
.slice()
|
|
641
|
-
.sort((left, right) => left.summaryId.localeCompare(right.summaryId))
|
|
642
|
-
.map((candidate) => `${candidate.summaryId} (${candidate.markerKind})`)
|
|
643
|
-
.join(", ");
|
|
644
|
-
lines.push(
|
|
645
|
-
"",
|
|
646
|
-
buildSection("๐งท Affected summaries", [summaryList]),
|
|
647
|
-
"",
|
|
648
|
-
buildSection("๐ ๏ธ Next step", [
|
|
649
|
-
`${formatCommand(`${VISIBLE_COMMAND} doctor apply`)} repairs these in place for the current conversation.`,
|
|
650
|
-
]),
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return lines.join("\n");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function buildDoctorCleanersText(params: {
|
|
658
|
-
db: DatabaseSync;
|
|
659
|
-
}): Promise<string> {
|
|
660
|
-
const scan = scanDoctorCleaners(params.db);
|
|
661
|
-
const lines = [
|
|
662
|
-
...buildHeaderLines(),
|
|
663
|
-
"",
|
|
664
|
-
"๐ฉบ Lossless Claw Doctor Clean",
|
|
665
|
-
"",
|
|
666
|
-
buildSection("๐ Global scan", [
|
|
667
|
-
buildStatLine("filters", formatNumber(scan.filters.length)),
|
|
668
|
-
buildStatLine("matched conversations", formatNumber(scan.totalDistinctConversations)),
|
|
669
|
-
buildStatLine("matched messages", formatNumber(scan.totalDistinctMessages)),
|
|
670
|
-
buildStatLine("mode", "read-only diagnostics"),
|
|
671
|
-
]),
|
|
672
|
-
];
|
|
673
|
-
|
|
674
|
-
if (scan.filters.every((filter) => filter.conversationCount === 0)) {
|
|
675
|
-
lines.push(
|
|
676
|
-
"",
|
|
677
|
-
buildSection("โ
Result", ["No high-confidence cleaner candidates detected."]),
|
|
678
|
-
);
|
|
679
|
-
return lines.join("\n");
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
for (const filter of scan.filters) {
|
|
683
|
-
lines.push(
|
|
684
|
-
"",
|
|
685
|
-
buildSection(`๐งน ${filter.label}`, [
|
|
686
|
-
buildStatLine("filter id", formatCommand(filter.id)),
|
|
687
|
-
buildStatLine("description", filter.description),
|
|
688
|
-
buildStatLine("matched conversations", formatNumber(filter.conversationCount)),
|
|
689
|
-
buildStatLine("matched messages", formatNumber(filter.messageCount)),
|
|
690
|
-
]),
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
if (filter.examples.length > 0) {
|
|
694
|
-
lines.push(
|
|
695
|
-
"",
|
|
696
|
-
buildSection(
|
|
697
|
-
"๐งท Examples",
|
|
698
|
-
filter.examples.map((example) => buildDoctorCleanerExampleLine(example)),
|
|
699
|
-
),
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
lines.push(
|
|
705
|
-
"",
|
|
706
|
-
buildSection("๐ ๏ธ Next step", [
|
|
707
|
-
`Review the examples, then run ${formatCommand(`${VISIBLE_COMMAND} doctor clean apply`)} to delete approved matches after Lossless Claw creates a backup.`,
|
|
708
|
-
]),
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
return lines.join("\n");
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function runQuickCheck(db: DatabaseSync): string {
|
|
715
|
-
const rows = db.prepare(`PRAGMA quick_check`).all() as Array<{ quick_check?: string }>;
|
|
716
|
-
const results = rows
|
|
717
|
-
.map((row) => row.quick_check)
|
|
718
|
-
.filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
719
|
-
|
|
720
|
-
if (results.length === 0) {
|
|
721
|
-
return "unknown";
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (results.length === 1 && results[0] === "ok") {
|
|
725
|
-
return "ok";
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return results.join("; ");
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function isPassingQuickCheck(result: string): boolean {
|
|
732
|
-
return result === "ok";
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
async function buildDoctorCleanersApplyText(params: {
|
|
736
|
-
db: DatabaseSync;
|
|
737
|
-
config: LcmConfig;
|
|
738
|
-
filterId?: DoctorCleanerId;
|
|
739
|
-
vacuum: boolean;
|
|
740
|
-
}): Promise<string> {
|
|
741
|
-
const filterIds = params.filterId ? [params.filterId] : undefined;
|
|
742
|
-
const unavailableReason = getDoctorCleanerApplyUnavailableReason(params.config.databasePath);
|
|
743
|
-
const lines = [
|
|
744
|
-
...buildHeaderLines(),
|
|
745
|
-
"",
|
|
746
|
-
"๐ฉบ Lossless Claw Doctor Clean Apply",
|
|
747
|
-
"",
|
|
748
|
-
buildSection("๐ Cleaner scope", [
|
|
749
|
-
buildStatLine(
|
|
750
|
-
"filters",
|
|
751
|
-
filterIds && filterIds.length > 0
|
|
752
|
-
? filterIds.map((filter) => formatCommand(filter)).join(", ")
|
|
753
|
-
: "all approved cleaner filters",
|
|
754
|
-
),
|
|
755
|
-
buildStatLine("vacuum requested", formatBoolean(params.vacuum)),
|
|
756
|
-
]),
|
|
757
|
-
"",
|
|
758
|
-
];
|
|
759
|
-
if (unavailableReason) {
|
|
760
|
-
lines.push(
|
|
761
|
-
buildSection("๐ ๏ธ Apply", [
|
|
762
|
-
buildStatLine("status", "unavailable"),
|
|
763
|
-
buildStatLine("reason", unavailableReason),
|
|
764
|
-
]),
|
|
765
|
-
);
|
|
766
|
-
return lines.join("\n");
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const before = scanDoctorCleaners(params.db, filterIds);
|
|
770
|
-
lines.splice(
|
|
771
|
-
lines.length - 1,
|
|
772
|
-
0,
|
|
773
|
-
buildSection("๐ Current matches", [
|
|
774
|
-
buildStatLine("matched conversations before apply", formatNumber(before.totalDistinctConversations)),
|
|
775
|
-
buildStatLine("matched messages before apply", formatNumber(before.totalDistinctMessages)),
|
|
776
|
-
]),
|
|
777
|
-
"",
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
if (before.totalDistinctConversations === 0) {
|
|
781
|
-
lines.push(
|
|
782
|
-
buildSection("๐ ๏ธ Apply", [
|
|
783
|
-
buildStatLine("status", "completed"),
|
|
784
|
-
buildStatLine("backup path", "skipped (no matches)"),
|
|
785
|
-
buildStatLine("deleted conversations", "0"),
|
|
786
|
-
buildStatLine("deleted messages", "0"),
|
|
787
|
-
buildStatLine("vacuumed", "no"),
|
|
788
|
-
buildStatLine("quick_check", "not run (no writes)"),
|
|
789
|
-
buildStatLine("result", "clean; no deletes ran"),
|
|
790
|
-
]),
|
|
791
|
-
);
|
|
792
|
-
return lines.join("\n");
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
let result: ReturnType<typeof applyDoctorCleaners>;
|
|
796
|
-
try {
|
|
797
|
-
result = applyDoctorCleaners(params.db, {
|
|
798
|
-
databasePath: params.config.databasePath,
|
|
799
|
-
filterIds,
|
|
800
|
-
vacuum: params.vacuum,
|
|
801
|
-
});
|
|
802
|
-
} catch (error) {
|
|
803
|
-
lines.push(
|
|
804
|
-
buildSection("๐ ๏ธ Apply", [
|
|
805
|
-
buildStatLine("status", "failed"),
|
|
806
|
-
buildStatLine(
|
|
807
|
-
"reason",
|
|
808
|
-
error instanceof Error ? error.message : "unknown cleaner apply failure",
|
|
809
|
-
),
|
|
810
|
-
]),
|
|
811
|
-
);
|
|
812
|
-
return lines.join("\n");
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
if (result.kind === "unavailable") {
|
|
816
|
-
lines.push(
|
|
817
|
-
buildSection("๐ ๏ธ Apply", [
|
|
818
|
-
buildStatLine("status", "unavailable"),
|
|
819
|
-
buildStatLine("reason", result.reason),
|
|
820
|
-
]),
|
|
821
|
-
);
|
|
822
|
-
return lines.join("\n");
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const quickCheck = runQuickCheck(params.db);
|
|
826
|
-
const quickCheckPassed = isPassingQuickCheck(quickCheck);
|
|
827
|
-
lines.push(
|
|
828
|
-
buildSection("๐ ๏ธ Apply", [
|
|
829
|
-
buildStatLine("status", quickCheckPassed ? "completed" : "warning"),
|
|
830
|
-
buildStatLine("backup path", result.backupPath),
|
|
831
|
-
buildStatLine("deleted conversations", formatNumber(result.deletedConversations)),
|
|
832
|
-
buildStatLine("deleted messages", formatNumber(result.deletedMessages)),
|
|
833
|
-
buildStatLine("vacuumed", formatBoolean(result.vacuumed)),
|
|
834
|
-
buildStatLine("quick_check", quickCheck),
|
|
835
|
-
buildStatLine(
|
|
836
|
-
"result",
|
|
837
|
-
quickCheckPassed
|
|
838
|
-
? result.deletedConversations > 0
|
|
839
|
-
? `removed ${formatNumber(result.deletedConversations)} conversation(s)`
|
|
840
|
-
: "clean; no deletes ran"
|
|
841
|
-
: "writes committed, but SQLite integrity verification reported problems; inspect the database or restore from the backup before continuing",
|
|
842
|
-
),
|
|
843
|
-
]),
|
|
844
|
-
);
|
|
845
|
-
|
|
846
|
-
return lines.join("\n");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
async function buildDoctorApplyText(params: {
|
|
850
|
-
ctx: PluginCommandContext;
|
|
851
|
-
db: DatabaseSync;
|
|
852
|
-
config: LcmConfig;
|
|
853
|
-
deps?: LcmDependencies;
|
|
854
|
-
summarize?: LcmSummarizeFn;
|
|
855
|
-
}): Promise<string> {
|
|
856
|
-
const current = await resolveCurrentConversation(params);
|
|
857
|
-
|
|
858
|
-
if (current.kind === "unavailable") {
|
|
859
|
-
return [
|
|
860
|
-
...buildHeaderLines(),
|
|
861
|
-
"",
|
|
862
|
-
"๐ฉบ Lossless Claw Doctor Apply",
|
|
863
|
-
"",
|
|
864
|
-
buildSection("๐ Current conversation", [
|
|
865
|
-
buildStatLine("status", "unavailable"),
|
|
866
|
-
buildStatLine("reason", current.reason),
|
|
867
|
-
buildStatLine("fallback", "Doctor apply is conversation-scoped, so no global repair ran."),
|
|
868
|
-
]),
|
|
869
|
-
].join("\n");
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const stats = getDoctorSummaryStats(params.db, current.stats.conversationId);
|
|
873
|
-
let result: Awaited<ReturnType<typeof applyScopedDoctorRepair>>;
|
|
874
|
-
try {
|
|
875
|
-
result = await applyScopedDoctorRepair({
|
|
876
|
-
db: params.db,
|
|
877
|
-
config: params.config,
|
|
878
|
-
conversationId: current.stats.conversationId,
|
|
879
|
-
deps: params.deps,
|
|
880
|
-
summarize: params.summarize,
|
|
881
|
-
runtimeConfig: params.ctx.config,
|
|
882
|
-
});
|
|
883
|
-
} catch (error) {
|
|
884
|
-
return [
|
|
885
|
-
...buildHeaderLines(),
|
|
886
|
-
"",
|
|
887
|
-
"๐ฉบ Lossless Claw Doctor Apply",
|
|
888
|
-
"",
|
|
889
|
-
buildSection("๐ Current conversation", [
|
|
890
|
-
buildStatLine("conversation id", formatNumber(current.stats.conversationId)),
|
|
891
|
-
buildStatLine(
|
|
892
|
-
"session key",
|
|
893
|
-
current.stats.sessionKey ? formatCommand(truncateMiddle(current.stats.sessionKey, 44)) : "missing",
|
|
894
|
-
),
|
|
895
|
-
buildStatLine("scope", "this conversation only"),
|
|
896
|
-
]),
|
|
897
|
-
"",
|
|
898
|
-
buildSection("๐ ๏ธ Apply", [
|
|
899
|
-
buildStatLine("mode", "in-place summary rewrite"),
|
|
900
|
-
buildStatLine("status", "failed"),
|
|
901
|
-
buildStatLine("reason", error instanceof Error ? error.message : "unknown repair failure"),
|
|
902
|
-
]),
|
|
903
|
-
].join("\n");
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const lines = [
|
|
907
|
-
...buildHeaderLines(),
|
|
908
|
-
"",
|
|
909
|
-
"๐ฉบ Lossless Claw Doctor Apply",
|
|
910
|
-
"",
|
|
911
|
-
buildSection("๐ Current conversation", [
|
|
912
|
-
buildStatLine("conversation id", formatNumber(current.stats.conversationId)),
|
|
913
|
-
buildStatLine(
|
|
914
|
-
"session key",
|
|
915
|
-
current.stats.sessionKey ? formatCommand(truncateMiddle(current.stats.sessionKey, 44)) : "missing",
|
|
916
|
-
),
|
|
917
|
-
buildStatLine("scope", "this conversation only"),
|
|
918
|
-
]),
|
|
919
|
-
"",
|
|
920
|
-
];
|
|
921
|
-
|
|
922
|
-
if (result.kind === "unavailable") {
|
|
923
|
-
lines.push(
|
|
924
|
-
buildSection("๐ ๏ธ Apply", [
|
|
925
|
-
buildStatLine("mode", "in-place summary rewrite"),
|
|
926
|
-
buildStatLine("status", "unavailable"),
|
|
927
|
-
buildStatLine("reason", result.reason),
|
|
928
|
-
]),
|
|
929
|
-
);
|
|
930
|
-
return lines.join("\n");
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
lines.push(
|
|
934
|
-
buildSection("๐ ๏ธ Apply", [
|
|
935
|
-
buildStatLine("mode", "in-place summary rewrite"),
|
|
936
|
-
buildStatLine("detected summaries", formatNumber(stats.total)),
|
|
937
|
-
buildStatLine("old-marker summaries", formatNumber(stats.old)),
|
|
938
|
-
buildStatLine("truncated-marker summaries", formatNumber(stats.truncated)),
|
|
939
|
-
buildStatLine("fallback-marker summaries", formatNumber(stats.fallback)),
|
|
940
|
-
buildStatLine("repaired summaries", formatNumber(result.repaired)),
|
|
941
|
-
buildStatLine("unchanged summaries", formatNumber(result.unchanged)),
|
|
942
|
-
buildStatLine("skipped summaries", formatNumber(result.skipped.length)),
|
|
943
|
-
buildStatLine(
|
|
944
|
-
"result",
|
|
945
|
-
stats.total === 0
|
|
946
|
-
? "clean; no writes ran"
|
|
947
|
-
: result.repaired > 0
|
|
948
|
-
? `repaired ${formatNumber(result.repaired)} summary(s) in place`
|
|
949
|
-
: "no repairs applied",
|
|
950
|
-
),
|
|
951
|
-
]),
|
|
952
|
-
);
|
|
953
|
-
|
|
954
|
-
if (result.repairedSummaryIds.length > 0) {
|
|
955
|
-
lines.push(
|
|
956
|
-
"",
|
|
957
|
-
buildSection("๐งท Repaired summaries", [result.repairedSummaryIds.join(", ")]),
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (result.skipped.length > 0) {
|
|
962
|
-
lines.push(
|
|
963
|
-
"",
|
|
964
|
-
buildSection(
|
|
965
|
-
"โ ๏ธ Deferred",
|
|
966
|
-
result.skipped.map((item) => `${item.summaryId}: ${item.reason}`),
|
|
967
|
-
),
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
return lines.join("\n");
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
export function createLcmCommand(params: {
|
|
975
|
-
db: DatabaseSync | (() => DatabaseSync | Promise<DatabaseSync>);
|
|
976
|
-
config: LcmConfig;
|
|
977
|
-
deps?: LcmDependencies;
|
|
978
|
-
summarize?: LcmSummarizeFn;
|
|
979
|
-
}): OpenClawPluginCommandDefinition {
|
|
980
|
-
const getDb = async (): Promise<DatabaseSync> =>
|
|
981
|
-
typeof params.db === "function" ? await params.db() : params.db;
|
|
982
|
-
|
|
983
|
-
return {
|
|
984
|
-
name: "lcm",
|
|
985
|
-
nativeNames: {
|
|
986
|
-
default: "lossless",
|
|
987
|
-
},
|
|
988
|
-
nativeProgressMessages: {
|
|
989
|
-
telegram: "Lossless Claw is working...",
|
|
990
|
-
},
|
|
991
|
-
description:
|
|
992
|
-
"Show Lossless Claw health, scan broken summaries, inspect high-confidence junk candidates, and run scoped doctor actions.",
|
|
993
|
-
acceptsArgs: true,
|
|
994
|
-
handler: async (ctx) => {
|
|
995
|
-
const parsed = parseLcmCommand(ctx.args);
|
|
996
|
-
switch (parsed.kind) {
|
|
997
|
-
case "status":
|
|
998
|
-
return { text: await buildStatusText({ ctx, db: await getDb(), config: params.config }) };
|
|
999
|
-
case "doctor":
|
|
1000
|
-
return parsed.apply
|
|
1001
|
-
? {
|
|
1002
|
-
text: await buildDoctorApplyText({
|
|
1003
|
-
ctx,
|
|
1004
|
-
db: await getDb(),
|
|
1005
|
-
config: params.config,
|
|
1006
|
-
deps: params.deps,
|
|
1007
|
-
summarize: params.summarize,
|
|
1008
|
-
}),
|
|
1009
|
-
}
|
|
1010
|
-
: { text: await buildDoctorText({ ctx, db: await getDb() }) };
|
|
1011
|
-
case "doctor_cleaners":
|
|
1012
|
-
return parsed.apply
|
|
1013
|
-
? {
|
|
1014
|
-
text: await buildDoctorCleanersApplyText({
|
|
1015
|
-
db: await getDb(),
|
|
1016
|
-
config: params.config,
|
|
1017
|
-
filterId: parsed.filterId,
|
|
1018
|
-
vacuum: parsed.vacuum,
|
|
1019
|
-
}),
|
|
1020
|
-
}
|
|
1021
|
-
: { text: await buildDoctorCleanersText({ db: await getDb() }) };
|
|
1022
|
-
case "help":
|
|
1023
|
-
return { text: buildHelpText(parsed.error) };
|
|
1024
|
-
}
|
|
1025
|
-
},
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
export const __testing = {
|
|
1030
|
-
parseLcmCommand,
|
|
1031
|
-
detectDoctorMarker,
|
|
1032
|
-
getDoctorSummaryStats,
|
|
1033
|
-
getLcmStatusStats,
|
|
1034
|
-
getConversationStatusStats,
|
|
1035
|
-
scanDoctorCleaners,
|
|
1036
|
-
resolveCurrentConversation,
|
|
1037
|
-
resolveContextEngineSlot,
|
|
1038
|
-
resolvePluginEnabled,
|
|
1039
|
-
resolvePluginSelected,
|
|
1040
|
-
};
|