@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
package/src/db/config.ts DELETED
@@ -1,345 +0,0 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
3
-
4
- export type CacheAwareCompactionConfig = {
5
- enabled: boolean;
6
- maxColdCacheCatchupPasses: number;
7
- hotCachePressureFactor: number;
8
- hotCacheBudgetHeadroomRatio: number;
9
- };
10
-
11
- export type DynamicLeafChunkTokensConfig = {
12
- enabled: boolean;
13
- max: number;
14
- };
15
-
16
- export type LcmConfig = {
17
- enabled: boolean;
18
- databasePath: string;
19
- /** Glob patterns for session keys to exclude from LCM storage entirely. */
20
- ignoreSessionPatterns: string[];
21
- /** Glob patterns for session keys that may read from LCM but never write to it. */
22
- statelessSessionPatterns: string[];
23
- /** When true, stateless session pattern matching is enforced. */
24
- skipStatelessSessions: boolean;
25
- contextThreshold: number;
26
- freshTailCount: number;
27
- newSessionRetainDepth: number;
28
- leafMinFanout: number;
29
- condensedMinFanout: number;
30
- condensedMinFanoutHard: number;
31
- incrementalMaxDepth: number;
32
- leafChunkTokens: number;
33
- /** Maximum raw parent-history tokens imported during first-time bootstrap. */
34
- bootstrapMaxTokens?: number;
35
- leafTargetTokens: number;
36
- condensedTargetTokens: number;
37
- maxExpandTokens: number;
38
- largeFileTokenThreshold: number;
39
- /** Provider override for compaction summarization. */
40
- summaryProvider: string;
41
- /** Model override for compaction summarization. */
42
- summaryModel: string;
43
- /** Provider override for large-file text summarization. */
44
- largeFileSummaryProvider: string;
45
- /** Model override for large-file text summarization. */
46
- largeFileSummaryModel: string;
47
- /** Provider override for lcm_expand_query sub-agent. */
48
- expansionProvider: string;
49
- /** Model override for lcm_expand_query sub-agent. */
50
- expansionModel: string;
51
- /** Max time to wait for delegated lcm_expand_query sub-agent completion. */
52
- delegationTimeoutMs: number;
53
- /** Max time to wait for a single model-backed LCM summarizer call. */
54
- summaryTimeoutMs: number;
55
- /** IANA timezone for timestamps in summaries (from TZ env or system default) */
56
- timezone: string;
57
- /** When true, retroactively delete HEARTBEAT_OK turn cycles from LCM storage. */
58
- pruneHeartbeatOk: boolean;
59
- /** Hard ceiling for assembly token budget — caps runtime-provided and fallback budgets. */
60
- maxAssemblyTokenBudget?: number;
61
- /** Maximum allowed overage factor for summaries relative to target tokens (default 3). */
62
- summaryMaxOverageFactor: number;
63
- /** Custom instructions injected into all summarization prompts. */
64
- customInstructions: string;
65
- /** Consecutive auth failures before the compaction circuit breaker trips (default 5). */
66
- circuitBreakerThreshold: number;
67
- /** Cooldown in milliseconds before the circuit breaker auto-resets (default 30 min). */
68
- circuitBreakerCooldownMs: number;
69
- /** Explicit fallback provider/model pairs for compaction summarization. */
70
- fallbackProviders: Array<{ provider: string; model: string }>;
71
- /** Cache-sensitive policy for incremental leaf compaction. */
72
- cacheAwareCompaction: CacheAwareCompactionConfig;
73
- /** Dynamic step-band policy for incremental leaf chunk sizing. */
74
- dynamicLeafChunkTokens: DynamicLeafChunkTokensConfig;
75
- };
76
-
77
- /** Safely coerce an unknown value to a finite number, or return undefined. */
78
- function toNumber(value: unknown): number | undefined {
79
- if (typeof value === "number" && Number.isFinite(value)) return value;
80
- if (typeof value === "string") {
81
- const n = Number(value);
82
- if (Number.isFinite(n)) return n;
83
- }
84
- return undefined;
85
- }
86
-
87
- /** Safely parse a finite integer from an environment string, or return undefined.
88
- * Unlike raw parseInt(), this returns undefined for NaN so ?? fallback works. */
89
- function parseFiniteInt(value: string | undefined): number | undefined {
90
- if (value === undefined) return undefined;
91
- const parsed = parseInt(value, 10);
92
- return Number.isFinite(parsed) ? parsed : undefined;
93
- }
94
-
95
- /** Safely parse a finite float from an environment string, or return undefined. */
96
- function parseFiniteNumber(value: string | undefined): number | undefined {
97
- if (value === undefined) return undefined;
98
- const parsed = parseFloat(value);
99
- return Number.isFinite(parsed) ? parsed : undefined;
100
- }
101
-
102
- /** Parse fallback providers from env string (format: "provider/model,provider/model"). */
103
- function parseFallbackProviders(value: string | undefined): Array<{ provider: string; model: string }> | undefined {
104
- if (!value?.trim()) return undefined;
105
- const entries: Array<{ provider: string; model: string }> = [];
106
- for (const part of value.split(",")) {
107
- const trimmed = part.trim();
108
- if (!trimmed) continue;
109
- const slashIdx = trimmed.indexOf("/");
110
- if (slashIdx > 0 && slashIdx < trimmed.length - 1) {
111
- const provider = trimmed.slice(0, slashIdx).trim();
112
- const model = trimmed.slice(slashIdx + 1).trim();
113
- if (provider && model) {
114
- entries.push({ provider, model });
115
- }
116
- }
117
- }
118
- return entries.length > 0 ? entries : undefined;
119
- }
120
-
121
- /** Parse fallback providers from plugin config array (object items only). */
122
- function toFallbackProviderArray(value: unknown): Array<{ provider: string; model: string }> | undefined {
123
- if (!Array.isArray(value)) return undefined;
124
- const entries: Array<{ provider: string; model: string }> = [];
125
- for (const item of value) {
126
- if (item && typeof item === "object" && !Array.isArray(item)) {
127
- const p = toStr((item as Record<string, unknown>).provider);
128
- const m = toStr((item as Record<string, unknown>).model);
129
- if (p && m) entries.push({ provider: p, model: m });
130
- }
131
- }
132
- return entries.length > 0 ? entries : undefined;
133
- }
134
-
135
- /** Safely coerce an unknown value to a boolean, or return undefined. */
136
- function toBool(value: unknown): boolean | undefined {
137
- if (typeof value === "boolean") return value;
138
- if (value === "true") return true;
139
- if (value === "false") return false;
140
- return undefined;
141
- }
142
-
143
- /** Safely coerce an unknown value to a trimmed non-empty string, or return undefined. */
144
- function toStr(value: unknown): string | undefined {
145
- if (typeof value === "string") {
146
- const trimmed = value.trim();
147
- return trimmed.length > 0 ? trimmed : undefined;
148
- }
149
- return undefined;
150
- }
151
-
152
- /** Coerce a plugin config value into a trimmed string array when possible. */
153
- function toStrArray(value: unknown): string[] | undefined {
154
- if (Array.isArray(value)) {
155
- const normalized = value
156
- .map((entry) => toStr(entry))
157
- .filter((entry): entry is string => typeof entry === "string");
158
- return normalized.length > 0 ? normalized : [];
159
- }
160
- const single = toStr(value);
161
- if (!single) {
162
- return undefined;
163
- }
164
- return single
165
- .split(",")
166
- .map((entry) => entry.trim())
167
- .filter(Boolean);
168
- }
169
-
170
- function toRecord(value: unknown): Record<string, unknown> | undefined {
171
- return value && typeof value === "object" && !Array.isArray(value)
172
- ? (value as Record<string, unknown>)
173
- : undefined;
174
- }
175
-
176
- /**
177
- * Resolve LCM configuration with three-tier precedence:
178
- * 1. Environment variables (highest — backward compat)
179
- * 2. Plugin config object (from plugins.entries.lossless-claw.config)
180
- * 3. Hardcoded defaults (lowest)
181
- */
182
- export function resolveLcmConfig(
183
- env: NodeJS.ProcessEnv = process.env,
184
- pluginConfig?: Record<string, unknown>,
185
- ): LcmConfig {
186
- const pc = pluginConfig ?? {};
187
- const cacheAwareCompaction = toRecord(pc.cacheAwareCompaction);
188
- const dynamicLeafChunkTokens = toRecord(pc.dynamicLeafChunkTokens);
189
- const resolvedLeafChunkTokens =
190
- parseFiniteInt(env.LCM_LEAF_CHUNK_TOKENS)
191
- ?? toNumber(pc.leafChunkTokens) ?? 20000;
192
- const resolvedBootstrapMaxTokens =
193
- parseFiniteInt(env.LCM_BOOTSTRAP_MAX_TOKENS)
194
- ?? toNumber(pc.bootstrapMaxTokens)
195
- ?? Math.max(6000, Math.floor(resolvedLeafChunkTokens * 0.3));
196
- const envDelegationTimeoutMs =
197
- env.LCM_DELEGATION_TIMEOUT_MS !== undefined
198
- ? toNumber(env.LCM_DELEGATION_TIMEOUT_MS)
199
- : undefined;
200
- const resolvedDynamicLeafChunkMax = Math.max(
201
- resolvedLeafChunkTokens,
202
- parseFiniteInt(env.LCM_DYNAMIC_LEAF_CHUNK_TOKENS_MAX)
203
- ?? toNumber(dynamicLeafChunkTokens?.max)
204
- ?? Math.floor(resolvedLeafChunkTokens * 2),
205
- );
206
- const resolvedHotCachePressureFactor = Math.max(
207
- 1,
208
- parseFiniteNumber(env.LCM_HOT_CACHE_PRESSURE_FACTOR)
209
- ?? toNumber(cacheAwareCompaction?.hotCachePressureFactor)
210
- ?? 4,
211
- );
212
- const resolvedHotCacheBudgetHeadroomRatio = Math.min(
213
- 0.95,
214
- Math.max(
215
- 0,
216
- parseFiniteNumber(env.LCM_HOT_CACHE_BUDGET_HEADROOM_RATIO)
217
- ?? toNumber(cacheAwareCompaction?.hotCacheBudgetHeadroomRatio)
218
- ?? 0.2,
219
- ),
220
- );
221
-
222
- return {
223
- enabled:
224
- env.LCM_ENABLED !== undefined
225
- ? env.LCM_ENABLED !== "false"
226
- : toBool(pc.enabled) ?? true,
227
- databasePath:
228
- env.LCM_DATABASE_PATH
229
- ?? toStr(pc.dbPath)
230
- ?? toStr(pc.databasePath)
231
- ?? join(homedir(), ".openclaw", "lcm.db"),
232
- ignoreSessionPatterns:
233
- env.LCM_IGNORE_SESSION_PATTERNS !== undefined
234
- ? env.LCM_IGNORE_SESSION_PATTERNS
235
- .split(",")
236
- .map((entry) => entry.trim())
237
- .filter(Boolean)
238
- : toStrArray(pc.ignoreSessionPatterns) ?? [],
239
- statelessSessionPatterns:
240
- env.LCM_STATELESS_SESSION_PATTERNS !== undefined
241
- ? env.LCM_STATELESS_SESSION_PATTERNS
242
- .split(",")
243
- .map((entry) => entry.trim())
244
- .filter(Boolean)
245
- : toStrArray(pc.statelessSessionPatterns) ?? [],
246
- skipStatelessSessions:
247
- env.LCM_SKIP_STATELESS_SESSIONS !== undefined
248
- ? env.LCM_SKIP_STATELESS_SESSIONS === "true"
249
- : toBool(pc.skipStatelessSessions) ?? true,
250
- contextThreshold:
251
- parseFiniteNumber(env.LCM_CONTEXT_THRESHOLD)
252
- ?? toNumber(pc.contextThreshold) ?? 0.75,
253
- freshTailCount:
254
- parseFiniteInt(env.LCM_FRESH_TAIL_COUNT)
255
- ?? toNumber(pc.freshTailCount) ?? 64,
256
- newSessionRetainDepth:
257
- parseFiniteInt(env.LCM_NEW_SESSION_RETAIN_DEPTH)
258
- ?? toNumber(pc.newSessionRetainDepth) ?? 2,
259
- leafMinFanout:
260
- parseFiniteInt(env.LCM_LEAF_MIN_FANOUT)
261
- ?? toNumber(pc.leafMinFanout) ?? 8,
262
- condensedMinFanout:
263
- parseFiniteInt(env.LCM_CONDENSED_MIN_FANOUT)
264
- ?? toNumber(pc.condensedMinFanout) ?? 4,
265
- condensedMinFanoutHard:
266
- parseFiniteInt(env.LCM_CONDENSED_MIN_FANOUT_HARD)
267
- ?? toNumber(pc.condensedMinFanoutHard) ?? 2,
268
- incrementalMaxDepth:
269
- parseFiniteInt(env.LCM_INCREMENTAL_MAX_DEPTH)
270
- ?? toNumber(pc.incrementalMaxDepth) ?? 1,
271
- leafChunkTokens: resolvedLeafChunkTokens,
272
- bootstrapMaxTokens: resolvedBootstrapMaxTokens,
273
- leafTargetTokens:
274
- parseFiniteInt(env.LCM_LEAF_TARGET_TOKENS)
275
- ?? toNumber(pc.leafTargetTokens) ?? 2400,
276
- condensedTargetTokens:
277
- parseFiniteInt(env.LCM_CONDENSED_TARGET_TOKENS)
278
- ?? toNumber(pc.condensedTargetTokens) ?? 2000,
279
- maxExpandTokens:
280
- parseFiniteInt(env.LCM_MAX_EXPAND_TOKENS)
281
- ?? toNumber(pc.maxExpandTokens) ?? 4000,
282
- largeFileTokenThreshold:
283
- parseFiniteInt(env.LCM_LARGE_FILE_TOKEN_THRESHOLD)
284
- ?? toNumber(pc.largeFileThresholdTokens)
285
- ?? toNumber(pc.largeFileTokenThreshold)
286
- ?? 25000,
287
- summaryProvider:
288
- env.LCM_SUMMARY_PROVIDER?.trim() ?? toStr(pc.summaryProvider) ?? "",
289
- summaryModel:
290
- env.LCM_SUMMARY_MODEL?.trim() ?? toStr(pc.summaryModel) ?? "",
291
- largeFileSummaryProvider:
292
- env.LCM_LARGE_FILE_SUMMARY_PROVIDER?.trim() ?? toStr(pc.largeFileSummaryProvider) ?? "",
293
- largeFileSummaryModel:
294
- env.LCM_LARGE_FILE_SUMMARY_MODEL?.trim() ?? toStr(pc.largeFileSummaryModel) ?? "",
295
- expansionProvider:
296
- env.LCM_EXPANSION_PROVIDER?.trim() ?? toStr(pc.expansionProvider) ?? "",
297
- expansionModel:
298
- env.LCM_EXPANSION_MODEL?.trim() ?? toStr(pc.expansionModel) ?? "",
299
- delegationTimeoutMs: envDelegationTimeoutMs ?? toNumber(pc.delegationTimeoutMs) ?? 120000,
300
- summaryTimeoutMs:
301
- parseFiniteInt(env.LCM_SUMMARY_TIMEOUT_MS)
302
- ?? toNumber(pc.summaryTimeoutMs) ?? 60000,
303
- timezone: env.TZ ?? toStr(pc.timezone) ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
304
- pruneHeartbeatOk:
305
- env.LCM_PRUNE_HEARTBEAT_OK !== undefined
306
- ? env.LCM_PRUNE_HEARTBEAT_OK === "true"
307
- : toBool(pc.pruneHeartbeatOk) ?? false,
308
- maxAssemblyTokenBudget:
309
- parseFiniteInt(env.LCM_MAX_ASSEMBLY_TOKEN_BUDGET)
310
- ?? toNumber(pc.maxAssemblyTokenBudget) ?? undefined,
311
- summaryMaxOverageFactor:
312
- parseFiniteNumber(env.LCM_SUMMARY_MAX_OVERAGE_FACTOR)
313
- ?? toNumber(pc.summaryMaxOverageFactor) ?? 3,
314
- customInstructions:
315
- env.LCM_CUSTOM_INSTRUCTIONS?.trim() ?? toStr(pc.customInstructions) ?? "",
316
- circuitBreakerThreshold:
317
- parseFiniteInt(env.LCM_CIRCUIT_BREAKER_THRESHOLD)
318
- ?? toNumber(pc.circuitBreakerThreshold) ?? 5,
319
- circuitBreakerCooldownMs:
320
- parseFiniteInt(env.LCM_CIRCUIT_BREAKER_COOLDOWN_MS)
321
- ?? toNumber(pc.circuitBreakerCooldownMs) ?? 1_800_000,
322
- fallbackProviders:
323
- parseFallbackProviders(env.LCM_FALLBACK_PROVIDERS)
324
- ?? toFallbackProviderArray(pc.fallbackProviders) ?? [],
325
- cacheAwareCompaction: {
326
- enabled:
327
- env.LCM_CACHE_AWARE_COMPACTION_ENABLED !== undefined
328
- ? env.LCM_CACHE_AWARE_COMPACTION_ENABLED !== "false"
329
- : toBool(cacheAwareCompaction?.enabled) ?? true,
330
- maxColdCacheCatchupPasses:
331
- parseFiniteInt(env.LCM_MAX_COLD_CACHE_CATCHUP_PASSES)
332
- ?? toNumber(cacheAwareCompaction?.maxColdCacheCatchupPasses)
333
- ?? 2,
334
- hotCachePressureFactor: resolvedHotCachePressureFactor,
335
- hotCacheBudgetHeadroomRatio: resolvedHotCacheBudgetHeadroomRatio,
336
- },
337
- dynamicLeafChunkTokens: {
338
- enabled:
339
- env.LCM_DYNAMIC_LEAF_CHUNK_TOKENS_ENABLED !== undefined
340
- ? env.LCM_DYNAMIC_LEAF_CHUNK_TOKENS_ENABLED === "true"
341
- : toBool(dynamicLeafChunkTokens?.enabled) ?? true,
342
- max: resolvedDynamicLeafChunkMax,
343
- },
344
- };
345
- }
@@ -1,141 +0,0 @@
1
- import { mkdirSync } from "node:fs";
2
- import { dirname, resolve } from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
4
-
5
- type ConnectionKey = string;
6
- const SQLITE_BUSY_TIMEOUT_MS = 5_000;
7
-
8
- const connectionsByPath = new Map<ConnectionKey, Set<DatabaseSync>>();
9
- const connectionIndex = new Map<DatabaseSync, ConnectionKey>();
10
-
11
- function isInMemoryPath(dbPath: string): boolean {
12
- const normalized = dbPath.trim();
13
- return normalized === ":memory:" || normalized.startsWith("file::memory:");
14
- }
15
-
16
- export function normalizePath(dbPath: string): ConnectionKey {
17
- if (isInMemoryPath(dbPath)) {
18
- const trimmed = dbPath.trim();
19
- return trimmed.length > 0 ? trimmed : ":memory:";
20
- }
21
- return resolve(dbPath);
22
- }
23
-
24
- function ensureDbDirectory(dbPath: string): void {
25
- if (isInMemoryPath(dbPath)) {
26
- return;
27
- }
28
- mkdirSync(dirname(dbPath), { recursive: true });
29
- }
30
-
31
- function configureConnection(db: DatabaseSync): DatabaseSync {
32
- db.exec("PRAGMA journal_mode = WAL");
33
- db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
34
- db.exec("PRAGMA foreign_keys = ON");
35
- // 64MB page cache (default 2MB is severely undersized for multi-GB databases
36
- // with concurrent agents). Memory is demand-allocated, released on close.
37
- db.exec("PRAGMA cache_size = -65536");
38
- // NORMAL is officially recommended for WAL mode — crash-safe for app crashes,
39
- // only risks data loss on power failure (OS/kernel crash). The bootstrap
40
- // process re-ingests any lost transactions from session files.
41
- db.exec("PRAGMA synchronous = NORMAL");
42
- // Keep temp tables/indexes in RAM (helps ordinal resequencing).
43
- db.exec("PRAGMA temp_store = MEMORY");
44
- return db;
45
- }
46
-
47
- function trackConnection(dbPath: string, db: DatabaseSync): void {
48
- const key = normalizePath(dbPath);
49
- let entries = connectionsByPath.get(key);
50
- if (!entries) {
51
- entries = new Set();
52
- connectionsByPath.set(key, entries);
53
- }
54
- entries.add(db);
55
- connectionIndex.set(db, key);
56
- }
57
-
58
- function untrackConnection(db: DatabaseSync): void {
59
- const key = connectionIndex.get(db);
60
- if (!key) {
61
- return;
62
- }
63
- const entries = connectionsByPath.get(key);
64
- if (entries) {
65
- entries.delete(db);
66
- if (entries.size === 0) {
67
- connectionsByPath.delete(key);
68
- }
69
- }
70
- connectionIndex.delete(db);
71
- }
72
-
73
- function closeDatabase(db: DatabaseSync | undefined): void {
74
- if (!db) {
75
- return;
76
- }
77
- try {
78
- // Update query planner statistics for tables that changed since last optimize.
79
- // Separate try so a SQLITE_BUSY/SQLITE_READONLY from optimize doesn't skip close.
80
- try { db.exec("PRAGMA optimize"); } catch { /* best-effort */ }
81
- db.close();
82
- } catch {
83
- // Ignore close failures; callers are shutting down anyway.
84
- } finally {
85
- untrackConnection(db);
86
- }
87
- }
88
-
89
- /**
90
- * Create a new SQLite connection for the given LCM database path.
91
- *
92
- * Connections are tracked so tests can close them by path via closeLcmConnection().
93
- */
94
- export function createLcmDatabaseConnection(dbPath: string): DatabaseSync {
95
- ensureDbDirectory(dbPath);
96
- const db = new DatabaseSync(dbPath);
97
- try {
98
- configureConnection(db);
99
- } catch (err) {
100
- try { db.close(); } catch { /* ignore cleanup failure */ }
101
- throw err;
102
- }
103
- trackConnection(dbPath, db);
104
- return db;
105
- }
106
-
107
- /**
108
- * Close tracked LCM connections.
109
- *
110
- * When a DatabaseSync instance is supplied, only that handle is closed.
111
- * When a path is supplied, all handles associated with the normalized path
112
- * are closed. When called with no arguments, all tracked connections are
113
- * closed. Intended primarily for tests.
114
- */
115
- export function closeLcmConnection(target?: string | DatabaseSync): void {
116
- if (target && typeof target !== "string") {
117
- closeDatabase(target);
118
- return;
119
- }
120
-
121
- if (typeof target === "string") {
122
- const key = normalizePath(target);
123
- const entries = connectionsByPath.get(key);
124
- if (!entries) {
125
- return;
126
- }
127
- for (const db of [...entries]) {
128
- closeDatabase(db);
129
- }
130
- connectionsByPath.delete(key);
131
- return;
132
- }
133
-
134
- for (const db of [...connectionIndex.keys()]) {
135
- closeDatabase(db);
136
- }
137
- connectionsByPath.clear();
138
- connectionIndex.clear();
139
- }
140
-
141
- export const getLcmConnection = createLcmDatabaseConnection;
@@ -1,42 +0,0 @@
1
- import type { DatabaseSync } from "node:sqlite";
2
-
3
- export type LcmDbFeatures = {
4
- fts5Available: boolean;
5
- };
6
-
7
- const featureCache = new WeakMap<DatabaseSync, LcmDbFeatures>();
8
-
9
- function probeFts5(db: DatabaseSync): boolean {
10
- try {
11
- db.exec("DROP TABLE IF EXISTS temp.__lcm_fts5_probe");
12
- db.exec("CREATE VIRTUAL TABLE temp.__lcm_fts5_probe USING fts5(content)");
13
- db.exec("DROP TABLE temp.__lcm_fts5_probe");
14
- return true;
15
- } catch {
16
- try {
17
- db.exec("DROP TABLE IF EXISTS temp.__lcm_fts5_probe");
18
- } catch {
19
- // Ignore cleanup failures after a failed probe.
20
- }
21
- return false;
22
- }
23
- }
24
-
25
- /**
26
- * Detect SQLite features exposed by the current Node runtime.
27
- *
28
- * The result is cached per DatabaseSync handle because the probe is runtime-
29
- * specific, not database-file-specific.
30
- */
31
- export function getLcmDbFeatures(db: DatabaseSync): LcmDbFeatures {
32
- const cached = featureCache.get(db);
33
- if (cached) {
34
- return cached;
35
- }
36
-
37
- const detected: LcmDbFeatures = {
38
- fts5Available: probeFts5(db),
39
- };
40
- featureCache.set(db, detected);
41
- return detected;
42
- }