agent-working-memory 0.15.7 → 0.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,30 @@
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
-
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
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
-
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
28
  /** A store inside the installed package is destroyed by the next `npm install -g`. */
29
29
  export function isInsidePackage(dbPath: string): boolean {
30
30
  return /[\\/]node_modules[\\/]/.test(dbPath);
@@ -47,134 +47,136 @@ export function isInsidePackage(dbPath: string): boolean {
47
47
  */
48
48
  export function resolveDbPath(packageRoot: string, explicit?: string | null): string {
49
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
- * behaviour 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
- existingEnv?: Record<string, string> | null;
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
+ * behaviour 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;
163
165
  /** Which surface this install is for — stamped onto every write as a `client=` tag. */
164
166
  client?: string;
165
- }): SetupContext {
166
- const cwd = process.cwd();
167
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
168
- const packageRoot = resolvePackageRoot();
169
- const mcpScript = join(packageRoot, 'src', 'mcp.ts');
170
- const mcpDist = join(packageRoot, 'dist', 'mcp.js');
171
- const hasDist = existsSync(mcpDist);
172
- const isWindows = process.platform === 'win32';
173
- const existing = opts.existingEnv ?? null;
174
-
175
- const agentId = opts.agentId
176
- ?? existing?.AWM_AGENT_ID
177
- ?? (opts.isGlobal ? deriveAgentFromDir('') : projectName);
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);
178
180
  // An existing AWM_DB_PATH is normally preserved — that is what makes re-running setup an
179
181
  // upgrade rather than a reset. There is one exception: a store INSIDE the installed
180
182
  // package is deleted by the next `npm install -g`, so preserving it would be preserving a
@@ -197,588 +199,589 @@ export function buildSetupContext(opts: {
197
199
  } else {
198
200
  dbPath = resolveDbPath(packageRoot, requested);
199
201
  }
200
- const hookPort = opts.hookPort ?? existing?.AWM_HOOK_PORT ?? '8401';
201
- const hookPortRange = opts.hookPortRange ?? existing?.AWM_HOOK_PORT_RANGE ?? '10';
202
- const hookSecret = resolveHookSecret(dbPath);
203
- const envVars = buildEnvVars(dbPath, agentId, hookPort, hookSecret, isWindows, { hookPortRange, existing, client: opts.client });
204
-
205
- return {
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 {
206
208
  rescuedDbFrom: rescuedFrom,
207
- cwd,
208
- projectName,
209
- agentId,
210
- dbPath,
211
- packageRoot,
212
- mcpDist,
213
- mcpScript,
214
- hasDist,
215
- hookSecret,
216
- hookPort,
217
- hookPortRange,
218
- installPrime: opts.installPrime ?? true,
219
- isGlobal: opts.isGlobal,
220
- isWindows,
221
- envVars,
222
- };
223
- }
224
-
225
- /**
226
- * Home directory. `AWM_SETUP_HOME` overrides it so `awm setup` / `awm doctor` can be
227
- * exercised against a scratch tree (tests do this; so does a cautious upgrade).
228
- */
229
- export function homedir(): string {
230
- return process.env.AWM_SETUP_HOME || osHomedir();
231
- }
232
-
233
- // ─── Instruction content ────────────────────────────────
234
-
235
- /**
236
- * Core AWM instruction snippet — shared across all adapters.
237
- * Each adapter wraps this in the appropriate file format.
238
- */
239
- /**
240
- * Upsert the AWM section into an instruction file (CLAUDE.md, AGENTS.md, .cursorrules).
241
- *
242
- * Behavior:
243
- * - File doesn't exist -> create with title + AWM_INSTRUCTION_CONTENT
244
- * - Section absent -> append
245
- * - Section present + identical -> skip
246
- * - Section present + stale -> REPLACE in place, preserve content above/below
247
- *
248
- * Section is bounded by `## Memory (AWM)` (with optional trailing modifier) at the
249
- * start, and the next `## ` heading or EOF at the end.
250
- *
251
- * Returns a short human-readable status string for the setup command output.
252
- */
253
- /** Opening marker for the generated block. Everything between the markers is replaced
254
- * on upgrade; anything outside them is preserved. The text is deliberately addressed to
255
- * whoever opens the file, because that is who needs to know. */
256
- export const AWM_GEN_BEGIN =
257
- '<!-- AWM:GENERATED:BEGIN — this block is REPLACED by `awm setup`. ' +
258
- 'Put your own notes OUTSIDE these markers and they will survive upgrades. -->';
259
- export const AWM_GEN_END = '<!-- AWM:GENERATED:END -->';
260
-
261
- /** Wrap generated content in the markers. */
262
- function wrapGenerated(content: string): string {
263
- return `${AWM_GEN_BEGIN}\n${content.trim()}\n${AWM_GEN_END}`;
264
- }
265
-
266
- export function upsertAwmSection(
267
- filePath: string,
268
- newContent: string,
269
- options: { titleIfNew?: string; suffix?: string; force?: boolean } = {},
270
- ): string {
271
- const fname = basename(filePath);
272
- const suffix = options.suffix ?? '';
273
- const force = options.force ?? false;
274
-
275
- if (!existsSync(filePath)) {
276
- const title = options.titleIfNew ?? `# ${basename(dirname(filePath))}`;
277
- mkdirSync(dirname(filePath), { recursive: true });
278
- writeFileSync(filePath, `${title}\n\n${wrapGenerated(newContent)}${suffix}`);
279
- return `${fname}: created with AWM workflow section`;
280
- }
281
-
282
- const existing = readFileSync(filePath, 'utf-8');
283
-
284
- // ---- Preferred path: the file already carries generated markers, so the boundary
285
- // between "ours" and "theirs" is explicit and only our block is touched.
286
- const gb = existing.indexOf(AWM_GEN_BEGIN);
287
- const ge = existing.indexOf(AWM_GEN_END);
288
- if (gb !== -1 && ge > gb) {
289
- const current = existing.slice(gb, ge + AWM_GEN_END.length);
290
- const desired = wrapGenerated(newContent);
291
- if (current.trimEnd() === desired.trimEnd()) {
292
- return `${fname}: AWM section already up-to-date (skipped)`;
293
- }
294
- writeFileSync(filePath, existing.slice(0, gb) + desired + existing.slice(ge + AWM_GEN_END.length));
295
- return `${fname}: AWM generated block updated (content outside the markers preserved)`;
296
- }
297
-
298
- // Find section bounds: `## Memory (AWM)` (possibly with ` — MANDATORY` etc.) until next `## ` or EOF
299
- const startRegex = /^## Memory \(AWM\)[^\n]*$/m;
300
- const startMatch = startRegex.exec(existing);
301
-
302
- if (!startMatch) {
303
- // Section not present — append
304
- writeFileSync(filePath, existing.trimEnd() + '\n\n' + wrapGenerated(newContent) + suffix);
305
- return `${fname}: appended AWM workflow section`;
306
- }
307
-
308
- // Find end: next `## ` heading after the section start, or EOF
309
- const afterStart = startMatch.index + startMatch[0].length;
310
- const nextHeadingRegex = /^## (?!Memory \(AWM\))/m;
311
- nextHeadingRegex.lastIndex = afterStart;
312
- const tail = existing.slice(afterStart);
313
- const nextMatch = nextHeadingRegex.exec(tail);
314
- const sectionEnd = nextMatch ? afterStart + nextMatch.index : existing.length;
315
-
316
- const currentSection = existing.slice(startMatch.index, sectionEnd).trimEnd();
317
- const desiredSection = (newContent + suffix).trimEnd();
318
-
319
- if (currentSection === desiredSection) {
320
- return `${fname}: AWM section already up-to-date (skipped)`;
321
- }
322
-
323
- // ---- LEGACY, UNMARKED SECTION.
324
- // Written before generated markers existed, so hand-added notes inside it are
325
- // indistinguishable from generated text. Replacing would silently delete them — on a
326
- // real install this measured 169 of 381 lines (44%), every one an operational finding
327
- // that cost real debugging. Back up and refuse; the caller opts in explicitly.
328
- if (!force) {
329
- const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('Z', '');
330
- const backup = `${filePath}.awm-backup-${stamp}`;
331
- try {
332
- writeFileSync(backup, existing);
333
- } catch {
334
- return `${fname}: AWM section is out of date, but it has no generated markers and the ` +
335
- `backup could not be written — REFUSING to touch it. Copy the file yourself, then re-run.`;
336
- }
337
- return `${fname}: NOT updated — the AWM section predates generated markers, so your own ` +
338
- `notes inside it cannot be told apart from generated text and would be lost. ` +
339
- `Backup written to ${basename(backup)}. To upgrade: move any notes you want to keep ` +
340
- `ABOVE the '## Memory (AWM)' heading (content outside the section is always preserved), ` +
341
- `then re-run with force. Nothing has been changed.`;
342
- }
343
-
344
- const before = existing.slice(0, startMatch.index).trimEnd();
345
- const after = existing.slice(sectionEnd).replace(/^\s*\n/, '');
346
- const rebuilt =
347
- (before ? before + '\n\n' : '') +
348
- wrapGenerated(newContent) + suffix +
349
- (after ? '\n\n' + after : '\n');
350
- writeFileSync(filePath, rebuilt);
351
- return `${fname}: AWM section replaced with a marked generated block (force)`;
352
- }
353
-
354
- export const AWM_INSTRUCTION_CONTENT = `
355
- ## Memory (AWM) — MANDATORY
356
-
357
- **AWM is THE memory system.** Use it via the \`agent-working-memory\` MCP server
358
- (preferred) or HTTP at \`http://127.0.0.1:8400\` (fallback). The file-based
359
- auto-memory at \`~/.claude/projects/.../memory/*.md\` is a LEGACY bootstrap path —
360
- **do not write new memories to it.** All persistent knowledge goes through AWM.
361
-
362
- If MCP tools aren't loaded at session start, use ToolSearch with
363
- \`select:mcp__agent-working-memory__memory_recall,mcp__agent-working-memory__memory_write\`
364
- to load them. If the MCP server isn't responsive, restart with \`/mcp\` or use
365
- the HTTP endpoints (\`POST /memory/write-batch\`, \`POST /memory/activate\`)
366
- directly — but **DO NOT fall back to markdown files**. Files drift the moment
367
- you write them; AWM stays current because every agent reads + writes the same store.
368
-
369
- ### Lifecycle (always do these, in this order)
370
- 1. **Session start**: call \`memory_restore\` to recover previous context. If it reports the
371
- store is empty/new (or recall keeps returning nothing), **warm-start first**: recall the
372
- \`onboard a new project\` skill and follow it — or call \`onboard_scan\` on the project's
373
- docs/repo, refine the candidates, run \`onboard_questions\`, and save the good ones with
374
- \`memory_write\` (canonical). A cold store is nearly useless until it's seeded.
375
- 2. **Starting a task**: call \`memory_task_begin\` (checkpoints + recalls relevant memories).
376
- 3. **During work**: call \`memory_recall\` BEFORE stating any fact, BEFORE searching
377
- the filesystem, BEFORE making architectural decisions. Recall is ~300ms — cheaper
378
- than one filesystem search.
379
- 4. **As you learn things**: call \`memory_write\` proactively. Don't batch.
380
- 5. **Finishing a task**: call \`memory_task_end\` with a summary.
381
- 6. **Hooks do the rest.** \`awm setup\` installs three: a **prime** hook that recalls against
382
- each prompt and injects what clears confidence (or nothing), and checkpoint hooks on
383
- compaction and session end (plus a 15-min timer). Primed context arrives labelled
384
- \`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
385
- arrives with no primed context, that is a signal too: recall explicitly before
386
- asserting anything.
387
-
388
- ### Write memory when:
389
- - A project decision is made or changed
390
- - A root cause is discovered after debugging
391
- - A reusable implementation pattern is established
392
- - A user preference, constraint, or requirement is clarified
393
- - A prior assumption is found to be wrong
394
- - A significant piece of work is completed
395
-
396
- ### Writing for recall (the highest-leverage section)
397
- A memory's recall quality is set the moment you write it. AWM is fast at
398
- finding what's findable — but if the write is shaped wrong, no retriever
399
- can rescue it. Be slightly more verbose at the front than feels natural:
400
- the first 1-2 sentences are what BM25, the embedding model, and concept
401
- extraction all see most strongly.
402
-
403
- - **Lead with the rule or fact.** Don't open with context or backstory.
404
- "Don't mock the database in integration tests." comes first; the reason
405
- comes second. Recall scans the head of the body, not the tail.
406
- - **Pick the most specific topic.** Not \`auth\` — \`auth-magic-link-rate-limit\`.
407
- Topic is a hard filter at recall time. Generic topics hide the memory in
408
- a noisy bucket where it competes with everything else in the area.
409
- - **Include 2+ retrievable identifiers.** File paths, function names, table
410
- columns, ticket IDs, exact error strings, the literal terms a future query
411
- will use. \`AccountingService.closePeriod()\` beats "the accounting code."
412
- \`tblMemberDetails.activation_date\` beats "the activation column."
413
- \`schema/072-period-close.sql\` beats "the migration."
414
- - **Write in the vocabulary of the future question.** When you imagine asking
415
- this in three months, what nouns will you use? Use those nouns. Don't
416
- paraphrase the user's domain language into your own neutral summary.
417
- - **Name the CATEGORY as well as the specifics — both, always.** Specifics
418
- make a memory precise; category words make it *reachable*. A memory that
419
- says "private plan memory peaked 88%, scale P1v3 -> P2v3" never says
420
- "Azure" or "App Service Plan", so a question asked in those words cannot
421
- find it — measured on a real store, that memory was not in the top 40
422
- candidates for "azure app service plan capacity increase". Write the
423
- system / product / domain nouns (Azure App Service Plan, Freshdesk ticket,
424
- MySQL connection pool, ShowConnect scoring) NEXT TO the identifiers.
425
- Note this cuts against "pick the most specific topic" above: that rule is
426
- about the \`topic\` TAG, not a licence to omit the category from the body.
427
- Measured on an 11k-engram store: 94% of tagged memories are missing at
428
- least one of their own topical terms from the body, and 66% of those
429
- terms never appear in the text at all.
430
- - **Tags are NOT a substitute for body text.** Only BM25 indexes tags. The
431
- embedding is built from \`concept + content\` and the cross-encoder rerank
432
- passage is built from \`concept + content\` — neither sees tags. So a word
433
- that exists only as a tag is invisible to two of the three retrieval
434
- channels, including the one that decides final ordering. Tag it *and*
435
- write it.
436
- - **Reserve canonical for stable invariants.** Decisions, requirements,
437
- hard facts, cross-agent shared context. Working class (default) is correct
438
- for findings, observations, and progress notes. The canonical floor is
439
- 0.7 salience — overusing it pollutes the canonical layer and the floor
440
- loses meaning.
441
- - **Include the why for feedback memories.** A rule without a reason can't
442
- be applied to edge cases. "Don't mock the database" is brittle. "Don't
443
- mock the database — last quarter mocked tests masked a broken migration"
444
- is portable to new situations.
445
-
446
- ### Tagging rules (REQUIRED — AWM's prefix-tag retrieval boost depends on these)
447
-
448
- Every \`memory_write\` should pass these structured fields. AWM stores each as a
449
- prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
450
- and entity-bridge boosts at recall time.
451
-
452
- | Field | Required? | Format | Example |
453
- |---|---|---|---|
454
- | \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
455
- | \`topic\` | **YES** | one or more lowercase area words | \`"database-migration"\`, \`"benchmarks"\` |
456
- | \`intent\` | **YES** | one of: \`decision\` / \`finding\` / \`todo\` / \`question\` / \`context\` | \`"finding"\` |
457
- | \`confidence_level\` | **YES** | \`verified\` (tested) / \`observed\` (read in code) / \`assumed\` (reasoning) | \`"verified"\` |
458
- | \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
459
- | \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
460
- | \`session_id\` | recommended | current conversation ID for entity-bridge boost | autogenerated |
461
- | \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=18360", "date=2026-05-11"]\` |
462
-
463
- **Always add identifier tags when present in the content:**
464
- - \`ticket=<id>\` for Freshdesk tickets
465
- - \`member=<id>\` for member IDs
466
- - \`horse=<id>\` for horse_member_id
467
- - \`usef=<id>\` for USEF lookups
468
- - \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
469
- - \`person=<Name>\` for stakeholder quotes / decisions
470
- - \`version=<X.Y.Z>\` for release-specific findings
471
-
472
- ### Entity index — exact-match recall for named things (default off)
473
- Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
474
- ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
475
- A query naming an entity ("ticket 19252", "Kaleigh Collett") can reach the memory through
476
- this index even when the wording doesn't lexically match — it's a deterministic exact
477
- lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
478
- this reason, not just for the BM25 boost described above.
479
-
480
- Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
481
- \`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
482
- guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
483
- surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
484
- people/things you refer to by name often); not yet the default pending evaluation.
485
-
486
- ### Temporal validity — memories that expire or start in the future
487
- \`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
488
- **operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
489
- a ticket status — so the memory expires instead of relying on you to remember it's
490
- stale. Recall renders \`[valid until …]\` on results carrying this field. Use
491
- \`valid_from\` for a fact that becomes true on a known future date (a policy change, a
492
- season that hasn't started yet). Don't set either for durable facts — most memories
493
- don't need them.
494
-
495
- ### Memory classes (controls how strictly the salience filter gates the write)
496
- - \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
497
- Use for: user-stated decisions, project requirements, verified architectural facts,
498
- cross-agent shared context. **In a hive (multi-agent) setup, always use \`canonical\`
499
- for writes that other agents must be able to recall** — the default \`working\` class
500
- may get filtered.
501
- - \`memory_class: working\` (default) — observations and findings. Salience-gated.
502
- - \`memory_class: ephemeral\` — short-lived context that should decay quickly.
503
-
504
- ### Salience auto-promotion (defense in depth)
505
- The salience filter automatically promotes certain content patterns even if you forget
506
- to set \`memory_class\` explicitly:
507
- - **User feedback** — content starting with "Robert said…", "Katherine directed…",
508
- "Nancy decided…" etc. auto-promotes to canonical. So quoting the user verbatim
509
- always preserves the decision.
510
- - **Verified operational records** — content with an action verb (Submitted, Finalized,
511
- Completed, Reconciled, Triaged, Posted, Resolved, Stamped, Pushed, Deployed, Migrated,
512
- Imported, Exported, Backfilled) plus 2+ concrete identifiers (ISO date \`YYYY-MM-DD\`,
513
- or contextual numeric IDs like "event 18969", "ticket #18330", "USEF 341980") gets
514
- a 0.45 salience floor. So batch summaries with real IDs survive even when topic
515
- terms repeat.
516
-
517
- If neither pattern applies and you want a memory to definitely survive, set
518
- \`memory_class: canonical\` explicitly. Don't rely on auto-promotion for important writes.
519
-
520
- ### Recall memory when:
521
- - **BEFORE stating ANY fact about how a system works** — recall first; if AWM doesn't
522
- have it, read the code. Never guess and present it as fact.
523
- - **BEFORE searching the filesystem** — recall first; AWM is faster and has cross-session
524
- knowledge that file search doesn't.
525
- - Starting work on a new task or subsystem
526
- - Re-entering code you haven't touched recently
527
- - After a failed attempt — check if there's prior knowledge
528
- - Before refactoring or making architectural changes
529
- - When a topic comes up that you might have prior context on
530
-
531
- Recall is fast (~300ms typical). Use it freely.
532
-
533
- ### Chain recalls — one hop per call
534
-
535
- AWM returns what matches your query. It does **not** walk from that answer to the next
536
- fact, and it is not meant to: it is active memory, holding what you need right now.
537
- Following a chain to the next thing is **your** job — recall, read, recall again, each
538
- result becoming the cue for the next query.
539
-
540
- So when a question refers to something **by its role instead of its name**, you have a
541
- chain, not a query:
542
-
543
- > *"What is the codename of the project owned by my scheduler?"*
544
-
545
- That is three lookups, not one:
546
-
547
- 1. recall \`who is my scheduler\` → a name
548
- 2. recall \`what project does <that name> own\` → a project
549
- 3. recall \`<that project> codename\` → the answer
550
-
551
- **The failure mode is answering from the first recall.** A single query blending all
552
- three terms returns the most *salient* related memory, not the correct one — measured,
553
- that returns the main project's codename with complete confidence, and it is wrong.
554
- Each hop on its own is an ordinary, reliable recall; the chain only breaks when you stop
555
- asking.
556
-
557
- Tells that you are looking at a chain: a possessive or relative clause naming an entity
558
- by role rather than identity (\`my scheduler\`, \`the owner of X\`, \`whoever signed
559
- off on Y\`), or a rule that operates on an attribute you have not looked up yet (\`the
560
- release tag of the project we discussed\` needs that project's codename first). If
561
- resolving the question requires a fact you would have to *derive*, recall the fact
562
- instead.
563
-
564
- ### Recall strategy (when one query isn't enough)
565
- AWM's adaptive retrieval handles most query variations natively — synonym
566
- expansion, multi-channel scoring, embedding + BM25 + reranker agreement.
567
- A single recall is usually enough.
568
-
569
- When it isn't:
570
- - **If the first recall returns nothing or returns the wrong things, reformulate.**
571
- Try a second query with different phrasing — synonyms, more specific nouns,
572
- the exact identifier from the code rather than the conceptual name. Two or
573
- three recalls cost less than one filesystem search.
574
- - **Use the words a domain expert would use, not generic English.** "Period
575
- close lock" not "accounting feature"; "magic link rate limit" not "auth issue."
576
- - **For broad exploration, pass \`mode: "exploratory"\`** — wider candidate
577
- pool, lower precision floor. For specific lookups, leave mode unset (auto).
578
- - **Don't ensemble more than 3 reformulations.** If three different phrasings
579
- return nothing, the memory probably isn't there — read the code instead of
580
- burning more recalls.
581
-
582
- ### Recall tuning — opt-in parameters
583
- Default \`memory_recall\` is tuned for the common case. Four opt-in parameters change
584
- the cost/quality tradeoff. Use them when the default doesn't match what you need.
585
-
586
- - **\`granularity: 'compact'\`** — every result carries a 200-char \`summary\`
587
- field with a query-aware snippet (the densest window of query terms in the
588
- content). Use this when you expect to scan 5+ results to find one — saves
589
- ~70% of recall output tokens. The full content stays available in
590
- \`engram.content\` if you want to drill into a specific result.
591
- - **\`granularity: 'auto'\`** — confidence-adaptive. If the top result is a clear
592
- winner, it gets a longer summary while the rest are compact. If confidence
593
- is uniform across results, everything is compact. Use when you don't know
594
- in advance whether one result will dominate.
595
- - **\`require_confidence\`** — abstention. The default (0.05) is already a light
596
- gate: when the top results don't stand out from the rest, recall returns
597
- nothing and the reply says **\`RECALL ABSTAINED\`** with the count it withheld.
598
- An empty result *without* that line is genuine absence. The gate is on the
599
- SHAPE of the score distribution, not relevance — so raising it silences
600
- specific questions with one clear winner first (measured: 0.25 abstained on
601
- two identifier queries scoring 0.26–0.38 while passing a vague one at 0.92).
602
- Leave it alone for ordinary recall; \`0.10\` is the ceiling that still helps for
603
- push-style use where nobody asked. When abstention fires, reformulate once
604
- with the exact identifier, then read the code — don't retry with a lower
605
- threshold to force an answer.
606
- - **\`workspace: "<name>"\`** — hive-mode recall across all agents in the
607
- workspace. Use when other agents may have written canonical knowledge you
608
- need. Default is agent-scoped (your own memories only). Can also be set
609
- globally via the \`AWM_WORKSPACE\` env var. Memories live in the agent pool
610
- they were written to; if a session lands in an unexpected pool, \`memory_whoami\`
611
- says which one before you conclude the memory is missing.
612
-
613
- ### Keep memory fresh
614
- - After recalling a memory, if you observe the real state is different → call
615
- \`memory_supersede\` immediately with the corrected version.
616
- - After using a recalled memory: call \`memory_feedback\` (useful/not-useful) **with the
617
- \`recall_id\`** printed at the end of the recall output (\`[recall_id: …]\`). That id
618
- joins the feedback to the recall that produced it, which is what lets AWM learn
619
- which recalls were worth their tokens; feedback without it is a bare vote.
620
- - If you discover a memory is factually wrong: \`memory_retract\` to remove it.
621
- - **If you bypass AWM (file-memory, in-context notes, "I'll just remember"), the memory
622
- drifts out of date. The system relies on you to keep it current. This is the #1
623
- failure mode.**
624
-
625
- ### Cognition recipes — YOU do the thinking, AWM keeps the result
626
- AWM contains no LLM. When memory needs real thinking — distilling a repeatable
627
- procedure, reflecting on a failure — AWM hands YOU a versioned recipe (prompt +
628
- strict output shape) and you run it as a SEPARATE focused pass, then write the
629
- result back as an ordinary memory with provenance.
630
-
631
- - \`memory_task_end\` responses include the recipe invitations. Honor the gates:
632
- skill-derivation only after a genuinely procedural task (3+ tool calls or a
633
- delegated sub-task); friction-lesson only after a failure/retry/wrong assumption.
634
- - Run each recipe as its own focused pass — do NOT bundle it with other
635
- reasoning; bundled passes reliably drop the output.
636
- - Write back exactly per the recipe's contract: \`origin_class: 'recipe'\` +
637
- \`recipe_id\` (e.g. \`skill-derivation@1\`), concept prefixed \`skill: \` or
638
- \`lesson: \`. AWM validates the shape and rejects malformed or unknown-recipe
639
- writes with the contract echoed back — fix and retry, don't drop the insight.
640
- - Re-deriving the same skill name reinforces the existing memory instead of
641
- duplicating it, so don't fear writing a skill you may have written before.
642
-
643
- ### Content fade — write-and-forget is safe
644
- Un-recalled engrams gradually fade their content while preserving cue pathways
645
- (concept + tags + embedding stay intact). This is Paper 1 — storage
646
- degradation. Practical implications:
647
-
648
- - **Don't manually purge memories** to "save space." The system already
649
- compresses unused content. Old memories stay findable via cue match even
650
- when their body has decayed.
651
- - **Don't over-pin with \`memory_class: canonical\`** to fight fade. Canonical
652
- only changes salience gating at write time, not fade behavior. Fade
653
- affects un-recalled engrams of any class.
654
- - **Recall keeps content alive.** Every recall touches the engram and resets
655
- its fade clock. Frequently-recalled memories stay full-fidelity automatically.
656
- - **Supersede is the right tool for stale facts.** When you observe a memory
657
- is outdated, call \`memory_supersede\` — the new version inherits the old
658
- one's coherent associations (counter-narrative replacement) so cue
659
- pathways carry forward to the replacement.
660
-
661
- ### Example — good vs bad memory_write
662
-
663
- **BAD** (no prefix tags, vague concept, can't be recalled by future queries):
664
- \`\`\`
665
- memory_write(
666
- concept="found a bug",
667
- content="The thing I was looking at was broken so I fixed it."
668
- )
669
- \`\`\`
670
-
671
- **GOOD** (rich identifiers, structured metadata, prefix tags):
672
- \`\`\`
673
- memory_write(
674
- concept="EquiHub period-close BLOCKED check missing server-side",
675
- content="apps/web/app/(accounting)/accounting/period-close/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in AccountingService.closePeriod() per schema/072-period-close.sql. Without server-side check a malicious request could bypass via direct API call.",
676
- project="EquiHub",
677
- topic="accounting",
678
- intent="finding",
679
- confidence_level="verified",
680
- source="debugging",
681
- memory_class="canonical",
682
- tags=["ticket=18360", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
683
- )
684
- \`\`\`
685
-
686
- ### Also:
687
- - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
688
- - \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
689
- talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
690
- agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
691
- which agent identity, or which running code you're dealing with — before reasoning
692
- about AWM's own state from a stale memory or an assumed port number.
693
- - AWM is shared across all agents in real time. When any agent writes or supersedes a
694
- memory, every other agent can recall it immediately — but only within the same
695
- workspace and agent scope.
696
-
697
- ### Output compression (token efficiency, output-only)
698
- When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
699
- array of records, query rows, a log dump, an API response — pass it through
700
- \`compress_output\` first. It re-encodes the data as TOON (a compact, lossless,
701
- schema-aware tabular form of JSON), cutting ~50-65% of the tokens at no
702
- comprehension cost. This is output-only: it never changes the data or your memories.
703
- - Use it on big STRUCTURED outputs, not on prose. Prose is returned unchanged —
704
- for trimming memory prose, use recall \`granularity: 'compact'\` instead.
705
- - It returns a \`ref\`; call \`retrieve_original(ref)\` if you later need the exact
706
- verbatim source (e.g. to hand it to another tool unchanged).
707
- - Don't bother for small outputs — it only compresses when the saving is worthwhile
708
- and falls back to plain JSON if TOON wouldn't reproduce the data exactly.
709
-
710
- ### Backend (SQLite vs PGlite)
711
- AWM ships two storage backends. The installer picks SQLite by default; both
712
- are functionally equivalent for cognitive workloads, but differ in operational
713
- guarantees:
714
-
715
- - **SQLite** (default) — embedded, **multi-process safe** via WAL mode. Best
716
- for single-machine setups and MCP scenarios where multiple Claude Code
717
- sessions may open the same database concurrently.
718
- - **PGlite** — embedded Postgres (WASM) with pgvector. **Single-process only**
719
- — two MCP processes against the same \`memory-pglite/\` directory will
720
- abort the second. Pick via \`AWM_STORE_BACKEND=pglite\` and
721
- \`AWM_DB_PATH=path/to/memory-pglite\`.
722
- - **Auto-detect** — if \`AWM_DB_PATH\` points to a directory that already
723
- exists, AWM detects PGlite; a file → SQLite. No explicit
724
- \`AWM_STORE_BACKEND\` needed when an existing DB is present.
725
-
726
- For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
727
- multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
728
-
729
- ### Diagnostics / escape hatches (env vars, only if you know why)
730
- Each optimisation is gated by an env var so it can be disabled for A/B testing if a
731
- regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
732
-
733
- Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
734
- - \`AWM_RERANK2=1\` — second-stage rerank by the cross-encoder's own score.
735
- - \`AWM_RERANK_WINDOW=query\` — 400-char rerank window on the densest query-term
736
- region instead of the prefix (25% → 87.5% on long memories).
737
- - \`AWM_RERANK_TAGS=1\` — tags fed into the rerank passage (+7.4pp s@1).
738
-
739
- Hook sidecar (0.14.2):
740
- - \`AWM_HOOK_PORT=8401\` / \`AWM_HOOK_PORT_RANGE=10\` — each MCP process binds the
741
- first free port in the range, so several sessions can run at once. The shipped
742
- hooks find their own session's sidecar by probing the range and matching
743
- \`agentId\` on \`/health\`; \`memory_whoami\` reports the port actually bound.
744
-
745
- Recall pipeline:
746
- - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
747
- pre-filter in recall. Reverts to scoring all active candidates.
748
- - \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (default off; measured
749
- no effect on the real-store benchmark, 0.14.5).
750
- - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
751
- Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
752
- - \`AWM_DISABLE_RERANK_SKIP=1\` — disables the cross-encoder skip on
753
- clear-winner queries. Forces every recall through the reranker.
754
- - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
755
- heuristic + LRU cache. Forces every recall through flan-t5-small.
756
-
757
- Write pipeline + lifecycle:
758
- - \`AWM_SLOW_WRITE_MS=250\` — any write slower than this logs one stderr
759
- line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
760
- embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
761
- first write/recall feels slow.
762
- - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
763
- can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
764
- preserves more reinforced detail; lower = leaner recall output.
765
- - \`AWM_REINFORCE_MERGE_CONTENT=0\` — disable content merge on reinforce.
766
- Reverts to pre-0.8.5 behavior (discard new content, only bump confidence).
767
- - \`AWM_NOVELTY_EMBED=0\` — disable the cosine channel in novelty
768
- computation. BM25-only fallback. Reverts to pre-0.8.5 novelty.
769
- - \`AWM_GRANULARITY_COMPACT_LEN=200\` — char budget for query-aware snippet
770
- in \`granularity: 'compact'\` mode.
771
- - \`AWM_GRANULARITY_FULL_LEN=1000\` — char budget for the top result in
772
- \`granularity: 'auto'\` mode when there's a clear winner.
773
-
774
- PGlite backend:
775
- - \`AWM_PGLITE_BM25_M=1\` — multiplier on PGlite \`ts_rank_cd\` to calibrate
776
- against SQLite FTS5 BM25 distribution. M=1 (default) is passthrough;
777
- higher M boosts PGlite scores at the cost of recall-ranking precision
778
- (see CHANGELOG 0.8.5 follow-up).
779
- - \`AWM_IVFFLAT_PROBES=5\` — pgvector ivfflat probes per query. Higher =
780
- more accurate, slower.
781
-
782
- In production, leave these all unset. Use only when diagnosing a suspected
783
- recall-quality regression.
784
- `.trimStart();
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 three: a **prime** hook that recalls against
385
+ each prompt and injects what clears confidence (or nothing), and checkpoint hooks on
386
+ compaction and session end (plus a 15-min timer). Primed context arrives labelled
387
+ \`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
388
+ arrives with no primed context, that is a signal too: recall explicitly before
389
+ asserting anything.
390
+
391
+ ### Write memory when:
392
+ - A project decision is made or changed
393
+ - A root cause is discovered after debugging
394
+ - A reusable implementation pattern is established
395
+ - A user preference, constraint, or requirement is clarified
396
+ - A prior assumption is found to be wrong
397
+ - A significant piece of work is completed
398
+
399
+ ### Writing for recall (the highest-leverage section)
400
+ A memory's recall quality is set the moment you write it. AWM is fast at
401
+ finding what's findable — but if the write is shaped wrong, no retriever
402
+ can rescue it. Be slightly more verbose at the front than feels natural:
403
+ the first 1-2 sentences are what BM25, the embedding model, and concept
404
+ extraction all see most strongly.
405
+
406
+ - **Lead with the rule or fact.** Don't open with context or backstory.
407
+ "Don't mock the database in integration tests." comes first; the reason
408
+ comes second. Recall scans the head of the body, not the tail.
409
+ - **Pick the most specific topic.** Not \`auth\` — \`auth-magic-link-rate-limit\`.
410
+ Topic is a hard filter at recall time. Generic topics hide the memory in
411
+ a noisy bucket where it competes with everything else in the area.
412
+ - **Include 2+ retrievable identifiers.** File paths, function names, table
413
+ columns, ticket IDs, exact error strings, the literal terms a future query
414
+ will use. \`AccountingService.closePeriod()\` beats "the accounting code."
415
+ \`tblMemberDetails.activation_date\` beats "the activation column."
416
+ \`schema/072-period-close.sql\` beats "the migration."
417
+ - **Write in the vocabulary of the future question.** When you imagine asking
418
+ this in three months, what nouns will you use? Use those nouns. Don't
419
+ paraphrase the user's domain language into your own neutral summary.
420
+ - **Name the CATEGORY as well as the specifics — both, always.** Specifics
421
+ make a memory precise; category words make it *reachable*. A memory that
422
+ says "private plan memory peaked 88%, scale P1v3 -> P2v3" never says
423
+ "Azure" or "App Service Plan", so a question asked in those words cannot
424
+ find it — measured on a real store, that memory was not in the top 40
425
+ candidates for "azure app service plan capacity increase". Write the
426
+ system / product / domain nouns (Azure App Service Plan, Freshdesk ticket,
427
+ MySQL connection pool, ShowConnect scoring) NEXT TO the identifiers.
428
+ Note this cuts against "pick the most specific topic" above: that rule is
429
+ about the \`topic\` TAG, not a licence to omit the category from the body.
430
+ Measured on an 11k-engram store: 94% of tagged memories are missing at
431
+ least one of their own topical terms from the body, and 66% of those
432
+ terms never appear in the text at all.
433
+ - **Tags are NOT a substitute for body text.** Only BM25 indexes tags. The
434
+ embedding is built from \`concept + content\` and the cross-encoder rerank
435
+ passage is built from \`concept + content\` — neither sees tags. So a word
436
+ that exists only as a tag is invisible to two of the three retrieval
437
+ channels, including the one that decides final ordering. Tag it *and*
438
+ write it.
439
+ - **Reserve canonical for stable invariants.** Decisions, requirements,
440
+ hard facts, cross-agent shared context. Working class (default) is correct
441
+ for findings, observations, and progress notes. The canonical floor is
442
+ 0.7 salience — overusing it pollutes the canonical layer and the floor
443
+ loses meaning.
444
+ - **Include the why for feedback memories.** A rule without a reason can't
445
+ be applied to edge cases. "Don't mock the database" is brittle. "Don't
446
+ mock the database — last quarter mocked tests masked a broken migration"
447
+ is portable to new situations.
448
+
449
+ ### Tagging rules (REQUIRED — AWM's prefix-tag retrieval boost depends on these)
450
+
451
+ Every \`memory_write\` should pass these structured fields. AWM stores each as a
452
+ prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
453
+ and entity-bridge boosts at recall time.
454
+
455
+ | Field | Required? | Format | Example |
456
+ |---|---|---|---|
457
+ | \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
458
+ | \`topic\` | **YES** | one or more lowercase area words | \`"database-migration"\`, \`"benchmarks"\` |
459
+ | \`intent\` | **YES** | one of: \`decision\` / \`finding\` / \`todo\` / \`question\` / \`context\` | \`"finding"\` |
460
+ | \`confidence_level\` | **YES** | \`verified\` (tested) / \`observed\` (read in code) / \`assumed\` (reasoning) | \`"verified"\` |
461
+ | \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
462
+ | \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
463
+ | \`session_id\` | recommended | current conversation ID for entity-bridge boost | autogenerated |
464
+ | \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=18360", "date=2026-05-11"]\` |
465
+
466
+ **Always add identifier tags when present in the content:**
467
+ - \`ticket=<id>\` for Freshdesk tickets
468
+ - \`member=<id>\` for member IDs
469
+ - \`horse=<id>\` for horse_member_id
470
+ - \`usef=<id>\` for USEF lookups
471
+ - \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
472
+ - \`person=<Name>\` for stakeholder quotes / decisions
473
+ - \`version=<X.Y.Z>\` for release-specific findings
474
+
475
+ ### Entity index — exact-match recall for named things (default off)
476
+ Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
477
+ ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
478
+ A query naming an entity ("ticket 19252", "Kaleigh Collett") can reach the memory through
479
+ this index even when the wording doesn't lexically match — it's a deterministic exact
480
+ lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
481
+ this reason, not just for the BM25 boost described above.
482
+
483
+ Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
484
+ \`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
485
+ guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
486
+ surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
487
+ people/things you refer to by name often); not yet the default pending evaluation.
488
+
489
+ ### Temporal validity — memories that expire or start in the future
490
+ \`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
491
+ **operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
492
+ a ticket status — so the memory expires instead of relying on you to remember it's
493
+ stale. Recall renders \`[valid until …]\` on results carrying this field. Use
494
+ \`valid_from\` for a fact that becomes true on a known future date (a policy change, a
495
+ season that hasn't started yet). Don't set either for durable facts — most memories
496
+ don't need them.
497
+
498
+ ### Memory classes (controls how strictly the salience filter gates the write)
499
+ - \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
500
+ Use for: user-stated decisions, project requirements, verified architectural facts,
501
+ cross-agent shared context. **In a hive (multi-agent) setup, always use \`canonical\`
502
+ for writes that other agents must be able to recall** — the default \`working\` class
503
+ may get filtered.
504
+ - \`memory_class: working\` (default) — observations and findings. Salience-gated.
505
+ - \`memory_class: ephemeral\` — short-lived context that should decay quickly.
506
+
507
+ ### Salience auto-promotion (defense in depth)
508
+ The salience filter automatically promotes certain content patterns even if you forget
509
+ to set \`memory_class\` explicitly:
510
+ - **User feedback** — content starting with "Robert said…", "Katherine directed…",
511
+ "Nancy decided…" etc. auto-promotes to canonical. So quoting the user verbatim
512
+ always preserves the decision.
513
+ - **Verified operational records** — content with an action verb (Submitted, Finalized,
514
+ Completed, Reconciled, Triaged, Posted, Resolved, Stamped, Pushed, Deployed, Migrated,
515
+ Imported, Exported, Backfilled) plus 2+ concrete identifiers (ISO date \`YYYY-MM-DD\`,
516
+ or contextual numeric IDs like "event 18969", "ticket #18330", "USEF 341980") gets
517
+ a 0.45 salience floor. So batch summaries with real IDs survive even when topic
518
+ terms repeat.
519
+
520
+ If neither pattern applies and you want a memory to definitely survive, set
521
+ \`memory_class: canonical\` explicitly. Don't rely on auto-promotion for important writes.
522
+
523
+ ### Recall memory when:
524
+ - **BEFORE stating ANY fact about how a system works** — recall first; if AWM doesn't
525
+ have it, read the code. Never guess and present it as fact.
526
+ - **BEFORE searching the filesystem** — recall first; AWM is faster and has cross-session
527
+ knowledge that file search doesn't.
528
+ - Starting work on a new task or subsystem
529
+ - Re-entering code you haven't touched recently
530
+ - After a failed attempt — check if there's prior knowledge
531
+ - Before refactoring or making architectural changes
532
+ - When a topic comes up that you might have prior context on
533
+
534
+ Recall is fast (~300ms typical). Use it freely.
535
+
536
+ ### Chain recalls — one hop per call
537
+
538
+ AWM returns what matches your query. It does **not** walk from that answer to the next
539
+ fact, and it is not meant to: it is active memory, holding what you need right now.
540
+ Following a chain to the next thing is **your** job — recall, read, recall again, each
541
+ result becoming the cue for the next query.
542
+
543
+ So when a question refers to something **by its role instead of its name**, you have a
544
+ chain, not a query:
545
+
546
+ > *"What is the codename of the project owned by my scheduler?"*
547
+
548
+ That is three lookups, not one:
549
+
550
+ 1. recall \`who is my scheduler\` → a name
551
+ 2. recall \`what project does <that name> own\` → a project
552
+ 3. recall \`<that project> codename\` → the answer
553
+
554
+ **The failure mode is answering from the first recall.** A single query blending all
555
+ three terms returns the most *salient* related memory, not the correct one — measured,
556
+ that returns the main project's codename with complete confidence, and it is wrong.
557
+ Each hop on its own is an ordinary, reliable recall; the chain only breaks when you stop
558
+ asking.
559
+
560
+ Tells that you are looking at a chain: a possessive or relative clause naming an entity
561
+ by role rather than identity (\`my scheduler\`, \`the owner of X\`, \`whoever signed
562
+ off on Y\`), or a rule that operates on an attribute you have not looked up yet (\`the
563
+ release tag of the project we discussed\` needs that project's codename first). If
564
+ resolving the question requires a fact you would have to *derive*, recall the fact
565
+ instead.
566
+
567
+ ### Recall strategy (when one query isn't enough)
568
+ AWM's adaptive retrieval handles most query variations natively — synonym
569
+ expansion, multi-channel scoring, embedding + BM25 + reranker agreement.
570
+ A single recall is usually enough.
571
+
572
+ When it isn't:
573
+ - **If the first recall returns nothing or returns the wrong things, reformulate.**
574
+ Try a second query with different phrasing — synonyms, more specific nouns,
575
+ the exact identifier from the code rather than the conceptual name. Two or
576
+ three recalls cost less than one filesystem search.
577
+ - **Use the words a domain expert would use, not generic English.** "Period
578
+ close lock" not "accounting feature"; "magic link rate limit" not "auth issue."
579
+ - **For broad exploration, pass \`mode: "exploratory"\`** — wider candidate
580
+ pool, lower precision floor. For specific lookups, leave mode unset (auto).
581
+ - **Don't ensemble more than 3 reformulations.** If three different phrasings
582
+ return nothing, the memory probably isn't there — read the code instead of
583
+ burning more recalls.
584
+
585
+ ### Recall tuning — opt-in parameters
586
+ Default \`memory_recall\` is tuned for the common case. Four opt-in parameters change
587
+ the cost/quality tradeoff. Use them when the default doesn't match what you need.
588
+
589
+ - **\`granularity: 'compact'\`** — every result carries a 200-char \`summary\`
590
+ field with a query-aware snippet (the densest window of query terms in the
591
+ content). Use this when you expect to scan 5+ results to find one — saves
592
+ ~70% of recall output tokens. The full content stays available in
593
+ \`engram.content\` if you want to drill into a specific result.
594
+ - **\`granularity: 'auto'\`** — confidence-adaptive. If the top result is a clear
595
+ winner, it gets a longer summary while the rest are compact. If confidence
596
+ is uniform across results, everything is compact. Use when you don't know
597
+ in advance whether one result will dominate.
598
+ - **\`require_confidence\`** — abstention. The default (0.05) is already a light
599
+ gate: when the top results don't stand out from the rest, recall returns
600
+ nothing and the reply says **\`RECALL ABSTAINED\`** with the count it withheld.
601
+ An empty result *without* that line is genuine absence. The gate is on the
602
+ SHAPE of the score distribution, not relevance — so raising it silences
603
+ specific questions with one clear winner first (measured: 0.25 abstained on
604
+ two identifier queries scoring 0.26–0.38 while passing a vague one at 0.92).
605
+ Leave it alone for ordinary recall; \`0.10\` is the ceiling that still helps for
606
+ push-style use where nobody asked. When abstention fires, reformulate once
607
+ with the exact identifier, then read the code — don't retry with a lower
608
+ threshold to force an answer.
609
+ - **\`workspace: "<name>"\`** — hive-mode recall across all agents in the
610
+ workspace. Use when other agents may have written canonical knowledge you
611
+ need. Default is agent-scoped (your own memories only). Can also be set
612
+ globally via the \`AWM_WORKSPACE\` env var. Memories live in the agent pool
613
+ they were written to; if a session lands in an unexpected pool, \`memory_whoami\`
614
+ says which one before you conclude the memory is missing.
615
+
616
+ ### Keep memory fresh
617
+ - After recalling a memory, if you observe the real state is different → call
618
+ \`memory_supersede\` immediately with the corrected version.
619
+ - After using a recalled memory: call \`memory_feedback\` (useful/not-useful) **with the
620
+ \`recall_id\`** printed at the end of the recall output (\`[recall_id: …]\`). That id
621
+ joins the feedback to the recall that produced it, which is what lets AWM learn
622
+ which recalls were worth their tokens; feedback without it is a bare vote.
623
+ - If you discover a memory is factually wrong: \`memory_retract\` to remove it.
624
+ - **If you bypass AWM (file-memory, in-context notes, "I'll just remember"), the memory
625
+ drifts out of date. The system relies on you to keep it current. This is the #1
626
+ failure mode.**
627
+
628
+ ### Cognition recipes — YOU do the thinking, AWM keeps the result
629
+ AWM contains no LLM. When memory needs real thinking — distilling a repeatable
630
+ procedure, reflecting on a failure — AWM hands YOU a versioned recipe (prompt +
631
+ strict output shape) and you run it as a SEPARATE focused pass, then write the
632
+ result back as an ordinary memory with provenance.
633
+
634
+ - \`memory_task_end\` responses include the recipe invitations. Honor the gates:
635
+ skill-derivation only after a genuinely procedural task (3+ tool calls or a
636
+ delegated sub-task); friction-lesson only after a failure/retry/wrong assumption.
637
+ - Run each recipe as its own focused pass — do NOT bundle it with other
638
+ reasoning; bundled passes reliably drop the output.
639
+ - Write back exactly per the recipe's contract: \`origin_class: 'recipe'\` +
640
+ \`recipe_id\` (e.g. \`skill-derivation@1\`), concept prefixed \`skill: \` or
641
+ \`lesson: \`. AWM validates the shape and rejects malformed or unknown-recipe
642
+ writes with the contract echoed back — fix and retry, don't drop the insight.
643
+ - Re-deriving the same skill name reinforces the existing memory instead of
644
+ duplicating it, so don't fear writing a skill you may have written before.
645
+
646
+ ### Content fade — write-and-forget is safe
647
+ Un-recalled engrams gradually fade their content while preserving cue pathways
648
+ (concept + tags + embedding stay intact). This is Paper 1 — storage
649
+ degradation. Practical implications:
650
+
651
+ - **Don't manually purge memories** to "save space." The system already
652
+ compresses unused content. Old memories stay findable via cue match even
653
+ when their body has decayed.
654
+ - **Don't over-pin with \`memory_class: canonical\`** to fight fade. Canonical
655
+ only changes salience gating at write time, not fade behavior. Fade
656
+ affects un-recalled engrams of any class.
657
+ - **Recall keeps content alive.** Every recall touches the engram and resets
658
+ its fade clock. Frequently-recalled memories stay full-fidelity automatically.
659
+ - **Supersede is the right tool for stale facts.** When you observe a memory
660
+ is outdated, call \`memory_supersede\` — the new version inherits the old
661
+ one's coherent associations (counter-narrative replacement) so cue
662
+ pathways carry forward to the replacement.
663
+
664
+ ### Example — good vs bad memory_write
665
+
666
+ **BAD** (no prefix tags, vague concept, can't be recalled by future queries):
667
+ \`\`\`
668
+ memory_write(
669
+ concept="found a bug",
670
+ content="The thing I was looking at was broken so I fixed it."
671
+ )
672
+ \`\`\`
673
+
674
+ **GOOD** (rich identifiers, structured metadata, prefix tags):
675
+ \`\`\`
676
+ memory_write(
677
+ concept="EquiHub period-close BLOCKED check missing server-side",
678
+ content="apps/web/app/(accounting)/accounting/period-close/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in AccountingService.closePeriod() per schema/072-period-close.sql. Without server-side check a malicious request could bypass via direct API call.",
679
+ project="EquiHub",
680
+ topic="accounting",
681
+ intent="finding",
682
+ confidence_level="verified",
683
+ source="debugging",
684
+ memory_class="canonical",
685
+ tags=["ticket=18360", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
686
+ )
687
+ \`\`\`
688
+
689
+ ### Also:
690
+ - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
691
+ - \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
692
+ talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
693
+ agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
694
+ which agent identity, or which running code you're dealing with — before reasoning
695
+ about AWM's own state from a stale memory or an assumed port number.
696
+ - AWM is shared across all agents in real time. When any agent writes or supersedes a
697
+ memory, every other agent can recall it immediately — but only within the same
698
+ workspace and agent scope.
699
+
700
+ ### Output compression (token efficiency, output-only)
701
+ When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
702
+ array of records, query rows, a log dump, an API response — pass it through
703
+ \`compress_output\` first. It re-encodes the data as TOON (a compact, lossless,
704
+ schema-aware tabular form of JSON), cutting ~50-65% of the tokens at no
705
+ comprehension cost. This is output-only: it never changes the data or your memories.
706
+ - Use it on big STRUCTURED outputs, not on prose. Prose is returned unchanged —
707
+ for trimming memory prose, use recall \`granularity: 'compact'\` instead.
708
+ - It returns a \`ref\`; call \`retrieve_original(ref)\` if you later need the exact
709
+ verbatim source (e.g. to hand it to another tool unchanged).
710
+ - Don't bother for small outputs — it only compresses when the saving is worthwhile
711
+ and falls back to plain JSON if TOON wouldn't reproduce the data exactly.
712
+
713
+ ### Backend (SQLite vs PGlite)
714
+ AWM ships two storage backends. The installer picks SQLite by default; both
715
+ are functionally equivalent for cognitive workloads, but differ in operational
716
+ guarantees:
717
+
718
+ - **SQLite** (default) — embedded, **multi-process safe** via WAL mode. Best
719
+ for single-machine setups and MCP scenarios where multiple Claude Code
720
+ sessions may open the same database concurrently.
721
+ - **PGlite** — embedded Postgres (WASM) with pgvector. **Single-process only**
722
+ — two MCP processes against the same \`memory-pglite/\` directory will
723
+ abort the second. Pick via \`AWM_STORE_BACKEND=pglite\` and
724
+ \`AWM_DB_PATH=path/to/memory-pglite\`.
725
+ - **Auto-detect** — if \`AWM_DB_PATH\` points to a directory that already
726
+ exists, AWM detects PGlite; a file → SQLite. No explicit
727
+ \`AWM_STORE_BACKEND\` needed when an existing DB is present.
728
+
729
+ For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
730
+ multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
731
+
732
+ ### Diagnostics / escape hatches (env vars, only if you know why)
733
+ Each optimisation is gated by an env var so it can be disabled for A/B testing if a
734
+ regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
735
+
736
+ Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
737
+ - \`AWM_RERANK2=1\` — second-stage rerank by the cross-encoder's own score.
738
+ - \`AWM_RERANK_WINDOW=query\` — 400-char rerank window on the densest query-term
739
+ region instead of the prefix (25% → 87.5% on long memories).
740
+ - \`AWM_RERANK_TAGS=1\` — tags fed into the rerank passage (+7.4pp s@1).
741
+
742
+ Hook sidecar (0.14.2):
743
+ - \`AWM_HOOK_PORT=8401\` / \`AWM_HOOK_PORT_RANGE=10\` — each MCP process binds the
744
+ first free port in the range, so several sessions can run at once. The shipped
745
+ hooks find their own session's sidecar by probing the range and matching
746
+ \`agentId\` on \`/health\`; \`memory_whoami\` reports the port actually bound.
747
+
748
+ Recall pipeline:
749
+ - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
750
+ pre-filter in recall. Reverts to scoring all active candidates.
751
+ - \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (default off; measured
752
+ no effect on the real-store benchmark, 0.14.5).
753
+ - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
754
+ Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
755
+ - \`AWM_DISABLE_RERANK_SKIP=1\` — disables the cross-encoder skip on
756
+ clear-winner queries. Forces every recall through the reranker.
757
+ - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
758
+ heuristic + LRU cache. Forces every recall through flan-t5-small.
759
+
760
+ Write pipeline + lifecycle:
761
+ - \`AWM_SLOW_WRITE_MS=250\` — any write slower than this logs one stderr
762
+ line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
763
+ embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
764
+ first write/recall feels slow.
765
+ - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
766
+ can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
767
+ preserves more reinforced detail; lower = leaner recall output.
768
+ - \`AWM_REINFORCE_MERGE_CONTENT=0\` — disable content merge on reinforce.
769
+ Reverts to pre-0.8.5 behavior (discard new content, only bump confidence).
770
+ - \`AWM_NOVELTY_EMBED=0\` — disable the cosine channel in novelty
771
+ computation. BM25-only fallback. Reverts to pre-0.8.5 novelty.
772
+ - \`AWM_GRANULARITY_COMPACT_LEN=200\` — char budget for query-aware snippet
773
+ in \`granularity: 'compact'\` mode.
774
+ - \`AWM_GRANULARITY_FULL_LEN=1000\` — char budget for the top result in
775
+ \`granularity: 'auto'\` mode when there's a clear winner.
776
+
777
+ PGlite backend:
778
+ - \`AWM_PGLITE_BM25_M=1\` — multiplier on PGlite \`ts_rank_cd\` to calibrate
779
+ against SQLite FTS5 BM25 distribution. M=1 (default) is passthrough;
780
+ higher M boosts PGlite scores at the cost of recall-ranking precision
781
+ (see CHANGELOG 0.8.5 follow-up).
782
+ - \`AWM_IVFFLAT_PROBES=5\` — pgvector ivfflat probes per query. Higher =
783
+ more accurate, slower.
784
+
785
+ In production, leave these all unset. Use only when diagnosing a suspected
786
+ recall-quality regression.
787
+ `.trimStart();