agent-working-memory 0.8.5 → 0.8.7
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/README.md +4 -2
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +108 -8
- package/dist/adapters/common.js.map +1 -1
- package/dist/api/routes.js +1 -1
- package/dist/cli/migrate.js +29 -29
- package/dist/cli.js +1 -1
- package/dist/coordination/circuit-breaker.js +23 -23
- package/dist/core/lite-compress.d.ts +26 -0
- package/dist/core/lite-compress.d.ts.map +1 -0
- package/dist/core/lite-compress.js +105 -0
- package/dist/core/lite-compress.js.map +1 -0
- package/dist/mcp.d.ts +5 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +58 -4
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite-schema.js +143 -143
- package/dist/storage/pglite.js +138 -138
- package/package.json +4 -3
- package/src/adapters/common.ts +108 -8
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +1 -1
- package/src/cli/migrate.ts +307 -307
- package/src/cli.ts +1 -1
- package/src/coordination/circuit-breaker.ts +83 -83
- package/src/coordination/failure-modes.ts +50 -50
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -110
- package/src/core/index.ts +5 -5
- package/src/core/lite-compress.ts +129 -0
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -194
- package/src/core/ml-worker.ts +281 -281
- package/src/core/query-expander.ts +122 -122
- package/src/core/reranker.ts +119 -119
- package/src/engine/confidence.ts +120 -120
- package/src/engine/connections.ts +162 -162
- package/src/engine/consolidation-scheduler.ts +242 -242
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -366
- package/src/engine/staging.ts +74 -74
- package/src/mcp.ts +70 -4
- package/src/storage/factory.ts +147 -147
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -166
- package/src/storage/pglite.ts +1363 -1363
- package/src/storage/store.ts +80 -80
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
package/src/engine/staging.ts
CHANGED
|
@@ -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 (
|
|
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.
|
|
89
|
+
const server = new McpServer({ name: 'agent-working-memory', version: '0.8.7' });
|
|
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
|
-
|
|
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.
|
|
144
|
+
version: '0.8.7',
|
|
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() {
|
package/src/storage/factory.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -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';
|