@martian-engineering/lossless-claw 0.6.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -6
- package/docs/agent-tools.md +16 -5
- package/docs/configuration.md +223 -214
- package/openclaw.plugin.json +123 -0
- package/package.json +1 -1
- package/skills/lossless-claw/SKILL.md +3 -2
- package/skills/lossless-claw/references/architecture.md +12 -0
- package/skills/lossless-claw/references/config.md +135 -3
- package/skills/lossless-claw/references/diagnostics.md +13 -0
- package/src/assembler.ts +17 -5
- package/src/compaction.ts +161 -53
- package/src/db/config.ts +102 -4
- package/src/db/connection.ts +35 -7
- package/src/db/features.ts +24 -5
- package/src/db/migration.ts +257 -78
- package/src/engine.ts +1007 -110
- package/src/estimate-tokens.ts +80 -0
- package/src/lcm-log.ts +37 -0
- package/src/plugin/index.ts +493 -101
- package/src/plugin/lcm-command.ts +288 -7
- package/src/plugin/lcm-doctor-apply.ts +1 -3
- package/src/plugin/lcm-doctor-cleaners.ts +655 -0
- package/src/plugin/shared-init.ts +59 -0
- package/src/prune.ts +391 -0
- package/src/retrieval.ts +8 -9
- package/src/startup-banner-log.ts +1 -0
- package/src/store/compaction-telemetry-store.ts +156 -0
- package/src/store/conversation-store.ts +6 -1
- package/src/store/fts5-sanitize.ts +25 -4
- package/src/store/full-text-sort.ts +21 -0
- package/src/store/index.ts +8 -0
- package/src/store/summary-store.ts +21 -14
- package/src/summarize.ts +55 -34
- package/src/tools/lcm-describe-tool.ts +9 -4
- package/src/tools/lcm-expand-query-tool.ts +609 -200
- package/src/tools/lcm-expand-tool.ts +9 -4
- package/src/tools/lcm-grep-tool.ts +22 -8
- package/src/types.ts +1 -0
|
@@ -6,6 +6,13 @@ import type { LcmSummarizeFn } from "../summarize.js";
|
|
|
6
6
|
import type { LcmDependencies } from "../types.js";
|
|
7
7
|
import type { OpenClawPluginCommandDefinition, PluginCommandContext } from "openclaw/plugin-sdk";
|
|
8
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";
|
|
9
16
|
import {
|
|
10
17
|
detectDoctorMarker,
|
|
11
18
|
getDoctorSummaryStats,
|
|
@@ -52,8 +59,11 @@ type CurrentConversationResolution =
|
|
|
52
59
|
type ParsedLcmCommand =
|
|
53
60
|
| { kind: "status" }
|
|
54
61
|
| { kind: "doctor"; apply: boolean }
|
|
62
|
+
| { kind: "doctor_cleaners"; apply: boolean; filterId?: DoctorCleanerId; vacuum: boolean }
|
|
55
63
|
| { kind: "help"; error?: string };
|
|
56
64
|
|
|
65
|
+
const DOCTOR_CLEANER_IDS = new Set<DoctorCleanerId>(getDoctorCleanerFilterIds());
|
|
66
|
+
|
|
57
67
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
58
68
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
59
69
|
? (value as Record<string, unknown>)
|
|
@@ -138,6 +148,32 @@ function splitArgs(rawArgs: string | undefined): string[] {
|
|
|
138
148
|
.filter(Boolean);
|
|
139
149
|
}
|
|
140
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
|
+
|
|
141
177
|
function parseLcmCommand(rawArgs: string | undefined): ParsedLcmCommand {
|
|
142
178
|
const tokens = splitArgs(rawArgs);
|
|
143
179
|
if (tokens.length === 0) {
|
|
@@ -154,19 +190,34 @@ function parseLcmCommand(rawArgs: string | undefined): ParsedLcmCommand {
|
|
|
154
190
|
if (rest.length === 0) {
|
|
155
191
|
return { kind: "doctor", apply: false };
|
|
156
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
|
+
}
|
|
157
207
|
if (rest.length === 1 && rest[0]?.toLowerCase() === "apply") {
|
|
158
208
|
return { kind: "doctor", apply: true };
|
|
159
209
|
}
|
|
160
210
|
return {
|
|
161
211
|
kind: "help",
|
|
162
|
-
error:
|
|
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.`,
|
|
163
214
|
};
|
|
164
215
|
case "help":
|
|
165
216
|
return { kind: "help" };
|
|
166
217
|
default:
|
|
167
218
|
return {
|
|
168
219
|
kind: "help",
|
|
169
|
-
error: `Unknown subcommand \`${head}\`. Supported: status, doctor, doctor apply.`,
|
|
220
|
+
error: `Unknown subcommand \`${head}\`. Supported: status, doctor, doctor clean, doctor apply, help.`,
|
|
170
221
|
};
|
|
171
222
|
}
|
|
172
223
|
}
|
|
@@ -423,6 +474,14 @@ function buildHelpText(error?: string): string {
|
|
|
423
474
|
buildStatLine(formatCommand(VISIBLE_COMMAND), "Show compact status output."),
|
|
424
475
|
buildStatLine(formatCommand(`${VISIBLE_COMMAND} status`), "Show plugin, Global, and current-conversation status."),
|
|
425
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
|
+
),
|
|
426
485
|
buildStatLine(formatCommand(`${VISIBLE_COMMAND} doctor apply`), "Repair broken summaries in the current conversation."),
|
|
427
486
|
]),
|
|
428
487
|
"",
|
|
@@ -435,6 +494,17 @@ function buildHelpText(error?: string): string {
|
|
|
435
494
|
return lines.join("\n");
|
|
436
495
|
}
|
|
437
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
|
+
|
|
438
508
|
async function buildStatusText(params: {
|
|
439
509
|
ctx: PluginCommandContext;
|
|
440
510
|
db: DatabaseSync;
|
|
@@ -584,6 +654,198 @@ async function buildDoctorText(params: {
|
|
|
584
654
|
return lines.join("\n");
|
|
585
655
|
}
|
|
586
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
|
+
|
|
587
849
|
async function buildDoctorApplyText(params: {
|
|
588
850
|
ctx: PluginCommandContext;
|
|
589
851
|
db: DatabaseSync;
|
|
@@ -710,35 +972,53 @@ async function buildDoctorApplyText(params: {
|
|
|
710
972
|
}
|
|
711
973
|
|
|
712
974
|
export function createLcmCommand(params: {
|
|
713
|
-
db: DatabaseSync;
|
|
975
|
+
db: DatabaseSync | (() => DatabaseSync | Promise<DatabaseSync>);
|
|
714
976
|
config: LcmConfig;
|
|
715
977
|
deps?: LcmDependencies;
|
|
716
978
|
summarize?: LcmSummarizeFn;
|
|
717
979
|
}): OpenClawPluginCommandDefinition {
|
|
980
|
+
const getDb = async (): Promise<DatabaseSync> =>
|
|
981
|
+
typeof params.db === "function" ? await params.db() : params.db;
|
|
982
|
+
|
|
718
983
|
return {
|
|
719
984
|
name: "lcm",
|
|
720
985
|
nativeNames: {
|
|
721
986
|
default: "lossless",
|
|
722
987
|
},
|
|
723
|
-
|
|
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.",
|
|
724
993
|
acceptsArgs: true,
|
|
725
994
|
handler: async (ctx) => {
|
|
726
995
|
const parsed = parseLcmCommand(ctx.args);
|
|
727
996
|
switch (parsed.kind) {
|
|
728
997
|
case "status":
|
|
729
|
-
return { text: await buildStatusText({ ctx, db:
|
|
998
|
+
return { text: await buildStatusText({ ctx, db: await getDb(), config: params.config }) };
|
|
730
999
|
case "doctor":
|
|
731
1000
|
return parsed.apply
|
|
732
1001
|
? {
|
|
733
1002
|
text: await buildDoctorApplyText({
|
|
734
1003
|
ctx,
|
|
735
|
-
db:
|
|
1004
|
+
db: await getDb(),
|
|
736
1005
|
config: params.config,
|
|
737
1006
|
deps: params.deps,
|
|
738
1007
|
summarize: params.summarize,
|
|
739
1008
|
}),
|
|
740
1009
|
}
|
|
741
|
-
: { text: await buildDoctorText({ ctx, db:
|
|
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() }) };
|
|
742
1022
|
case "help":
|
|
743
1023
|
return { text: buildHelpText(parsed.error) };
|
|
744
1024
|
}
|
|
@@ -752,6 +1032,7 @@ export const __testing = {
|
|
|
752
1032
|
getDoctorSummaryStats,
|
|
753
1033
|
getLcmStatusStats,
|
|
754
1034
|
getConversationStatusStats,
|
|
1035
|
+
scanDoctorCleaners,
|
|
755
1036
|
resolveCurrentConversation,
|
|
756
1037
|
resolveContextEngineSlot,
|
|
757
1038
|
resolvePluginEnabled,
|
|
@@ -6,6 +6,7 @@ import type { LcmSummarizeFn } from "../summarize.js";
|
|
|
6
6
|
import { createLcmSummarizeFromLegacyParams } from "../summarize.js";
|
|
7
7
|
import type { LcmDependencies } from "../types.js";
|
|
8
8
|
import { detectDoctorMarker, loadDoctorTargets, type DoctorTargetRecord } from "./lcm-doctor-shared.js";
|
|
9
|
+
import { estimateTokens } from "../estimate-tokens.js";
|
|
9
10
|
|
|
10
11
|
type SummaryOverride = {
|
|
11
12
|
content: string;
|
|
@@ -524,9 +525,6 @@ function parseSqliteTimestamp(value: string | null | undefined): Date | null {
|
|
|
524
525
|
return null;
|
|
525
526
|
}
|
|
526
527
|
|
|
527
|
-
function estimateTokens(text: string): number {
|
|
528
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
529
|
-
}
|
|
530
528
|
|
|
531
529
|
function updateSummaryFts(db: DatabaseSync, summaryId: string, content: string): void {
|
|
532
530
|
try {
|