memorix 1.1.7 → 1.1.8

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 (174) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CLAUDE.md +6 -1
  3. package/README.md +21 -0
  4. package/README.zh-CN.md +21 -0
  5. package/TEAM.md +86 -86
  6. package/dist/cli/index.js +730 -150
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/dashboard/static/index.html +201 -201
  9. package/dist/dashboard/static/style.css +3584 -3584
  10. package/dist/index.js +15 -8
  11. package/dist/index.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +13 -0
  13. package/dist/memcode-runtime/package.json +4 -4
  14. package/dist/sdk.js +15 -8
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
  17. package/docs/DESIGN_DECISIONS.md +357 -357
  18. package/docs/SETUP.md +10 -0
  19. package/docs/dev-log/progress.txt +35 -26
  20. package/package.json +1 -1
  21. package/src/audit/index.ts +156 -156
  22. package/src/cli/commands/agent-integrations.ts +623 -0
  23. package/src/cli/commands/audit-list.ts +89 -89
  24. package/src/cli/commands/background.ts +659 -659
  25. package/src/cli/commands/cleanup.ts +255 -255
  26. package/src/cli/commands/doctor.ts +23 -0
  27. package/src/cli/commands/formation.ts +48 -48
  28. package/src/cli/commands/git-hook-install.ts +111 -111
  29. package/src/cli/commands/handoff.ts +66 -66
  30. package/src/cli/commands/hooks-status.ts +63 -63
  31. package/src/cli/commands/ingest-commit.ts +153 -153
  32. package/src/cli/commands/ingest-image.ts +73 -73
  33. package/src/cli/commands/ingest-log.ts +180 -180
  34. package/src/cli/commands/ingest.ts +44 -44
  35. package/src/cli/commands/integrate-shared.ts +15 -15
  36. package/src/cli/commands/lock.ts +96 -96
  37. package/src/cli/commands/message.ts +121 -121
  38. package/src/cli/commands/poll.ts +70 -70
  39. package/src/cli/commands/purge-all-memory.ts +85 -85
  40. package/src/cli/commands/purge-project-memory.ts +83 -83
  41. package/src/cli/commands/reasoning.ts +132 -132
  42. package/src/cli/commands/repair.ts +60 -0
  43. package/src/cli/commands/retention.ts +108 -108
  44. package/src/cli/commands/serve-shared.ts +118 -118
  45. package/src/cli/commands/setup.ts +3 -3
  46. package/src/cli/commands/skills.ts +123 -123
  47. package/src/cli/commands/task.ts +192 -192
  48. package/src/cli/commands/transfer.ts +73 -73
  49. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  50. package/src/cli/index.ts +3 -1
  51. package/src/cli/tui/ChatView.tsx +234 -234
  52. package/src/cli/tui/CommandBar.tsx +312 -312
  53. package/src/cli/tui/ContextRail.tsx +118 -118
  54. package/src/cli/tui/HeaderBar.tsx +72 -72
  55. package/src/cli/tui/LogoBanner.tsx +51 -51
  56. package/src/cli/tui/Panels.tsx +632 -632
  57. package/src/cli/tui/Sidebar.tsx +179 -179
  58. package/src/cli/tui/chat-service.ts +742 -742
  59. package/src/cli/tui/data.ts +547 -547
  60. package/src/cli/tui/index.ts +41 -41
  61. package/src/cli/tui/markdown-render.tsx +371 -371
  62. package/src/cli/tui/theme.ts +178 -178
  63. package/src/cli/tui/use-mouse.ts +157 -157
  64. package/src/cli/tui/useNavigation.ts +56 -56
  65. package/src/cli/update-checker.ts +211 -211
  66. package/src/cli/version.ts +7 -7
  67. package/src/cli/workbench.ts +1 -1
  68. package/src/codegraph/context-pack.ts +5 -1
  69. package/src/codegraph/lite-provider.ts +5 -1
  70. package/src/codegraph/project-context.ts +5 -1
  71. package/src/compact/token-budget.ts +74 -74
  72. package/src/config/behavior.ts +59 -59
  73. package/src/dashboard/project-classification.ts +64 -64
  74. package/src/dashboard/static/index.html +201 -201
  75. package/src/dashboard/static/style.css +3584 -3584
  76. package/src/embedding/fastembed-provider.ts +142 -142
  77. package/src/embedding/transformers-provider.ts +111 -111
  78. package/src/git/extractor.ts +209 -209
  79. package/src/git/hooks-path.ts +85 -85
  80. package/src/git/noise-filter.ts +210 -210
  81. package/src/hooks/installers/index.ts +4 -4
  82. package/src/hooks/official-skills.ts +1 -1
  83. package/src/hooks/pattern-detector.ts +173 -173
  84. package/src/hooks/rules/memorix-agent-rules.md +2 -2
  85. package/src/hooks/significance-filter.ts +250 -250
  86. package/src/llm/memory-manager.ts +328 -328
  87. package/src/llm/provider.ts +885 -885
  88. package/src/llm/quality.ts +248 -248
  89. package/src/memory/attribution-guard.ts +249 -249
  90. package/src/memory/auto-relations.ts +107 -107
  91. package/src/memory/consolidation.ts +302 -302
  92. package/src/memory/disclosure-policy.ts +141 -141
  93. package/src/memory/entity-extractor.ts +197 -197
  94. package/src/memory/formation/evaluate.ts +217 -217
  95. package/src/memory/formation/extract.ts +361 -361
  96. package/src/memory/formation/index.ts +417 -417
  97. package/src/memory/formation/resolve.ts +344 -344
  98. package/src/memory/formation/types.ts +315 -315
  99. package/src/memory/freshness.ts +122 -122
  100. package/src/memory/graph.ts +197 -197
  101. package/src/memory/refs.ts +94 -94
  102. package/src/memory/retention.ts +433 -433
  103. package/src/memory/secret-filter.ts +79 -79
  104. package/src/memory/session.ts +523 -523
  105. package/src/multimodal/image-loader.ts +143 -143
  106. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  107. package/src/orchestrate/adapters/claude.ts +111 -111
  108. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  109. package/src/orchestrate/adapters/codex.ts +41 -41
  110. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  111. package/src/orchestrate/adapters/gemini.ts +42 -42
  112. package/src/orchestrate/adapters/index.ts +73 -73
  113. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  114. package/src/orchestrate/adapters/opencode.ts +47 -47
  115. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  116. package/src/orchestrate/adapters/types.ts +77 -77
  117. package/src/orchestrate/capability-router.ts +284 -284
  118. package/src/orchestrate/context-compact.ts +188 -188
  119. package/src/orchestrate/cost-tracker.ts +219 -219
  120. package/src/orchestrate/error-recovery.ts +191 -191
  121. package/src/orchestrate/evidence.ts +140 -140
  122. package/src/orchestrate/ledger.ts +110 -110
  123. package/src/orchestrate/memorix-bridge.ts +380 -380
  124. package/src/orchestrate/output-budget.ts +80 -80
  125. package/src/orchestrate/permission.ts +152 -152
  126. package/src/orchestrate/pipeline-trace.ts +131 -131
  127. package/src/orchestrate/prompt-builder.ts +155 -155
  128. package/src/orchestrate/ring-buffer.ts +37 -37
  129. package/src/orchestrate/task-graph.ts +389 -389
  130. package/src/orchestrate/verify-gate.ts +219 -219
  131. package/src/orchestrate/worktree.ts +232 -232
  132. package/src/project/aliases.ts +374 -374
  133. package/src/project/detector.ts +268 -268
  134. package/src/rules/adapters/claude-code.ts +99 -99
  135. package/src/rules/adapters/codex.ts +97 -97
  136. package/src/rules/adapters/copilot.ts +124 -124
  137. package/src/rules/adapters/cursor.ts +114 -114
  138. package/src/rules/adapters/kiro.ts +126 -126
  139. package/src/rules/adapters/trae.ts +56 -56
  140. package/src/rules/adapters/windsurf.ts +83 -83
  141. package/src/rules/syncer.ts +235 -235
  142. package/src/sdk.ts +327 -327
  143. package/src/search/intent-detector.ts +289 -289
  144. package/src/search/query-expansion.ts +52 -52
  145. package/src/server/formation-timeout.ts +27 -27
  146. package/src/skills/mini-skills.ts +386 -386
  147. package/src/store/chat-store.ts +119 -119
  148. package/src/store/file-lock.ts +100 -100
  149. package/src/store/graph-store.ts +249 -249
  150. package/src/store/mini-skill-store.ts +349 -349
  151. package/src/store/obs-store.ts +255 -255
  152. package/src/store/persistence-json.ts +212 -212
  153. package/src/store/persistence.ts +291 -291
  154. package/src/store/project-affinity.ts +195 -195
  155. package/src/store/session-store.ts +259 -259
  156. package/src/store/sqlite-store.ts +339 -339
  157. package/src/team/event-bus.ts +76 -76
  158. package/src/team/file-locks.ts +173 -173
  159. package/src/team/handoff.ts +167 -167
  160. package/src/team/messages.ts +203 -203
  161. package/src/team/poll.ts +132 -132
  162. package/src/team/tasks.ts +211 -211
  163. package/src/wiki/generator.ts +237 -237
  164. package/src/wiki/knowledge-graph.ts +334 -334
  165. package/src/wiki/types.ts +85 -85
  166. package/src/workspace/mcp-adapters/codex.ts +191 -191
  167. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  168. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  169. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  170. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  171. package/src/workspace/mcp-adapters/trae.ts +134 -134
  172. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  173. package/src/workspace/sanitizer.ts +60 -60
  174. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,255 +1,255 @@
1
- /**
2
- * ObservationStore — unified persistence abstraction for observations.
3
- *
4
- * Backends:
5
- * - SqliteBackend (sqlite-store.ts) — WAL-mode SQLite with generation tracking
6
- * - DegradedBackend — read-only empty store when SQLite is unavailable
7
- *
8
- * JSON is no longer a runtime writable backend.
9
- * observations.json is only used as a one-time migration source into SQLite.
10
- *
11
- * All observation persistence flows through this interface.
12
- */
13
-
14
- import type { Observation } from '../types.js';
15
-
16
- /**
17
- * Raw transaction handle for compound atomic operations.
18
- *
19
- * Inside an `atomic()` block the caller gets a StoreTransaction whose methods
20
- * operate directly on the underlying storage WITHOUT acquiring their own lock.
21
- * The outer `atomic()` already holds the lock / transaction.
22
- */
23
- export interface StoreTransaction {
24
- /** Load all observations (raw, no lock). */
25
- loadAll(): Promise<Observation[]>;
26
- /** Load the ID counter (raw, no lock). */
27
- loadIdCounter(): Promise<number>;
28
- /** Save all observations (raw, no lock). */
29
- saveAll(obs: Observation[]): Promise<void>;
30
- /** Save the ID counter (raw, no lock). */
31
- saveIdCounter(nextId: number): Promise<void>;
32
- }
33
-
34
- export interface ObservationStore {
35
- // ── Lifecycle ──────────────────────────────────────────────────────
36
-
37
- /** One-time init: open DB/files, run migration if needed */
38
- init(dataDir: string): Promise<void>;
39
-
40
- // ── Read ───────────────────────────────────────────────────────────
41
-
42
- /** Load all observations into memory. Called at startup after init(). */
43
- loadAll(): Promise<Observation[]>;
44
-
45
- /** Load the current next-ID counter value. */
46
- loadIdCounter(): Promise<number>;
47
-
48
- // ── Write — single mutations ───────────────────────────────────────
49
-
50
- /** Insert a new observation. Bumps generation (if applicable). */
51
- insert(obs: Observation): Promise<void>;
52
-
53
- /** Update an existing observation in-place (matched by obs.id). */
54
- update(obs: Observation): Promise<void>;
55
-
56
- /** Remove a single observation by ID. */
57
- remove(id: number): Promise<void>;
58
-
59
- // ── Write — batch ──────────────────────────────────────────────────
60
-
61
- /**
62
- * Replace the entire observation set atomically.
63
- * Used by consolidation, cleanup, and project-ID migration.
64
- */
65
- bulkReplace(obs: Observation[]): Promise<void>;
66
-
67
- /** Remove multiple observations by ID in one operation. */
68
- bulkRemoveByIds(ids: number[]): Promise<void>;
69
-
70
- /** Persist the next-ID counter. */
71
- saveIdCounter(nextId: number): Promise<void>;
72
-
73
- // ── Compound atomic operations ─────────────────────────────────────
74
-
75
- /**
76
- * Execute fn while holding an exclusive lock (file lock for JSON,
77
- * transaction for SQLite). The StoreTransaction provides raw load/save
78
- * methods that operate within the lock scope.
79
- *
80
- * Used by storeObservation for compound topicKey-TOCTOU + ID-assignment.
81
- */
82
- atomic<T>(fn: (tx: StoreTransaction) => Promise<T>): Promise<T>;
83
-
84
- // ── Freshness (cross-process coherence) ────────────────────────────
85
-
86
- /**
87
- * Check if another process has mutated the store since our last read.
88
- * If yes, the caller should reload observations[] and rebuild the Orama index.
89
- *
90
- * - SqliteBackend: compares storage_generation in meta table vs local knownGeneration
91
- * - DegradedBackend: no-op, returns false (no data to refresh)
92
- *
93
- * @returns true if the local cache is stale and was refreshed
94
- */
95
- ensureFresh(): Promise<boolean>;
96
-
97
- /** Current known generation counter (local). */
98
- getGeneration(): number;
99
-
100
- // ── Lifecycle ─────────────────────────────────────────────────────
101
-
102
- /** Close the backend (release DB handles, file locks, etc.). */
103
- close(): void;
104
-
105
- // ── Diagnostics ────────────────────────────────────────────────────
106
-
107
- /** Which backend is active: 'sqlite' or 'degraded' (read-only). */
108
- getBackendName(): 'sqlite' | 'degraded';
109
- }
110
-
111
- // ── Singleton store access ─────────────────────────────────────────
112
-
113
- let _store: ObservationStore | null = null;
114
- let _storeDataDir: string | null = null;
115
-
116
- /** Get the active ObservationStore singleton. Throws if not yet initialized. */
117
- export function getObservationStore(): ObservationStore {
118
- if (!_store) {
119
- throw new Error('[memorix] ObservationStore not initialized — call initObservationStore() first');
120
- }
121
- return _store;
122
- }
123
-
124
- /** Set the active ObservationStore singleton (called once during startup). */
125
- export function setObservationStore(store: ObservationStore): void {
126
- _store = store;
127
- }
128
-
129
- /** Reset the singleton (for tests only). Detaches from the backend.
130
- * Call closeAllDatabases() separately if you need to release the shared DB handle. */
131
- export function resetObservationStore(): void {
132
- if (_store) {
133
- try { _store.close(); } catch { /* best-effort */ }
134
- }
135
- _store = null;
136
- _storeDataDir = null;
137
- }
138
-
139
- /**
140
- * Create a fresh ObservationStore instance for a specific data directory
141
- * without touching the process-wide singleton. This is useful for long-lived
142
- * multi-project hosts (for example serve-http embedded dashboard APIs) where
143
- * requests may need to read different project data dirs concurrently.
144
- */
145
- export async function createObservationStore(dataDir: string): Promise<ObservationStore> {
146
- // Try SQLite first (optionalDependencies — may not be installed)
147
- try {
148
- const { SqliteBackend } = await import('./sqlite-store.js');
149
- const store = new SqliteBackend();
150
- await store.init(dataDir);
151
- return store;
152
- } catch (err) {
153
- console.error(`[memorix] SQLite backend unavailable — degraded mode (read-only): ${err instanceof Error ? err.message : err}`);
154
- }
155
-
156
- // No writable JSON fallback — degraded read-only mode instead
157
- // observations.json is only used as migration source, not runtime backend
158
- const store = new DegradedBackend();
159
- await store.init(dataDir);
160
- return store;
161
- }
162
-
163
- /**
164
- * Initialize the ObservationStore singleton for the given data directory.
165
- *
166
- * Tries SQLite first. If unavailable, falls back to DegradedBackend (read-only).
167
- *
168
- * Idempotent: if already initialized for the same dataDir, returns the existing store.
169
- */
170
- export async function initObservationStore(dataDir: string): Promise<ObservationStore> {
171
- if (_store && _storeDataDir === dataDir) {
172
- return _store;
173
- }
174
-
175
- // Close previous store if switching directories
176
- if (_store) {
177
- try { _store.close(); } catch { /* best-effort */ }
178
- _store = null;
179
- _storeDataDir = null;
180
- }
181
-
182
- const store = await createObservationStore(dataDir);
183
- _store = store;
184
- _storeDataDir = dataDir;
185
- return store;
186
- }
187
-
188
- // ── DegradedBackend (read-only when SQLite unavailable) ──────────
189
-
190
- /**
191
- * DegradedBackend — ObservationStore that is read-only and empty.
192
- *
193
- * Used when better-sqlite3 is unavailable. All write operations throw.
194
- * This ensures the system does not silently fall back to writing observations.json
195
- * as a runtime canonical store.
196
- */
197
- export class DegradedBackend implements ObservationStore {
198
- private dataDir: string = '';
199
-
200
- async init(dataDir: string): Promise<void> {
201
- this.dataDir = dataDir;
202
- }
203
-
204
- async loadAll(): Promise<Observation[]> {
205
- return [];
206
- }
207
-
208
- async loadIdCounter(): Promise<number> {
209
- return 1;
210
- }
211
-
212
- async insert(_obs: Observation): Promise<void> {
213
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
214
- }
215
-
216
- async update(_obs: Observation): Promise<void> {
217
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
218
- }
219
-
220
- async remove(_id: number): Promise<void> {
221
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
222
- }
223
-
224
- async bulkReplace(_obs: Observation[]): Promise<void> {
225
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
226
- }
227
-
228
- async bulkRemoveByIds(_ids: number[]): Promise<void> {
229
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
230
- }
231
-
232
- async saveIdCounter(_nextId: number): Promise<void> {
233
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
234
- }
235
-
236
- async atomic<T>(_fn: (tx: StoreTransaction) => Promise<T>): Promise<T> {
237
- throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
238
- }
239
-
240
- async ensureFresh(): Promise<boolean> {
241
- return false;
242
- }
243
-
244
- getGeneration(): number {
245
- return 0;
246
- }
247
-
248
- close(): void {
249
- // No resources to release
250
- }
251
-
252
- getBackendName(): 'sqlite' | 'degraded' {
253
- return 'degraded';
254
- }
255
- }
1
+ /**
2
+ * ObservationStore — unified persistence abstraction for observations.
3
+ *
4
+ * Backends:
5
+ * - SqliteBackend (sqlite-store.ts) — WAL-mode SQLite with generation tracking
6
+ * - DegradedBackend — read-only empty store when SQLite is unavailable
7
+ *
8
+ * JSON is no longer a runtime writable backend.
9
+ * observations.json is only used as a one-time migration source into SQLite.
10
+ *
11
+ * All observation persistence flows through this interface.
12
+ */
13
+
14
+ import type { Observation } from '../types.js';
15
+
16
+ /**
17
+ * Raw transaction handle for compound atomic operations.
18
+ *
19
+ * Inside an `atomic()` block the caller gets a StoreTransaction whose methods
20
+ * operate directly on the underlying storage WITHOUT acquiring their own lock.
21
+ * The outer `atomic()` already holds the lock / transaction.
22
+ */
23
+ export interface StoreTransaction {
24
+ /** Load all observations (raw, no lock). */
25
+ loadAll(): Promise<Observation[]>;
26
+ /** Load the ID counter (raw, no lock). */
27
+ loadIdCounter(): Promise<number>;
28
+ /** Save all observations (raw, no lock). */
29
+ saveAll(obs: Observation[]): Promise<void>;
30
+ /** Save the ID counter (raw, no lock). */
31
+ saveIdCounter(nextId: number): Promise<void>;
32
+ }
33
+
34
+ export interface ObservationStore {
35
+ // ── Lifecycle ──────────────────────────────────────────────────────
36
+
37
+ /** One-time init: open DB/files, run migration if needed */
38
+ init(dataDir: string): Promise<void>;
39
+
40
+ // ── Read ───────────────────────────────────────────────────────────
41
+
42
+ /** Load all observations into memory. Called at startup after init(). */
43
+ loadAll(): Promise<Observation[]>;
44
+
45
+ /** Load the current next-ID counter value. */
46
+ loadIdCounter(): Promise<number>;
47
+
48
+ // ── Write — single mutations ───────────────────────────────────────
49
+
50
+ /** Insert a new observation. Bumps generation (if applicable). */
51
+ insert(obs: Observation): Promise<void>;
52
+
53
+ /** Update an existing observation in-place (matched by obs.id). */
54
+ update(obs: Observation): Promise<void>;
55
+
56
+ /** Remove a single observation by ID. */
57
+ remove(id: number): Promise<void>;
58
+
59
+ // ── Write — batch ──────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Replace the entire observation set atomically.
63
+ * Used by consolidation, cleanup, and project-ID migration.
64
+ */
65
+ bulkReplace(obs: Observation[]): Promise<void>;
66
+
67
+ /** Remove multiple observations by ID in one operation. */
68
+ bulkRemoveByIds(ids: number[]): Promise<void>;
69
+
70
+ /** Persist the next-ID counter. */
71
+ saveIdCounter(nextId: number): Promise<void>;
72
+
73
+ // ── Compound atomic operations ─────────────────────────────────────
74
+
75
+ /**
76
+ * Execute fn while holding an exclusive lock (file lock for JSON,
77
+ * transaction for SQLite). The StoreTransaction provides raw load/save
78
+ * methods that operate within the lock scope.
79
+ *
80
+ * Used by storeObservation for compound topicKey-TOCTOU + ID-assignment.
81
+ */
82
+ atomic<T>(fn: (tx: StoreTransaction) => Promise<T>): Promise<T>;
83
+
84
+ // ── Freshness (cross-process coherence) ────────────────────────────
85
+
86
+ /**
87
+ * Check if another process has mutated the store since our last read.
88
+ * If yes, the caller should reload observations[] and rebuild the Orama index.
89
+ *
90
+ * - SqliteBackend: compares storage_generation in meta table vs local knownGeneration
91
+ * - DegradedBackend: no-op, returns false (no data to refresh)
92
+ *
93
+ * @returns true if the local cache is stale and was refreshed
94
+ */
95
+ ensureFresh(): Promise<boolean>;
96
+
97
+ /** Current known generation counter (local). */
98
+ getGeneration(): number;
99
+
100
+ // ── Lifecycle ─────────────────────────────────────────────────────
101
+
102
+ /** Close the backend (release DB handles, file locks, etc.). */
103
+ close(): void;
104
+
105
+ // ── Diagnostics ────────────────────────────────────────────────────
106
+
107
+ /** Which backend is active: 'sqlite' or 'degraded' (read-only). */
108
+ getBackendName(): 'sqlite' | 'degraded';
109
+ }
110
+
111
+ // ── Singleton store access ─────────────────────────────────────────
112
+
113
+ let _store: ObservationStore | null = null;
114
+ let _storeDataDir: string | null = null;
115
+
116
+ /** Get the active ObservationStore singleton. Throws if not yet initialized. */
117
+ export function getObservationStore(): ObservationStore {
118
+ if (!_store) {
119
+ throw new Error('[memorix] ObservationStore not initialized — call initObservationStore() first');
120
+ }
121
+ return _store;
122
+ }
123
+
124
+ /** Set the active ObservationStore singleton (called once during startup). */
125
+ export function setObservationStore(store: ObservationStore): void {
126
+ _store = store;
127
+ }
128
+
129
+ /** Reset the singleton (for tests only). Detaches from the backend.
130
+ * Call closeAllDatabases() separately if you need to release the shared DB handle. */
131
+ export function resetObservationStore(): void {
132
+ if (_store) {
133
+ try { _store.close(); } catch { /* best-effort */ }
134
+ }
135
+ _store = null;
136
+ _storeDataDir = null;
137
+ }
138
+
139
+ /**
140
+ * Create a fresh ObservationStore instance for a specific data directory
141
+ * without touching the process-wide singleton. This is useful for long-lived
142
+ * multi-project hosts (for example serve-http embedded dashboard APIs) where
143
+ * requests may need to read different project data dirs concurrently.
144
+ */
145
+ export async function createObservationStore(dataDir: string): Promise<ObservationStore> {
146
+ // Try SQLite first (optionalDependencies — may not be installed)
147
+ try {
148
+ const { SqliteBackend } = await import('./sqlite-store.js');
149
+ const store = new SqliteBackend();
150
+ await store.init(dataDir);
151
+ return store;
152
+ } catch (err) {
153
+ console.error(`[memorix] SQLite backend unavailable — degraded mode (read-only): ${err instanceof Error ? err.message : err}`);
154
+ }
155
+
156
+ // No writable JSON fallback — degraded read-only mode instead
157
+ // observations.json is only used as migration source, not runtime backend
158
+ const store = new DegradedBackend();
159
+ await store.init(dataDir);
160
+ return store;
161
+ }
162
+
163
+ /**
164
+ * Initialize the ObservationStore singleton for the given data directory.
165
+ *
166
+ * Tries SQLite first. If unavailable, falls back to DegradedBackend (read-only).
167
+ *
168
+ * Idempotent: if already initialized for the same dataDir, returns the existing store.
169
+ */
170
+ export async function initObservationStore(dataDir: string): Promise<ObservationStore> {
171
+ if (_store && _storeDataDir === dataDir) {
172
+ return _store;
173
+ }
174
+
175
+ // Close previous store if switching directories
176
+ if (_store) {
177
+ try { _store.close(); } catch { /* best-effort */ }
178
+ _store = null;
179
+ _storeDataDir = null;
180
+ }
181
+
182
+ const store = await createObservationStore(dataDir);
183
+ _store = store;
184
+ _storeDataDir = dataDir;
185
+ return store;
186
+ }
187
+
188
+ // ── DegradedBackend (read-only when SQLite unavailable) ──────────
189
+
190
+ /**
191
+ * DegradedBackend — ObservationStore that is read-only and empty.
192
+ *
193
+ * Used when better-sqlite3 is unavailable. All write operations throw.
194
+ * This ensures the system does not silently fall back to writing observations.json
195
+ * as a runtime canonical store.
196
+ */
197
+ export class DegradedBackend implements ObservationStore {
198
+ private dataDir: string = '';
199
+
200
+ async init(dataDir: string): Promise<void> {
201
+ this.dataDir = dataDir;
202
+ }
203
+
204
+ async loadAll(): Promise<Observation[]> {
205
+ return [];
206
+ }
207
+
208
+ async loadIdCounter(): Promise<number> {
209
+ return 1;
210
+ }
211
+
212
+ async insert(_obs: Observation): Promise<void> {
213
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
214
+ }
215
+
216
+ async update(_obs: Observation): Promise<void> {
217
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
218
+ }
219
+
220
+ async remove(_id: number): Promise<void> {
221
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
222
+ }
223
+
224
+ async bulkReplace(_obs: Observation[]): Promise<void> {
225
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
226
+ }
227
+
228
+ async bulkRemoveByIds(_ids: number[]): Promise<void> {
229
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
230
+ }
231
+
232
+ async saveIdCounter(_nextId: number): Promise<void> {
233
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
234
+ }
235
+
236
+ async atomic<T>(_fn: (tx: StoreTransaction) => Promise<T>): Promise<T> {
237
+ throw new Error('[memorix] Cannot write observations: SQLite backend unavailable (degraded mode)');
238
+ }
239
+
240
+ async ensureFresh(): Promise<boolean> {
241
+ return false;
242
+ }
243
+
244
+ getGeneration(): number {
245
+ return 0;
246
+ }
247
+
248
+ close(): void {
249
+ // No resources to release
250
+ }
251
+
252
+ getBackendName(): 'sqlite' | 'degraded' {
253
+ return 'degraded';
254
+ }
255
+ }