agent-working-memory 0.16.1 → 0.18.1

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 (170) hide show
  1. package/README.md +321 -318
  2. package/dist/adapters/common.d.ts +34 -0
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +528 -444
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +19 -0
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/cli/migrate.d.ts.map +1 -1
  10. package/dist/cli/migrate.js +31 -29
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/core/asked-value.d.ts +37 -0
  13. package/dist/core/asked-value.d.ts.map +1 -0
  14. package/dist/core/asked-value.js +77 -0
  15. package/dist/core/asked-value.js.map +1 -0
  16. package/dist/core/conflicts.d.ts +89 -0
  17. package/dist/core/conflicts.d.ts.map +1 -0
  18. package/dist/core/conflicts.js +267 -0
  19. package/dist/core/conflicts.js.map +1 -0
  20. package/dist/core/embeddings.d.ts +14 -2
  21. package/dist/core/embeddings.d.ts.map +1 -1
  22. package/dist/core/embeddings.js +32 -10
  23. package/dist/core/embeddings.js.map +1 -1
  24. package/dist/core/format-recall.d.ts +21 -1
  25. package/dist/core/format-recall.d.ts.map +1 -1
  26. package/dist/core/format-recall.js +60 -5
  27. package/dist/core/format-recall.js.map +1 -1
  28. package/dist/core/ml-worker.d.ts +24 -1
  29. package/dist/core/ml-worker.d.ts.map +1 -1
  30. package/dist/core/ml-worker.js +165 -4
  31. package/dist/core/ml-worker.js.map +1 -1
  32. package/dist/core/model-daemon-client.d.ts +77 -0
  33. package/dist/core/model-daemon-client.d.ts.map +1 -0
  34. package/dist/core/model-daemon-client.js +383 -0
  35. package/dist/core/model-daemon-client.js.map +1 -0
  36. package/dist/core/model-daemon-server.d.ts +71 -0
  37. package/dist/core/model-daemon-server.d.ts.map +1 -0
  38. package/dist/core/model-daemon-server.js +363 -0
  39. package/dist/core/model-daemon-server.js.map +1 -0
  40. package/dist/core/model-daemon.d.ts +137 -0
  41. package/dist/core/model-daemon.d.ts.map +1 -0
  42. package/dist/core/model-daemon.js +231 -0
  43. package/dist/core/model-daemon.js.map +1 -0
  44. package/dist/core/query-expander.d.ts +5 -0
  45. package/dist/core/query-expander.d.ts.map +1 -1
  46. package/dist/core/query-expander.js +3 -2
  47. package/dist/core/query-expander.js.map +1 -1
  48. package/dist/core/recall-config.d.ts +1 -1
  49. package/dist/core/recall-config.d.ts.map +1 -1
  50. package/dist/core/recall-config.js +5 -0
  51. package/dist/core/recall-config.js.map +1 -1
  52. package/dist/core/rerank-window.d.ts +7 -2
  53. package/dist/core/rerank-window.d.ts.map +1 -1
  54. package/dist/core/rerank-window.js +9 -4
  55. package/dist/core/rerank-window.js.map +1 -1
  56. package/dist/core/rerank2.d.ts +52 -2
  57. package/dist/core/rerank2.d.ts.map +1 -1
  58. package/dist/core/rerank2.js +60 -3
  59. package/dist/core/rerank2.js.map +1 -1
  60. package/dist/core/reranker.d.ts +19 -0
  61. package/dist/core/reranker.d.ts.map +1 -1
  62. package/dist/core/reranker.js +76 -12
  63. package/dist/core/reranker.js.map +1 -1
  64. package/dist/core/salience.d.ts +2 -0
  65. package/dist/core/salience.d.ts.map +1 -1
  66. package/dist/core/salience.js +45 -18
  67. package/dist/core/salience.js.map +1 -1
  68. package/dist/core/trust.d.ts +32 -0
  69. package/dist/core/trust.d.ts.map +1 -0
  70. package/dist/core/trust.js +66 -0
  71. package/dist/core/trust.js.map +1 -0
  72. package/dist/core/validity.d.ts +21 -0
  73. package/dist/core/validity.d.ts.map +1 -0
  74. package/dist/core/validity.js +41 -0
  75. package/dist/core/validity.js.map +1 -0
  76. package/dist/core/whoami.d.ts +3 -0
  77. package/dist/core/whoami.d.ts.map +1 -1
  78. package/dist/core/whoami.js +14 -0
  79. package/dist/core/whoami.js.map +1 -1
  80. package/dist/core/write-pipeline.d.ts +48 -0
  81. package/dist/core/write-pipeline.d.ts.map +1 -1
  82. package/dist/core/write-pipeline.js +104 -4
  83. package/dist/core/write-pipeline.js.map +1 -1
  84. package/dist/engine/activation.d.ts.map +1 -1
  85. package/dist/engine/activation.js +27 -4
  86. package/dist/engine/activation.js.map +1 -1
  87. package/dist/engine/consolidation.d.ts.map +1 -1
  88. package/dist/engine/consolidation.js +67 -5
  89. package/dist/engine/consolidation.js.map +1 -1
  90. package/dist/hooks/prime-query.d.ts +57 -0
  91. package/dist/hooks/prime-query.d.ts.map +1 -0
  92. package/dist/hooks/prime-query.js +388 -0
  93. package/dist/hooks/prime-query.js.map +1 -0
  94. package/dist/hooks/prime.d.ts +8 -0
  95. package/dist/hooks/prime.d.ts.map +1 -1
  96. package/dist/hooks/prime.js +21 -2
  97. package/dist/hooks/prime.js.map +1 -1
  98. package/dist/hooks/sidecar.d.ts +7 -0
  99. package/dist/hooks/sidecar.d.ts.map +1 -1
  100. package/dist/hooks/sidecar.js +10 -2
  101. package/dist/hooks/sidecar.js.map +1 -1
  102. package/dist/index.js +11 -3
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp.js +148 -103
  105. package/dist/mcp.js.map +1 -1
  106. package/dist/model-daemon.d.ts +8 -0
  107. package/dist/model-daemon.d.ts.map +1 -0
  108. package/dist/model-daemon.js +45 -0
  109. package/dist/model-daemon.js.map +1 -0
  110. package/dist/storage/factory.d.ts.map +1 -1
  111. package/dist/storage/factory.js +5 -0
  112. package/dist/storage/factory.js.map +1 -1
  113. package/dist/storage/pglite-schema.d.ts.map +1 -1
  114. package/dist/storage/pglite-schema.js +176 -170
  115. package/dist/storage/pglite-schema.js.map +1 -1
  116. package/dist/storage/pglite.d.ts +5 -0
  117. package/dist/storage/pglite.d.ts.map +1 -1
  118. package/dist/storage/pglite.js +165 -153
  119. package/dist/storage/pglite.js.map +1 -1
  120. package/dist/storage/postgres.d.ts +5 -0
  121. package/dist/storage/postgres.d.ts.map +1 -1
  122. package/dist/storage/postgres.js +165 -153
  123. package/dist/storage/postgres.js.map +1 -1
  124. package/dist/storage/sqlite.d.ts +22 -0
  125. package/dist/storage/sqlite.d.ts.map +1 -1
  126. package/dist/storage/sqlite.js +400 -363
  127. package/dist/storage/sqlite.js.map +1 -1
  128. package/dist/types/engram.d.ts +33 -0
  129. package/dist/types/engram.d.ts.map +1 -1
  130. package/dist/types/engram.js.map +1 -1
  131. package/package.json +106 -106
  132. package/plugin/.claude-plugin/plugin.json +1 -1
  133. package/plugin/README.md +1 -1
  134. package/plugin/bin/awm-mcp-launcher.cjs +2 -2
  135. package/plugin/skills/awm-memory/SKILL.md +16 -5
  136. package/src/adapters/common.ts +881 -800
  137. package/src/api/routes.ts +20 -0
  138. package/src/cli/migrate.ts +309 -307
  139. package/src/core/asked-value.ts +74 -0
  140. package/src/core/conflicts.ts +280 -0
  141. package/src/core/embeddings.ts +35 -10
  142. package/src/core/format-recall.ts +83 -25
  143. package/src/core/ml-worker.ts +458 -281
  144. package/src/core/model-daemon-client.ts +374 -0
  145. package/src/core/model-daemon-server.ts +344 -0
  146. package/src/core/model-daemon.ts +243 -0
  147. package/src/core/query-expander.ts +127 -126
  148. package/src/core/recall-config.ts +5 -0
  149. package/src/core/rerank-window.ts +164 -159
  150. package/src/core/rerank2.ts +143 -82
  151. package/src/core/reranker.ts +78 -11
  152. package/src/core/salience.ts +45 -18
  153. package/src/core/trust.ts +65 -0
  154. package/src/core/validity.ts +47 -0
  155. package/src/core/whoami.ts +136 -120
  156. package/src/core/write-pipeline.ts +143 -4
  157. package/src/engine/activation.ts +27 -4
  158. package/src/engine/consolidation.ts +950 -887
  159. package/src/hooks/prime-query.ts +375 -0
  160. package/src/hooks/prime.ts +25 -2
  161. package/src/hooks/sidecar.ts +13 -3
  162. package/src/index.ts +255 -248
  163. package/src/mcp.ts +1521 -1475
  164. package/src/model-daemon.ts +48 -0
  165. package/src/storage/factory.ts +25 -19
  166. package/src/storage/pglite-schema.ts +199 -193
  167. package/src/storage/pglite.ts +1513 -1498
  168. package/src/storage/postgres.ts +1613 -1598
  169. package/src/storage/sqlite.ts +2056 -2019
  170. package/src/types/engram.ts +30 -0
@@ -1,800 +1,881 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- /**
5
- * Shared utilities for CLI adapters.
6
- *
7
- * Extracted from the original setup() in cli.ts — path resolution, secrets,
8
- * environment variables, MCP command building, and the AWM instruction snippet.
9
- */
10
-
11
- import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'node:fs';
12
- import { resolve, join, dirname, basename } from 'node:path';
13
- import { randomBytes } from 'node:crypto';
14
- import { homedir as osHomedir } from 'node:os';
15
- import { fileURLToPath } from 'node:url';
16
- import type { SetupContext } from './types.js';
17
- import { deriveAgentFromDir } from '../core/agent-id.js';
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
-
22
- /** Resolve the AWM package root (where src/ and dist/ live). */
23
- export function resolvePackageRoot(): string {
24
- // __dirname is src/adapters/ at dev time, dist/adapters/ at build time
25
- return resolve(__dirname, '..', '..');
26
- }
27
-
28
- /** A store inside the installed package is destroyed by the next `npm install -g`. */
29
- export function isInsidePackage(dbPath: string): boolean {
30
- return /[\\/]node_modules[\\/]/.test(dbPath);
31
- }
32
-
33
- /**
34
- * Resolve the database path — default `~/.awm/memory.db`.
35
- *
36
- * It used to default to `<packageRoot>/data/memory.db`, i.e. INSIDE the installed npm
37
- * package. That is a data-loss bug, not an untidiness: `npm install -g
38
- * agent-working-memory@latest` renames the package directory aside and deletes it, so an
39
- * upgrade takes every memory with it, and `npm uninstall -g` does the same silently.
40
- *
41
- * Reported from a real machine: the upgrade failed with EBUSY because a running AWM
42
- * process held memory.db open. That failure was the LUCKY outcome — with Claude Code
43
- * closed, the upgrade would have succeeded and the store would have gone with it.
44
- *
45
- * `~/.awm/memory.db` is what the plugin and Desktop launchers already used, so until now
46
- * the same product had two different defaults depending on how it was installed.
47
- */
48
- export function resolveDbPath(packageRoot: string, explicit?: string | null): string {
49
- const dbPath = explicit ?? join(homedir(), '.awm', 'memory.db');
50
- const dbDir = dirname(dbPath);
51
- if (!existsSync(dbDir)) {
52
- mkdirSync(dbDir, { recursive: true });
53
- }
54
- return dbPath;
55
- }
56
-
57
- /** Read or generate the hook secret token. */
58
- export function resolveHookSecret(dbPath: string): string {
59
- const secretPath = join(dirname(dbPath), '.awm-hook-secret');
60
- if (existsSync(secretPath)) {
61
- const existing = readFileSync(secretPath, 'utf-8').trim();
62
- if (existing) return existing;
63
- }
64
- const secret = randomBytes(32).toString('hex');
65
- mkdirSync(dirname(secretPath), { recursive: true });
66
- writeFileSync(secretPath, secret + '\n');
67
- return secret;
68
- }
69
-
70
- /**
71
- * Retrieval settings `awm setup` turns on for a new install (0.14.6).
72
- *
73
- * All three ship default-off in the engine so an existing process never changes
74
- * behavior on upgrade, but the README has recommended them together since 0.14.0:
75
- * second-stage rerank by the cross-encoder's own score, a 400-char rerank window
76
- * on the densest query-term region (25% → 87.5% on long memories), and tags in the
77
- * rerank passage (+7.4pp). A value the user already has in their config wins.
78
- */
79
- export const RECOMMENDED_ENV: Readonly<Record<string, string>> = {
80
- AWM_RERANK2: '1',
81
- AWM_RERANK_WINDOW: 'query',
82
- AWM_RERANK_TAGS: '1',
83
- };
84
-
85
- /** Keys `awm setup` owns outright; everything else in an existing env block is the user's. */
86
- export const OWNED_ENV_KEYS = ['AWM_DB_PATH', 'AWM_AGENT_ID', 'AWM_HOOK_PORT', 'AWM_HOOK_PORT_RANGE', 'AWM_HOOK_SECRET', 'AWM_CLIENT'] as const;
87
-
88
- /**
89
- * Build environment variables for the MCP server process.
90
- *
91
- * Layering, lowest to highest: recommended defaults → whatever the user's current
92
- * entry already carries (rerank flags they tuned, AWM_WORKSPACE, backend choice…)
93
- * → the keys setup owns. Before 0.14.6 the entry was replaced wholesale, which
94
- * dropped hand-set flags on every re-run.
95
- */
96
- export function buildEnvVars(
97
- dbPath: string,
98
- agentId: string,
99
- hookPort: string,
100
- hookSecret: string,
101
- isWindows: boolean,
102
- opts: { hookPortRange?: string; existing?: Record<string, string> | null; client?: string } = {},
103
- ): Record<string, string> {
104
- const owned: Record<string, string> = {
105
- AWM_DB_PATH: isWindows ? dbPath.split('\\').join('/') : dbPath,
106
- AWM_AGENT_ID: agentId,
107
- AWM_HOOK_PORT: hookPort,
108
- AWM_HOOK_SECRET: hookSecret,
109
- };
110
- if (opts.hookPortRange && opts.hookPortRange !== '10') owned.AWM_HOOK_PORT_RANGE = opts.hookPortRange;
111
- return { ...RECOMMENDED_ENV, ...(opts.existing ?? {}), ...owned };
112
- }
113
-
114
- /**
115
- * Resolve the MCP server command + args.
116
- *
117
- * Prefers absolute path to dist/mcp.js (works from any cwd).
118
- * Falls back to npx tsx src/mcp.ts for dev mode.
119
- */
120
- export function resolveMcpCommand(ctx: SetupContext): {
121
- command: string;
122
- args: string[];
123
- } {
124
- if (ctx.hasDist) {
125
- return {
126
- command: 'node',
127
- args: [ctx.mcpDist.replace(/\\/g, '/')],
128
- };
129
- }
130
- // Dev fallback
131
- if (ctx.isWindows) {
132
- return {
133
- command: 'cmd',
134
- args: ['/c', 'npx', 'tsx', ctx.mcpScript.replace(/\\/g, '/')],
135
- };
136
- }
137
- return {
138
- command: 'npx',
139
- args: ['tsx', ctx.mcpScript],
140
- };
141
- }
142
-
143
- /**
144
- * Build a full SetupContext from parsed CLI flags.
145
- *
146
- * Precedence for every owned value: an explicit flag → the value in the entry a
147
- * previous `awm setup` wrote (`existingEnv`) → the default. So re-running setup to
148
- * pick up new hooks or guidance never repoints the database, renames the agent or
149
- * moves the sidecar port underneath a working install.
150
- *
151
- * Global default agent id is `work` (was `claude` before 0.14.6): it matches what
152
- * the server itself resolves to when AWM_AGENT_ID is unset (see core/agent-id.ts),
153
- * so a config with and without the key name the same pool.
154
- */
155
- export function buildSetupContext(opts: {
156
- agentId?: string;
157
- dbPath?: string | null;
158
- isGlobal: boolean;
159
- hookPort?: string;
160
- hookPortRange?: string;
161
- installPrime?: boolean;
162
- /** `--force`: replace a legacy unmarked instruction section rather than refusing. */
163
- forceInstructions?: boolean;
164
- existingEnv?: Record<string, string> | null;
165
- /** Which surface this install is for — stamped onto every write as a `client=` tag. */
166
- client?: string;
167
- }): SetupContext {
168
- const cwd = process.cwd();
169
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
170
- const packageRoot = resolvePackageRoot();
171
- const mcpScript = join(packageRoot, 'src', 'mcp.ts');
172
- const mcpDist = join(packageRoot, 'dist', 'mcp.js');
173
- const hasDist = existsSync(mcpDist);
174
- const isWindows = process.platform === 'win32';
175
- const existing = opts.existingEnv ?? null;
176
-
177
- const agentId = opts.agentId
178
- ?? existing?.AWM_AGENT_ID
179
- ?? (opts.isGlobal ? deriveAgentFromDir('') : projectName);
180
- // An existing AWM_DB_PATH is normally preserved — that is what makes re-running setup an
181
- // upgrade rather than a reset. There is one exception: a store INSIDE the installed
182
- // package is deleted by the next `npm install -g`, so preserving it would be preserving a
183
- // data-loss bug. Move it to the safe default, copying the file rather than pointing at an
184
- // empty one, and leave the original where it is so nothing is destroyed by the rescue.
185
- const requested = opts.dbPath ?? existing?.AWM_DB_PATH ?? null;
186
- let dbPath: string;
187
- let rescuedFrom: string | null = null;
188
- if (!opts.dbPath && requested && isInsidePackage(requested)) {
189
- const safe = resolveDbPath(packageRoot, null);
190
- if (existsSync(requested) && !existsSync(safe)) {
191
- mkdirSync(dirname(safe), { recursive: true });
192
- copyFileSync(requested, safe);
193
- for (const side of ['-wal', '-shm']) {
194
- if (existsSync(requested + side)) copyFileSync(requested + side, safe + side);
195
- }
196
- rescuedFrom = requested;
197
- }
198
- dbPath = safe;
199
- } else {
200
- dbPath = resolveDbPath(packageRoot, requested);
201
- }
202
- const hookPort = opts.hookPort ?? existing?.AWM_HOOK_PORT ?? '8401';
203
- const hookPortRange = opts.hookPortRange ?? existing?.AWM_HOOK_PORT_RANGE ?? '10';
204
- const hookSecret = resolveHookSecret(dbPath);
205
- const envVars = buildEnvVars(dbPath, agentId, hookPort, hookSecret, isWindows, { hookPortRange, existing, client: opts.client });
206
-
207
- return {
208
- rescuedDbFrom: rescuedFrom,
209
- cwd,
210
- projectName,
211
- agentId,
212
- dbPath,
213
- packageRoot,
214
- mcpDist,
215
- mcpScript,
216
- hasDist,
217
- hookSecret,
218
- hookPort,
219
- hookPortRange,
220
- installPrime: opts.installPrime ?? true,
221
- forceInstructions: opts.forceInstructions ?? false,
222
- isGlobal: opts.isGlobal,
223
- isWindows,
224
- envVars,
225
- };
226
- }
227
-
228
- /**
229
- * Home directory. `AWM_SETUP_HOME` overrides it so `awm setup` / `awm doctor` can be
230
- * exercised against a scratch tree (tests do this; so does a cautious upgrade).
231
- */
232
- export function homedir(): string {
233
- return process.env.AWM_SETUP_HOME || osHomedir();
234
- }
235
-
236
- // ─── Instruction content ────────────────────────────────
237
-
238
- /**
239
- * Core AWM instruction snippet — shared across all adapters.
240
- * Each adapter wraps this in the appropriate file format.
241
- */
242
- /**
243
- * Upsert the AWM section into an instruction file (CLAUDE.md, AGENTS.md, .cursorrules).
244
- *
245
- * Behavior:
246
- * - File doesn't exist -> create with title + AWM_INSTRUCTION_CONTENT
247
- * - Section absent -> append
248
- * - Section present + identical -> skip
249
- * - Section present + stale -> REPLACE in place, preserve content above/below
250
- *
251
- * Section is bounded by `## Memory (AWM)` (with optional trailing modifier) at the
252
- * start, and the next `## ` heading or EOF at the end.
253
- *
254
- * Returns a short human-readable status string for the setup command output.
255
- */
256
- /** Opening marker for the generated block. Everything between the markers is replaced
257
- * on upgrade; anything outside them is preserved. The text is deliberately addressed to
258
- * whoever opens the file, because that is who needs to know. */
259
- export const AWM_GEN_BEGIN =
260
- '<!-- AWM:GENERATED:BEGIN — this block is REPLACED by `awm setup`. ' +
261
- 'Put your own notes OUTSIDE these markers and they will survive upgrades. -->';
262
- export const AWM_GEN_END = '<!-- AWM:GENERATED:END -->';
263
-
264
- /** Wrap generated content in the markers. */
265
- function wrapGenerated(content: string): string {
266
- return `${AWM_GEN_BEGIN}\n${content.trim()}\n${AWM_GEN_END}`;
267
- }
268
-
269
- export function upsertAwmSection(
270
- filePath: string,
271
- newContent: string,
272
- options: { titleIfNew?: string; suffix?: string; force?: boolean } = {},
273
- ): string {
274
- const fname = basename(filePath);
275
- const suffix = options.suffix ?? '';
276
- const force = options.force ?? false;
277
-
278
- if (!existsSync(filePath)) {
279
- const title = options.titleIfNew ?? `# ${basename(dirname(filePath))}`;
280
- mkdirSync(dirname(filePath), { recursive: true });
281
- writeFileSync(filePath, `${title}\n\n${wrapGenerated(newContent)}${suffix}`);
282
- return `${fname}: created with AWM workflow section`;
283
- }
284
-
285
- const existing = readFileSync(filePath, 'utf-8');
286
-
287
- // ---- Preferred path: the file already carries generated markers, so the boundary
288
- // between "ours" and "theirs" is explicit and only our block is touched.
289
- const gb = existing.indexOf(AWM_GEN_BEGIN);
290
- const ge = existing.indexOf(AWM_GEN_END);
291
- if (gb !== -1 && ge > gb) {
292
- const current = existing.slice(gb, ge + AWM_GEN_END.length);
293
- const desired = wrapGenerated(newContent);
294
- if (current.trimEnd() === desired.trimEnd()) {
295
- return `${fname}: AWM section already up-to-date (skipped)`;
296
- }
297
- writeFileSync(filePath, existing.slice(0, gb) + desired + existing.slice(ge + AWM_GEN_END.length));
298
- return `${fname}: AWM generated block updated (content outside the markers preserved)`;
299
- }
300
-
301
- // Find section bounds: `## Memory (AWM)` (possibly with ` — MANDATORY` etc.) until next `## ` or EOF
302
- const startRegex = /^## Memory \(AWM\)[^\n]*$/m;
303
- const startMatch = startRegex.exec(existing);
304
-
305
- if (!startMatch) {
306
- // Section not present — append
307
- writeFileSync(filePath, existing.trimEnd() + '\n\n' + wrapGenerated(newContent) + suffix);
308
- return `${fname}: appended AWM workflow section`;
309
- }
310
-
311
- // Find end: next `## ` heading after the section start, or EOF
312
- const afterStart = startMatch.index + startMatch[0].length;
313
- const nextHeadingRegex = /^## (?!Memory \(AWM\))/m;
314
- nextHeadingRegex.lastIndex = afterStart;
315
- const tail = existing.slice(afterStart);
316
- const nextMatch = nextHeadingRegex.exec(tail);
317
- const sectionEnd = nextMatch ? afterStart + nextMatch.index : existing.length;
318
-
319
- const currentSection = existing.slice(startMatch.index, sectionEnd).trimEnd();
320
- const desiredSection = (newContent + suffix).trimEnd();
321
-
322
- if (currentSection === desiredSection) {
323
- return `${fname}: AWM section already up-to-date (skipped)`;
324
- }
325
-
326
- // ---- LEGACY, UNMARKED SECTION.
327
- // Written before generated markers existed, so hand-added notes inside it are
328
- // indistinguishable from generated text. Replacing would silently delete them — on a
329
- // real install this measured 169 of 381 lines (44%), every one an operational finding
330
- // that cost real debugging. Back up and refuse; the caller opts in explicitly.
331
- if (!force) {
332
- const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('Z', '');
333
- const backup = `${filePath}.awm-backup-${stamp}`;
334
- try {
335
- writeFileSync(backup, existing);
336
- } catch {
337
- return `${fname}: AWM section is out of date, but it has no generated markers and the ` +
338
- `backup could not be written — REFUSING to touch it. Copy the file yourself, then re-run.`;
339
- }
340
- return `${fname}: NOT updated — the AWM section predates generated markers, so your own ` +
341
- `notes inside it cannot be told apart from generated text and would be lost. ` +
342
- `Backup written to ${basename(backup)}. To upgrade: move any notes you want to keep ` +
343
- `ABOVE the '## Memory (AWM)' heading (content outside the section is always preserved), ` +
344
- `then re-run with --force. Nothing has been changed.`;
345
- }
346
-
347
- const before = existing.slice(0, startMatch.index).trimEnd();
348
- const after = existing.slice(sectionEnd).replace(/^\s*\n/, '');
349
- const rebuilt =
350
- (before ? before + '\n\n' : '') +
351
- wrapGenerated(newContent) + suffix +
352
- (after ? '\n\n' + after : '\n');
353
- writeFileSync(filePath, rebuilt);
354
- return `${fname}: AWM section replaced with a marked generated block (force)`;
355
- }
356
-
357
- export const AWM_INSTRUCTION_CONTENT = `
358
- ## Memory (AWM) — MANDATORY
359
-
360
- **AWM is THE memory system.** Use it via the \`agent-working-memory\` MCP server
361
- (preferred) or HTTP at \`http://127.0.0.1:8400\` (fallback). The file-based
362
- auto-memory at \`~/.claude/projects/.../memory/*.md\` is a LEGACY bootstrap path —
363
- **do not write new memories to it.** All persistent knowledge goes through AWM.
364
-
365
- If MCP tools aren't loaded at session start, use ToolSearch with
366
- \`select:mcp__agent-working-memory__memory_recall,mcp__agent-working-memory__memory_write\`
367
- to load them. If the MCP server isn't responsive, restart with \`/mcp\` or use
368
- the HTTP endpoints (\`POST /memory/write-batch\`, \`POST /memory/activate\`)
369
- directly — but **DO NOT fall back to markdown files**. Files drift the moment
370
- you write them; AWM stays current because every agent reads + writes the same store.
371
-
372
- ### Lifecycle (always do these, in this order)
373
- 1. **Session start**: call \`memory_restore\` to recover previous context. If it reports the
374
- store is empty/new (or recall keeps returning nothing), **warm-start first**: recall the
375
- \`onboard a new project\` skill and follow it — or call \`onboard_scan\` on the project's
376
- docs/repo, refine the candidates, run \`onboard_questions\`, and save the good ones with
377
- \`memory_write\` (canonical). A cold store is nearly useless until it's seeded.
378
- 2. **Starting a task**: call \`memory_task_begin\` (checkpoints + recalls relevant memories).
379
- 3. **During work**: call \`memory_recall\` BEFORE stating any fact, BEFORE searching
380
- the filesystem, BEFORE making architectural decisions. Recall is ~300ms — cheaper
381
- than one filesystem search.
382
- 4. **As you learn things**: call \`memory_write\` proactively. Don't batch.
383
- 5. **Finishing a task**: call \`memory_task_end\` with a summary.
384
- 6. **Hooks do the rest.** \`awm setup\` installs five: a **prime** hook that recalls against
385
- each prompt and injects what clears confidence (or nothing), checkpoint hooks on
386
- compaction and session end (plus a 15-min timer), a local write/recall reminder on
387
- Stop, and a reminder after Bash/PowerShell calls that a data change is not done
388
- until it is written. Primed context arrives labeled
389
- \`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
390
- arrives with no primed context, that is a signal too: recall explicitly before
391
- asserting anything.
392
-
393
- ### Write memory when:
394
- - A project decision is made or changed
395
- - A root cause is discovered after debugging
396
- - A reusable implementation pattern is established
397
- - A user preference, constraint, or requirement is clarified
398
- - A prior assumption is found to be wrong
399
- - A significant piece of work is completed
400
-
401
- ### Writing for recall (the highest-leverage section)
402
- A memory's recall quality is set the moment you write it. AWM is fast at
403
- finding what's findable — but if the write is shaped wrong, no retriever
404
- can rescue it. Be slightly more verbose at the front than feels natural:
405
- the first 1-2 sentences are what BM25, the embedding model, and concept
406
- extraction all see most strongly.
407
-
408
- - **Lead with the rule or fact.** Don't open with context or backstory.
409
- "Don't mock the database in integration tests." comes first; the reason
410
- comes second. Recall scans the head of the body, not the tail.
411
- - **Pick the most specific topic.** Not \`auth\` — \`auth-magic-link-rate-limit\`.
412
- Topic is a hard filter at recall time. Generic topics hide the memory in
413
- a noisy bucket where it competes with everything else in the area.
414
- - **Include 2+ retrievable identifiers.** File paths, function names, table
415
- columns, ticket IDs, exact error strings, the literal terms a future query
416
- will use. \`SettlementService.closeRun()\` beats "the accounting code."
417
- \`tblMemberDetails.activation_date\` beats "the activation column."
418
- \`schema/072-settlement-lock.sql\` beats "the migration."
419
- - **Write in the vocabulary of the future question.** When you imagine asking
420
- this in three months, what nouns will you use? Use those nouns. Don't
421
- paraphrase the user's domain language into your own neutral summary.
422
- - **Name the CATEGORY as well as the specifics — both, always.** Specifics
423
- make a memory precise; category words make it *reachable*. A memory that
424
- says "private plan memory peaked 88%, scale P1v3 -> P2v3" never says
425
- "Azure" or "App Service Plan", so a question asked in those words cannot
426
- find it — measured on a real store, that memory was not in the top 40
427
- candidates for "azure app service plan capacity increase". Write the
428
- system / product / domain nouns (Azure App Service Plan, Freshdesk ticket,
429
- MySQL connection pool, ShowConnect scoring) NEXT TO the identifiers.
430
- Note this cuts against "pick the most specific topic" above: that rule is
431
- about the \`topic\` TAG, not a licence to omit the category from the body.
432
- Measured on an 11k-engram store: 94% of tagged memories are missing at
433
- least one of their own topical terms from the body, and 66% of those
434
- terms never appear in the text at all.
435
- - **Tags are NOT a substitute for body text.** The embedding is built from
436
- \`concept + content\` only, so it never sees tags. BM25 indexes them. The
437
- cross-encoder rerank passage sees ONLY \`topic=\`/\`proj=\`/\`project=\` tags, and
438
- only when \`AWM_RERANK_TAGS=1\` — which \`awm setup\`, the plugin and the Desktop
439
- bundle all switch on, so assume it is on — capped at 80 characters. So a word
440
- that exists only as a tag is invisible to the embedding entirely, and
441
- invisible to the ranker unless it is one of those three prefixes inside that
442
- budget. Tag it *and* write it.
443
- - **Reserve canonical for stable invariants.** Decisions, requirements,
444
- hard facts, cross-agent shared context. Working class (default) is correct
445
- for findings, observations, and progress notes. The canonical floor is
446
- 0.7 salience — overusing it pollutes the canonical layer and the floor
447
- loses meaning.
448
- - **Include the why for feedback memories.** A rule without a reason can't
449
- be applied to edge cases. "Don't mock the database" is brittle. "Don't
450
- mock the database — last quarter mocked tests masked a broken migration"
451
- is portable to new situations.
452
-
453
- ### Tagging rules (REQUIRED — AWM's prefix-tag retrieval boost depends on these)
454
-
455
- Every \`memory_write\` should pass these structured fields. AWM stores each as a
456
- prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
457
- and entity-bridge boosts at recall time.
458
-
459
- (“Required” below means *required for good recall* — the MCP schema accepts every
460
- one of these as optional, so a write without them succeeds and simply retrieves
461
- worse.)
462
-
463
- | Field | Required? | Format | Example |
464
- |---|---|---|---|
465
- | \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
466
- | \`topic\` | **YES** | one or more lowercase area words | \`"database-migration"\`, \`"benchmarks"\` |
467
- | \`intent\` | **YES** | one of: \`decision\` / \`finding\` / \`todo\` / \`question\` / \`context\` | \`"finding"\` |
468
- | \`confidence_level\` | **YES** | \`verified\` (tested) / \`observed\` (read in code) / \`assumed\` (reasoning) | \`"verified"\` |
469
- | \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
470
- | \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
471
- | \`session_id\` | recommended | current conversation ID for entity-bridge boost | autogenerated |
472
- | \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=10001", "date=2026-05-11"]\` |
473
-
474
- **Always add identifier tags when present in the content:**
475
- - \`ticket=<id>\` for Freshdesk tickets
476
- - \`member=<id>\` for member IDs
477
- - \`asset=<id>\` for a domain record id
478
- - \`usef=<id>\` for USEF lookups
479
- - \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
480
- - \`person=<Name>\` for stakeholder quotes / decisions
481
- - \`version=<X.Y.Z>\` for release-specific findings
482
-
483
- ### Entity index — exact-match recall for named things (default off)
484
- Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
485
- ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
486
- A query naming an entity ("ticket 10003", "Dana Whitfield") can reach the memory through
487
- this index even when the wording doesn't lexically match — it's a deterministic exact
488
- lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
489
- this reason, not just for the BM25 boost described above.
490
-
491
- Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
492
- \`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
493
- guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
494
- surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
495
- people/things you refer to by name often); not yet the default pending evaluation.
496
-
497
- ### Temporal validity — memories that expire or start in the future
498
- \`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
499
- **operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
500
- a ticket status — so the memory expires instead of relying on you to remember it's
501
- stale. Recall renders \`[valid until …]\` on results carrying this field. Use
502
- \`valid_from\` for a fact that becomes true on a known future date (a policy change, a
503
- season that hasn't started yet). Don't set either for durable facts — most memories
504
- don't need them.
505
-
506
- ### Memory classes (controls how strictly the salience filter gates the write)
507
- - \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
508
- Use for: user-stated decisions, project requirements, verified architectural facts,
509
- cross-agent shared context. **In a hive (multi-agent) setup, always use \`canonical\`
510
- for writes that other agents must be able to recall** — the default \`working\` class
511
- may get filtered.
512
- - \`memory_class: working\` (default) — observations and findings. Salience-gated.
513
- - \`memory_class: ephemeral\` — short-lived context that should decay quickly.
514
-
515
- ### Salience auto-promotion (defense in depth)
516
- The salience filter automatically promotes certain content patterns even if you forget
517
- to set \`memory_class\` explicitly:
518
- - **User feedback** — content starting with "Robert said…", "Katherine directed…",
519
- "Nancy decided…" etc. auto-promotes to canonical. So quoting the user verbatim
520
- always preserves the decision.
521
- - **Verified operational records** — content with an action verb (Submitted, Finalized,
522
- Completed, Reconciled, Triaged, Posted, Resolved, Stamped, Pushed, Deployed, Migrated,
523
- Imported, Exported, Backfilled) plus 2+ concrete identifiers (ISO date \`YYYY-MM-DD\`,
524
- or contextual numeric IDs like "event 18969", "ticket #18330", "USEF 341980") gets
525
- a 0.45 salience floor. So batch summaries with real IDs survive even when topic
526
- terms repeat.
527
-
528
- If neither pattern applies and you want a memory to definitely survive, set
529
- \`memory_class: canonical\` explicitly. Don't rely on auto-promotion for important writes.
530
-
531
- ### Recall memory when:
532
- - **BEFORE stating ANY fact about how a system works** — recall first; if AWM doesn't
533
- have it, read the code. Never guess and present it as fact.
534
- - **BEFORE searching the filesystem** — recall first; AWM is faster and has cross-session
535
- knowledge that file search doesn't.
536
- - Starting work on a new task or subsystem
537
- - Re-entering code you haven't touched recently
538
- - After a failed attempt — check if there's prior knowledge
539
- - Before refactoring or making architectural changes
540
- - When a topic comes up that you might have prior context on
541
-
542
- Recall is fast (~300ms typical). Use it freely.
543
-
544
- ### Chain recalls — one hop per call
545
-
546
- AWM returns what matches your query. It does **not** walk from that answer to the next
547
- fact, and it is not meant to: it is active memory, holding what you need right now.
548
- Following a chain to the next thing is **your** job — recall, read, recall again, each
549
- result becoming the cue for the next query.
550
-
551
- So when a question refers to something **by its role instead of its name**, you have a
552
- chain, not a query:
553
-
554
- > *"What is the codename of the project owned by my scheduler?"*
555
-
556
- That is three lookups, not one:
557
-
558
- 1. recall \`who is my scheduler\` → a name
559
- 2. recall \`what project does <that name> own\` → a project
560
- 3. recall \`<that project> codename\` → the answer
561
-
562
- **The failure mode is answering from the first recall.** A single query blending all
563
- three terms returns the most *salient* related memory, not the correct one — measured,
564
- that returns the main project's codename with complete confidence, and it is wrong.
565
- Each hop on its own is an ordinary, reliable recall; the chain only breaks when you stop
566
- asking.
567
-
568
- Tells that you are looking at a chain: a possessive or relative clause naming an entity
569
- by role rather than identity (\`my scheduler\`, \`the owner of X\`, \`whoever signed
570
- off on Y\`), or a rule that operates on an attribute you have not looked up yet (\`the
571
- release tag of the project we discussed\` needs that project's codename first). If
572
- resolving the question requires a fact you would have to *derive*, recall the fact
573
- instead.
574
-
575
- ### Recall strategy (when one query isn't enough)
576
- AWM's adaptive retrieval handles most query variations natively — synonym
577
- expansion, multi-channel scoring, embedding + BM25 + reranker agreement.
578
- A single recall is usually enough.
579
-
580
- When it isn't:
581
- - **If the first recall returns nothing or returns the wrong things, reformulate.**
582
- Try a second query with different phrasing — synonyms, more specific nouns,
583
- the exact identifier from the code rather than the conceptual name. Two or
584
- three recalls cost less than one filesystem search.
585
- - **Use the words a domain expert would use, not generic English.** "Period
586
- close lock" not "accounting feature"; "magic link rate limit" not "auth issue."
587
- - **For broad exploration, pass \`mode: "exploratory"\`** — wider candidate
588
- pool, lower precision floor. For specific lookups, leave mode unset (auto).
589
- - **Don't ensemble more than 3 reformulations.** If three different phrasings
590
- return nothing, the memory probably isn't there — read the code instead of
591
- burning more recalls.
592
-
593
- ### Recall tuning — opt-in parameters
594
- Default \`memory_recall\` is tuned for the common case. Four opt-in parameters change
595
- the cost/quality tradeoff. Use them when the default doesn't match what you need.
596
-
597
- - **\`granularity: 'compact'\`** — every result carries a 200-char \`summary\`
598
- field with a query-aware snippet (the densest window of query terms in the
599
- content). Use this when you expect to scan 5+ results to find one — saves
600
- ~70% of recall output tokens. The full content stays available in
601
- \`engram.content\` if you want to drill into a specific result.
602
- - **\`granularity: 'auto'\`** — confidence-adaptive. If the top result is a clear
603
- winner, it gets a longer summary while the rest are compact. If confidence
604
- is uniform across results, everything is compact. Use when you don't know
605
- in advance whether one result will dominate.
606
- - **\`require_confidence\`** — abstention. The default (0.05) is already a light
607
- gate: when the top results don't stand out from the rest, recall returns
608
- nothing and the reply says **\`RECALL ABSTAINED\`** with the count it withheld.
609
- An empty result *without* that line is genuine absence. The gate is on the
610
- SHAPE of the score distribution, not relevance — so raising it silences
611
- specific questions with one clear winner first (measured: 0.25 abstained on
612
- two identifier queries scoring 0.26–0.38 while passing a vague one at 0.92).
613
- Leave it alone for ordinary recall; \`0.10\` is the ceiling that still helps for
614
- push-style use where nobody asked. When abstention fires, reformulate once
615
- with the exact identifier, then read the code — don't retry with a lower
616
- threshold to force an answer.
617
- - **\`workspace: "<name>"\`** — hive-mode recall across all agents in the
618
- workspace. Use when other agents may have written canonical knowledge you
619
- need. Default is agent-scoped (your own memories only). Can also be set
620
- globally via the \`AWM_WORKSPACE\` env var. Memories live in the agent pool
621
- they were written to; if a session lands in an unexpected pool, \`memory_whoami\`
622
- says which one before you conclude the memory is missing.
623
-
624
- ### Keep memory fresh
625
- - After recalling a memory, if you observe the real state is different → call
626
- \`memory_supersede\` immediately with the corrected version.
627
- - After using a recalled memory: call \`memory_feedback\` (useful/not-useful) **with the
628
- \`recall_id\`** printed at the end of the recall output (\`[recall_id: …]\`). That id
629
- joins the feedback to the recall that produced it, which is what lets AWM learn
630
- which recalls were worth their tokens; feedback without it is a bare vote.
631
- - If you discover a memory is factually wrong: \`memory_retract\` to remove it.
632
- - **If you bypass AWM (file-memory, in-context notes, "I'll just remember"), the memory
633
- drifts out of date. The system relies on you to keep it current. This is the #1
634
- failure mode.**
635
-
636
- ### Cognition recipes — YOU do the thinking, AWM keeps the result
637
- AWM contains no LLM. When memory needs real thinking — distilling a repeatable
638
- procedure, reflecting on a failure — AWM hands YOU a versioned recipe (prompt +
639
- strict output shape) and you run it as a SEPARATE focused pass, then write the
640
- result back as an ordinary memory with provenance.
641
-
642
- - \`memory_task_end\` responses include the recipe invitations. Honor the gates:
643
- skill-derivation only after a genuinely procedural task (3+ tool calls or a
644
- delegated sub-task); friction-lesson only after a failure/retry/wrong assumption.
645
- - Run each recipe as its own focused pass — do NOT bundle it with other
646
- reasoning; bundled passes reliably drop the output.
647
- - Write back exactly per the recipe's contract: \`origin_class: 'recipe'\` +
648
- \`recipe_id\` (e.g. \`skill-derivation@1\`), concept prefixed \`skill: \` or
649
- \`lesson: \`. AWM validates the shape and rejects malformed or unknown-recipe
650
- writes with the contract echoed back — fix and retry, don't drop the insight.
651
- - Re-deriving the same skill name reinforces the existing memory instead of
652
- duplicating it, so don't fear writing a skill you may have written before.
653
-
654
- ### Content fade — write-and-forget is safe
655
- Un-recalled engrams gradually fade their content while preserving cue pathways
656
- (concept + tags + embedding stay intact). This is Paper 1 — storage
657
- degradation. Practical implications:
658
-
659
- - **Don't manually purge memories** to "save space." The system already
660
- compresses unused content. Old memories stay findable via cue match even
661
- when their body has decayed.
662
- - **Don't over-pin with \`memory_class: canonical\`** to fight fade. Canonical
663
- only changes salience gating at write time, not fade behavior. Fade
664
- affects un-recalled engrams of any class.
665
- - **Recall keeps content alive.** Every recall touches the engram and resets
666
- its fade clock. Frequently-recalled memories stay full-fidelity automatically.
667
- - **Supersede is the right tool for stale facts.** When you observe a memory
668
- is outdated, call \`memory_supersede\` — the new version inherits the old
669
- one's coherent associations (counter-narrative replacement) so cue
670
- pathways carry forward to the replacement.
671
-
672
- ### Example — good vs bad memory_write
673
-
674
- **BAD** (no prefix tags, vague concept, can't be recalled by future queries):
675
- \`\`\`
676
- memory_write(
677
- concept="found a bug",
678
- content="The thing I was looking at was broken so I fixed it."
679
- )
680
- \`\`\`
681
-
682
- **GOOD** (rich identifiers, structured metadata, prefix tags):
683
- \`\`\`
684
- memory_write(
685
- concept="EquiHub period-close BLOCKED check missing server-side",
686
- content="apps/web/app/(billing)/billing/settlement-lock/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in SettlementService.closeRun() per schema/072-settlement-lock.sql. Without server-side check a malicious request could bypass via direct API call.",
687
- project="EquiHub",
688
- topic="accounting",
689
- intent="finding",
690
- confidence_level="verified",
691
- source="debugging",
692
- memory_class="canonical",
693
- tags=["ticket=10001", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
694
- )
695
- \`\`\`
696
-
697
- ### Also:
698
- - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
699
- - \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
700
- talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
701
- agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
702
- which agent identity, or which running code you're dealing with — before reasoning
703
- about AWM's own state from a stale memory or an assumed port number.
704
- - AWM is shared across all agents in real time. When any agent writes or supersedes a
705
- memory, every other agent can recall it immediately — but only within the same
706
- workspace and agent scope.
707
-
708
- ### Output compression (token efficiency, output-only)
709
- When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
710
- array of records, query rows, a log dump, an API response — pass it through
711
- \`compress_output\` first. It re-encodes the data as TOON (a compact, lossless,
712
- schema-aware tabular form of JSON), cutting ~50-65% of the tokens at no
713
- comprehension cost. This is output-only: it never changes the data or your memories.
714
- - Use it on big STRUCTURED outputs, not on prose. Prose is returned unchanged —
715
- for trimming memory prose, use recall \`granularity: 'compact'\` instead.
716
- - It returns a \`ref\`; call \`retrieve_original(ref)\` if you later need the exact
717
- verbatim source (e.g. to hand it to another tool unchanged).
718
- - Don't bother for small outputs — it only compresses when the saving is worthwhile
719
- and falls back to plain JSON if TOON wouldn't reproduce the data exactly.
720
-
721
- ### Backend (SQLite vs PGlite)
722
- AWM ships two storage backends. The installer picks SQLite by default; both
723
- are functionally equivalent for cognitive workloads, but differ in operational
724
- guarantees:
725
-
726
- - **SQLite** (default) — embedded, **multi-process safe** via WAL mode. Best
727
- for single-machine setups and MCP scenarios where multiple Claude Code
728
- sessions may open the same database concurrently.
729
- - **PGlite** — embedded Postgres (WASM) with pgvector. **Single-process only**
730
- — two MCP processes against the same \`memory-pglite/\` directory will
731
- abort the second. Pick via \`AWM_STORE_BACKEND=pglite\` and
732
- \`AWM_DB_PATH=path/to/memory-pglite\`.
733
- - **Auto-detect** — if \`AWM_DB_PATH\` points to a directory that already
734
- exists, AWM detects PGlite; a file → SQLite. No explicit
735
- \`AWM_STORE_BACKEND\` needed when an existing DB is present.
736
-
737
- For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
738
- multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
739
-
740
- ### Diagnostics / escape hatches (env vars, only if you know why)
741
- Each optimization is gated by an env var so it can be disabled for A/B testing if a
742
- regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
743
-
744
- Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
745
- - \`AWM_RERANK2=1\` — second-stage rerank by the cross-encoder's own score.
746
- - \`AWM_RERANK_WINDOW=query\` — 400-char rerank window on the densest query-term
747
- region instead of the prefix (25% → 87.5% on long memories).
748
- - \`AWM_RERANK_TAGS=1\` — tags fed into the rerank passage (+7.4pp s@1).
749
-
750
- Hook sidecar (0.14.2):
751
- - \`AWM_HOOK_PORT=8401\` / \`AWM_HOOK_PORT_RANGE=10\` — each MCP process binds the
752
- first free port in the range, so several sessions can run at once. The shipped
753
- hooks find their own session's sidecar by probing the range and matching
754
- \`agentId\` on \`/health\`; \`memory_whoami\` reports the port actually bound.
755
-
756
- Recall pipeline:
757
- - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
758
- pre-filter in recall. Reverts to scoring all active candidates.
759
- - \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (default off; measured
760
- no effect on the real-store benchmark, 0.14.5).
761
- - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
762
- Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
763
- - \`AWM_FEEDBACK_BM25=1\` — re-enables Rocchio pseudo-relevance feedback
764
- (phase 3.5), a third BM25 pass over the query plus five terms harvested from
765
- the top 3 results. Default OFF since 2026-10-09: measured at 6.7–7.0% of every
766
- warm recall for no measurable accuracy — 3 gold ranks moved in 950 queries
767
- across three suites, two of them the wrong way. On the public corpus — the only
768
- suite with recall headroom left — it is inert per query, not just in aggregate:
769
- 0 of 200 queries differ in rank, top-1 or score.
770
- - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
771
- heuristic + LRU cache. Forces every recall through flan-t5-small.
772
-
773
- Write pipeline + lifecycle:
774
- - \`AWM_SLOW_WRITE_MS=250\` — any write slower than this logs one stderr
775
- line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
776
- embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
777
- first write/recall feels slow.
778
- - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
779
- can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
780
- preserves more reinforced detail; lower = leaner recall output.
781
- - \`AWM_REINFORCE_MERGE_CONTENT=0\` — disable content merge on reinforce.
782
- Reverts to pre-0.8.5 behavior (discard new content, only bump confidence).
783
- - \`AWM_NOVELTY_EMBED=0\` — disable the cosine channel in novelty
784
- computation. BM25-only fallback. Reverts to pre-0.8.5 novelty.
785
- - \`AWM_GRANULARITY_COMPACT_LEN=200\` — char budget for query-aware snippet
786
- in \`granularity: 'compact'\` mode.
787
- - \`AWM_GRANULARITY_FULL_LEN=1000\` — char budget for the top result in
788
- \`granularity: 'auto'\` mode when there's a clear winner.
789
-
790
- PGlite backend:
791
- - \`AWM_PGLITE_BM25_M=1\` — multiplier on PGlite \`ts_rank_cd\` to calibrate
792
- against SQLite FTS5 BM25 distribution. M=1 (default) is passthrough;
793
- higher M boosts PGlite scores at the cost of recall-ranking precision
794
- (see CHANGELOG 0.8.5 follow-up).
795
- - \`AWM_IVFFLAT_PROBES=5\` — pgvector ivfflat probes per query. Higher =
796
- more accurate, slower.
797
-
798
- In production, leave these all unset. Use only when diagnosing a suspected
799
- recall-quality regression.
800
- `.trimStart();
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Shared utilities for CLI adapters.
6
+ *
7
+ * Extracted from the original setup() in cli.ts — path resolution, secrets,
8
+ * environment variables, MCP command building, and the AWM instruction snippet.
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, realpathSync } from 'node:fs';
12
+ import { resolve, join, dirname, basename } from 'node:path';
13
+ import { randomBytes } from 'node:crypto';
14
+ import { homedir as osHomedir } from 'node:os';
15
+ import { fileURLToPath } from 'node:url';
16
+ import type { SetupContext } from './types.js';
17
+ import { deriveAgentFromDir } from '../core/agent-id.js';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ /** Resolve the AWM package root (where src/ and dist/ live). */
23
+ export function resolvePackageRoot(): string {
24
+ // __dirname is src/adapters/ at dev time, dist/adapters/ at build time
25
+ return resolve(__dirname, '..', '..');
26
+ }
27
+
28
+ /** A store inside the installed package is destroyed by the next `npm install -g`. */
29
+ export function isInsidePackage(dbPath: string): boolean {
30
+ return /[\\/]node_modules[\\/]/.test(dbPath);
31
+ }
32
+
33
+ /**
34
+ * Resolve the database path — default `~/.awm/memory.db`.
35
+ *
36
+ * It used to default to `<packageRoot>/data/memory.db`, i.e. INSIDE the installed npm
37
+ * package. That is a data-loss bug, not an untidiness: `npm install -g
38
+ * agent-working-memory@latest` renames the package directory aside and deletes it, so an
39
+ * upgrade takes every memory with it, and `npm uninstall -g` does the same silently.
40
+ *
41
+ * Reported from a real machine: the upgrade failed with EBUSY because a running AWM
42
+ * process held memory.db open. That failure was the LUCKY outcome — with Claude Code
43
+ * closed, the upgrade would have succeeded and the store would have gone with it.
44
+ *
45
+ * `~/.awm/memory.db` is what the plugin and Desktop launchers already used, so until now
46
+ * the same product had two different defaults depending on how it was installed.
47
+ */
48
+ export function resolveDbPath(packageRoot: string, explicit?: string | null): string {
49
+ const dbPath = explicit ?? join(homedir(), '.awm', 'memory.db');
50
+ const dbDir = dirname(dbPath);
51
+ if (!existsSync(dbDir)) {
52
+ mkdirSync(dbDir, { recursive: true });
53
+ }
54
+ return dbPath;
55
+ }
56
+
57
+ /** The default live store, i.e. what a path resolves to when nothing was specified. */
58
+ export function isLiveStorePath(dbPath: string): boolean {
59
+ const target = canonicalPath(dbPath);
60
+ return [join(homedir(), '.awm', 'memory.db'), join(homedir(), '.awm', 'memory-pglite')]
61
+ .some(live => canonicalPath(live) === target);
62
+ }
63
+
64
+ /**
65
+ * A path in the form two spellings of the same file share: real path when it exists
66
+ * (junctions, symlinks, 8.3 short names), no `\\?\` prefix, and case-folded on
67
+ * Windows, whose file system ignores case (`c:\Users` and `C:\users` are one file).
68
+ */
69
+ function canonicalPath(p: string): string {
70
+ let out = resolve(p.replace(/^\\\\\?\\/, ''));
71
+ try { out = realpathSync.native(out); } catch {
72
+ // Not there yet (a fresh machine): resolve the folder, which may itself be a junction.
73
+ try { out = join(realpathSync.native(dirname(out)), basename(out)); } catch { /* compare as given */ }
74
+ }
75
+ return process.platform === 'win32' ? out.toLowerCase() : out;
76
+ }
77
+
78
+ /**
79
+ * True when this process is a test or benchmark run.
80
+ *
81
+ * `VITEST` is set by the runner itself, so it needs no cooperation from the test.
82
+ * `AWM_BENCH=1` is for a bench or eval script to set; nothing sets it automatically.
83
+ * Both are absent in every production path: the MCP server, the HTTP server, the CLI
84
+ * and the hook sidecars.
85
+ *
86
+ * Not `NODE_ENV=test`: a developer shell that exports it would hand it to the MCP server
87
+ * Claude Code starts from there, and the server would refuse its own store and exit,
88
+ * which shows up only as "Connection closed".
89
+ */
90
+ export function isTestOrBenchContext(): boolean {
91
+ return process.env.VITEST === 'true' || process.env.VITEST === '1'
92
+ || process.env.AWM_BENCH === '1';
93
+ }
94
+
95
+ /**
96
+ * Refuse to open the LIVE store from a test or benchmark process (A-3, 2026-10-12).
97
+ *
98
+ * Every launcher defaults `AWM_DB_PATH` to `~/.awm/memory.db` — correct for the product
99
+ * and wrong for a harness that forgot to override it, because the harness writes.
100
+ * 4,745 LoCoMo benchmark fixture engrams were found in the live store across 51 unpinned
101
+ * UUID agent spaces and purged on 2026-10-11; nine real memories sat among them and a
102
+ * naive filter would have destroyed those too.
103
+ *
104
+ * The current eval path is not the offender — `tests/realstore-eval/` reads
105
+ * `snapshot/store-backfilled.db` and the newest fixture found was dated 2026-06-17, four
106
+ * months earlier. So this is a guard against the DEFAULT, placed where the default is
107
+ * produced, not a fix for a live bug.
108
+ *
109
+ * Escape hatch: `AWM_ALLOW_LIVE_STORE=1`, for the deliberate case of measuring against
110
+ * the real store (which `docs/eval-gap-2026-10-11.md` does want, read-only). Loud and
111
+ * opt-in beats a silent default.
112
+ */
113
+ export function assertNotLiveStoreUnderTest(dbPath: string): void {
114
+ if (process.env.AWM_ALLOW_LIVE_STORE === '1') return;
115
+ if (!isTestOrBenchContext()) return;
116
+ if (!isLiveStorePath(dbPath)) return;
117
+ throw new Error(
118
+ `AWM refused to open the live store from a test/bench process.\n`
119
+ + ` path: ${dbPath}\n`
120
+ + ` because: this process looks like a harness (VITEST or AWM_BENCH=1) and\n`
121
+ + ` a harness that writes to the live store contaminates it — 4,745 fixture\n`
122
+ + ` engrams were purged from it on 2026-10-11 for exactly this reason.\n`
123
+ + ` fix: set AWM_DB_PATH to a scratch file, or AWM_ALLOW_LIVE_STORE=1 if you mean it.`,
124
+ );
125
+ }
126
+
127
+ /** Read or generate the hook secret token. */
128
+ export function resolveHookSecret(dbPath: string): string {
129
+ const secretPath = join(dirname(dbPath), '.awm-hook-secret');
130
+ if (existsSync(secretPath)) {
131
+ const existing = readFileSync(secretPath, 'utf-8').trim();
132
+ if (existing) return existing;
133
+ }
134
+ const secret = randomBytes(32).toString('hex');
135
+ mkdirSync(dirname(secretPath), { recursive: true });
136
+ writeFileSync(secretPath, secret + '\n');
137
+ return secret;
138
+ }
139
+
140
+ /**
141
+ * Retrieval settings `awm setup` turns on for a new install (0.14.6).
142
+ *
143
+ * All three ship default-off in the engine so an existing process never changes
144
+ * behavior on upgrade, but the README has recommended them together since 0.14.0:
145
+ * second-stage rerank by the cross-encoder's own score, a 400-char rerank window
146
+ * on the densest query-term region (25% → 87.5% on long memories), and tags in the
147
+ * rerank passage (+7.4pp). A value the user already has in their config wins.
148
+ */
149
+ export const RECOMMENDED_ENV: Readonly<Record<string, string>> = {
150
+ AWM_RERANK2: '1',
151
+ AWM_RERANK_WINDOW: 'query',
152
+ AWM_RERANK_TAGS: '1',
153
+ };
154
+
155
+ /** Keys `awm setup` owns outright; everything else in an existing env block is the user's. */
156
+ export const OWNED_ENV_KEYS = ['AWM_DB_PATH', 'AWM_AGENT_ID', 'AWM_HOOK_PORT', 'AWM_HOOK_PORT_RANGE', 'AWM_HOOK_SECRET', 'AWM_CLIENT'] as const;
157
+
158
+ /**
159
+ * Build environment variables for the MCP server process.
160
+ *
161
+ * Layering, lowest to highest: recommended defaults → whatever the user's current
162
+ * entry already carries (rerank flags they tuned, AWM_WORKSPACE, backend choice…)
163
+ * → the keys setup owns. Before 0.14.6 the entry was replaced wholesale, which
164
+ * dropped hand-set flags on every re-run.
165
+ */
166
+ export function buildEnvVars(
167
+ dbPath: string,
168
+ agentId: string,
169
+ hookPort: string,
170
+ hookSecret: string,
171
+ isWindows: boolean,
172
+ opts: { hookPortRange?: string; existing?: Record<string, string> | null; client?: string } = {},
173
+ ): Record<string, string> {
174
+ const owned: Record<string, string> = {
175
+ AWM_DB_PATH: isWindows ? dbPath.split('\\').join('/') : dbPath,
176
+ AWM_AGENT_ID: agentId,
177
+ AWM_HOOK_PORT: hookPort,
178
+ AWM_HOOK_SECRET: hookSecret,
179
+ };
180
+ if (opts.hookPortRange && opts.hookPortRange !== '10') owned.AWM_HOOK_PORT_RANGE = opts.hookPortRange;
181
+ return { ...RECOMMENDED_ENV, ...(opts.existing ?? {}), ...owned };
182
+ }
183
+
184
+ /**
185
+ * Resolve the MCP server command + args.
186
+ *
187
+ * Prefers absolute path to dist/mcp.js (works from any cwd).
188
+ * Falls back to npx tsx src/mcp.ts for dev mode.
189
+ */
190
+ export function resolveMcpCommand(ctx: SetupContext): {
191
+ command: string;
192
+ args: string[];
193
+ } {
194
+ if (ctx.hasDist) {
195
+ return {
196
+ command: 'node',
197
+ args: [ctx.mcpDist.replace(/\\/g, '/')],
198
+ };
199
+ }
200
+ // Dev fallback
201
+ if (ctx.isWindows) {
202
+ return {
203
+ command: 'cmd',
204
+ args: ['/c', 'npx', 'tsx', ctx.mcpScript.replace(/\\/g, '/')],
205
+ };
206
+ }
207
+ return {
208
+ command: 'npx',
209
+ args: ['tsx', ctx.mcpScript],
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Build a full SetupContext from parsed CLI flags.
215
+ *
216
+ * Precedence for every owned value: an explicit flag → the value in the entry a
217
+ * previous `awm setup` wrote (`existingEnv`) → the default. So re-running setup to
218
+ * pick up new hooks or guidance never repoints the database, renames the agent or
219
+ * moves the sidecar port underneath a working install.
220
+ *
221
+ * Global default agent id is `work` (was `claude` before 0.14.6): it matches what
222
+ * the server itself resolves to when AWM_AGENT_ID is unset (see core/agent-id.ts),
223
+ * so a config with and without the key name the same pool.
224
+ */
225
+ export function buildSetupContext(opts: {
226
+ agentId?: string;
227
+ dbPath?: string | null;
228
+ isGlobal: boolean;
229
+ hookPort?: string;
230
+ hookPortRange?: string;
231
+ installPrime?: boolean;
232
+ /** `--force`: replace a legacy unmarked instruction section rather than refusing. */
233
+ forceInstructions?: boolean;
234
+ existingEnv?: Record<string, string> | null;
235
+ /** Which surface this install is for — stamped onto every write as a `client=` tag. */
236
+ client?: string;
237
+ }): SetupContext {
238
+ const cwd = process.cwd();
239
+ const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
240
+ const packageRoot = resolvePackageRoot();
241
+ const mcpScript = join(packageRoot, 'src', 'mcp.ts');
242
+ const mcpDist = join(packageRoot, 'dist', 'mcp.js');
243
+ const hasDist = existsSync(mcpDist);
244
+ const isWindows = process.platform === 'win32';
245
+ const existing = opts.existingEnv ?? null;
246
+
247
+ const agentId = opts.agentId
248
+ ?? existing?.AWM_AGENT_ID
249
+ ?? (opts.isGlobal ? deriveAgentFromDir('') : projectName);
250
+ // An existing AWM_DB_PATH is normally preserved — that is what makes re-running setup an
251
+ // upgrade rather than a reset. There is one exception: a store INSIDE the installed
252
+ // package is deleted by the next `npm install -g`, so preserving it would be preserving a
253
+ // data-loss bug. Move it to the safe default, copying the file rather than pointing at an
254
+ // empty one, and leave the original where it is so nothing is destroyed by the rescue.
255
+ const requested = opts.dbPath ?? existing?.AWM_DB_PATH ?? null;
256
+ let dbPath: string;
257
+ let rescuedFrom: string | null = null;
258
+ if (!opts.dbPath && requested && isInsidePackage(requested)) {
259
+ const safe = resolveDbPath(packageRoot, null);
260
+ if (existsSync(requested) && !existsSync(safe)) {
261
+ mkdirSync(dirname(safe), { recursive: true });
262
+ copyFileSync(requested, safe);
263
+ for (const side of ['-wal', '-shm']) {
264
+ if (existsSync(requested + side)) copyFileSync(requested + side, safe + side);
265
+ }
266
+ rescuedFrom = requested;
267
+ }
268
+ dbPath = safe;
269
+ } else {
270
+ dbPath = resolveDbPath(packageRoot, requested);
271
+ }
272
+ const hookPort = opts.hookPort ?? existing?.AWM_HOOK_PORT ?? '8401';
273
+ const hookPortRange = opts.hookPortRange ?? existing?.AWM_HOOK_PORT_RANGE ?? '10';
274
+ const hookSecret = resolveHookSecret(dbPath);
275
+ const envVars = buildEnvVars(dbPath, agentId, hookPort, hookSecret, isWindows, { hookPortRange, existing, client: opts.client });
276
+
277
+ return {
278
+ rescuedDbFrom: rescuedFrom,
279
+ cwd,
280
+ projectName,
281
+ agentId,
282
+ dbPath,
283
+ packageRoot,
284
+ mcpDist,
285
+ mcpScript,
286
+ hasDist,
287
+ hookSecret,
288
+ hookPort,
289
+ hookPortRange,
290
+ installPrime: opts.installPrime ?? true,
291
+ forceInstructions: opts.forceInstructions ?? false,
292
+ isGlobal: opts.isGlobal,
293
+ isWindows,
294
+ envVars,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Home directory. `AWM_SETUP_HOME` overrides it so `awm setup` / `awm doctor` can be
300
+ * exercised against a scratch tree (tests do this; so does a cautious upgrade).
301
+ */
302
+ export function homedir(): string {
303
+ return process.env.AWM_SETUP_HOME || osHomedir();
304
+ }
305
+
306
+ // ─── Instruction content ────────────────────────────────
307
+
308
+ /**
309
+ * Core AWM instruction snippet — shared across all adapters.
310
+ * Each adapter wraps this in the appropriate file format.
311
+ */
312
+ /**
313
+ * Upsert the AWM section into an instruction file (CLAUDE.md, AGENTS.md, .cursorrules).
314
+ *
315
+ * Behavior:
316
+ * - File doesn't exist -> create with title + AWM_INSTRUCTION_CONTENT
317
+ * - Section absent -> append
318
+ * - Section present + identical -> skip
319
+ * - Section present + stale -> REPLACE in place, preserve content above/below
320
+ *
321
+ * Section is bounded by `## Memory (AWM)` (with optional trailing modifier) at the
322
+ * start, and the next `## ` heading or EOF at the end.
323
+ *
324
+ * Returns a short human-readable status string for the setup command output.
325
+ */
326
+ /** Opening marker for the generated block. Everything between the markers is replaced
327
+ * on upgrade; anything outside them is preserved. The text is deliberately addressed to
328
+ * whoever opens the file, because that is who needs to know. */
329
+ export const AWM_GEN_BEGIN =
330
+ '<!-- AWM:GENERATED:BEGIN — this block is REPLACED by `awm setup`. ' +
331
+ 'Put your own notes OUTSIDE these markers and they will survive upgrades. -->';
332
+ export const AWM_GEN_END = '<!-- AWM:GENERATED:END -->';
333
+
334
+ /** Wrap generated content in the markers. */
335
+ function wrapGenerated(content: string): string {
336
+ return `${AWM_GEN_BEGIN}\n${content.trim()}\n${AWM_GEN_END}`;
337
+ }
338
+
339
+ export function upsertAwmSection(
340
+ filePath: string,
341
+ newContent: string,
342
+ options: { titleIfNew?: string; suffix?: string; force?: boolean } = {},
343
+ ): string {
344
+ const fname = basename(filePath);
345
+ const suffix = options.suffix ?? '';
346
+ const force = options.force ?? false;
347
+
348
+ if (!existsSync(filePath)) {
349
+ const title = options.titleIfNew ?? `# ${basename(dirname(filePath))}`;
350
+ mkdirSync(dirname(filePath), { recursive: true });
351
+ writeFileSync(filePath, `${title}\n\n${wrapGenerated(newContent)}${suffix}`);
352
+ return `${fname}: created with AWM workflow section`;
353
+ }
354
+
355
+ const existing = readFileSync(filePath, 'utf-8');
356
+
357
+ // ---- Preferred path: the file already carries generated markers, so the boundary
358
+ // between "ours" and "theirs" is explicit and only our block is touched.
359
+ const gb = existing.indexOf(AWM_GEN_BEGIN);
360
+ const ge = existing.indexOf(AWM_GEN_END);
361
+ if (gb !== -1 && ge > gb) {
362
+ const current = existing.slice(gb, ge + AWM_GEN_END.length);
363
+ const desired = wrapGenerated(newContent);
364
+ if (current.trimEnd() === desired.trimEnd()) {
365
+ return `${fname}: AWM section already up-to-date (skipped)`;
366
+ }
367
+ writeFileSync(filePath, existing.slice(0, gb) + desired + existing.slice(ge + AWM_GEN_END.length));
368
+ return `${fname}: AWM generated block updated (content outside the markers preserved)`;
369
+ }
370
+
371
+ // Find section bounds: `## Memory (AWM)` (possibly with ` — MANDATORY` etc.) until next `## ` or EOF
372
+ const startRegex = /^## Memory \(AWM\)[^\n]*$/m;
373
+ const startMatch = startRegex.exec(existing);
374
+
375
+ if (!startMatch) {
376
+ // Section not present — append
377
+ writeFileSync(filePath, existing.trimEnd() + '\n\n' + wrapGenerated(newContent) + suffix);
378
+ return `${fname}: appended AWM workflow section`;
379
+ }
380
+
381
+ // Find end: next `## ` heading after the section start, or EOF
382
+ const afterStart = startMatch.index + startMatch[0].length;
383
+ const nextHeadingRegex = /^## (?!Memory \(AWM\))/m;
384
+ nextHeadingRegex.lastIndex = afterStart;
385
+ const tail = existing.slice(afterStart);
386
+ const nextMatch = nextHeadingRegex.exec(tail);
387
+ const sectionEnd = nextMatch ? afterStart + nextMatch.index : existing.length;
388
+
389
+ const currentSection = existing.slice(startMatch.index, sectionEnd).trimEnd();
390
+ const desiredSection = (newContent + suffix).trimEnd();
391
+
392
+ if (currentSection === desiredSection) {
393
+ return `${fname}: AWM section already up-to-date (skipped)`;
394
+ }
395
+
396
+ // ---- LEGACY, UNMARKED SECTION.
397
+ // Written before generated markers existed, so hand-added notes inside it are
398
+ // indistinguishable from generated text. Replacing would silently delete them — on a
399
+ // real install this measured 169 of 381 lines (44%), every one an operational finding
400
+ // that cost real debugging. Back up and refuse; the caller opts in explicitly.
401
+ if (!force) {
402
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('Z', '');
403
+ const backup = `${filePath}.awm-backup-${stamp}`;
404
+ try {
405
+ writeFileSync(backup, existing);
406
+ } catch {
407
+ return `${fname}: AWM section is out of date, but it has no generated markers and the ` +
408
+ `backup could not be written — REFUSING to touch it. Copy the file yourself, then re-run.`;
409
+ }
410
+ return `${fname}: NOT updated — the AWM section predates generated markers, so your own ` +
411
+ `notes inside it cannot be told apart from generated text and would be lost. ` +
412
+ `Backup written to ${basename(backup)}. To upgrade: move any notes you want to keep ` +
413
+ `ABOVE the '## Memory (AWM)' heading (content outside the section is always preserved), ` +
414
+ `then re-run with --force. Nothing has been changed.`;
415
+ }
416
+
417
+ const before = existing.slice(0, startMatch.index).trimEnd();
418
+ const after = existing.slice(sectionEnd).replace(/^\s*\n/, '');
419
+ const rebuilt =
420
+ (before ? before + '\n\n' : '') +
421
+ wrapGenerated(newContent) + suffix +
422
+ (after ? '\n\n' + after : '\n');
423
+ writeFileSync(filePath, rebuilt);
424
+ return `${fname}: AWM section replaced with a marked generated block (force)`;
425
+ }
426
+
427
+ export const AWM_INSTRUCTION_CONTENT = `
428
+ ## Memory (AWM) — MANDATORY
429
+
430
+ **AWM is THE memory system.** Use it via the \`agent-working-memory\` MCP server
431
+ (preferred) or HTTP at \`http://127.0.0.1:8400\` (fallback). The file-based
432
+ auto-memory at \`~/.claude/projects/.../memory/*.md\` is a LEGACY bootstrap path —
433
+ **do not write new memories to it.** All persistent knowledge goes through AWM.
434
+
435
+ If MCP tools aren't loaded at session start, use ToolSearch with
436
+ \`select:mcp__agent-working-memory__memory_recall,mcp__agent-working-memory__memory_write\`
437
+ to load them. If the MCP server isn't responsive, restart with \`/mcp\` or use
438
+ the HTTP endpoints (\`POST /memory/write-batch\`, \`POST /memory/activate\`)
439
+ directly — but **DO NOT fall back to markdown files**. Files drift the moment
440
+ you write them; AWM stays current because every agent reads + writes the same store.
441
+
442
+ ### Lifecycle (always do these, in this order)
443
+ 1. **Session start**: call \`memory_restore\` to recover previous context. If it reports the
444
+ store is empty/new (or recall keeps returning nothing), **warm-start first**: recall the
445
+ \`onboard a new project\` skill and follow it — or call \`onboard_scan\` on the project's
446
+ docs/repo, refine the candidates, run \`onboard_questions\`, and save the good ones with
447
+ \`memory_write\` (canonical). A cold store is nearly useless until it's seeded.
448
+ 2. **Starting a task**: call \`memory_task_begin\` (checkpoints + recalls relevant memories).
449
+ 3. **During work**: call \`memory_recall\` BEFORE stating any fact, BEFORE searching
450
+ the filesystem, BEFORE making architectural decisions. Recall is ~300ms — cheaper
451
+ than one filesystem search.
452
+ 4. **As you learn things**: call \`memory_write\` proactively. Don't batch.
453
+ 5. **Finishing a task**: call \`memory_task_end\` with a summary.
454
+ 6. **Hooks do the rest.** \`awm setup\` installs five: a **prime** hook that recalls against
455
+ each prompt and injects what clears confidence (or nothing), checkpoint hooks on
456
+ compaction and session end (plus a 15-min timer), a local write/recall reminder on
457
+ Stop, and a reminder after Bash/PowerShell calls that a data change is not done
458
+ until it is written. Primed context arrives labeled
459
+ \`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
460
+ arrives with no primed context, that is a signal too: recall explicitly before
461
+ asserting anything.
462
+
463
+ ### Write memory when:
464
+ - A project decision is made or changed
465
+ - A root cause is discovered after debugging
466
+ - A reusable implementation pattern is established
467
+ - A user preference, constraint, or requirement is clarified
468
+ - A prior assumption is found to be wrong
469
+ - A significant piece of work is completed
470
+
471
+ ### Writing for recall (the highest-leverage section)
472
+ A memory's recall quality is set the moment you write it. AWM is fast at
473
+ finding what's findable — but if the write is shaped wrong, no retriever
474
+ can rescue it. Be slightly more verbose at the front than feels natural:
475
+ the first 1-2 sentences are what BM25, the embedding model, and concept
476
+ extraction all see most strongly.
477
+
478
+ - **Lead with the rule or fact.** Don't open with context or backstory.
479
+ "Don't mock the database in integration tests." comes first; the reason
480
+ comes second. Recall scans the head of the body, not the tail.
481
+ - **Pick the most specific topic.** Not \`auth\` — \`auth-magic-link-rate-limit\`.
482
+ Topic is a hard filter at recall time. Generic topics hide the memory in
483
+ a noisy bucket where it competes with everything else in the area.
484
+ - **Include 2+ retrievable identifiers.** File paths, function names, table
485
+ columns, ticket IDs, exact error strings, the literal terms a future query
486
+ will use. \`SettlementService.closeRun()\` beats "the accounting code."
487
+ \`tblMemberDetails.activation_date\` beats "the activation column."
488
+ \`schema/072-settlement-lock.sql\` beats "the migration."
489
+ - **Write in the vocabulary of the future question.** When you imagine asking
490
+ this in three months, what nouns will you use? Use those nouns. Don't
491
+ paraphrase the user's domain language into your own neutral summary.
492
+ - **Name the CATEGORY as well as the specifics — both, always.** Specifics
493
+ make a memory precise; category words make it *reachable*. A memory that
494
+ says "private plan memory peaked 88%, scale P1v3 -> P2v3" never says
495
+ "Azure" or "App Service Plan", so a question asked in those words cannot
496
+ find it — measured on a real store, that memory was not in the top 40
497
+ candidates for "azure app service plan capacity increase". Write the
498
+ system / product / domain nouns (Azure App Service Plan, Freshdesk ticket,
499
+ MySQL connection pool, ShowConnect scoring) NEXT TO the identifiers.
500
+ Note this cuts against "pick the most specific topic" above: that rule is
501
+ about the \`topic\` TAG, not a licence to omit the category from the body.
502
+ Measured on an 11k-engram store: 94% of tagged memories are missing at
503
+ least one of their own topical terms from the body, and 66% of those
504
+ terms never appear in the text at all.
505
+ - **Tags are NOT a substitute for body text.** The embedding is built from
506
+ \`concept + content\` only, so it never sees tags. BM25 indexes them. The
507
+ cross-encoder rerank passage sees ONLY \`topic=\`/\`proj=\`/\`project=\` tags, and
508
+ only when \`AWM_RERANK_TAGS=1\` — which \`awm setup\`, the plugin and the Desktop
509
+ bundle all switch on, so assume it is on — capped at 80 characters. So a word
510
+ that exists only as a tag is invisible to the embedding entirely, and
511
+ invisible to the ranker unless it is one of those three prefixes inside that
512
+ budget. Tag it *and* write it.
513
+ - **Reserve canonical for stable invariants.** Decisions, requirements,
514
+ hard facts, cross-agent shared context. Working class (default) is correct
515
+ for findings, observations, and progress notes. The canonical floor is
516
+ 0.7 salience — overusing it pollutes the canonical layer and the floor
517
+ loses meaning.
518
+ - **Include the why for feedback memories.** A rule without a reason can't
519
+ be applied to edge cases. "Don't mock the database" is brittle. "Don't
520
+ mock the database — last quarter mocked tests masked a broken migration"
521
+ is portable to new situations.
522
+
523
+ ### Tagging rules (REQUIRED — AWM's prefix-tag retrieval boost depends on these)
524
+
525
+ Every \`memory_write\` should pass these structured fields. AWM stores each as a
526
+ prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
527
+ and entity-bridge boosts at recall time.
528
+
529
+ (“Required” below means *required for good recall* — the MCP schema accepts every
530
+ one of these as optional, so a write without them succeeds and simply retrieves
531
+ worse.)
532
+
533
+ | Field | Required? | Format | Example |
534
+ |---|---|---|---|
535
+ | \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
536
+ | \`topic\` | **YES** | one or more lowercase area words | \`"database-migration"\`, \`"benchmarks"\` |
537
+ | \`intent\` | **YES** | one of: \`decision\` / \`finding\` / \`todo\` / \`question\` / \`context\` | \`"finding"\` |
538
+ | \`confidence_level\` | **YES** | \`verified\` (tested) / \`observed\` (read in code) / \`assumed\` (reasoning) | \`"verified"\` |
539
+ | \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
540
+ | \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
541
+ | \`session_id\` | recommended | current conversation ID for entity-bridge boost | autogenerated |
542
+ | \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=10001", "date=2026-05-11"]\` |
543
+
544
+ **Always add identifier tags when present in the content:**
545
+ - \`ticket=<id>\` for Freshdesk tickets
546
+ - \`member=<id>\` for member IDs
547
+ - \`asset=<id>\` for a domain record id
548
+ - \`usef=<id>\` for USEF lookups
549
+ - \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
550
+ - \`person=<Name>\` for stakeholder quotes / decisions
551
+ - \`version=<X.Y.Z>\` for release-specific findings
552
+
553
+ ### Entity index — exact-match recall for named things (default off)
554
+ Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
555
+ ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
556
+ A query naming an entity ("ticket 10003", "Dana Whitfield") can reach the memory through
557
+ this index even when the wording doesn't lexically match — it's a deterministic exact
558
+ lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
559
+ this reason, not just for the BM25 boost described above.
560
+
561
+ Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
562
+ \`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
563
+ guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
564
+ surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
565
+ people/things you refer to by name often); not yet the default pending evaluation.
566
+
567
+ ### Temporal validity — memories that expire or start in the future
568
+ \`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
569
+ **operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
570
+ a ticket status — so the memory expires instead of relying on you to remember it's
571
+ stale. Recall renders \`[valid until …]\` while the window holds; once it has passed
572
+ the memory is down-ranked like a superseded one and labelled \`⚠ EXPIRED\`. Use
573
+ \`valid_from\` for a fact that becomes true on a known future date (a policy change, a
574
+ season that hasn't started yet). Don't set either for durable facts — most memories
575
+ don't need them.
576
+
577
+ ### Memory classes (controls how strictly the salience filter gates the write)
578
+ - \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
579
+ Use for: user-stated decisions, project requirements, verified architectural facts,
580
+ cross-agent shared context. **In a hive (multi-agent) setup, always use \`canonical\`
581
+ for writes that other agents must be able to recall** — the default \`working\` class
582
+ may get filtered.
583
+ - \`memory_class: working\` (default) — observations and findings. Salience-gated.
584
+ - \`memory_class: ephemeral\` — short-lived context that should decay quickly.
585
+
586
+ ### Salience auto-promotion (defense in depth)
587
+ The salience filter automatically promotes certain content patterns even if you forget
588
+ to set \`memory_class\` explicitly:
589
+ - **User feedback** — content starting with a configured stakeholder name plus a feedback
590
+ verb ("<name> said…", "<name> directed…", "<name> decided…") auto-promotes to canonical,
591
+ so quoting the user verbatim preserves the decision. **This is OFF unless an operator
592
+ sets \`AWM_FEEDBACK_NAMES\`** (it ships empty); when in doubt, pass
593
+ \`memory_class: 'canonical'\` explicitly rather than relying on it.
594
+ - **Verified operational records** — content with an action verb (Submitted, Finalized,
595
+ Completed, Reconciled, Triaged, Posted, Resolved, Stamped, Pushed, Deployed, Migrated,
596
+ Imported, Exported, Backfilled) plus 2+ concrete identifiers (ISO date \`YYYY-MM-DD\`,
597
+ or contextual numeric IDs like "event 18969", "ticket #18330", "USEF 341980") gets
598
+ a 0.45 salience floor. So batch summaries with real IDs survive even when topic
599
+ terms repeat.
600
+
601
+ If neither pattern applies and you want a memory to definitely survive, set
602
+ \`memory_class: canonical\` explicitly. Don't rely on auto-promotion for important writes.
603
+
604
+ ### Recall memory when:
605
+ - **BEFORE stating ANY fact about how a system works** — recall first; if AWM doesn't
606
+ have it, read the code. Never guess and present it as fact.
607
+ - **BEFORE searching the filesystem** — recall first; AWM is faster and has cross-session
608
+ knowledge that file search doesn't.
609
+ - Starting work on a new task or subsystem
610
+ - Re-entering code you haven't touched recently
611
+ - After a failed attempt — check if there's prior knowledge
612
+ - Before refactoring or making architectural changes
613
+ - When a topic comes up that you might have prior context on
614
+
615
+ Recall is fast (~300ms typical). Use it freely.
616
+
617
+ ### Chain recalls — one hop per call
618
+
619
+ AWM returns what matches your query. It does **not** walk from that answer to the next
620
+ fact, and it is not meant to: it is active memory, holding what you need right now.
621
+ Following a chain to the next thing is **your** job — recall, read, recall again, each
622
+ result becoming the cue for the next query.
623
+
624
+ So when a question refers to something **by its role instead of its name**, you have a
625
+ chain, not a query:
626
+
627
+ > *"What is the codename of the project owned by my scheduler?"*
628
+
629
+ That is three lookups, not one:
630
+
631
+ 1. recall \`who is my scheduler\` → a name
632
+ 2. recall \`what project does <that name> own\` → a project
633
+ 3. recall \`<that project> codename\` → the answer
634
+
635
+ **The failure mode is answering from the first recall.** A single query blending all
636
+ three terms returns the most *salient* related memory, not the correct one — measured,
637
+ that returns the main project's codename with complete confidence, and it is wrong.
638
+ Each hop on its own is an ordinary, reliable recall; the chain only breaks when you stop
639
+ asking.
640
+
641
+ Tells that you are looking at a chain: a possessive or relative clause naming an entity
642
+ by role rather than identity (\`my scheduler\`, \`the owner of X\`, \`whoever signed
643
+ off on Y\`), or a rule that operates on an attribute you have not looked up yet (\`the
644
+ release tag of the project we discussed\` needs that project's codename first). If
645
+ resolving the question requires a fact you would have to *derive*, recall the fact
646
+ instead.
647
+
648
+ ### Recall strategy (when one query isn't enough)
649
+ AWM's adaptive retrieval handles most query variations natively — synonym
650
+ expansion, multi-channel scoring, embedding + BM25 + reranker agreement.
651
+ A single recall is usually enough.
652
+
653
+ When it isn't:
654
+ - **If the first recall returns nothing or returns the wrong things, reformulate.**
655
+ Try a second query with different phrasing — synonyms, more specific nouns,
656
+ the exact identifier from the code rather than the conceptual name. Two or
657
+ three recalls cost less than one filesystem search.
658
+ - **Use the words a domain expert would use, not generic English.** "Period
659
+ close lock" not "accounting feature"; "magic link rate limit" not "auth issue."
660
+ - **For broad exploration, pass \`mode: "exploratory"\`** — wider candidate
661
+ pool, lower precision floor. For specific lookups, leave mode unset (auto).
662
+ - **Don't ensemble more than 3 reformulations.** If three different phrasings
663
+ return nothing, the memory probably isn't there — read the code instead of
664
+ burning more recalls.
665
+
666
+ ### Recall tuning — opt-in parameters
667
+ Default \`memory_recall\` is tuned for the common case. Four opt-in parameters change
668
+ the cost/quality tradeoff. Use them when the default doesn't match what you need.
669
+
670
+ - **\`granularity: 'compact'\`** — every result carries a 200-char \`summary\`
671
+ field with a query-aware snippet (the densest window of query terms in the
672
+ content). Use this when you expect to scan 5+ results to find one — saves
673
+ ~70% of recall output tokens. The full content stays available in
674
+ \`engram.content\` if you want to drill into a specific result.
675
+ - **\`granularity: 'auto'\`** — confidence-adaptive. If the top result is a clear
676
+ winner, it gets a longer summary while the rest are compact. If confidence
677
+ is uniform across results, everything is compact. Use when you don't know
678
+ in advance whether one result will dominate.
679
+ - **\`require_confidence\`** — abstention. The default (0.05) is already a light
680
+ gate: when the top results don't stand out from the rest, recall returns
681
+ nothing and the reply says **\`RECALL ABSTAINED\`** with the count it withheld.
682
+ An empty result *without* that line is genuine absence. The gate is on the
683
+ SHAPE of the score distribution, not relevance — so raising it silences
684
+ specific questions with one clear winner first (measured: 0.25 abstained on
685
+ two identifier queries scoring 0.26–0.38 while passing a vague one at 0.92).
686
+ Leave it alone for ordinary recall; \`0.10\` is the ceiling that still helps for
687
+ push-style use where nobody asked. When abstention fires, reformulate once
688
+ with the exact identifier, then read the code — don't retry with a lower
689
+ threshold to force an answer.
690
+ - **\`workspace: "<name>"\`** — hive-mode recall across all agents in the
691
+ workspace. Use when other agents may have written canonical knowledge you
692
+ need. Default is agent-scoped (your own memories only). Can also be set
693
+ globally via the \`AWM_WORKSPACE\` env var. Memories live in the agent pool
694
+ they were written to; if a session lands in an unexpected pool, \`memory_whoami\`
695
+ says which one before you conclude the memory is missing.
696
+
697
+ ### Keep memory fresh
698
+ - After recalling a memory, if you observe the real state is different → call
699
+ \`memory_supersede\` immediately with the corrected version.
700
+ - When a fact has CHANGED (a time, a price, a status, a decision) and you don't have the
701
+ old memory's id: \`memory_write\` with \`correction: true\`. AWM supersedes the closest
702
+ memory about the same thing and tells you which. Written as a plain observation, a
703
+ changed value merges into or ranks below the stale version — that is how "departs 8:40"
704
+ lost to "departs 10:15" in the MWA assessment.
705
+ - A recall line that says \`⚠ POSSIBLE CONFLICT\`, \`⚠ EXPIRED\` or \`⚠ SUPERSEDED\` is
706
+ telling you not to state that value from memory: read the source, then supersede or
707
+ correct the stale one.
708
+ - After using a recalled memory: call \`memory_feedback\` (useful/not-useful) **with the
709
+ \`recall_id\`** printed at the end of the recall output (\`[recall_id: …]\`). That id
710
+ joins the feedback to the recall that produced it, which is what lets AWM learn
711
+ which recalls were worth their tokens; feedback without it is a bare vote.
712
+ - If you discover a memory is factually wrong: \`memory_retract\` to remove it.
713
+ - **If you bypass AWM (file-memory, in-context notes, "I'll just remember"), the memory
714
+ drifts out of date. The system relies on you to keep it current. This is the #1
715
+ failure mode.**
716
+
717
+ ### Cognition recipes — YOU do the thinking, AWM keeps the result
718
+ AWM contains no LLM. When memory needs real thinking — distilling a repeatable
719
+ procedure, reflecting on a failure — AWM hands YOU a versioned recipe (prompt +
720
+ strict output shape) and you run it as a SEPARATE focused pass, then write the
721
+ result back as an ordinary memory with provenance.
722
+
723
+ - \`memory_task_end\` responses include the recipe invitations. Honor the gates:
724
+ skill-derivation only after a genuinely procedural task (3+ tool calls or a
725
+ delegated sub-task); friction-lesson only after a failure/retry/wrong assumption.
726
+ - Run each recipe as its own focused pass — do NOT bundle it with other
727
+ reasoning; bundled passes reliably drop the output.
728
+ - Write back exactly per the recipe's contract: \`origin_class: 'recipe'\` +
729
+ \`recipe_id\` (e.g. \`skill-derivation@1\`), concept prefixed \`skill: \` or
730
+ \`lesson: \`. AWM validates the shape and rejects malformed or unknown-recipe
731
+ writes with the contract echoed back — fix and retry, don't drop the insight.
732
+ - Re-deriving the same skill name reinforces the existing memory instead of
733
+ duplicating it, so don't fear writing a skill you may have written before.
734
+
735
+ ### Content fade — write-and-forget is safe
736
+ Un-recalled engrams gradually fade their content while preserving cue pathways
737
+ (concept + tags + embedding stay intact). This is Paper 1 — storage
738
+ degradation. Practical implications:
739
+
740
+ - **Don't manually purge memories** to "save space." The system already
741
+ compresses unused content. Old memories stay findable via cue match even
742
+ when their body has decayed.
743
+ - **Don't over-pin with \`memory_class: canonical\`** to fight fade. Canonical
744
+ only changes salience gating at write time, not fade behavior. Fade
745
+ affects un-recalled engrams of any class.
746
+ - **Recall keeps content alive.** Every recall touches the engram and resets
747
+ its fade clock. Frequently-recalled memories stay full-fidelity automatically.
748
+ - **Supersede is the right tool for stale facts.** When you observe a memory
749
+ is outdated, call \`memory_supersede\` — the new version inherits the old
750
+ one's coherent associations (counter-narrative replacement) so cue
751
+ pathways carry forward to the replacement.
752
+
753
+ ### Example — good vs bad memory_write
754
+
755
+ **BAD** (no prefix tags, vague concept, can't be recalled by future queries):
756
+ \`\`\`
757
+ memory_write(
758
+ concept="found a bug",
759
+ content="The thing I was looking at was broken so I fixed it."
760
+ )
761
+ \`\`\`
762
+
763
+ **GOOD** (rich identifiers, structured metadata, prefix tags):
764
+ \`\`\`
765
+ memory_write(
766
+ concept="EquiHub period-close BLOCKED check missing server-side",
767
+ content="apps/web/app/(billing)/billing/settlement-lock/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in SettlementService.closeRun() per schema/072-settlement-lock.sql. Without server-side check a malicious request could bypass via direct API call.",
768
+ project="EquiHub",
769
+ topic="accounting",
770
+ intent="finding",
771
+ confidence_level="verified",
772
+ source="debugging",
773
+ memory_class="canonical",
774
+ tags=["ticket=10001", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
775
+ )
776
+ \`\`\`
777
+
778
+ ### Also:
779
+ - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
780
+ - \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
781
+ talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
782
+ agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
783
+ which agent identity, or which running code you're dealing with — before reasoning
784
+ about AWM's own state from a stale memory or an assumed port number.
785
+ - AWM is shared across all agents in real time. When any agent writes or supersedes a
786
+ memory, every other agent can recall it immediately — but only within the same
787
+ workspace and agent scope.
788
+
789
+ ### Output compression (token efficiency, output-only)
790
+ When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
791
+ array of records, query rows, a log dump, an API response — pass it through
792
+ \`compress_output\` first. It re-encodes the data as TOON (a compact, lossless,
793
+ schema-aware tabular form of JSON), cutting ~50-65% of the tokens at no
794
+ comprehension cost. This is output-only: it never changes the data or your memories.
795
+ - Use it on big STRUCTURED outputs, not on prose. Prose is returned unchanged —
796
+ for trimming memory prose, use recall \`granularity: 'compact'\` instead.
797
+ - It returns a \`ref\`; call \`retrieve_original(ref)\` if you later need the exact
798
+ verbatim source (e.g. to hand it to another tool unchanged).
799
+ - Don't bother for small outputs — it only compresses when the saving is worthwhile
800
+ and falls back to plain JSON if TOON wouldn't reproduce the data exactly.
801
+
802
+ ### Backend (SQLite vs PGlite)
803
+ AWM ships two storage backends. The installer picks SQLite by default; both
804
+ are functionally equivalent for cognitive workloads, but differ in operational
805
+ guarantees:
806
+
807
+ - **SQLite** (default) — embedded, **multi-process safe** via WAL mode. Best
808
+ for single-machine setups and MCP scenarios where multiple Claude Code
809
+ sessions may open the same database concurrently.
810
+ - **PGlite** — embedded Postgres (WASM) with pgvector. **Single-process only**
811
+ — two MCP processes against the same \`memory-pglite/\` directory will
812
+ abort the second. Pick via \`AWM_STORE_BACKEND=pglite\` and
813
+ \`AWM_DB_PATH=path/to/memory-pglite\`.
814
+ - **Auto-detect** — if \`AWM_DB_PATH\` points to a directory that already
815
+ exists, AWM detects PGlite; a file → SQLite. No explicit
816
+ \`AWM_STORE_BACKEND\` needed when an existing DB is present.
817
+
818
+ For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
819
+ multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
820
+
821
+ ### Diagnostics / escape hatches (env vars, only if you know why)
822
+ Each optimization is gated by an env var so it can be disabled for A/B testing if a
823
+ regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
824
+
825
+ Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
826
+ - \`AWM_RERANK2=1\` — second-stage rerank by the cross-encoder's own score.
827
+ - \`AWM_RERANK_WINDOW=query\` — 400-char rerank window on the densest query-term
828
+ region instead of the prefix (25% → 87.5% on long memories).
829
+ - \`AWM_RERANK_TAGS=1\` — tags fed into the rerank passage (+7.4pp s@1).
830
+
831
+ Hook sidecar (0.14.2):
832
+ - \`AWM_HOOK_PORT=8401\` / \`AWM_HOOK_PORT_RANGE=10\` — each MCP process binds the
833
+ first free port in the range, so several sessions can run at once. The shipped
834
+ hooks find their own session's sidecar by probing the range and matching
835
+ \`agentId\` on \`/health\`; \`memory_whoami\` reports the port actually bound.
836
+
837
+ Recall pipeline:
838
+ - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
839
+ pre-filter in recall. Reverts to scoring all active candidates.
840
+ - \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (default off; measured
841
+ no effect on the real-store benchmark, 0.14.5).
842
+ - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
843
+ Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
844
+ - \`AWM_FEEDBACK_BM25=1\` — re-enables Rocchio pseudo-relevance feedback
845
+ (phase 3.5), a third BM25 pass over the query plus five terms harvested from
846
+ the top 3 results. Default OFF since 2026-10-09: measured at 6.7–7.0% of every
847
+ warm recall for no measurable accuracy — 3 gold ranks moved in 950 queries
848
+ across three suites, two of them the wrong way. On the public corpus — the only
849
+ suite with recall headroom left — it is inert per query, not just in aggregate:
850
+ 0 of 200 queries differ in rank, top-1 or score.
851
+ - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
852
+ heuristic + LRU cache. Forces every recall through flan-t5-small.
853
+
854
+ Write pipeline + lifecycle:
855
+ - \`AWM_SLOW_WRITE_MS=250\` — any write slower than this logs one stderr
856
+ line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
857
+ embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
858
+ first write/recall feels slow.
859
+ - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
860
+ can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
861
+ preserves more reinforced detail; lower = leaner recall output.
862
+ - \`AWM_REINFORCE_MERGE_CONTENT=0\` — disable content merge on reinforce.
863
+ Reverts to pre-0.8.5 behavior (discard new content, only bump confidence).
864
+ - \`AWM_NOVELTY_EMBED=0\` — disable the cosine channel in novelty
865
+ computation. BM25-only fallback. Reverts to pre-0.8.5 novelty.
866
+ - \`AWM_GRANULARITY_COMPACT_LEN=200\` — char budget for query-aware snippet
867
+ in \`granularity: 'compact'\` mode.
868
+ - \`AWM_GRANULARITY_FULL_LEN=1000\` — char budget for the top result in
869
+ \`granularity: 'auto'\` mode when there's a clear winner.
870
+
871
+ PGlite backend:
872
+ - \`AWM_PGLITE_BM25_M=1\` — multiplier on PGlite \`ts_rank_cd\` to calibrate
873
+ against SQLite FTS5 BM25 distribution. M=1 (default) is passthrough;
874
+ higher M boosts PGlite scores at the cost of recall-ranking precision
875
+ (see CHANGELOG 0.8.5 follow-up).
876
+ - \`AWM_IVFFLAT_PROBES=5\` — pgvector ivfflat probes per query. Higher =
877
+ more accurate, slower.
878
+
879
+ In production, leave these all unset. Use only when diagnosing a suspected
880
+ recall-quality regression.
881
+ `.trimStart();