agent-working-memory 0.15.9 → 0.16.0

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 (77) hide show
  1. package/AGENTS.md +36 -0
  2. package/README.md +23 -12
  3. package/dist/adapters/common.d.ts +1 -1
  4. package/dist/adapters/common.d.ts.map +1 -1
  5. package/dist/adapters/common.js +33 -20
  6. package/dist/adapters/common.js.map +1 -1
  7. package/dist/core/embeddings.d.ts +1 -1
  8. package/dist/core/embeddings.js +1 -1
  9. package/dist/core/entity-extract.js +1 -1
  10. package/dist/core/query-expander.d.ts +4 -2
  11. package/dist/core/query-expander.d.ts.map +1 -1
  12. package/dist/core/query-expander.js +4 -2
  13. package/dist/core/query-expander.js.map +1 -1
  14. package/dist/core/recall-config.d.ts +1 -1
  15. package/dist/core/recall-config.d.ts.map +1 -1
  16. package/dist/core/recall-config.js +6 -1
  17. package/dist/core/recall-config.js.map +1 -1
  18. package/dist/core/recall-telemetry.d.ts +15 -0
  19. package/dist/core/recall-telemetry.d.ts.map +1 -0
  20. package/dist/core/recall-telemetry.js +108 -0
  21. package/dist/core/recall-telemetry.js.map +1 -0
  22. package/dist/core/rerank-window.d.ts +5 -4
  23. package/dist/core/rerank-window.d.ts.map +1 -1
  24. package/dist/core/rerank-window.js +5 -4
  25. package/dist/core/rerank-window.js.map +1 -1
  26. package/dist/core/reranker.d.ts +2 -1
  27. package/dist/core/reranker.d.ts.map +1 -1
  28. package/dist/core/reranker.js +23 -3
  29. package/dist/core/reranker.js.map +1 -1
  30. package/dist/core/salience.js +1 -1
  31. package/dist/core/salience.js.map +1 -1
  32. package/dist/core/token-budget.d.ts +2 -2
  33. package/dist/core/token-budget.js +2 -2
  34. package/dist/core/whoami.d.ts +1 -1
  35. package/dist/engine/activation.d.ts +30 -13
  36. package/dist/engine/activation.d.ts.map +1 -1
  37. package/dist/engine/activation.js +159 -50
  38. package/dist/engine/activation.js.map +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/mcp.js +98 -98
  42. package/dist/mcp.js.map +1 -1
  43. package/dist/storage/pglite.d.ts +28 -0
  44. package/dist/storage/pglite.d.ts.map +1 -1
  45. package/dist/storage/pglite.js +55 -0
  46. package/dist/storage/pglite.js.map +1 -1
  47. package/dist/storage/postgres.d.ts +28 -0
  48. package/dist/storage/postgres.d.ts.map +1 -1
  49. package/dist/storage/postgres.js +55 -0
  50. package/dist/storage/postgres.js.map +1 -1
  51. package/dist/storage/sqlite.d.ts +28 -0
  52. package/dist/storage/sqlite.d.ts.map +1 -1
  53. package/dist/storage/sqlite.js +56 -0
  54. package/dist/storage/sqlite.js.map +1 -1
  55. package/llms.txt +437 -0
  56. package/package.json +7 -1
  57. package/plugin/.claude-plugin/plugin.json +1 -1
  58. package/plugin/README.md +1 -1
  59. package/plugin/bin/awm-mcp-launcher.cjs +2 -2
  60. package/plugin/skills/awm-memory/SKILL.md +33 -20
  61. package/src/adapters/common.ts +33 -20
  62. package/src/core/embeddings.ts +1 -1
  63. package/src/core/entity-extract.ts +1 -1
  64. package/src/core/query-expander.ts +4 -2
  65. package/src/core/recall-config.ts +6 -1
  66. package/src/core/recall-telemetry.ts +131 -0
  67. package/src/core/rerank-window.ts +159 -158
  68. package/src/core/reranker.ts +24 -3
  69. package/src/core/salience.ts +1 -1
  70. package/src/core/token-budget.ts +160 -160
  71. package/src/core/whoami.ts +120 -120
  72. package/src/engine/activation.ts +159 -51
  73. package/src/index.ts +1 -1
  74. package/src/mcp.ts +1475 -1475
  75. package/src/storage/pglite.ts +49 -0
  76. package/src/storage/postgres.ts +49 -0
  77. package/src/storage/sqlite.ts +51 -0
@@ -71,7 +71,7 @@ export function resolveHookSecret(dbPath: string): string {
71
71
  * Retrieval settings `awm setup` turns on for a new install (0.14.6).
72
72
  *
73
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:
74
+ * behavior on upgrade, but the README has recommended them together since 0.14.0:
75
75
  * second-stage rerank by the cross-encoder's own score, a 400-char rerank window
76
76
  * on the densest query-term region (25% → 87.5% on long memories), and tags in the
77
77
  * rerank passage (+7.4pp). A value the user already has in their config wins.
@@ -381,9 +381,11 @@ you write them; AWM stays current because every agent reads + writes the same st
381
381
  than one filesystem search.
382
382
  4. **As you learn things**: call \`memory_write\` proactively. Don't batch.
383
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
384
+ 6. **Hooks do the rest.** \`awm setup\` installs five: a **prime** hook that recalls against
385
+ each prompt and injects what clears confidence (or nothing), checkpoint hooks on
386
+ compaction and session end (plus a 15-min timer), a local write/recall reminder on
387
+ Stop, and a reminder after Bash/PowerShell calls that a data change is not done
388
+ until it is written. Primed context arrives labeled
387
389
  \`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
388
390
  arrives with no primed context, that is a signal too: recall explicitly before
389
391
  asserting anything.
@@ -411,9 +413,9 @@ extraction all see most strongly.
411
413
  a noisy bucket where it competes with everything else in the area.
412
414
  - **Include 2+ retrievable identifiers.** File paths, function names, table
413
415
  columns, ticket IDs, exact error strings, the literal terms a future query
414
- will use. \`AccountingService.closePeriod()\` beats "the accounting code."
416
+ will use. \`SettlementService.closeRun()\` beats "the accounting code."
415
417
  \`tblMemberDetails.activation_date\` beats "the activation column."
416
- \`schema/072-period-close.sql\` beats "the migration."
418
+ \`schema/072-settlement-lock.sql\` beats "the migration."
417
419
  - **Write in the vocabulary of the future question.** When you imagine asking
418
420
  this in three months, what nouns will you use? Use those nouns. Don't
419
421
  paraphrase the user's domain language into your own neutral summary.
@@ -430,12 +432,14 @@ extraction all see most strongly.
430
432
  Measured on an 11k-engram store: 94% of tagged memories are missing at
431
433
  least one of their own topical terms from the body, and 66% of those
432
434
  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.
435
+ - **Tags are NOT a substitute for body text.** The embedding is built from
436
+ \`concept + content\` only, so it never sees tags. BM25 indexes them. The
437
+ cross-encoder rerank passage sees ONLY \`topic=\`/\`proj=\`/\`project=\` tags, and
438
+ only when \`AWM_RERANK_TAGS=1\` — which \`awm setup\`, the plugin and the Desktop
439
+ bundle all switch on, so assume it is on — capped at 80 characters. So a word
440
+ that exists only as a tag is invisible to the embedding entirely, and
441
+ invisible to the ranker unless it is one of those three prefixes inside that
442
+ budget. Tag it *and* write it.
439
443
  - **Reserve canonical for stable invariants.** Decisions, requirements,
440
444
  hard facts, cross-agent shared context. Working class (default) is correct
441
445
  for findings, observations, and progress notes. The canonical floor is
@@ -452,6 +456,10 @@ Every \`memory_write\` should pass these structured fields. AWM stores each as a
452
456
  prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
453
457
  and entity-bridge boosts at recall time.
454
458
 
459
+ (“Required” below means *required for good recall* — the MCP schema accepts every
460
+ one of these as optional, so a write without them succeeds and simply retrieves
461
+ worse.)
462
+
455
463
  | Field | Required? | Format | Example |
456
464
  |---|---|---|---|
457
465
  | \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
@@ -461,12 +469,12 @@ and entity-bridge boosts at recall time.
461
469
  | \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
462
470
  | \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
463
471
  | \`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"]\` |
472
+ | \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=10001", "date=2026-05-11"]\` |
465
473
 
466
474
  **Always add identifier tags when present in the content:**
467
475
  - \`ticket=<id>\` for Freshdesk tickets
468
476
  - \`member=<id>\` for member IDs
469
- - \`horse=<id>\` for horse_member_id
477
+ - \`asset=<id>\` for a domain record id
470
478
  - \`usef=<id>\` for USEF lookups
471
479
  - \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
472
480
  - \`person=<Name>\` for stakeholder quotes / decisions
@@ -475,7 +483,7 @@ and entity-bridge boosts at recall time.
475
483
  ### Entity index — exact-match recall for named things (default off)
476
484
  Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
477
485
  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
486
+ A query naming an entity ("ticket 10003", "Dana Whitfield") can reach the memory through
479
487
  this index even when the wording doesn't lexically match — it's a deterministic exact
480
488
  lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
481
489
  this reason, not just for the BM25 boost described above.
@@ -675,14 +683,14 @@ memory_write(
675
683
  \`\`\`
676
684
  memory_write(
677
685
  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.",
686
+ content="apps/web/app/(billing)/billing/settlement-lock/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in SettlementService.closeRun() per schema/072-settlement-lock.sql. Without server-side check a malicious request could bypass via direct API call.",
679
687
  project="EquiHub",
680
688
  topic="accounting",
681
689
  intent="finding",
682
690
  confidence_level="verified",
683
691
  source="debugging",
684
692
  memory_class="canonical",
685
- tags=["ticket=18360", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
693
+ tags=["ticket=10001", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
686
694
  )
687
695
  \`\`\`
688
696
 
@@ -730,7 +738,7 @@ For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
730
738
  multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
731
739
 
732
740
  ### 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
741
+ Each optimization is gated by an env var so it can be disabled for A/B testing if a
734
742
  regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
735
743
 
736
744
  Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
@@ -752,8 +760,13 @@ Recall pipeline:
752
760
  no effect on the real-store benchmark, 0.14.5).
753
761
  - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
754
762
  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.
763
+ - \`AWM_FEEDBACK_BM25=1\` — re-enables Rocchio pseudo-relevance feedback
764
+ (phase 3.5), a third BM25 pass over the query plus five terms harvested from
765
+ the top 3 results. Default OFF since 2026-10-09: measured at 6.7–7.0% of every
766
+ warm recall for no measurable accuracy — 3 gold ranks moved in 950 queries
767
+ across three suites, two of them the wrong way. On the public corpus — the only
768
+ suite with recall headroom left — it is inert per query, not just in aggregate:
769
+ 0 of 200 queries differ in rank, top-1 or score.
757
770
  - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
758
771
  heuristic + LRU cache. Forces every recall through flan-t5-small.
759
772
 
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Embedding Engine - vector embeddings via the ML worker pool.
5
5
  *
6
- * Default model: bge-small-en-v1.5 (384 dimensions, ~90MB, MTEB retrieval-optimized).
6
+ * Default model: bge-small-en-v1.5 (384 dimensions, 133MB ONNX, MTEB retrieval-optimized).
7
7
  * Configurable via AWM_EMBED_MODEL env var.
8
8
  *
9
9
  * AWM 0.8.x: inference dispatches through ml-worker.ts. The worker_threads
@@ -5,7 +5,7 @@
5
5
  * Entity extraction for the entity inverted index (D9, 2026-07-30).
6
6
  *
7
7
  * v1 is deliberately conservative for precision: entities come ONLY from
8
- * structured sources — prefix tags (`person=Seetha`, `ticket=18999`) and
8
+ * structured sources — prefix tags (`person=Avery`, `ticket=10002`) and
9
9
  * auto-tagger `entity:` tags — never from free-text guessing. Each entity is
10
10
  * normalized to `key:value` lowercase so lookups are exact.
11
11
  *
@@ -3,8 +3,10 @@
3
3
  /**
4
4
  * Query Expander - rewrites queries with synonyms and related terms.
5
5
  *
6
- * Uses Xenova/flan-t5-small (~80MB ONNX) to expand search queries with
7
- * related terms that improve BM25 recall.
6
+ * Uses Xenova/flan-t5-small — a 141MB encoder plus a 233MB merged decoder, so
7
+ * 374MB of ONNX and a 377MB download with its tokenizer. By far the largest of
8
+ * the three models AWM fetches. Expands search queries with related terms that
9
+ * improve BM25 recall.
8
10
  *
9
11
  * AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
10
12
  * — worker_threads reverted because onnxruntime-node bindings cross isolate
@@ -41,6 +41,10 @@ export const RECALL_FLAGS = [
41
41
  'AWM_RERANK_POOL',
42
42
  'AWM_RERANK_TAGS',
43
43
  'AWM_RERANK_TAGS_LEN',
44
+ // Cross-encoder precision — changes every reranker score, so it changes order.
45
+ // Omitted until 2026-10-09, during which both arms of a dtype sweep printed the
46
+ // SAME arm= label; the whole point of this list is that that cannot happen.
47
+ 'AWM_RERANKER_DTYPE',
44
48
  // Derived retrieval text (embedding side)
45
49
  'AWM_RETRIEVAL_TEXT',
46
50
  // Project-dialect alias expansion
@@ -49,7 +53,8 @@ export const RECALL_FLAGS = [
49
53
  // Embedding model — changes every stored vector, so it belongs here
50
54
  'AWM_EMBED_MODEL',
51
55
  'AWM_EMBED_DIMS',
52
- 'AWM_DISABLE_RERANK_SKIP',
56
+ // Rocchio pseudo-relevance feedback (phase 3.5)
57
+ 'AWM_FEEDBACK_BM25',
53
58
  // Spreading activation (D11 — parked)
54
59
  'AWM_SPREAD',
55
60
  'AWM_SPREAD_INJECT',
@@ -0,0 +1,131 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Recall-path telemetry — per-stage wall time for one `activate()` call.
6
+ *
7
+ * WHY THIS EXISTS
8
+ * ---------------
9
+ * The write path has had phase telemetry since D1 (`write-telemetry.ts`), but
10
+ * the READ path — the hot one, the one every latency claim in the docs is
11
+ * about — had only a single total. Every attribution of that total was
12
+ * therefore a reading of the code rather than a measurement, and two comments
13
+ * in this repository assert "the reranker is ~90% of warm recall latency"
14
+ * citing nothing but each other. This module exists so that number can be
15
+ * checked, and so a reviewer can check it too.
16
+ *
17
+ * USE
18
+ * ---
19
+ * AWM_PROFILE_RECALL=1 one stderr line per recall
20
+ * AWM_PROFILE_RECALL_OUT=path.jsonl append one JSON object per recall
21
+ *
22
+ * Both unset ⇒ `startRecallProfile()` returns a shared no-op and the only
23
+ * cost on the hot path is a handful of empty method calls.
24
+ *
25
+ * DESIGN
26
+ * ------
27
+ * Labels are FLAT and must not overlap: each `begin`/`end` pair measures a
28
+ * span that no other pair contains. That is what makes `unaccounted`
29
+ * (total minus the sum of all spans) meaningful — it is the synchronous
30
+ * JavaScript that no stage claims, and if it is large that is itself the
31
+ * finding. Repeated labels accumulate, so a per-agent loop can reuse one.
32
+ *
33
+ * Additive module: nothing here changes an existing export or code path.
34
+ *
35
+ * `node:fs` is imported statically. A lazy `require()` here was silently dead:
36
+ * this package is ESM, `require` is not defined, and the throw landed in the
37
+ * catch that exists so profiling can never break a recall — so the first run
38
+ * produced an empty capture and an honest-looking zero.
39
+ */
40
+ import { appendFileSync } from 'node:fs';
41
+
42
+ const STDERR = process.env.AWM_PROFILE_RECALL === '1';
43
+ const OUT = process.env.AWM_PROFILE_RECALL_OUT || '';
44
+ const ON = STDERR || !!OUT;
45
+
46
+ export interface RecallProfiler {
47
+ /** Open a span. Nesting a span inside another breaks `unaccounted`. */
48
+ begin(label: string): void;
49
+ /** Close the most recent span with this label, accumulating its duration. */
50
+ end(label: string): void;
51
+ /** Record a scalar fact about this recall (pool size, counts, flags). */
52
+ note(key: string, value: number | string | boolean): void;
53
+ /** Emit the record. Safe to call once; later calls are ignored. */
54
+ finish(meta?: Record<string, number | string | boolean>): void;
55
+ }
56
+
57
+ const NOOP: RecallProfiler = {
58
+ begin() {}, end() {}, note() {}, finish() {},
59
+ };
60
+
61
+ class Profiler implements RecallProfiler {
62
+ private readonly t0 = performance.now();
63
+ private readonly open = new Map<string, number>();
64
+ private readonly sums = new Map<string, number>();
65
+ private readonly calls = new Map<string, number>();
66
+ private readonly notes: Record<string, number | string | boolean> = {};
67
+ private done = false;
68
+
69
+ begin(label: string): void {
70
+ this.open.set(label, performance.now());
71
+ }
72
+
73
+ end(label: string): void {
74
+ const started = this.open.get(label);
75
+ if (started === undefined) return; // unbalanced end — ignore, never throw
76
+ this.open.delete(label);
77
+ const ms = performance.now() - started;
78
+ this.sums.set(label, (this.sums.get(label) ?? 0) + ms);
79
+ this.calls.set(label, (this.calls.get(label) ?? 0) + 1);
80
+ }
81
+
82
+ note(key: string, value: number | string | boolean): void {
83
+ this.notes[key] = value;
84
+ }
85
+
86
+ finish(meta: Record<string, number | string | boolean> = {}): void {
87
+ if (this.done) return;
88
+ this.done = true;
89
+ const totalMs = performance.now() - this.t0;
90
+
91
+ const stages: Record<string, number> = {};
92
+ let accounted = 0;
93
+ for (const [label, ms] of this.sums) {
94
+ stages[label] = Math.round(ms * 100) / 100;
95
+ accounted += ms;
96
+ }
97
+
98
+ const record = {
99
+ totalMs: Math.round(totalMs * 100) / 100,
100
+ unaccountedMs: Math.round((totalMs - accounted) * 100) / 100,
101
+ stages,
102
+ calls: Object.fromEntries(this.calls),
103
+ ...this.notes,
104
+ ...meta,
105
+ };
106
+
107
+ if (OUT) {
108
+ try {
109
+ appendFileSync(OUT, JSON.stringify(record) + '\n');
110
+ } catch { /* profiling must never break a recall */ }
111
+ }
112
+
113
+ if (STDERR) {
114
+ const top = Object.entries(stages).sort((a, b) => b[1] - a[1]).slice(0, 6)
115
+ .map(([k, v]) => `${k}=${Math.round(v)}ms`).join(' ');
116
+ process.stderr.write(
117
+ `[awm] recall ${Math.round(totalMs)}ms ${top} unaccounted=${Math.round(record.unaccountedMs)}ms\n`,
118
+ );
119
+ }
120
+ }
121
+ }
122
+
123
+ /** One profiler per `activate()` call, or a shared no-op when disabled. */
124
+ export function startRecallProfile(): RecallProfiler {
125
+ return ON ? new Profiler() : NOOP;
126
+ }
127
+
128
+ /** Whether recall profiling is active — for callers that want to skip setup work. */
129
+ export function recallProfileEnabled(): boolean {
130
+ return ON;
131
+ }