memorix 1.1.7 → 1.1.9

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 (185) hide show
  1. package/CHANGELOG.md +22 -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 +852 -214
  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 +129 -62
  11. package/dist/index.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +22 -0
  13. package/dist/memcode-runtime/package.json +4 -4
  14. package/dist/sdk.js +129 -62
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
  17. package/docs/API_REFERENCE.md +2 -0
  18. package/docs/CONFIGURATION.md +18 -0
  19. package/docs/DESIGN_DECISIONS.md +357 -357
  20. package/docs/SETUP.md +10 -0
  21. package/docs/dev-log/progress.txt +23 -30
  22. package/package.json +1 -1
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/agent-integrations.ts +623 -0
  25. package/src/cli/commands/audit-list.ts +89 -89
  26. package/src/cli/commands/background.ts +659 -659
  27. package/src/cli/commands/cleanup.ts +255 -255
  28. package/src/cli/commands/codegraph.ts +4 -0
  29. package/src/cli/commands/config-get.ts +9 -2
  30. package/src/cli/commands/doctor.ts +26 -0
  31. package/src/cli/commands/formation.ts +48 -48
  32. package/src/cli/commands/git-hook-install.ts +111 -111
  33. package/src/cli/commands/handoff.ts +66 -66
  34. package/src/cli/commands/hooks-status.ts +63 -63
  35. package/src/cli/commands/ingest-commit.ts +153 -153
  36. package/src/cli/commands/ingest-image.ts +73 -73
  37. package/src/cli/commands/ingest-log.ts +180 -180
  38. package/src/cli/commands/ingest.ts +44 -44
  39. package/src/cli/commands/integrate-shared.ts +15 -15
  40. package/src/cli/commands/lock.ts +96 -96
  41. package/src/cli/commands/message.ts +121 -121
  42. package/src/cli/commands/poll.ts +70 -70
  43. package/src/cli/commands/purge-all-memory.ts +85 -85
  44. package/src/cli/commands/purge-project-memory.ts +83 -83
  45. package/src/cli/commands/reasoning.ts +132 -132
  46. package/src/cli/commands/repair.ts +60 -0
  47. package/src/cli/commands/retention.ts +108 -108
  48. package/src/cli/commands/serve-shared.ts +118 -118
  49. package/src/cli/commands/setup.ts +3 -3
  50. package/src/cli/commands/skills.ts +123 -123
  51. package/src/cli/commands/task.ts +192 -192
  52. package/src/cli/commands/transfer.ts +73 -73
  53. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  54. package/src/cli/index.ts +3 -1
  55. package/src/cli/tui/ChatView.tsx +234 -234
  56. package/src/cli/tui/CommandBar.tsx +312 -312
  57. package/src/cli/tui/ContextRail.tsx +118 -118
  58. package/src/cli/tui/HeaderBar.tsx +72 -72
  59. package/src/cli/tui/LogoBanner.tsx +51 -51
  60. package/src/cli/tui/Panels.tsx +632 -632
  61. package/src/cli/tui/Sidebar.tsx +179 -179
  62. package/src/cli/tui/chat-service.ts +742 -742
  63. package/src/cli/tui/data.ts +547 -547
  64. package/src/cli/tui/index.ts +41 -41
  65. package/src/cli/tui/markdown-render.tsx +371 -371
  66. package/src/cli/tui/theme.ts +178 -178
  67. package/src/cli/tui/use-mouse.ts +157 -157
  68. package/src/cli/tui/useNavigation.ts +56 -56
  69. package/src/cli/update-checker.ts +211 -211
  70. package/src/cli/version.ts +7 -7
  71. package/src/cli/workbench.ts +1 -1
  72. package/src/codegraph/auto-context.ts +6 -0
  73. package/src/codegraph/context-pack.ts +7 -6
  74. package/src/codegraph/exclude.ts +47 -0
  75. package/src/codegraph/lite-provider.ts +5 -24
  76. package/src/codegraph/project-context.ts +13 -15
  77. package/src/compact/token-budget.ts +74 -74
  78. package/src/config/behavior.ts +59 -59
  79. package/src/config/resolved-config.ts +6 -0
  80. package/src/config/toml-loader.ts +4 -0
  81. package/src/config/yaml-loader.ts +7 -0
  82. package/src/dashboard/project-classification.ts +64 -64
  83. package/src/dashboard/static/index.html +201 -201
  84. package/src/dashboard/static/style.css +3584 -3584
  85. package/src/embedding/fastembed-provider.ts +142 -142
  86. package/src/embedding/transformers-provider.ts +111 -111
  87. package/src/git/extractor.ts +209 -209
  88. package/src/git/hooks-path.ts +85 -85
  89. package/src/git/noise-filter.ts +210 -210
  90. package/src/hooks/installers/index.ts +4 -4
  91. package/src/hooks/official-skills.ts +1 -1
  92. package/src/hooks/pattern-detector.ts +173 -173
  93. package/src/hooks/rules/memorix-agent-rules.md +2 -2
  94. package/src/hooks/significance-filter.ts +250 -250
  95. package/src/llm/memory-manager.ts +328 -328
  96. package/src/llm/provider.ts +885 -885
  97. package/src/llm/quality.ts +248 -248
  98. package/src/memory/attribution-guard.ts +249 -249
  99. package/src/memory/auto-relations.ts +107 -107
  100. package/src/memory/consolidation.ts +302 -302
  101. package/src/memory/disclosure-policy.ts +141 -141
  102. package/src/memory/entity-extractor.ts +197 -197
  103. package/src/memory/formation/evaluate.ts +217 -217
  104. package/src/memory/formation/extract.ts +361 -361
  105. package/src/memory/formation/index.ts +417 -417
  106. package/src/memory/formation/resolve.ts +344 -344
  107. package/src/memory/formation/types.ts +315 -315
  108. package/src/memory/freshness.ts +122 -122
  109. package/src/memory/graph.ts +197 -197
  110. package/src/memory/refs.ts +94 -94
  111. package/src/memory/retention.ts +433 -433
  112. package/src/memory/secret-filter.ts +79 -79
  113. package/src/memory/session.ts +523 -523
  114. package/src/multimodal/image-loader.ts +143 -143
  115. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  116. package/src/orchestrate/adapters/claude.ts +111 -111
  117. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  118. package/src/orchestrate/adapters/codex.ts +41 -41
  119. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  120. package/src/orchestrate/adapters/gemini.ts +42 -42
  121. package/src/orchestrate/adapters/index.ts +73 -73
  122. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  123. package/src/orchestrate/adapters/opencode.ts +47 -47
  124. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  125. package/src/orchestrate/adapters/types.ts +77 -77
  126. package/src/orchestrate/capability-router.ts +284 -284
  127. package/src/orchestrate/context-compact.ts +188 -188
  128. package/src/orchestrate/cost-tracker.ts +219 -219
  129. package/src/orchestrate/error-recovery.ts +191 -191
  130. package/src/orchestrate/evidence.ts +140 -140
  131. package/src/orchestrate/ledger.ts +110 -110
  132. package/src/orchestrate/memorix-bridge.ts +380 -380
  133. package/src/orchestrate/output-budget.ts +80 -80
  134. package/src/orchestrate/permission.ts +152 -152
  135. package/src/orchestrate/pipeline-trace.ts +131 -131
  136. package/src/orchestrate/prompt-builder.ts +155 -155
  137. package/src/orchestrate/ring-buffer.ts +37 -37
  138. package/src/orchestrate/task-graph.ts +389 -389
  139. package/src/orchestrate/verify-gate.ts +219 -219
  140. package/src/orchestrate/worktree.ts +232 -232
  141. package/src/project/aliases.ts +374 -374
  142. package/src/project/detector.ts +268 -268
  143. package/src/rules/adapters/claude-code.ts +99 -99
  144. package/src/rules/adapters/codex.ts +97 -97
  145. package/src/rules/adapters/copilot.ts +124 -124
  146. package/src/rules/adapters/cursor.ts +114 -114
  147. package/src/rules/adapters/kiro.ts +126 -126
  148. package/src/rules/adapters/trae.ts +56 -56
  149. package/src/rules/adapters/windsurf.ts +83 -83
  150. package/src/rules/syncer.ts +235 -235
  151. package/src/sdk.ts +327 -327
  152. package/src/search/intent-detector.ts +289 -289
  153. package/src/search/query-expansion.ts +52 -52
  154. package/src/server/formation-timeout.ts +27 -27
  155. package/src/server.ts +3 -0
  156. package/src/skills/mini-skills.ts +386 -386
  157. package/src/store/chat-store.ts +119 -119
  158. package/src/store/file-lock.ts +100 -100
  159. package/src/store/graph-store.ts +249 -249
  160. package/src/store/mini-skill-store.ts +349 -349
  161. package/src/store/obs-store.ts +255 -255
  162. package/src/store/orama-store.ts +15 -8
  163. package/src/store/persistence-json.ts +212 -212
  164. package/src/store/persistence.ts +291 -291
  165. package/src/store/project-affinity.ts +195 -195
  166. package/src/store/session-store.ts +259 -259
  167. package/src/store/sqlite-store.ts +339 -339
  168. package/src/team/event-bus.ts +76 -76
  169. package/src/team/file-locks.ts +173 -173
  170. package/src/team/handoff.ts +167 -167
  171. package/src/team/messages.ts +203 -203
  172. package/src/team/poll.ts +132 -132
  173. package/src/team/tasks.ts +211 -211
  174. package/src/wiki/generator.ts +237 -237
  175. package/src/wiki/knowledge-graph.ts +334 -334
  176. package/src/wiki/types.ts +85 -85
  177. package/src/workspace/mcp-adapters/codex.ts +191 -191
  178. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  179. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  180. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  181. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  182. package/src/workspace/mcp-adapters/trae.ts +134 -134
  183. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  184. package/src/workspace/sanitizer.ts +60 -60
  185. 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
+ }
@@ -8,7 +8,7 @@
8
8
  * Vector search (embeddings) will be added in P1 phase.
9
9
  */
10
10
 
11
- import { create, insert, search, remove, update, count, type AnyOrama } from '@orama/orama';
11
+ import { create, insert, search, remove, update, count, getByID, type AnyOrama } from '@orama/orama';
12
12
  import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer } from '../types.js';
13
13
  import { OBSERVATION_ICONS, type ObservationType } from '../types.js';
14
14
  import { resolveKnowledgeLayer } from '../skills/mini-skills.js';
@@ -52,9 +52,13 @@ function makeEntryKey(projectId: string | undefined, observationId: number): str
52
52
  return `${projectId ?? ''}::${observationId}`;
53
53
  }
54
54
 
55
- function rememberObservationDoc(doc: MemorixDocument): void {
56
- if (!doc.projectId || typeof doc.observationId !== 'number') return;
57
- docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), doc);
55
+ function rememberObservationDoc(doc: MemorixDocument): MemorixDocument {
56
+ const publicDoc = { ...doc };
57
+ delete publicDoc.embedding;
58
+ if (doc.projectId && typeof doc.observationId === 'number') {
59
+ docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
60
+ }
61
+ return publicDoc;
58
62
  }
59
63
 
60
64
  function isCommandLikeQuery(query: string): boolean {
@@ -243,15 +247,15 @@ export async function batchGenerateEmbeddings(texts: string[]): Promise<(number[
243
247
  */
244
248
  export async function hydrateIndex(observations: any[]): Promise<number> {
245
249
  const database = await getDb();
246
- const currentCount = await count(database);
247
- if (currentCount > 0) return 0; // already hydrated
248
250
 
249
251
  let inserted = 0;
250
252
  for (const obs of observations) {
251
253
  if (!obs || !obs.id || !obs.projectId) continue;
252
254
  try {
255
+ const id = makeOramaObservationId(obs.projectId, obs.id);
256
+ if (getByID(database, id)) continue;
253
257
  const doc: MemorixDocument = {
254
- id: makeOramaObservationId(obs.projectId, obs.id),
258
+ id,
255
259
  observationId: obs.id,
256
260
  entityName: obs.entityName || '',
257
261
  type: obs.type || 'discovery',
@@ -376,6 +380,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
376
380
  let searchParams: Record<string, unknown> = {
377
381
  term: originalQuery,
378
382
  limit: requestLimit,
383
+ includeVectors: true,
379
384
  ...(Object.keys(filters).length > 0 ? { where: filters } : {}),
380
385
  // Search specific fields (not tokens, accessCount, etc.)
381
386
  properties: ['title', 'entityName', 'narrative', 'facts', 'concepts', 'filesModified'],
@@ -458,6 +463,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
458
463
  const vectorOnlyParams: Record<string, unknown> = {
459
464
  term: '',
460
465
  limit: requestLimit,
466
+ includeVectors: true,
461
467
  ...(Object.keys(filters).length > 0 ? { where: filters } : {}),
462
468
  mode: 'vector',
463
469
  vector: {
@@ -887,6 +893,7 @@ export async function getObservationsByIds(
887
893
 
888
894
  const searchResult = await search(database, {
889
895
  term: '',
896
+ includeVectors: true,
890
897
  where: {
891
898
  observationId: { eq: id },
892
899
  ...(projectId ? { projectId } : {}),
@@ -895,7 +902,7 @@ export async function getObservationsByIds(
895
902
  });
896
903
 
897
904
  if (searchResult.hits.length > 0) {
898
- results.push(searchResult.hits[0].document as unknown as MemorixDocument);
905
+ results.push(rememberObservationDoc(searchResult.hits[0].document as unknown as MemorixDocument));
899
906
  }
900
907
  }
901
908