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.
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
package/src/store/obs-store.ts
CHANGED
|
@@ -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
|
+
}
|