agent-working-memory 0.12.0 → 0.12.3

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.
@@ -300,6 +300,29 @@ and entity-bridge boosts at recall time.
300
300
  - \`person=<Name>\` for stakeholder quotes / decisions
301
301
  - \`version=<X.Y.Z>\` for release-specific findings
302
302
 
303
+ ### Entity index — exact-match recall for named things (default off)
304
+ Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
305
+ ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
306
+ A query naming an entity ("ticket 19252", "Kaleigh Collett") can reach the memory through
307
+ this index even when the wording doesn't lexically match — it's a deterministic exact
308
+ lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
309
+ this reason, not just for the BM25 boost described above.
310
+
311
+ Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
312
+ \`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
313
+ guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
314
+ surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
315
+ people/things you refer to by name often); not yet the default pending evaluation.
316
+
317
+ ### Temporal validity — memories that expire or start in the future
318
+ \`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
319
+ **operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
320
+ a ticket status — so the memory expires instead of relying on you to remember it's
321
+ stale. Recall renders \`[valid until …]\` on results carrying this field. Use
322
+ \`valid_from\` for a fact that becomes true on a known future date (a policy change, a
323
+ season that hasn't started yet). Don't set either for durable facts — most memories
324
+ don't need them.
325
+
303
326
  ### Memory classes (controls how strictly the salience filter gates the write)
304
327
  - \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
305
328
  Use for: user-stated decisions, project requirements, verified architectural facts,
@@ -456,8 +479,14 @@ memory_write(
456
479
 
457
480
  ### Also:
458
481
  - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
482
+ - \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
483
+ talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
484
+ agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
485
+ which agent identity, or which running code you're dealing with — before reasoning
486
+ about AWM's own state from a stale memory or an assumed port number.
459
487
  - AWM is shared across all agents in real time. When any agent writes or supersedes a
460
- memory, every other agent can recall it immediately.
488
+ memory, every other agent can recall it immediately — but only within the same
489
+ workspace and agent scope.
461
490
 
462
491
  ### Output compression (token efficiency, output-only)
463
492
  When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
@@ -500,11 +529,7 @@ for A/B testing if a regression appears in your workload:
500
529
  Recall pipeline (0.7.x):
501
530
  - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
502
531
  pre-filter in recall. Reverts to scoring all active candidates.
503
- - \`AWM_SLOW_WRITE_MS\` — slow-write telemetry threshold in ms (default 250;
504
- 0 disables the always-on slow-write stderr line).
505
- - \`memory_whoami\` (MCP) / \`GET /whoami\` — identify the instance (agent, mode,
506
- backend, store path, code provenance, sibling agent spaces) when unsure
507
- which AWM you are talking to.
532
+ - \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (0.12.x, default off).
508
533
  - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
509
534
  Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
510
535
  - \`AWM_DISABLE_RERANK_SKIP=1\` — disables the cross-encoder skip on
@@ -512,7 +537,11 @@ Recall pipeline (0.7.x):
512
537
  - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
513
538
  heuristic + LRU cache. Forces every recall through flan-t5-small.
514
539
 
515
- Write pipeline + lifecycle (0.8.x):
540
+ Write pipeline + lifecycle (0.8.x, plus 0.12.x telemetry):
541
+ - \`AWM_SLOW_WRITE_MS=250\` (0.12.x) — any write slower than this logs one stderr
542
+ line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
543
+ embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
544
+ first write/recall feels slow.
516
545
  - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
517
546
  can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
518
547
  preserves more reinforced detail; lower = leaner recall output.
@@ -37,7 +37,7 @@ async function loadInProcess(): Promise<FeatureExtractionPipeline> {
37
37
  inProcessInitPromise = pipeline('feature-extraction', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
38
38
  inProcessInstance = pipe;
39
39
  noteModelLoad(performance.now() - tLoadStart);
40
- console.log(`Embedding model loaded in-process: ${MODEL_ID} (${DIMENSIONS}d)`);
40
+ console.error(`Embedding model loaded in-process: ${MODEL_ID} (${DIMENSIONS}d)`);
41
41
  return pipe;
42
42
  });
43
43
  return inProcessInitPromise;
@@ -0,0 +1,25 @@
1
+ import type { ActivationResult } from '../types/engram.js';
2
+
3
+ /**
4
+ * Formats one memory_recall result line for the MCP text response.
5
+ *
6
+ * Extracted out of the inline closure in mcp.ts (0.12.1) so the format itself
7
+ * — specifically, that every result carries its engram id — can be unit
8
+ * tested without booting the MCP server (mcp.ts has a top-level `await` and
9
+ * opens the store as a side effect of import, so it cannot be imported by a
10
+ * test directly).
11
+ *
12
+ * The id is placed right after the score, not at the end of the line: result
13
+ * bodies can be long, and a consumer scanning for `[id: ...]` shouldn't have
14
+ * to read past a paragraph of content to find it.
15
+ */
16
+ export function formatRecallResultLine(r: ActivationResult, index: number): string {
17
+ const body = r.summary ?? r.engram.content;
18
+ const chain = r.engram.supersededBy
19
+ ? ` ⚠ SUPERSEDED by ${r.engram.supersededBy} — treat as historical; recall/fetch the successor before relying on this.`
20
+ : '';
21
+ const validity = r.engram.validTo
22
+ ? ` [valid until ${r.engram.validTo}]`
23
+ : '';
24
+ return `${index + 1}. **${r.engram.concept}** (${r.score.toFixed(3)}) [id: ${r.engram.id}]${validity}: ${body}${chain}`;
25
+ }
@@ -30,7 +30,7 @@ async function loadInProcess(): Promise<Text2TextGenerationPipeline> {
30
30
  if (inProcessInitPromise) return inProcessInitPromise;
31
31
  inProcessInitPromise = pipeline('text2text-generation', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
32
32
  inProcessInstance = pipe as Text2TextGenerationPipeline;
33
- console.log(`Query expander loaded in-process: ${MODEL_ID}`);
33
+ console.error(`Query expander loaded in-process: ${MODEL_ID}`);
34
34
  return inProcessInstance;
35
35
  });
36
36
  return inProcessInitPromise;
@@ -35,7 +35,7 @@ async function ensureLoaded(): Promise<void> {
35
35
  initPromise = (async () => {
36
36
  tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
37
37
  model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: 'fp32' });
38
- console.log(`Re-ranker model loaded in-process: ${MODEL_ID}`);
38
+ console.error(`Re-ranker model loaded in-process: ${MODEL_ID}`);
39
39
  })();
40
40
  return initPromise;
41
41
  }
@@ -28,6 +28,28 @@ export interface SidecarDeps {
28
28
  secret: string | null;
29
29
  port: number;
30
30
  onConsolidate?: (agentId: string, reason: string) => void;
31
+ /**
32
+ * 0.12.2: warm recall for hooks. The sidecar runs in the same process as
33
+ * the MCP server's activation engine and its loaded ML models, so exposing
34
+ * recall here gives Claude Code hooks (e.g. a UserPromptSubmit PRIME hook)
35
+ * warm-latency recall with no standing server — the process lifecycle is
36
+ * owned by the session that spawned it. Optional so older callers and
37
+ * tests keep working unchanged.
38
+ */
39
+ activate?: (q: {
40
+ context: string;
41
+ limit?: number;
42
+ requireConfidence?: number;
43
+ granularity?: 'full' | 'compact' | 'auto';
44
+ }) => Promise<Array<{
45
+ engram: {
46
+ id: string; concept: string; content: string;
47
+ createdAt?: string; memoryClass?: string; validTo?: string | null;
48
+ };
49
+ score: number;
50
+ summary?: string;
51
+ confidence?: number;
52
+ }>>;
31
53
  }
32
54
 
33
55
  interface HookInput {
@@ -187,6 +209,39 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
187
209
  return;
188
210
  }
189
211
 
212
+ // POST /memory/activate — warm recall for hooks (0.12.2). Behind the auth
213
+ // gate above. Returns a trimmed subset of the HTTP API's response shape
214
+ // ({results: [{engram, score, summary, confidence}]}) so a hook written
215
+ // against either endpoint needs no branching.
216
+ if (req.url === '/memory/activate' && req.method === 'POST') {
217
+ if (!deps.activate) {
218
+ json(res, 501, { error: 'activate not wired on this sidecar' });
219
+ return;
220
+ }
221
+ try {
222
+ const body = JSON.parse((await readBody(req)) || '{}') as {
223
+ context?: string; query?: string; limit?: number;
224
+ requireConfidence?: number; granularity?: 'full' | 'compact' | 'auto';
225
+ };
226
+ const context = body.context ?? body.query;
227
+ if (!context) {
228
+ json(res, 400, { error: 'context (or query) is required' });
229
+ return;
230
+ }
231
+ const results = await deps.activate({
232
+ context,
233
+ limit: body.limit,
234
+ requireConfidence: body.requireConfidence,
235
+ granularity: body.granularity,
236
+ });
237
+ log(agentId, 'hook:recall', `"${context.slice(0, 80)}" → ${results.length} results (sidecar)`);
238
+ json(res, 200, { results });
239
+ } catch (err) {
240
+ json(res, 500, { error: (err as Error).message });
241
+ }
242
+ return;
243
+ }
244
+
190
245
  // POST /hooks/checkpoint — auto-checkpoint from hook events
191
246
  if (req.url === '/hooks/checkpoint' && req.method === 'POST') {
192
247
  try {
package/src/mcp.ts CHANGED
@@ -73,7 +73,9 @@ import type { ConsciousState } from './types/checkpoint.js';
73
73
  import type { SalienceEventType } from './core/salience.js';
74
74
  import type { TaskStatus, TaskPriority } from './types/engram.js';
75
75
  import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
76
- import { embed } from './core/embeddings.js';
76
+ import { embed, getEmbedder } from './core/embeddings.js';
77
+ import { getReranker } from './core/reranker.js';
78
+ import { getExpander } from './core/query-expander.js';
77
79
  import { startSidecar } from './hooks/sidecar.js';
78
80
  import { initLogger, log, getLogPath } from './core/logger.js';
79
81
  import { VERSION } from './version.js';
@@ -83,6 +85,7 @@ import { queryPeerDecisions, formatPeerDecisions } from './coordination/peer-dec
83
85
  import { startLoopLagMonitor } from './core/write-telemetry.js';
84
86
  import { buildWhoami, formatWhoami } from './core/whoami.js';
85
87
  import { renderTaskEndInvitation, validateRecipeWrite, recipeSlug, getRecipe } from './recipes/index.js';
88
+ import { formatRecallResultLine } from './core/format-recall.js';
86
89
 
87
90
  // --- Incognito Mode ---
88
91
  // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
@@ -461,22 +464,11 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
461
464
  };
462
465
  }
463
466
 
464
- const lines = results.map((r, i) => {
465
- // Confidence-adaptive output (Paper 3: cognitive teaming). When the caller
466
- // requested 'compact' or 'auto' granularity, surface the engine-computed
467
- // summary instead of the full content same engram, less to read.
468
- const body = r.summary ?? r.engram.content;
469
- // D8 (2026-07-30): conflict surfacing — a superseded memory that still
470
- // ranks is shown WITH its replacement pointer instead of silently
471
- // down-ranked. The model should trust the successor.
472
- const chain = r.engram.supersededBy
473
- ? ` ⚠ SUPERSEDED by ${r.engram.supersededBy} — treat as historical; recall/fetch the successor before relying on this.`
474
- : '';
475
- const validity = r.engram.validTo
476
- ? ` [valid until ${r.engram.validTo}]`
477
- : '';
478
- return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${validity}: ${body}${chain}`;
479
- });
467
+ // Confidence-adaptive output (Paper 3: cognitive teaming) and D8
468
+ // (2026-07-30) conflict surfacing both live in the shared formatter now —
469
+ // see core/format-recall.ts for why it's extracted (0.12.1: unit-testable
470
+ // without booting the server) and why the id sits after the score.
471
+ const lines = results.map(formatRecallResultLine);
480
472
 
481
473
  return {
482
474
  content: [{
@@ -564,7 +556,7 @@ Use this when:
564
556
 
565
557
  The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`,
566
558
  {
567
- old_engram_id: z.string().describe('ID of the outdated memory'),
559
+ old_engram_id: z.string().describe('ID of the outdated memory (from memory_recall results, or memory_write\'s own response if you just wrote it)'),
568
560
  new_engram_id: z.string().describe('ID of the replacement memory'),
569
561
  reason: z.string().optional().describe('Why the old memory is outdated'),
570
562
  },
@@ -1288,6 +1280,35 @@ async function main() {
1288
1280
  agentId: AGENT_ID,
1289
1281
  secret: HOOK_SECRET,
1290
1282
  port: HOOK_PORT,
1283
+ // 0.12.2: warm recall for hooks — the sidecar shares this process's
1284
+ // activation engine and loaded models, so a UserPromptSubmit hook can get
1285
+ // warm-latency recall without any standing server. Trimmed result shape
1286
+ // (no embeddings/phase scores — hooks don't need them and the vectors
1287
+ // alone would 10x the payload).
1288
+ activate: async (q) => {
1289
+ const results = await activationEngine.activate({
1290
+ agentId: AGENT_ID,
1291
+ context: q.context,
1292
+ limit: q.limit,
1293
+ requireConfidence: q.requireConfidence,
1294
+ granularity: q.granularity,
1295
+ });
1296
+ return results.map(r => ({
1297
+ engram: {
1298
+ id: r.engram.id,
1299
+ concept: r.engram.concept,
1300
+ content: r.engram.content,
1301
+ createdAt: r.engram.createdAt instanceof Date
1302
+ ? r.engram.createdAt.toISOString()
1303
+ : (r.engram.createdAt as unknown as string | undefined),
1304
+ memoryClass: r.engram.memoryClass,
1305
+ validTo: r.engram.validTo,
1306
+ },
1307
+ score: r.score,
1308
+ summary: r.summary,
1309
+ confidence: r.confidence,
1310
+ }));
1311
+ },
1291
1312
  onConsolidate: async (agentId, reason) => {
1292
1313
  console.error(`[mcp] consolidation triggered: ${reason}`);
1293
1314
  const result = await consolidationEngine.consolidate(agentId);
@@ -1296,6 +1317,31 @@ async function main() {
1296
1317
  },
1297
1318
  });
1298
1319
 
1320
+ // 0.12.2: eager warm — fire-and-forget, mirrors index.ts:208-218. Without
1321
+ // this, every Claude Code session paid the full cold cost (~3s measured on
1322
+ // a 29.7k-engram store: slim cache ~0.9s + three model loads ~1.8s) on its
1323
+ // FIRST recall, which is exactly the "first recall is slow → recall gets
1324
+ // avoided" failure mode. Warming here overlaps session startup instead.
1325
+ // All output is stderr-safe (stdout carries JSON-RPC frames).
1326
+ // Escape hatch: AWM_NO_EAGER_WARM=1 restores lazy loading.
1327
+ if (process.env.AWM_NO_EAGER_WARM !== '1') {
1328
+ getEmbedder().catch(err => console.error('Embedding model unavailable:', err.message));
1329
+ getReranker().catch(err => console.error('Reranker model unavailable:', err.message));
1330
+ getExpander().catch(err => console.error('Query expander model unavailable:', err.message));
1331
+ if (BACKEND === 'sqlite') {
1332
+ setImmediate(() => {
1333
+ try {
1334
+ const t0 = Date.now();
1335
+ (store as unknown as EngramStore).warmSlimCache();
1336
+ const stats = (store as unknown as EngramStore).getSlimCacheStats();
1337
+ console.error(`Slim cache warmed: ${stats.size} entries in ${Date.now() - t0}ms`);
1338
+ } catch (err) {
1339
+ console.error(`Slim cache warm failed: ${(err as Error).message}`);
1340
+ }
1341
+ });
1342
+ }
1343
+ }
1344
+
1299
1345
  // Coordination MCP tools (opt-in via AWM_COORDINATION=true)
1300
1346
  // AWM 0.8.x: coordination requires SQLite (uses store.getDb()). On PGlite,
1301
1347
  // coordination is auto-disabled with a warning; re-enable when coordination