@jonathangu/openclawbrain 0.3.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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/bin/openclawbrain.js +15 -0
  4. package/docs/END_STATE.md +244 -0
  5. package/docs/EVIDENCE.md +128 -0
  6. package/docs/RELEASE_CONTRACT.md +91 -0
  7. package/docs/agent-tools.md +106 -0
  8. package/docs/architecture.md +224 -0
  9. package/docs/configuration.md +178 -0
  10. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
  11. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
  12. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
  13. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
  14. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
  15. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
  16. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
  17. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
  18. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
  19. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
  20. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
  21. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
  22. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
  23. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
  24. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
  25. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
  26. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
  27. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
  28. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
  29. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
  30. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
  31. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
  32. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
  33. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
  34. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
  35. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
  36. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
  37. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
  38. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
  39. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
  40. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
  41. package/docs/evidence/README.md +16 -0
  42. package/docs/fts5.md +161 -0
  43. package/docs/tui.md +506 -0
  44. package/index.ts +1372 -0
  45. package/openclaw.plugin.json +136 -0
  46. package/package.json +66 -0
  47. package/src/assembler.ts +804 -0
  48. package/src/brain-cli.ts +316 -0
  49. package/src/brain-core/decay.ts +35 -0
  50. package/src/brain-core/episode.ts +82 -0
  51. package/src/brain-core/graph.ts +321 -0
  52. package/src/brain-core/health.ts +116 -0
  53. package/src/brain-core/mutator.ts +281 -0
  54. package/src/brain-core/pack.ts +117 -0
  55. package/src/brain-core/policy.ts +153 -0
  56. package/src/brain-core/replay.ts +1 -0
  57. package/src/brain-core/teacher.ts +105 -0
  58. package/src/brain-core/trace.ts +40 -0
  59. package/src/brain-core/traverse.ts +230 -0
  60. package/src/brain-core/types.ts +405 -0
  61. package/src/brain-core/update.ts +123 -0
  62. package/src/brain-harvest/human.ts +46 -0
  63. package/src/brain-harvest/scanner.ts +98 -0
  64. package/src/brain-harvest/self.ts +147 -0
  65. package/src/brain-runtime/assembler-extension.ts +230 -0
  66. package/src/brain-runtime/evidence-detectors.ts +68 -0
  67. package/src/brain-runtime/graph-io.ts +72 -0
  68. package/src/brain-runtime/harvester-extension.ts +98 -0
  69. package/src/brain-runtime/service.ts +659 -0
  70. package/src/brain-runtime/tools.ts +109 -0
  71. package/src/brain-runtime/worker-state.ts +106 -0
  72. package/src/brain-runtime/worker-supervisor.ts +169 -0
  73. package/src/brain-store/embedding.ts +179 -0
  74. package/src/brain-store/init.ts +347 -0
  75. package/src/brain-store/migrations.ts +188 -0
  76. package/src/brain-store/store.ts +816 -0
  77. package/src/brain-worker/child-runner.ts +321 -0
  78. package/src/brain-worker/jobs.ts +12 -0
  79. package/src/brain-worker/mutation-job.ts +5 -0
  80. package/src/brain-worker/promotion-job.ts +5 -0
  81. package/src/brain-worker/protocol.ts +79 -0
  82. package/src/brain-worker/teacher-job.ts +5 -0
  83. package/src/brain-worker/update-job.ts +5 -0
  84. package/src/brain-worker/worker.ts +422 -0
  85. package/src/compaction.ts +1332 -0
  86. package/src/db/config.ts +265 -0
  87. package/src/db/connection.ts +72 -0
  88. package/src/db/features.ts +42 -0
  89. package/src/db/migration.ts +561 -0
  90. package/src/engine.ts +1995 -0
  91. package/src/expansion-auth.ts +351 -0
  92. package/src/expansion-policy.ts +303 -0
  93. package/src/expansion.ts +383 -0
  94. package/src/integrity.ts +600 -0
  95. package/src/large-files.ts +527 -0
  96. package/src/openclaw-bridge.ts +22 -0
  97. package/src/retrieval.ts +357 -0
  98. package/src/store/conversation-store.ts +748 -0
  99. package/src/store/fts5-sanitize.ts +29 -0
  100. package/src/store/full-text-fallback.ts +74 -0
  101. package/src/store/index.ts +29 -0
  102. package/src/store/summary-store.ts +918 -0
  103. package/src/summarize.ts +847 -0
  104. package/src/tools/common.ts +53 -0
  105. package/src/tools/lcm-conversation-scope.ts +76 -0
  106. package/src/tools/lcm-describe-tool.ts +234 -0
  107. package/src/tools/lcm-expand-query-tool.ts +594 -0
  108. package/src/tools/lcm-expand-tool.delegation.ts +556 -0
  109. package/src/tools/lcm-expand-tool.ts +448 -0
  110. package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
  111. package/src/tools/lcm-grep-tool.ts +200 -0
  112. package/src/transcript-repair.ts +301 -0
  113. package/src/types.ts +149 -0
@@ -0,0 +1,265 @@
1
+ import { homedir } from "os";
2
+ import { dirname, join } from "path";
3
+
4
+ export type OpenClawBrainRuntimeConfig = {
5
+ enabled: boolean;
6
+ root: string;
7
+ budgetFraction: number;
8
+ maxHops: number;
9
+ maxSeeds: number;
10
+ semanticThreshold: number;
11
+ servingTemperature: number;
12
+ learningTemperature: number;
13
+ learningRate: number;
14
+ baselineAlpha: number;
15
+ decayRate: number;
16
+ trainerIntervalMs: number;
17
+ workerMode?: "child" | "in_process";
18
+ workerHeartbeatTimeoutMs?: number;
19
+ workerRestartDelayMs?: number;
20
+ teacherEnabled: boolean;
21
+ teacherProvider: string;
22
+ teacherModel: string;
23
+ mutationsEnabled: boolean;
24
+ replayEpisodeCount: number;
25
+ minFiredPerQuery: number;
26
+ maxDormantPercent: number;
27
+ maxOrphanCount: number;
28
+ shadowMode: boolean;
29
+ embeddingProvider: string;
30
+ embeddingModel: string;
31
+ embeddingBaseUrl: string;
32
+ };
33
+
34
+ export type LcmConfig = {
35
+ enabled: boolean;
36
+ databasePath: string;
37
+ contextThreshold: number;
38
+ freshTailCount: number;
39
+ leafMinFanout: number;
40
+ condensedMinFanout: number;
41
+ condensedMinFanoutHard: number;
42
+ incrementalMaxDepth: number;
43
+ leafChunkTokens: number;
44
+ leafTargetTokens: number;
45
+ condensedTargetTokens: number;
46
+ maxExpandTokens: number;
47
+ largeFileTokenThreshold: number;
48
+ /** Provider override for large-file text summarization. */
49
+ largeFileSummaryProvider: string;
50
+ /** Model override for large-file text summarization. */
51
+ largeFileSummaryModel: string;
52
+ autocompactDisabled: boolean;
53
+ /** IANA timezone for timestamps in summaries (from TZ env or system default) */
54
+ timezone: string;
55
+ /** When true, retroactively delete HEARTBEAT_OK turn cycles from LCM storage. */
56
+ pruneHeartbeatOk: boolean;
57
+ /** OpenClawBrain v2 runtime settings. */
58
+ brain?: OpenClawBrainRuntimeConfig;
59
+ };
60
+
61
+ /** Safely coerce an unknown value to a finite number, or return undefined. */
62
+ function toNumber(value: unknown): number | undefined {
63
+ if (typeof value === "number" && Number.isFinite(value)) return value;
64
+ if (typeof value === "string") {
65
+ const n = Number(value);
66
+ if (Number.isFinite(n)) return n;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ /** Safely coerce an unknown value to a boolean, or return undefined. */
72
+ function toBool(value: unknown): boolean | undefined {
73
+ if (typeof value === "boolean") return value;
74
+ if (value === "true") return true;
75
+ if (value === "false") return false;
76
+ return undefined;
77
+ }
78
+
79
+ /** Safely coerce an unknown value to a trimmed non-empty string, or return undefined. */
80
+ function toStr(value: unknown): string | undefined {
81
+ if (typeof value === "string") {
82
+ const trimmed = value.trim();
83
+ return trimmed.length > 0 ? trimmed : undefined;
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ /**
89
+ * Resolve LCM + OpenClawBrain configuration with three-tier precedence:
90
+ * 1. Environment variables (highest — backward compat)
91
+ * 2. Plugin config object (from plugins.entries.openclawbrain.config)
92
+ * 3. Hardcoded defaults (lowest)
93
+ */
94
+ export function resolveLcmConfig(
95
+ env: NodeJS.ProcessEnv = process.env,
96
+ pluginConfig?: Record<string, unknown>,
97
+ ): LcmConfig {
98
+ const pc = pluginConfig ?? {};
99
+ const databasePath =
100
+ env.LCM_DATABASE_PATH
101
+ ?? toStr(pc.dbPath)
102
+ ?? toStr(pc.databasePath)
103
+ ?? join(homedir(), ".openclaw", "lcm.db");
104
+ const brainRoot =
105
+ env.OPENCLAWBRAIN_ROOT?.trim()
106
+ ?? toStr(pc.brainRoot)
107
+ ?? join(dirname(databasePath), "openclawbrain");
108
+
109
+ return {
110
+ enabled:
111
+ env.LCM_ENABLED !== undefined
112
+ ? env.LCM_ENABLED !== "false"
113
+ : toBool(pc.enabled) ?? true,
114
+ databasePath,
115
+ contextThreshold:
116
+ (env.LCM_CONTEXT_THRESHOLD !== undefined ? parseFloat(env.LCM_CONTEXT_THRESHOLD) : undefined)
117
+ ?? toNumber(pc.contextThreshold) ?? 0.75,
118
+ freshTailCount:
119
+ (env.LCM_FRESH_TAIL_COUNT !== undefined ? parseInt(env.LCM_FRESH_TAIL_COUNT, 10) : undefined)
120
+ ?? toNumber(pc.freshTailCount) ?? 32,
121
+ leafMinFanout:
122
+ (env.LCM_LEAF_MIN_FANOUT !== undefined ? parseInt(env.LCM_LEAF_MIN_FANOUT, 10) : undefined)
123
+ ?? toNumber(pc.leafMinFanout) ?? 8,
124
+ condensedMinFanout:
125
+ (env.LCM_CONDENSED_MIN_FANOUT !== undefined ? parseInt(env.LCM_CONDENSED_MIN_FANOUT, 10) : undefined)
126
+ ?? toNumber(pc.condensedMinFanout) ?? 4,
127
+ condensedMinFanoutHard:
128
+ (env.LCM_CONDENSED_MIN_FANOUT_HARD !== undefined ? parseInt(env.LCM_CONDENSED_MIN_FANOUT_HARD, 10) : undefined)
129
+ ?? toNumber(pc.condensedMinFanoutHard) ?? 2,
130
+ incrementalMaxDepth:
131
+ (env.LCM_INCREMENTAL_MAX_DEPTH !== undefined ? parseInt(env.LCM_INCREMENTAL_MAX_DEPTH, 10) : undefined)
132
+ ?? toNumber(pc.incrementalMaxDepth) ?? 0,
133
+ leafChunkTokens:
134
+ (env.LCM_LEAF_CHUNK_TOKENS !== undefined ? parseInt(env.LCM_LEAF_CHUNK_TOKENS, 10) : undefined)
135
+ ?? toNumber(pc.leafChunkTokens) ?? 20000,
136
+ leafTargetTokens:
137
+ (env.LCM_LEAF_TARGET_TOKENS !== undefined ? parseInt(env.LCM_LEAF_TARGET_TOKENS, 10) : undefined)
138
+ ?? toNumber(pc.leafTargetTokens) ?? 1200,
139
+ condensedTargetTokens:
140
+ (env.LCM_CONDENSED_TARGET_TOKENS !== undefined ? parseInt(env.LCM_CONDENSED_TARGET_TOKENS, 10) : undefined)
141
+ ?? toNumber(pc.condensedTargetTokens) ?? 2000,
142
+ maxExpandTokens:
143
+ (env.LCM_MAX_EXPAND_TOKENS !== undefined ? parseInt(env.LCM_MAX_EXPAND_TOKENS, 10) : undefined)
144
+ ?? toNumber(pc.maxExpandTokens) ?? 4000,
145
+ largeFileTokenThreshold:
146
+ (env.LCM_LARGE_FILE_TOKEN_THRESHOLD !== undefined ? parseInt(env.LCM_LARGE_FILE_TOKEN_THRESHOLD, 10) : undefined)
147
+ ?? toNumber(pc.largeFileThresholdTokens)
148
+ ?? toNumber(pc.largeFileTokenThreshold)
149
+ ?? 25000,
150
+ largeFileSummaryProvider:
151
+ env.LCM_LARGE_FILE_SUMMARY_PROVIDER?.trim() ?? toStr(pc.largeFileSummaryProvider) ?? "",
152
+ largeFileSummaryModel:
153
+ env.LCM_LARGE_FILE_SUMMARY_MODEL?.trim() ?? toStr(pc.largeFileSummaryModel) ?? "",
154
+ autocompactDisabled:
155
+ env.LCM_AUTOCOMPACT_DISABLED !== undefined
156
+ ? env.LCM_AUTOCOMPACT_DISABLED === "true"
157
+ : toBool(pc.autocompactDisabled) ?? false,
158
+ timezone: env.TZ ?? toStr(pc.timezone) ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
159
+ pruneHeartbeatOk:
160
+ env.LCM_PRUNE_HEARTBEAT_OK !== undefined
161
+ ? env.LCM_PRUNE_HEARTBEAT_OK === "true"
162
+ : toBool(pc.pruneHeartbeatOk) ?? false,
163
+ brain: {
164
+ enabled:
165
+ env.OPENCLAWBRAIN_ENABLED !== undefined
166
+ ? env.OPENCLAWBRAIN_ENABLED !== "false"
167
+ : toBool(pc.brainEnabled) ?? true,
168
+ root: brainRoot,
169
+ budgetFraction:
170
+ (env.OPENCLAWBRAIN_BUDGET_FRACTION !== undefined
171
+ ? parseFloat(env.OPENCLAWBRAIN_BUDGET_FRACTION)
172
+ : undefined) ?? toNumber(pc.brainBudgetFraction) ?? 0.3,
173
+ maxHops:
174
+ (env.OPENCLAWBRAIN_MAX_HOPS !== undefined
175
+ ? parseInt(env.OPENCLAWBRAIN_MAX_HOPS, 10)
176
+ : undefined) ?? toNumber(pc.brainMaxHops) ?? 8,
177
+ maxSeeds:
178
+ (env.OPENCLAWBRAIN_MAX_SEEDS !== undefined
179
+ ? parseInt(env.OPENCLAWBRAIN_MAX_SEEDS, 10)
180
+ : undefined) ?? toNumber(pc.brainMaxSeeds) ?? 10,
181
+ semanticThreshold:
182
+ (env.OPENCLAWBRAIN_SEMANTIC_THRESHOLD !== undefined
183
+ ? parseFloat(env.OPENCLAWBRAIN_SEMANTIC_THRESHOLD)
184
+ : undefined) ?? toNumber(pc.brainSemanticThreshold) ?? 0.7,
185
+ servingTemperature:
186
+ (env.OPENCLAWBRAIN_SERVING_TEMPERATURE !== undefined
187
+ ? parseFloat(env.OPENCLAWBRAIN_SERVING_TEMPERATURE)
188
+ : undefined) ?? toNumber(pc.brainServingTemperature) ?? 0.1,
189
+ learningTemperature:
190
+ (env.OPENCLAWBRAIN_LEARNING_TEMPERATURE !== undefined
191
+ ? parseFloat(env.OPENCLAWBRAIN_LEARNING_TEMPERATURE)
192
+ : undefined) ?? toNumber(pc.brainLearningTemperature) ?? 1.0,
193
+ learningRate:
194
+ (env.OPENCLAWBRAIN_LEARNING_RATE !== undefined
195
+ ? parseFloat(env.OPENCLAWBRAIN_LEARNING_RATE)
196
+ : undefined) ?? toNumber(pc.brainLearningRate) ?? 0.01,
197
+ baselineAlpha:
198
+ (env.OPENCLAWBRAIN_BASELINE_ALPHA !== undefined
199
+ ? parseFloat(env.OPENCLAWBRAIN_BASELINE_ALPHA)
200
+ : undefined) ?? toNumber(pc.brainBaselineAlpha) ?? 0.1,
201
+ decayRate:
202
+ (env.OPENCLAWBRAIN_DECAY_RATE !== undefined
203
+ ? parseFloat(env.OPENCLAWBRAIN_DECAY_RATE)
204
+ : undefined) ?? toNumber(pc.brainDecayRate) ?? 0.995,
205
+ trainerIntervalMs:
206
+ (env.OPENCLAWBRAIN_TRAINER_INTERVAL_MS !== undefined
207
+ ? parseInt(env.OPENCLAWBRAIN_TRAINER_INTERVAL_MS, 10)
208
+ : undefined) ?? toNumber(pc.brainTrainerIntervalMs) ?? 30_000,
209
+ workerMode:
210
+ env.OPENCLAWBRAIN_WORKER_MODE?.trim() === "in_process"
211
+ ? "in_process"
212
+ : ((toStr(pc.brainWorkerMode) === "in_process" ? "in_process" : undefined) ?? "child"),
213
+ workerHeartbeatTimeoutMs:
214
+ (env.OPENCLAWBRAIN_WORKER_HEARTBEAT_TIMEOUT_MS !== undefined
215
+ ? parseInt(env.OPENCLAWBRAIN_WORKER_HEARTBEAT_TIMEOUT_MS, 10)
216
+ : undefined) ?? toNumber(pc.brainWorkerHeartbeatTimeoutMs) ?? 90_000,
217
+ workerRestartDelayMs:
218
+ (env.OPENCLAWBRAIN_WORKER_RESTART_DELAY_MS !== undefined
219
+ ? parseInt(env.OPENCLAWBRAIN_WORKER_RESTART_DELAY_MS, 10)
220
+ : undefined) ?? toNumber(pc.brainWorkerRestartDelayMs) ?? 5_000,
221
+ teacherEnabled:
222
+ env.OPENCLAWBRAIN_TEACHER_ENABLED !== undefined
223
+ ? env.OPENCLAWBRAIN_TEACHER_ENABLED !== "false"
224
+ : toBool(pc.brainTeacherEnabled) ?? true,
225
+ teacherProvider:
226
+ env.OPENCLAWBRAIN_TEACHER_PROVIDER?.trim() ?? toStr(pc.brainTeacherProvider) ?? "",
227
+ teacherModel:
228
+ env.OPENCLAWBRAIN_TEACHER_MODEL?.trim() ?? toStr(pc.brainTeacherModel) ?? "",
229
+ mutationsEnabled:
230
+ env.OPENCLAWBRAIN_MUTATIONS_ENABLED !== undefined
231
+ ? env.OPENCLAWBRAIN_MUTATIONS_ENABLED !== "false"
232
+ : toBool(pc.brainMutationsEnabled) ?? true,
233
+ replayEpisodeCount:
234
+ (env.OPENCLAWBRAIN_REPLAY_EPISODE_COUNT !== undefined
235
+ ? parseInt(env.OPENCLAWBRAIN_REPLAY_EPISODE_COUNT, 10)
236
+ : undefined) ?? toNumber(pc.brainReplayEpisodeCount) ?? 100,
237
+ minFiredPerQuery:
238
+ (env.OPENCLAWBRAIN_MIN_FIRED_PER_QUERY !== undefined
239
+ ? parseFloat(env.OPENCLAWBRAIN_MIN_FIRED_PER_QUERY)
240
+ : undefined) ?? toNumber(pc.brainMinFiredPerQuery) ?? 1.0,
241
+ maxDormantPercent:
242
+ (env.OPENCLAWBRAIN_MAX_DORMANT_PERCENT !== undefined
243
+ ? parseFloat(env.OPENCLAWBRAIN_MAX_DORMANT_PERCENT)
244
+ : undefined) ?? toNumber(pc.brainMaxDormantPercent) ?? 0.3,
245
+ maxOrphanCount:
246
+ (env.OPENCLAWBRAIN_MAX_ORPHAN_COUNT !== undefined
247
+ ? parseInt(env.OPENCLAWBRAIN_MAX_ORPHAN_COUNT, 10)
248
+ : undefined) ?? toNumber(pc.brainMaxOrphanCount) ?? 10,
249
+ shadowMode:
250
+ env.OPENCLAWBRAIN_SHADOW_MODE !== undefined
251
+ ? env.OPENCLAWBRAIN_SHADOW_MODE === "true"
252
+ : toBool(pc.brainShadowMode) ?? false,
253
+ embeddingProvider:
254
+ env.OPENCLAWBRAIN_EMBEDDING_PROVIDER?.trim()
255
+ ?? toStr(pc.brainEmbeddingProvider)
256
+ ?? "openai",
257
+ embeddingModel:
258
+ env.OPENCLAWBRAIN_EMBEDDING_MODEL?.trim() ?? toStr(pc.brainEmbeddingModel) ?? "",
259
+ embeddingBaseUrl:
260
+ env.OPENCLAWBRAIN_EMBEDDING_BASE_URL?.trim()
261
+ ?? toStr(pc.brainEmbeddingBaseUrl)
262
+ ?? "",
263
+ },
264
+ };
265
+ }
@@ -0,0 +1,72 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { mkdirSync } from "fs";
3
+ import { dirname } from "path";
4
+
5
+ type ConnectionEntry = {
6
+ db: DatabaseSync;
7
+ refs: number;
8
+ };
9
+
10
+ const _connections = new Map<string, ConnectionEntry>();
11
+
12
+ function isConnectionHealthy(db: DatabaseSync): boolean {
13
+ try {
14
+ db.prepare("SELECT 1").get();
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function forceCloseConnection(entry: ConnectionEntry): void {
22
+ try {
23
+ entry.db.close();
24
+ } catch {
25
+ // Ignore close failures; caller is already replacing/removing this handle.
26
+ }
27
+ }
28
+
29
+ export function getLcmConnection(dbPath: string): DatabaseSync {
30
+ const existing = _connections.get(dbPath);
31
+ if (existing) {
32
+ if (isConnectionHealthy(existing.db)) {
33
+ existing.refs += 1;
34
+ return existing.db;
35
+ }
36
+ forceCloseConnection(existing);
37
+ _connections.delete(dbPath);
38
+ }
39
+
40
+ // Ensure parent directory exists
41
+ mkdirSync(dirname(dbPath), { recursive: true });
42
+
43
+ const db = new DatabaseSync(dbPath);
44
+
45
+ // Enable WAL mode for better concurrent read performance
46
+ db.exec("PRAGMA journal_mode = WAL");
47
+ // Enable foreign key enforcement
48
+ db.exec("PRAGMA foreign_keys = ON");
49
+
50
+ _connections.set(dbPath, { db, refs: 1 });
51
+ return db;
52
+ }
53
+
54
+ export function closeLcmConnection(dbPath?: string): void {
55
+ if (typeof dbPath === "string" && dbPath.trim()) {
56
+ const entry = _connections.get(dbPath);
57
+ if (!entry) {
58
+ return;
59
+ }
60
+ entry.refs = Math.max(0, entry.refs - 1);
61
+ if (entry.refs === 0) {
62
+ forceCloseConnection(entry);
63
+ _connections.delete(dbPath);
64
+ }
65
+ return;
66
+ }
67
+
68
+ for (const entry of _connections.values()) {
69
+ forceCloseConnection(entry);
70
+ }
71
+ _connections.clear();
72
+ }
@@ -0,0 +1,42 @@
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
+ }