agent-working-memory 0.8.6 → 0.8.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 (53) hide show
  1. package/README.md +46 -2
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +13 -0
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/api/routes.js +8 -8
  6. package/dist/cli/migrate.js +29 -29
  7. package/dist/cli.js +105 -105
  8. package/dist/coordination/circuit-breaker.js +23 -23
  9. package/dist/core/lite-compress.d.ts +26 -0
  10. package/dist/core/lite-compress.d.ts.map +1 -0
  11. package/dist/core/lite-compress.js +105 -0
  12. package/dist/core/lite-compress.js.map +1 -0
  13. package/dist/mcp.d.ts +5 -1
  14. package/dist/mcp.d.ts.map +1 -1
  15. package/dist/mcp.js +138 -84
  16. package/dist/mcp.js.map +1 -1
  17. package/dist/storage/pglite-schema.js +143 -143
  18. package/dist/storage/pglite.js +138 -138
  19. package/package.json +4 -3
  20. package/src/adapters/common.ts +13 -0
  21. package/src/api/index.ts +3 -3
  22. package/src/api/routes.ts +1 -1
  23. package/src/cli/migrate.ts +307 -307
  24. package/src/cli.ts +1 -1
  25. package/src/coordination/circuit-breaker.ts +83 -83
  26. package/src/coordination/failure-modes.ts +50 -50
  27. package/src/core/decay.ts +63 -63
  28. package/src/core/embeddings.ts +110 -110
  29. package/src/core/index.ts +5 -5
  30. package/src/core/lite-compress.ts +129 -0
  31. package/src/core/logger.ts +36 -36
  32. package/src/core/ml-worker-entry.ts +194 -194
  33. package/src/core/ml-worker.ts +281 -281
  34. package/src/core/query-expander.ts +122 -122
  35. package/src/core/reranker.ts +119 -119
  36. package/src/engine/confidence.ts +120 -120
  37. package/src/engine/connections.ts +162 -162
  38. package/src/engine/consolidation-scheduler.ts +242 -242
  39. package/src/engine/eval.ts +102 -102
  40. package/src/engine/eviction.ts +101 -101
  41. package/src/engine/index.ts +8 -8
  42. package/src/engine/retraction.ts +366 -366
  43. package/src/engine/staging.ts +74 -74
  44. package/src/mcp.ts +70 -4
  45. package/src/storage/factory.ts +147 -147
  46. package/src/storage/index.ts +3 -3
  47. package/src/storage/pglite-schema.ts +166 -166
  48. package/src/storage/pglite.ts +1363 -1363
  49. package/src/storage/store.ts +80 -80
  50. package/src/types/agent.ts +67 -67
  51. package/src/types/checkpoint.ts +46 -46
  52. package/src/types/eval.ts +100 -100
  53. package/src/types/index.ts +6 -6
@@ -1,74 +1,74 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Staging Buffer — weak signal handler.
5
- *
6
- * Observations that don't meet the salience threshold for active memory
7
- * go to staging. The staging buffer periodically:
8
- * 1. Checks staged engrams against active memory for resonance
9
- * 2. Promotes resonant engrams to active
10
- * 3. Discards expired engrams that never resonated
11
- *
12
- * Modeled on hippocampal consolidation — provisional encoding
13
- * that only persists if reactivated.
14
- */
15
-
16
- import type { IEngramStore as EngramStore } from '../storage/store.js';
17
- import type { ActivationEngine } from './activation.js';
18
-
19
- export class StagingBuffer {
20
- private store: EngramStore;
21
- private engine: ActivationEngine;
22
- private checkInterval: ReturnType<typeof setInterval> | null = null;
23
-
24
- constructor(store: EngramStore, engine: ActivationEngine) {
25
- this.store = store;
26
- this.engine = engine;
27
- }
28
-
29
- /**
30
- * Start the periodic staging check.
31
- */
32
- start(intervalMs: number = 60_000): void {
33
- this.checkInterval = setInterval(() => this.sweep(), intervalMs);
34
- }
35
-
36
- stop(): void {
37
- if (this.checkInterval) {
38
- clearInterval(this.checkInterval);
39
- this.checkInterval = null;
40
- }
41
- }
42
-
43
- /**
44
- * Sweep staged engrams: promote or discard.
45
- */
46
- async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
47
- const promoted: string[] = [];
48
- const discarded: string[] = [];
49
-
50
- const expired = await this.store.getExpiredStaging();
51
- for (const engram of expired) {
52
- // Check if this engram resonates with active memory
53
- const results = await this.engine.activate({
54
- agentId: engram.agentId,
55
- context: `${engram.concept} ${engram.content}`,
56
- limit: 3,
57
- minScore: 0.3,
58
- internal: true,
59
- });
60
-
61
- if (results.length > 0) {
62
- // Resonance found — promote to active
63
- await this.store.updateStage(engram.id, 'active');
64
- promoted.push(engram.id);
65
- } else {
66
- // No resonance — discard
67
- await this.store.deleteEngram(engram.id);
68
- discarded.push(engram.id);
69
- }
70
- }
71
-
72
- return { promoted, discarded };
73
- }
74
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Staging Buffer — weak signal handler.
5
+ *
6
+ * Observations that don't meet the salience threshold for active memory
7
+ * go to staging. The staging buffer periodically:
8
+ * 1. Checks staged engrams against active memory for resonance
9
+ * 2. Promotes resonant engrams to active
10
+ * 3. Discards expired engrams that never resonated
11
+ *
12
+ * Modeled on hippocampal consolidation — provisional encoding
13
+ * that only persists if reactivated.
14
+ */
15
+
16
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
17
+ import type { ActivationEngine } from './activation.js';
18
+
19
+ export class StagingBuffer {
20
+ private store: EngramStore;
21
+ private engine: ActivationEngine;
22
+ private checkInterval: ReturnType<typeof setInterval> | null = null;
23
+
24
+ constructor(store: EngramStore, engine: ActivationEngine) {
25
+ this.store = store;
26
+ this.engine = engine;
27
+ }
28
+
29
+ /**
30
+ * Start the periodic staging check.
31
+ */
32
+ start(intervalMs: number = 60_000): void {
33
+ this.checkInterval = setInterval(() => this.sweep(), intervalMs);
34
+ }
35
+
36
+ stop(): void {
37
+ if (this.checkInterval) {
38
+ clearInterval(this.checkInterval);
39
+ this.checkInterval = null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Sweep staged engrams: promote or discard.
45
+ */
46
+ async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
47
+ const promoted: string[] = [];
48
+ const discarded: string[] = [];
49
+
50
+ const expired = await this.store.getExpiredStaging();
51
+ for (const engram of expired) {
52
+ // Check if this engram resonates with active memory
53
+ const results = await this.engine.activate({
54
+ agentId: engram.agentId,
55
+ context: `${engram.concept} ${engram.content}`,
56
+ limit: 3,
57
+ minScore: 0.3,
58
+ internal: true,
59
+ });
60
+
61
+ if (results.length > 0) {
62
+ // Resonance found — promote to active
63
+ await this.store.updateStage(engram.id, 'active');
64
+ promoted.push(engram.id);
65
+ } else {
66
+ // No resonance — discard
67
+ await this.store.deleteEngram(engram.id);
68
+ discarded.push(engram.id);
69
+ }
70
+ }
71
+
72
+ return { promoted, discarded };
73
+ }
74
+ }
package/src/mcp.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * Runs as a stdio-based MCP server that Claude Code connects to directly.
7
7
  * Uses the storage and engine layers in-process (no HTTP overhead).
8
8
  *
9
- * Tools exposed (12):
9
+ * Tools exposed (16):
10
10
  * memory_write — store a memory (salience filter decides disposition)
11
11
  * memory_recall — activate memories by context (cognitive retrieval)
12
12
  * memory_feedback — report whether a recalled memory was useful
@@ -19,6 +19,10 @@
19
19
  * memory_task_update — change task status, priority, or blocking
20
20
  * memory_task_list — list tasks filtered by status
21
21
  * memory_task_next — get the highest-priority actionable task
22
+ * memory_task_begin — start a task (auto-checkpoint + recall)
23
+ * memory_task_end — end a task (write summary + checkpoint)
24
+ * compress_output — encode structured tool output as TOON (token-efficient, lossless)
25
+ * retrieve_original — get the verbatim source for a compress_output ref
22
26
  *
23
27
  * Run: npx tsx src/mcp.ts
24
28
  * Config: add to ~/.claude.json or .mcp.json
@@ -71,6 +75,7 @@ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
71
75
  import { embed } from './core/embeddings.js';
72
76
  import { startSidecar } from './hooks/sidecar.js';
73
77
  import { initLogger, log, getLogPath } from './core/logger.js';
78
+ import { liteCompress, retrieveOriginal } from './core/lite-compress.js';
74
79
  import { queryPeerDecisions, formatPeerDecisions } from './coordination/peer-decisions.js';
75
80
 
76
81
  // --- Incognito Mode ---
@@ -81,7 +86,7 @@ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO
81
86
 
82
87
  if (INCOGNITO) {
83
88
  console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
84
- const server = new McpServer({ name: 'agent-working-memory', version: '0.8.6' });
89
+ const server = new McpServer({ name: 'agent-working-memory', version: '0.8.8' });
85
90
  const transport = new StdioServerTransport();
86
91
  server.connect(transport).catch(err => {
87
92
  console.error('MCP server failed:', err);
@@ -94,7 +99,20 @@ if (INCOGNITO) {
94
99
 
95
100
  const BACKEND: StoreBackend = getConfiguredBackend();
96
101
  const DB_PATH = process.env.AWM_DB_PATH ?? (BACKEND === 'pglite' ? 'memory-pglite' : 'memory.db');
97
- const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? 'claude-code';
102
+
103
+ // Fallback agent selection when AWM_AGENT_ID/WORKER_NAME are unset: derive from
104
+ // the project directory so plain `claude` launches still bind to the right
105
+ // store. Personal-Projects -> 'personal'; everything else -> 'work' (the
106
+ // primary store). MUST stay in sync with the SessionStart hook
107
+ // (~/.claude/hooks/awm-session-start.ps1) so the hook's restore and the
108
+ // server's reads/writes never diverge. Guard the AWM package's own path
109
+ // (it lives under Personal-Projects) so a stray server cwd can't mis-bind.
110
+ function deriveAgentFromDir(): string {
111
+ const dir = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replace(/\\/g, '/');
112
+ if (/\/AgentSynapse\//i.test(dir)) return 'work';
113
+ return /\/Personal-Projects(\/|$)/i.test(dir) ? 'personal' : 'work';
114
+ }
115
+ const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? deriveAgentFromDir();
98
116
  const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
99
117
  const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
100
118
 
@@ -123,7 +141,7 @@ let coordDb: import('better-sqlite3').Database | null = null;
123
141
 
124
142
  const server = new McpServer({
125
143
  name: 'agent-working-memory',
126
- version: '0.8.6',
144
+ version: '0.8.8',
127
145
  });
128
146
 
129
147
  server.registerResource(
@@ -1080,6 +1098,54 @@ This captures what was accomplished so future sessions can recall it.`,
1080
1098
  }
1081
1099
  );
1082
1100
 
1101
+ server.tool(
1102
+ 'compress_output',
1103
+ `Compress a STRUCTURED tool output (JSON object/array, query rows, log records) into TOON —
1104
+ a compact, schema-aware tabular encoding — before putting it in your context. Cuts ~50-65%
1105
+ of the tokens on uniform arrays at zero comprehension cost (validated: models read TOON as
1106
+ accurately as JSON). Use this on large tool results you need to keep in context.
1107
+
1108
+ Output-only and safe: it never changes the data. Non-JSON / prose is returned unchanged.
1109
+ TOON is only emitted when it reproduces the input exactly (self-verified round-trip);
1110
+ otherwise you get plain JSON back. When compressed, you also get a 'ref' — call
1111
+ retrieve_original(ref) to get the verbatim source back if you ever need it.`,
1112
+ {
1113
+ output: z.string().describe('The tool output to compress — JSON text (preferred) or any string. Non-JSON is returned unchanged.'),
1114
+ min_saving_chars: z.number().optional().describe('Only emit TOON if it saves at least this many characters (default 40).'),
1115
+ },
1116
+ async (params) => {
1117
+ const r = liteCompress(params.output, { minSavingChars: params.min_saving_chars });
1118
+ log(AGENT_ID, 'compress', `${r.format} ${r.charsBefore}->${r.charsAfter} chars (${(r.ratio * 100).toFixed(0)}%)${r.ref ? ` ref=${r.ref}` : ''}`);
1119
+ const header = r.format === 'toon'
1120
+ ? `[TOON, ${(r.ratio * 100).toFixed(0)}% smaller — compact lossless JSON; read as data, ref=${r.ref}]\n`
1121
+ : '';
1122
+ return {
1123
+ content: [{ type: 'text' as const, text: header + r.text }],
1124
+ };
1125
+ }
1126
+ );
1127
+
1128
+ server.tool(
1129
+ 'retrieve_original',
1130
+ `Retrieve the verbatim original text for a 'ref' returned by compress_output. Use this when
1131
+ you need the exact, uncompressed source (e.g. to pass it to another tool unchanged). Returns
1132
+ an error if the ref has expired (originals are kept for the most recent compressions only).`,
1133
+ {
1134
+ ref: z.string().describe('The ref handle returned by compress_output (e.g. "awm_orig_12").'),
1135
+ },
1136
+ async (params) => {
1137
+ const original = retrieveOriginal(params.ref);
1138
+ if (original === undefined) {
1139
+ return {
1140
+ content: [{ type: 'text' as const, text: `Error: ref "${params.ref}" not found or expired.` }],
1141
+ };
1142
+ }
1143
+ return {
1144
+ content: [{ type: 'text' as const, text: original }],
1145
+ };
1146
+ }
1147
+ );
1148
+
1083
1149
  // --- Start ---
1084
1150
 
1085
1151
  async function main() {
@@ -1,147 +1,147 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Storage backend factory.
5
- *
6
- * Picks between SQLite (mature, full features) and PGlite (portable, async,
7
- * pgvector). Both backends satisfy the IEngramStore contract — the cognitive
8
- * engines work with either through `await`.
9
- *
10
- * AWM_STORE_BACKEND=sqlite — better-sqlite3 + FTS5 + BLOB embeddings
11
- * AWM_STORE_BACKEND=pglite — PGlite + pgvector + tsvector
12
- *
13
- * **Backend selection (v0.8.5):** auto-detect on disk when the env var is
14
- * unset, so upgrading users on existing `memory.db` files keep working
15
- * without setting anything. Order of precedence:
16
- *
17
- * 1. `AWM_STORE_BACKEND` env var (explicit override — always wins).
18
- * 2. Auto-detect: if `memory-pglite/` exists on disk → pglite.
19
- * 3. Auto-detect: else if `memory.db` exists on disk → sqlite.
20
- * 4. Fall back to sqlite (the default for fresh installs).
21
- *
22
- * If `AWM_STORE_BACKEND` is set but the on-disk state disagrees (e.g. env
23
- * says pglite but only `memory.db` is present), `openStore` prints a one-line
24
- * warning suggesting `awm migrate`. The env var still wins — we never
25
- * silently switch backends behind the user's back.
26
- *
27
- * The `AWM_DB_PATH` env var carries the file path (SQLite) or directory
28
- * path (PGlite). When unset, default is `memory.db` (sqlite) or
29
- * `memory-pglite/` (pglite).
30
- *
31
- * **Feature parity gaps** (see `docs/pglite-feature-parity.md`):
32
- * a few legacy code paths reach for SQLite-specific methods
33
- * (coordination plugin needs `store.getDb()`, slim-cache warming, hot
34
- * backups). PGlite-backed AWM is fully functional for the cognitive
35
- * engines (write, recall, consolidation, retraction, eviction). Opt-in
36
- * `AWM_STORE_BACKEND=pglite` skips the SQLite-only extras.
37
- */
38
-
39
- import { existsSync, statSync } from 'node:fs';
40
- import type { IEngramStore } from './store.js';
41
-
42
- export type StoreBackend = 'sqlite' | 'pglite';
43
-
44
- /**
45
- * Auto-detect the backend from on-disk state. Used when `AWM_STORE_BACKEND`
46
- * is unset. Looks in the current working directory; honors `AWM_DB_PATH`
47
- * if it points at a known shape.
48
- */
49
- function detectBackendFromDisk(): StoreBackend | null {
50
- // If AWM_DB_PATH is set, infer from its shape.
51
- const explicitPath = process.env.AWM_DB_PATH;
52
- if (explicitPath && existsSync(explicitPath)) {
53
- try {
54
- const stat = statSync(explicitPath);
55
- if (stat.isDirectory()) return 'pglite'; // PGlite uses a directory
56
- if (stat.isFile()) return 'sqlite'; // SQLite is a single file
57
- } catch { /* fall through */ }
58
- }
59
-
60
- // Otherwise look for the conventional defaults in cwd.
61
- // PGlite directory wins over SQLite file when both exist — assume the
62
- // user actively migrated and forgot to set the env var. We warn below.
63
- if (existsSync('memory-pglite')) return 'pglite';
64
- if (existsSync('memory.db')) return 'sqlite';
65
- return null;
66
- }
67
-
68
- export function getConfiguredBackend(): StoreBackend {
69
- const raw = process.env.AWM_STORE_BACKEND;
70
- if (raw !== undefined && raw !== '') {
71
- const normalized = raw.toLowerCase();
72
- if (normalized === 'pglite') return 'pglite';
73
- if (normalized === 'sqlite') return 'sqlite';
74
- console.warn(`Unknown AWM_STORE_BACKEND=${raw}; falling back to sqlite`);
75
- return 'sqlite';
76
- }
77
- // Env unset → auto-detect from on-disk state.
78
- const detected = detectBackendFromDisk();
79
- return detected ?? 'sqlite';
80
- }
81
-
82
- export function getConfiguredPath(): string {
83
- if (process.env.AWM_DB_PATH) return process.env.AWM_DB_PATH;
84
- return getConfiguredBackend() === 'pglite' ? 'memory-pglite' : 'memory.db';
85
- }
86
-
87
- /**
88
- * Print a one-line warning to stderr when the configured backend disagrees
89
- * with what's actually on disk. We never fail or silently switch backends
90
- * — the explicit configuration always wins. The warning helps users notice
91
- * that they may have stranded data on the other backend.
92
- */
93
- function warnIfBackendDisagreesWithDisk(backend: StoreBackend, path: string): void {
94
- if (process.env.AWM_SUPPRESS_BACKEND_WARNINGS === '1') return;
95
-
96
- // Only warn when env var was explicit (auto-detect already follows disk).
97
- if (!process.env.AWM_STORE_BACKEND) return;
98
-
99
- const otherPath = backend === 'pglite' ? 'memory.db' : 'memory-pglite';
100
- const otherBackend = backend === 'pglite' ? 'sqlite' : 'pglite';
101
-
102
- // The "real" check: configured target doesn't exist yet (fresh / empty) AND
103
- // the other backend's conventional file exists with data. Likely stranded.
104
- const configuredExists = existsSync(path);
105
- const otherExists = existsSync(otherPath);
106
-
107
- if (!configuredExists && otherExists) {
108
- console.warn(
109
- `[awm] AWM_STORE_BACKEND=${backend} but no data at "${path}". ` +
110
- `Existing ${otherBackend} data at "${otherPath}" — run \`awm migrate\` ` +
111
- `to convert it to ${backend}, or unset AWM_STORE_BACKEND to use ${otherBackend}. ` +
112
- `Suppress with AWM_SUPPRESS_BACKEND_WARNINGS=1.`,
113
- );
114
- }
115
- }
116
-
117
- /**
118
- * Open a store using the env-configured (or auto-detected) backend and path.
119
- * Returns the concrete store class (cast to IEngramStore at call sites
120
- * that need the async contract; SQLite callers can keep the concrete class
121
- * to retain access to SQLite-specific methods like getDb()).
122
- *
123
- * Never fails on backend/disk mismatch — only warns. Existing users
124
- * upgrading without setting env vars get auto-detect; users on the legacy
125
- * `memory.db` keep working without touching anything.
126
- */
127
- export async function openStore(): Promise<{
128
- store: IEngramStore;
129
- backend: StoreBackend;
130
- path: string;
131
- }> {
132
- const backend = getConfiguredBackend();
133
- const path = getConfiguredPath();
134
-
135
- warnIfBackendDisagreesWithDisk(backend, path);
136
-
137
- if (backend === 'pglite') {
138
- const { PGliteEngramStore } = await import('./pglite.js');
139
- const store = new PGliteEngramStore(path);
140
- await store.ready();
141
- return { store: store as unknown as IEngramStore, backend, path };
142
- }
143
-
144
- const { EngramStore } = await import('./sqlite.js');
145
- const store = new EngramStore(path);
146
- return { store: store as unknown as IEngramStore, backend, path };
147
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Storage backend factory.
5
+ *
6
+ * Picks between SQLite (mature, full features) and PGlite (portable, async,
7
+ * pgvector). Both backends satisfy the IEngramStore contract — the cognitive
8
+ * engines work with either through `await`.
9
+ *
10
+ * AWM_STORE_BACKEND=sqlite — better-sqlite3 + FTS5 + BLOB embeddings
11
+ * AWM_STORE_BACKEND=pglite — PGlite + pgvector + tsvector
12
+ *
13
+ * **Backend selection (v0.8.5):** auto-detect on disk when the env var is
14
+ * unset, so upgrading users on existing `memory.db` files keep working
15
+ * without setting anything. Order of precedence:
16
+ *
17
+ * 1. `AWM_STORE_BACKEND` env var (explicit override — always wins).
18
+ * 2. Auto-detect: if `memory-pglite/` exists on disk → pglite.
19
+ * 3. Auto-detect: else if `memory.db` exists on disk → sqlite.
20
+ * 4. Fall back to sqlite (the default for fresh installs).
21
+ *
22
+ * If `AWM_STORE_BACKEND` is set but the on-disk state disagrees (e.g. env
23
+ * says pglite but only `memory.db` is present), `openStore` prints a one-line
24
+ * warning suggesting `awm migrate`. The env var still wins — we never
25
+ * silently switch backends behind the user's back.
26
+ *
27
+ * The `AWM_DB_PATH` env var carries the file path (SQLite) or directory
28
+ * path (PGlite). When unset, default is `memory.db` (sqlite) or
29
+ * `memory-pglite/` (pglite).
30
+ *
31
+ * **Feature parity gaps** (see `docs/pglite-feature-parity.md`):
32
+ * a few legacy code paths reach for SQLite-specific methods
33
+ * (coordination plugin needs `store.getDb()`, slim-cache warming, hot
34
+ * backups). PGlite-backed AWM is fully functional for the cognitive
35
+ * engines (write, recall, consolidation, retraction, eviction). Opt-in
36
+ * `AWM_STORE_BACKEND=pglite` skips the SQLite-only extras.
37
+ */
38
+
39
+ import { existsSync, statSync } from 'node:fs';
40
+ import type { IEngramStore } from './store.js';
41
+
42
+ export type StoreBackend = 'sqlite' | 'pglite';
43
+
44
+ /**
45
+ * Auto-detect the backend from on-disk state. Used when `AWM_STORE_BACKEND`
46
+ * is unset. Looks in the current working directory; honors `AWM_DB_PATH`
47
+ * if it points at a known shape.
48
+ */
49
+ function detectBackendFromDisk(): StoreBackend | null {
50
+ // If AWM_DB_PATH is set, infer from its shape.
51
+ const explicitPath = process.env.AWM_DB_PATH;
52
+ if (explicitPath && existsSync(explicitPath)) {
53
+ try {
54
+ const stat = statSync(explicitPath);
55
+ if (stat.isDirectory()) return 'pglite'; // PGlite uses a directory
56
+ if (stat.isFile()) return 'sqlite'; // SQLite is a single file
57
+ } catch { /* fall through */ }
58
+ }
59
+
60
+ // Otherwise look for the conventional defaults in cwd.
61
+ // PGlite directory wins over SQLite file when both exist — assume the
62
+ // user actively migrated and forgot to set the env var. We warn below.
63
+ if (existsSync('memory-pglite')) return 'pglite';
64
+ if (existsSync('memory.db')) return 'sqlite';
65
+ return null;
66
+ }
67
+
68
+ export function getConfiguredBackend(): StoreBackend {
69
+ const raw = process.env.AWM_STORE_BACKEND;
70
+ if (raw !== undefined && raw !== '') {
71
+ const normalized = raw.toLowerCase();
72
+ if (normalized === 'pglite') return 'pglite';
73
+ if (normalized === 'sqlite') return 'sqlite';
74
+ console.warn(`Unknown AWM_STORE_BACKEND=${raw}; falling back to sqlite`);
75
+ return 'sqlite';
76
+ }
77
+ // Env unset → auto-detect from on-disk state.
78
+ const detected = detectBackendFromDisk();
79
+ return detected ?? 'sqlite';
80
+ }
81
+
82
+ export function getConfiguredPath(): string {
83
+ if (process.env.AWM_DB_PATH) return process.env.AWM_DB_PATH;
84
+ return getConfiguredBackend() === 'pglite' ? 'memory-pglite' : 'memory.db';
85
+ }
86
+
87
+ /**
88
+ * Print a one-line warning to stderr when the configured backend disagrees
89
+ * with what's actually on disk. We never fail or silently switch backends
90
+ * — the explicit configuration always wins. The warning helps users notice
91
+ * that they may have stranded data on the other backend.
92
+ */
93
+ function warnIfBackendDisagreesWithDisk(backend: StoreBackend, path: string): void {
94
+ if (process.env.AWM_SUPPRESS_BACKEND_WARNINGS === '1') return;
95
+
96
+ // Only warn when env var was explicit (auto-detect already follows disk).
97
+ if (!process.env.AWM_STORE_BACKEND) return;
98
+
99
+ const otherPath = backend === 'pglite' ? 'memory.db' : 'memory-pglite';
100
+ const otherBackend = backend === 'pglite' ? 'sqlite' : 'pglite';
101
+
102
+ // The "real" check: configured target doesn't exist yet (fresh / empty) AND
103
+ // the other backend's conventional file exists with data. Likely stranded.
104
+ const configuredExists = existsSync(path);
105
+ const otherExists = existsSync(otherPath);
106
+
107
+ if (!configuredExists && otherExists) {
108
+ console.warn(
109
+ `[awm] AWM_STORE_BACKEND=${backend} but no data at "${path}". ` +
110
+ `Existing ${otherBackend} data at "${otherPath}" — run \`awm migrate\` ` +
111
+ `to convert it to ${backend}, or unset AWM_STORE_BACKEND to use ${otherBackend}. ` +
112
+ `Suppress with AWM_SUPPRESS_BACKEND_WARNINGS=1.`,
113
+ );
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Open a store using the env-configured (or auto-detected) backend and path.
119
+ * Returns the concrete store class (cast to IEngramStore at call sites
120
+ * that need the async contract; SQLite callers can keep the concrete class
121
+ * to retain access to SQLite-specific methods like getDb()).
122
+ *
123
+ * Never fails on backend/disk mismatch — only warns. Existing users
124
+ * upgrading without setting env vars get auto-detect; users on the legacy
125
+ * `memory.db` keep working without touching anything.
126
+ */
127
+ export async function openStore(): Promise<{
128
+ store: IEngramStore;
129
+ backend: StoreBackend;
130
+ path: string;
131
+ }> {
132
+ const backend = getConfiguredBackend();
133
+ const path = getConfiguredPath();
134
+
135
+ warnIfBackendDisagreesWithDisk(backend, path);
136
+
137
+ if (backend === 'pglite') {
138
+ const { PGliteEngramStore } = await import('./pglite.js');
139
+ const store = new PGliteEngramStore(path);
140
+ await store.ready();
141
+ return { store: store as unknown as IEngramStore, backend, path };
142
+ }
143
+
144
+ const { EngramStore } = await import('./sqlite.js');
145
+ const store = new EngramStore(path);
146
+ return { store: store as unknown as IEngramStore, backend, path };
147
+ }
@@ -1,3 +1,3 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- export * from './sqlite.js';
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export * from './sqlite.js';