agent-working-memory 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +200 -238
  2. package/dist/adapters/common.d.ts +6 -0
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +457 -362
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +24 -8
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/core/alias-map.d.ts +16 -0
  10. package/dist/core/alias-map.d.ts.map +1 -0
  11. package/dist/core/alias-map.js +102 -0
  12. package/dist/core/alias-map.js.map +1 -0
  13. package/dist/core/embeddings.d.ts +17 -0
  14. package/dist/core/embeddings.d.ts.map +1 -1
  15. package/dist/core/embeddings.js +50 -1
  16. package/dist/core/embeddings.js.map +1 -1
  17. package/dist/core/recall-config.d.ts +52 -0
  18. package/dist/core/recall-config.d.ts.map +1 -0
  19. package/dist/core/recall-config.js +110 -0
  20. package/dist/core/recall-config.js.map +1 -0
  21. package/dist/core/rerank-window.d.ts +61 -0
  22. package/dist/core/rerank-window.d.ts.map +1 -0
  23. package/dist/core/rerank-window.js +153 -0
  24. package/dist/core/rerank-window.js.map +1 -0
  25. package/dist/core/rerank2.d.ts +62 -0
  26. package/dist/core/rerank2.d.ts.map +1 -0
  27. package/dist/core/rerank2.js +75 -0
  28. package/dist/core/rerank2.js.map +1 -0
  29. package/dist/core/retrieval-text.d.ts +55 -0
  30. package/dist/core/retrieval-text.d.ts.map +1 -0
  31. package/dist/core/retrieval-text.js +87 -0
  32. package/dist/core/retrieval-text.js.map +1 -0
  33. package/dist/core/temporal-query.d.ts +61 -0
  34. package/dist/core/temporal-query.d.ts.map +1 -0
  35. package/dist/core/temporal-query.js +168 -0
  36. package/dist/core/temporal-query.js.map +1 -0
  37. package/dist/core/token-budget.d.ts +75 -0
  38. package/dist/core/token-budget.d.ts.map +1 -0
  39. package/dist/core/token-budget.js +136 -0
  40. package/dist/core/token-budget.js.map +1 -0
  41. package/dist/core/whoami.d.ts +11 -0
  42. package/dist/core/whoami.d.ts.map +1 -1
  43. package/dist/core/whoami.js +10 -0
  44. package/dist/core/whoami.js.map +1 -1
  45. package/dist/core/write-pipeline.d.ts.map +1 -1
  46. package/dist/core/write-pipeline.js +6 -3
  47. package/dist/core/write-pipeline.js.map +1 -1
  48. package/dist/engine/activation.d.ts.map +1 -1
  49. package/dist/engine/activation.js +146 -32
  50. package/dist/engine/activation.js.map +1 -1
  51. package/dist/hooks/prime.d.ts +77 -0
  52. package/dist/hooks/prime.d.ts.map +1 -0
  53. package/dist/hooks/prime.js +92 -0
  54. package/dist/hooks/prime.js.map +1 -0
  55. package/dist/hooks/sidecar.d.ts.map +1 -1
  56. package/dist/hooks/sidecar.js +39 -0
  57. package/dist/hooks/sidecar.js.map +1 -1
  58. package/dist/mcp.js +160 -107
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/storage/pglite.d.ts.map +1 -1
  61. package/dist/storage/pglite.js +10 -2
  62. package/dist/storage/pglite.js.map +1 -1
  63. package/dist/storage/postgres.d.ts.map +1 -1
  64. package/dist/storage/postgres.js +10 -2
  65. package/dist/storage/postgres.js.map +1 -1
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +12 -2
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/types/engram.d.ts +31 -0
  70. package/dist/types/engram.d.ts.map +1 -1
  71. package/package.json +3 -2
  72. package/src/adapters/common.ts +666 -567
  73. package/src/api/routes.ts +1015 -999
  74. package/src/core/alias-map.ts +97 -0
  75. package/src/core/embeddings.ts +172 -115
  76. package/src/core/recall-config.ts +115 -0
  77. package/src/core/rerank-window.ts +158 -0
  78. package/src/core/rerank2.ts +82 -0
  79. package/src/core/retrieval-text.ts +82 -0
  80. package/src/core/temporal-query.ts +193 -0
  81. package/src/core/token-budget.ts +160 -0
  82. package/src/core/whoami.ts +110 -92
  83. package/src/core/write-pipeline.ts +6 -3
  84. package/src/engine/activation.ts +1579 -1468
  85. package/src/hooks/prime.ts +136 -0
  86. package/src/hooks/sidecar.ts +43 -0
  87. package/src/mcp.ts +1443 -1387
  88. package/src/storage/pglite.ts +10 -2
  89. package/src/storage/postgres.ts +10 -2
  90. package/src/storage/sqlite.ts +12 -2
  91. package/src/types/engram.ts +32 -0
package/dist/mcp.js CHANGED
@@ -80,6 +80,7 @@ import { startLoopLagMonitor } from './core/write-telemetry.js';
80
80
  import { buildWhoami, formatWhoami } from './core/whoami.js';
81
81
  import { renderTaskEndInvitation, validateRecipeWrite, recipeSlug, getRecipe } from './recipes/index.js';
82
82
  import { formatRecallResultLine } from './core/format-recall.js';
83
+ import { packRecallByBudget, formatTokenFooter, estimateTokens } from './core/token-budget.js';
83
84
  // --- Incognito Mode ---
84
85
  // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
85
86
  // No DB, no engines, no sidecar — just a bare MCP server that exposes nothing.
@@ -196,15 +197,15 @@ else {
196
197
  return 'unclassified';
197
198
  }
198
199
  // --- Tools ---
199
- server.tool('memory_write', `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
200
-
201
- CALL THIS PROACTIVELY — do not wait to be asked. Write memories when you:
202
- - Discover something about the codebase, bugs, or architecture
203
- - Make a decision and want to remember why
204
- - Encounter and resolve an error
205
- - Learn a user preference or project pattern
206
- - Complete a significant piece of work
207
-
200
+ server.tool('memory_write', `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
201
+
202
+ CALL THIS PROACTIVELY — do not wait to be asked. Write memories when you:
203
+ - Discover something about the codebase, bugs, or architecture
204
+ - Make a decision and want to remember why
205
+ - Encounter and resolve an error
206
+ - Learn a user preference or project pattern
207
+ - Complete a significant piece of work
208
+
208
209
  The concept should be a short label (3-8 words). The content should be the full detail.`, {
209
210
  concept: z.string().describe('Short label for this memory (3-8 words)'),
210
211
  content: z.string().describe('Full detail of what was learned'),
@@ -355,27 +356,28 @@ The concept should be a short label (3-8 words). The content should be the full
355
356
  }],
356
357
  };
357
358
  });
358
- server.tool('memory_recall', `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
359
-
360
- ALWAYS call this when:
361
- - Starting work on a project or topic (recall what you know)
362
- - Debugging (recall similar errors and solutions)
363
- - Making decisions (recall past decisions and outcomes)
364
- - The user mentions a topic you might have stored memories about
365
-
366
- Accepts either "query" or "context" parameter — both work identically.
359
+ server.tool('memory_recall', `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
360
+
361
+ ALWAYS call this when:
362
+ - Starting work on a project or topic (recall what you know)
363
+ - Debugging (recall similar errors and solutions)
364
+ - Making decisions (recall past decisions and outcomes)
365
+ - The user mentions a topic you might have stored memories about
366
+
367
+ Accepts either "query" or "context" parameter — both work identically.
367
368
  Returns the most relevant memories ranked by text relevance, temporal recency, and associative strength.`, {
368
369
  query: z.string().optional().describe('What to search for — describe the situation, question, or topic'),
369
370
  context: z.string().optional().describe('Alias for query (either works)'),
370
- limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
371
+ limit: z.number().optional().default(3).describe('Max memories to return (default 3). Measured on 400 real-store probes: k=3 is the only token-POSITIVE setting (+115 tok/recall, against -680 at k=5) and is 41% faster at p50, because `limit` sizes the cross-encoder rerank pool and rerank is ~90% of warm recall time. It costs ~0.5pp success@1 versus k=5 — about 3 queries in 400, inside noise. Raise it explicitly when you want breadth for orientation rather than a specific fact; that is the case this default deliberately does not optimise for.'),
371
372
  min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
372
373
  include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
373
374
  use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
374
375
  use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
375
376
  memory_type: z.enum(['episodic', 'semantic', 'procedural']).optional().describe('Filter by memory type (omit to search all types)'),
376
377
  workspace: z.string().optional().describe('Search across all agents in this workspace (hive mode). Omit for agent-scoped recall only.'),
377
- require_confidence: z.number().optional().describe('Opt-in: abstain (return []) when recall confidence is below this threshold. Typical values: 0.10 (strict), 0.25 (balanced), 0.40 (aggressive). Confidence is the shape of the result-score distribution; low confidence indicates a noisy or best-of-bad-bunch recall.'),
378
+ require_confidence: z.number().optional().default(0.05).describe('Abstain (return nothing) when recall confidence is below this threshold. Defaults to 0.05 — a LIGHT filter, chosen from measurement: it halves the rate of answering off-topic queries at zero cost to hit rate. Raise it only for push-style use where nobody asked (0.25 is what the prime hook uses). Do NOT raise it for ordinary recall: a miss is expensive, because the agent then reads the codebase instead (~2,106 tokens), so aggressive thresholds measurably destroy value — 0.20+ cut net tokens saved by 25% in tests/abstention-eval. Pass 0 to disable. NOTE: this is NOT min_score — it gates on the SHAPE of the score distribution across the whole result set, not per-result relevance, so memories that pass min_score can still be withheld. When that happens the reply says RECALL ABSTAINED and reports how many were withheld; an empty result is only absence when it does not.'),
378
379
  granularity: z.enum(['full', 'compact', 'auto']).optional().describe('Output granularity (Paper 3: cognitive teaming). "full" (default): no change. "compact": every result carries a short summary field. "auto": confidence-adaptive — top result gets a longer summary when there is a clear winner, otherwise everything is compact for scanning.'),
380
+ max_tokens: z.number().optional().describe('Token budget for the response. `limit` is a COUNT and is token-blind — 5 results may cost 400 tokens or 4,000. Use this when context is tight: results are packed by value-per-token until the budget is reached, the top-scored match always gets first refusal, and the reply reports what it cost and what was withheld. Omit for no budget (everything is returned, still with accounting).'),
379
381
  }, async (params) => {
380
382
  const queryText = params.query ?? params.context;
381
383
  if (!queryText) {
@@ -388,6 +390,9 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
388
390
  }
389
391
  // Use workspace from param, env var, or omit for agent-scoped
390
392
  const workspace = params.workspace ?? process.env.AWM_WORKSPACE ?? undefined;
393
+ // Set only when a gate WITHHELD results. Distinguishes abstention from absence,
394
+ // which an empty array cannot.
395
+ let abstained;
391
396
  const results = await activationEngine.activate({
392
397
  agentId: AGENT_ID,
393
398
  context: queryText,
@@ -400,6 +405,7 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
400
405
  workspace,
401
406
  requireConfidence: params.require_confidence,
402
407
  granularity: params.granularity,
408
+ onAbstain: (info) => { abstained = info; },
403
409
  });
404
410
  // Auto-checkpoint: track recall
405
411
  try {
@@ -413,27 +419,69 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
413
419
  ? formatPeerDecisions(queryPeerDecisions(coordDb, AGENT_ID, queryText))
414
420
  : '';
415
421
  if (results.length === 0) {
422
+ // Two very different conditions used to render identically as
423
+ // "No relevant memories found." — a claim of absence the system cannot make
424
+ // when a gate withheld matches. Callers read it as absence and stopped
425
+ // looking, for memories scoring well above minScore.
426
+ const text = abstained
427
+ ? [
428
+ `RECALL ABSTAINED — this is NOT "no memories exist".`,
429
+ ``,
430
+ `${abstained.candidates} candidate${abstained.candidates === 1 ? '' : 's'} matched ` +
431
+ `(best score ${abstained.topScore.toFixed(3)}) and ${abstained.candidates === 1 ? 'was' : 'were'} ` +
432
+ `withheld because recall confidence ` +
433
+ `${abstained.confidence !== undefined ? abstained.confidence.toFixed(3) + ' ' : ''}` +
434
+ `fell below your require_confidence of ${abstained.threshold ?? '?'}.`,
435
+ ``,
436
+ `require_confidence is NOT min_score. It gates on the SHAPE of the score` +
437
+ ` distribution across the whole result set, not on how relevant any single` +
438
+ ` memory is — so results that comfortably pass min_score can still be withheld here.`,
439
+ ``,
440
+ `To see them: re-run this query with require_confidence: 0.`,
441
+ `Do not conclude the memories are absent without doing that.`,
442
+ ].join('\n') + peerSuffix
443
+ : 'No relevant memories found.' + peerSuffix;
444
+ return { content: [{ type: 'text', text }] };
445
+ }
446
+ // Confidence-adaptive output (Paper 3: cognitive teaming) and D8
447
+ // (2026-07-30) conflict surfacing both live in the shared formatter now —
448
+ // see core/format-recall.ts for why it's extracted (0.12.1: unit-testable
449
+ // without booting the server) and why the id sits after the score.
450
+ // 0.13.3: pack to a token budget (if given) and always report the spend.
451
+ // `limit` is a count and is token-blind; max_tokens bounds what this call
452
+ // actually costs the caller's context. See core/token-budget.ts for why the
453
+ // top-scored result gets first refusal rather than pure density packing.
454
+ // Reserve what the rest of the reply will cost, so max_tokens bounds the
455
+ // WHOLE response and not just the result lines. FOOTER_ALLOWANCE is a
456
+ // fixed upper bound on the accounting line (measured at ~15-25 tokens; the
457
+ // unit test asserts it stays under 40).
458
+ const FOOTER_ALLOWANCE = 40;
459
+ const reserved = estimateTokens(peerSuffix) + FOOTER_ALLOWANCE;
460
+ const packed = packRecallByBudget(results, formatRecallResultLine, params.max_tokens, reserved);
461
+ // A budget too small to admit even the best match must not look like "no
462
+ // memories found" — that is a different fact and would send the caller off
463
+ // to read code that AWM could have answered. Say what actually happened so
464
+ // the fix (raise the budget) is obvious.
465
+ if (packed.kept === 0 && packed.total > 0) {
416
466
  return {
417
467
  content: [{
418
468
  type: 'text',
419
- text: 'No relevant memories found.' + peerSuffix,
469
+ text: `${packed.total} memories matched, but none fit a ${params.max_tokens}-token budget `
470
+ + `(smallest result is ~${packed.withheldTokens} tok across ${packed.total}). `
471
+ + `Raise max_tokens, or use granularity: 'compact' to shrink each result.`
472
+ + peerSuffix,
420
473
  }],
421
474
  };
422
475
  }
423
- // Confidence-adaptive output (Paper 3: cognitive teaming) and D8
424
- // (2026-07-30) conflict surfacing both live in the shared formatter now —
425
- // see core/format-recall.ts for why it's extracted (0.12.1: unit-testable
426
- // without booting the server) and why the id sits after the score.
427
- const lines = results.map(formatRecallResultLine);
428
476
  return {
429
477
  content: [{
430
478
  type: 'text',
431
- text: lines.join('\n') + peerSuffix,
479
+ text: packed.lines.join('\n') + peerSuffix + formatTokenFooter(packed, params.max_tokens),
432
480
  }],
433
481
  };
434
482
  });
435
- server.tool('memory_feedback', `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
436
-
483
+ server.tool('memory_feedback', `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
484
+
437
485
  Always call this after using a recalled memory so the system learns what's valuable.`, {
438
486
  engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
439
487
  useful: z.boolean().describe('Was this memory actually helpful?'),
@@ -456,8 +504,8 @@ Always call this after using a recalled memory so the system learns what's valua
456
504
  }],
457
505
  };
458
506
  });
459
- server.tool('memory_retract', `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
460
-
507
+ server.tool('memory_retract', `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
508
+
461
509
  Use this when you discover a memory contains incorrect information.`, {
462
510
  engram_id: z.string().describe('ID of the wrong memory'),
463
511
  reason: z.string().describe('Why is this memory wrong?'),
@@ -481,13 +529,13 @@ Use this when you discover a memory contains incorrect information.`, {
481
529
  }],
482
530
  };
483
531
  });
484
- server.tool('memory_supersede', `Replace an outdated memory with a newer one. Unlike retraction (which marks memories as wrong), supersession marks the old memory as outdated but historically correct.
485
-
486
- Use this when:
487
- - A status or count has changed (e.g., "5 reviews done" → "7 reviews done")
488
- - Architecture or infrastructure evolved (e.g., "two-repo model" → "three-repo model")
489
- - A schedule or plan was updated
490
-
532
+ server.tool('memory_supersede', `Replace an outdated memory with a newer one. Unlike retraction (which marks memories as wrong), supersession marks the old memory as outdated but historically correct.
533
+
534
+ Use this when:
535
+ - A status or count has changed (e.g., "5 reviews done" → "7 reviews done")
536
+ - Architecture or infrastructure evolved (e.g., "two-repo model" → "three-repo model")
537
+ - A schedule or plan was updated
538
+
491
539
  The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`, {
492
540
  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)'),
493
541
  new_engram_id: z.string().describe('ID of the replacement memory'),
@@ -518,7 +566,7 @@ The old memory stays in the database (searchable for history) but is heavily dow
518
566
  const info = await buildWhoami(store, AGENT_ID, 'mcp');
519
567
  return { content: [{ type: 'text', text: formatWhoami(info) }] };
520
568
  });
521
- server.tool('memory_stats', `Get memory health stats — how many memories, confidence levels, association count, and system performance.
569
+ server.tool('memory_stats', `Get memory health stats — how many memories, confidence levels, association count, and system performance.
522
570
  Also shows the activity log path so the user can tail it to see what's happening.`, {}, async () => {
523
571
  const metrics = await evalEngine.computeMetrics(AGENT_ID);
524
572
  const checkpoint = await store.getCheckpoint(AGENT_ID);
@@ -549,13 +597,13 @@ Also shows the activity log path so the user can tail it to see what's happening
549
597
  };
550
598
  });
551
599
  // --- Checkpointing Tools ---
552
- server.tool('memory_checkpoint', `Save your current execution state so you can recover after context compaction.
553
-
554
- ALWAYS call this before:
555
- - Long operations (multi-file generation, large refactors, overnight work)
556
- - Anything that might fill the context window
557
- - Switching to a different task
558
-
600
+ server.tool('memory_checkpoint', `Save your current execution state so you can recover after context compaction.
601
+
602
+ ALWAYS call this before:
603
+ - Long operations (multi-file generation, large refactors, overnight work)
604
+ - Anything that might fill the context window
605
+ - Switching to a different task
606
+
559
607
  Also call periodically during long sessions to avoid losing state. The state is saved per-agent and overwrites any previous checkpoint.`, {
560
608
  current_task: z.string().describe('What you are currently working on'),
561
609
  decisions: z.array(z.string()).optional().default([])
@@ -589,14 +637,14 @@ Also call periodically during long sessions to avoid losing state. The state is
589
637
  }],
590
638
  };
591
639
  });
592
- server.tool('memory_restore', `Restore your previous execution state after context compaction or at session start.
593
-
594
- Returns:
595
- - Your saved execution state (task, decisions, next steps, files)
596
- - Recently recalled memories for context
597
- - Your last write for continuity
598
- - How long you were idle
599
-
640
+ server.tool('memory_restore', `Restore your previous execution state after context compaction or at session start.
641
+
642
+ Returns:
643
+ - Your saved execution state (task, decisions, next steps, files)
644
+ - Recently recalled memories for context
645
+ - Your last write for continuity
646
+ - How long you were idle
647
+
600
648
  Use this at the start of every session or after compaction to pick up where you left off.`, {}, async () => {
601
649
  const checkpoint = await store.getCheckpoint(AGENT_ID);
602
650
  // Cold-store nudge: an empty store means the agent has nothing to recall — offer to warm-start.
@@ -630,7 +678,11 @@ Use this at the start of every session or after compaction to pick up where you
630
678
  const results = await activationEngine.activate({
631
679
  agentId: AGENT_ID,
632
680
  context: recallContext,
633
- limit: 5,
681
+ // 3, not 5 — measured token-positive (+115/recall vs -680) and 41% faster.
682
+ // The case is stronger here than for explicit recall: this is push-style, so
683
+ // nobody asked for these results and every one of them costs context the user
684
+ // did not request.
685
+ limit: 3,
634
686
  minScore: 0.05,
635
687
  useReranker: true,
636
688
  useExpansion: true,
@@ -715,9 +767,9 @@ Use this at the start of every session or after compaction to pick up where you
715
767
  if (coordDb) {
716
768
  try {
717
769
  const myAgent = coordDb.prepare(`SELECT id FROM coord_agents WHERE name = ? AND status != 'dead' ORDER BY last_seen DESC LIMIT 1`).get(AGENT_ID);
718
- const peerDecisions = coordDb.prepare(`SELECT d.summary, a.name AS author_name, d.created_at
719
- FROM coord_decisions d JOIN coord_agents a ON d.author_id = a.id
720
- WHERE d.author_id != ? AND d.created_at > datetime('now', '-30 minutes')
770
+ const peerDecisions = coordDb.prepare(`SELECT d.summary, a.name AS author_name, d.created_at
771
+ FROM coord_decisions d JOIN coord_agents a ON d.author_id = a.id
772
+ WHERE d.author_id != ? AND d.created_at > datetime('now', '-30 minutes')
721
773
  ORDER BY d.created_at DESC LIMIT 10`).all(myAgent?.id ?? '');
722
774
  if (peerDecisions.length > 0) {
723
775
  parts.push(`\n**Peer decisions (last 30 min):**`);
@@ -736,11 +788,11 @@ Use this at the start of every session or after compaction to pick up where you
736
788
  };
737
789
  });
738
790
  // --- Onboarding Tools (warm-start a cold store) ---
739
- server.tool('onboard_scan', `Scan a project's documentation + repository and return CANDIDATE memories to seed a cold store.
740
-
741
- Use this when the store is empty / you're new to a project. The scan is deterministic
742
- (real file contents, not guesses) — YOUR job is to refine the candidates into atomic,
743
- recall-shaped memories, run the interview (onboard_questions), confirm with the user, then
791
+ server.tool('onboard_scan', `Scan a project's documentation + repository and return CANDIDATE memories to seed a cold store.
792
+
793
+ Use this when the store is empty / you're new to a project. The scan is deterministic
794
+ (real file contents, not guesses) — YOUR job is to refine the candidates into atomic,
795
+ recall-shaped memories, run the interview (onboard_questions), confirm with the user, then
744
796
  save the good ones with memory_write (memory_class="canonical"). Nothing is saved by this tool.`, {
745
797
  docs: z.array(z.string()).optional()
746
798
  .describe('Doc files/dirs to scan (Markdown/text). Defaults to the repo (or cwd).'),
@@ -765,18 +817,18 @@ save the good ones with memory_write (memory_class="canonical"). Nothing is save
765
817
  ].join('\n');
766
818
  return { content: [{ type: 'text', text }] };
767
819
  });
768
- server.tool('onboard_questions', `Return the onboarding interview questions. Ask the user ONE at a time, starting with the
820
+ server.tool('onboard_questions', `Return the onboarding interview questions. Ask the user ONE at a time, starting with the
769
821
  goal of the memory system, and ask follow-ups for clarity. Turn each answer into a canonical memory.`, {}, async () => ({
770
822
  content: [{ type: 'text', text: INTERVIEW_QUESTIONS.map((q, i) => `${i + 1}. ${q}`).join('\n') }],
771
823
  }));
772
824
  // --- Task Management Tools ---
773
- server.tool('memory_task_add', `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
774
-
775
- Use this when:
776
- - You identify work that needs doing but can't do it right now
777
- - The user mentions something to do later
778
- - You want to park a sub-task while focusing on something more urgent
779
-
825
+ server.tool('memory_task_add', `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
826
+
827
+ Use this when:
828
+ - You identify work that needs doing but can't do it right now
829
+ - The user mentions something to do later
830
+ - You want to park a sub-task while focusing on something more urgent
831
+
780
832
  Tasks automatically get high salience so they won't be discarded.`, {
781
833
  concept: z.string().describe('Short task title (3-10 words)'),
782
834
  content: z.string().describe('Full task description — what needs doing, context, acceptance criteria'),
@@ -816,11 +868,11 @@ Tasks automatically get high salience so they won't be discarded.`, {
816
868
  }],
817
869
  };
818
870
  });
819
- server.tool('memory_task_update', `Update a task's status or priority. Use this to:
820
- - Start working on a task (open → in_progress)
821
- - Mark a task done (→ done)
822
- - Block a task on another (→ blocked)
823
- - Reprioritize (change priority)
871
+ server.tool('memory_task_update', `Update a task's status or priority. Use this to:
872
+ - Start working on a task (open → in_progress)
873
+ - Mark a task done (→ done)
874
+ - Block a task on another (→ blocked)
875
+ - Reprioritize (change priority)
824
876
  - Unblock a task (clear blocked_by)`, {
825
877
  task_id: z.string().describe('ID of the task to update'),
826
878
  status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
@@ -850,8 +902,8 @@ Tasks automatically get high salience so they won't be discarded.`, {
850
902
  }],
851
903
  };
852
904
  });
853
- server.tool('memory_task_list', `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
854
-
905
+ server.tool('memory_task_list', `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
906
+
855
907
  Use at the start of a session to see what's pending, or to check blocked/done tasks.`, {
856
908
  status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
857
909
  .describe('Filter by status (omit to see all active tasks)'),
@@ -877,10 +929,10 @@ Use at the start of a session to see what's pending, or to check blocked/done ta
877
929
  }],
878
930
  };
879
931
  });
880
- server.tool('memory_task_next', `Get the single most important task to work on next.
881
-
882
- Prioritizes: in_progress tasks first (finish what you started), then by priority level, then oldest first. Skips blocked and done tasks.
883
-
932
+ server.tool('memory_task_next', `Get the single most important task to work on next.
933
+
934
+ Prioritizes: in_progress tasks first (finish what you started), then by priority level, then oldest first. Skips blocked and done tasks.
935
+
884
936
  Use this when you finish a task or need to decide what to do next.`, {}, async () => {
885
937
  const next = await store.getNextTask(AGENT_ID);
886
938
  if (!next) {
@@ -896,13 +948,13 @@ Use this when you finish a task or need to decide what to do next.`, {}, async (
896
948
  };
897
949
  });
898
950
  // --- Task Bracket Tools ---
899
- server.tool('memory_task_begin', `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
900
-
901
- CALL THIS when starting:
902
- - A multi-step operation (doc generation, large refactor, migration)
903
- - Work on a new topic or project area
904
- - Anything that might fill the context window
905
-
951
+ server.tool('memory_task_begin', `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
952
+
953
+ CALL THIS when starting:
954
+ - A multi-step operation (doc generation, large refactor, migration)
955
+ - Work on a new topic or project area
956
+ - Anything that might fill the context window
957
+
906
958
  This ensures your state is saved before you start, and primes recall with relevant context.`, {
907
959
  topic: z.string().describe('What task are you starting? (3-15 words)'),
908
960
  files: z.array(z.string()).optional().default([])
@@ -928,7 +980,8 @@ This ensures your state is saved before you start, and primes recall with releva
928
980
  const results = await activationEngine.activate({
929
981
  agentId: AGENT_ID,
930
982
  context: params.topic,
931
- limit: 5,
983
+ // 3, not 5 — see memory_recall. Push-style auto-recall on task start.
984
+ limit: 3,
932
985
  minScore: 0.05,
933
986
  useReranker: true,
934
987
  useExpansion: true,
@@ -953,13 +1006,13 @@ This ensures your state is saved before you start, and primes recall with releva
953
1006
  }],
954
1007
  };
955
1008
  });
956
- server.tool('memory_task_end', `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
957
-
958
- CALL THIS when you finish:
959
- - A multi-step operation
960
- - Before switching to a different topic
961
- - At the end of a work session
962
-
1009
+ server.tool('memory_task_end', `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
1010
+
1011
+ CALL THIS when you finish:
1012
+ - A multi-step operation
1013
+ - Before switching to a different topic
1014
+ - At the end of a work session
1015
+
963
1016
  This captures what was accomplished so future sessions can recall it.`, {
964
1017
  summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
965
1018
  tags: z.array(z.string()).optional().default([])
@@ -1035,14 +1088,14 @@ This captures what was accomplished so future sessions can recall it.`, {
1035
1088
  }],
1036
1089
  };
1037
1090
  });
1038
- server.tool('compress_output', `Compress a STRUCTURED tool output (JSON object/array, query rows, log records) into TOON —
1039
- a compact, schema-aware tabular encoding — before putting it in your context. Cuts ~50-65%
1040
- of the tokens on uniform arrays at zero comprehension cost (validated: models read TOON as
1041
- accurately as JSON). Use this on large tool results you need to keep in context.
1042
-
1043
- Output-only and safe: it never changes the data. Non-JSON / prose is returned unchanged.
1044
- TOON is only emitted when it reproduces the input exactly (self-verified round-trip);
1045
- otherwise you get plain JSON back. When compressed, you also get a 'ref' — call
1091
+ server.tool('compress_output', `Compress a STRUCTURED tool output (JSON object/array, query rows, log records) into TOON —
1092
+ a compact, schema-aware tabular encoding — before putting it in your context. Cuts ~50-65%
1093
+ of the tokens on uniform arrays at zero comprehension cost (validated: models read TOON as
1094
+ accurately as JSON). Use this on large tool results you need to keep in context.
1095
+
1096
+ Output-only and safe: it never changes the data. Non-JSON / prose is returned unchanged.
1097
+ TOON is only emitted when it reproduces the input exactly (self-verified round-trip);
1098
+ otherwise you get plain JSON back. When compressed, you also get a 'ref' — call
1046
1099
  retrieve_original(ref) to get the verbatim source back if you ever need it.`, {
1047
1100
  output: z.string().describe('The tool output to compress — JSON text (preferred) or any string. Non-JSON is returned unchanged.'),
1048
1101
  min_saving_chars: z.number().optional().describe('Only emit TOON if it saves at least this many characters (default 40).'),
@@ -1056,8 +1109,8 @@ retrieve_original(ref) to get the verbatim source back if you ever need it.`, {
1056
1109
  content: [{ type: 'text', text: header + r.text }],
1057
1110
  };
1058
1111
  });
1059
- server.tool('retrieve_original', `Retrieve the verbatim original text for a 'ref' returned by compress_output. Use this when
1060
- you need the exact, uncompressed source (e.g. to pass it to another tool unchanged). Returns
1112
+ server.tool('retrieve_original', `Retrieve the verbatim original text for a 'ref' returned by compress_output. Use this when
1113
+ you need the exact, uncompressed source (e.g. to pass it to another tool unchanged). Returns
1061
1114
  an error if the ref has expired (originals are kept for the most recent compressions only).`, {
1062
1115
  ref: z.string().describe('The ref handle returned by compress_output (e.g. "awm_orig_12").'),
1063
1116
  }, async (params) => {