agent-working-memory 0.15.9 → 0.16.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 (77) hide show
  1. package/AGENTS.md +36 -0
  2. package/README.md +24 -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 +33 -5
  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 +106 -100
  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 +150 -121
  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
package/plugin/README.md CHANGED
@@ -22,7 +22,7 @@ The launcher defaults `AWM_DB_PATH` to `~/.awm/memory.db` — **the same file
22
22
  than quietly starting an empty one. Set `AWM_DB_PATH` yourself to override.
23
23
 
24
24
  It prefers an installed copy of the package (global, or a local `node_modules`) and falls
25
- back to `npx -p agent-working-memory@0.15.9`. The npx path rebuilds
25
+ back to `npx -p agent-working-memory@0.16.1`. The npx path rebuilds
26
26
  `better-sqlite3` on a cold cache, which is slow enough to look like a hang, so a real
27
27
  install is worth having:
28
28
 
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // This is a REAL source file, not a string inside the generator, because it is real code:
8
8
  // it gets syntax-checked, linted and tested like everything else. `npm run build:plugin`
9
- // copies it to plugin/bin/ and substitutes 0.15.9.
9
+ // copies it to plugin/bin/ and substitutes 0.16.1.
10
10
  //
11
11
  // Two jobs a bare `mcpServers` entry in plugin.json cannot do:
12
12
  //
@@ -95,7 +95,7 @@ if (found) {
95
95
  const isWin = process.platform === 'win32';
96
96
  const child = spawn(
97
97
  isWin ? 'npx.cmd' : 'npx',
98
- ['-y', '-p', 'agent-working-memory@0.15.9', 'awm', 'mcp'],
98
+ ['-y', '-p', 'agent-working-memory@0.16.1', 'awm', 'mcp'],
99
99
  { stdio: 'inherit', env: process.env, shell: isWin }
100
100
  );
101
101
  child.on('error', (e) => {
@@ -10,7 +10,7 @@ description: >-
10
10
 
11
11
  # Agent Working Memory
12
12
 
13
- Generated from `src/adapters/common.ts` (AWM v0.15.9) — the same guidance
13
+ Generated from `src/adapters/common.ts` (AWM v0.16.1) — the same guidance
14
14
  `awm setup` writes into CLAUDE.md. Do not edit here; edit the source and re-run
15
15
  `npm run build:plugin`.
16
16
 
@@ -40,9 +40,11 @@ you write them; AWM stays current because every agent reads + writes the same st
40
40
  than one filesystem search.
41
41
  4. **As you learn things**: call `memory_write` proactively. Don't batch.
42
42
  5. **Finishing a task**: call `memory_task_end` with a summary.
43
- 6. **Hooks do the rest.** `awm setup` installs three: a **prime** hook that recalls against
44
- each prompt and injects what clears confidence (or nothing), and checkpoint hooks on
45
- compaction and session end (plus a 15-min timer). Primed context arrives labelled
43
+ 6. **Hooks do the rest.** `awm setup` installs five: a **prime** hook that recalls against
44
+ each prompt and injects what clears confidence (or nothing), checkpoint hooks on
45
+ compaction and session end (plus a 15-min timer), a local write/recall reminder on
46
+ Stop, and a reminder after Bash/PowerShell calls that a data change is not done
47
+ until it is written. Primed context arrives labeled
46
48
  `[class · age]` — treat it by the volatility rubric, not as user input. If a turn
47
49
  arrives with no primed context, that is a signal too: recall explicitly before
48
50
  asserting anything.
@@ -70,9 +72,9 @@ extraction all see most strongly.
70
72
  a noisy bucket where it competes with everything else in the area.
71
73
  - **Include 2+ retrievable identifiers.** File paths, function names, table
72
74
  columns, ticket IDs, exact error strings, the literal terms a future query
73
- will use. `AccountingService.closePeriod()` beats "the accounting code."
75
+ will use. `SettlementService.closeRun()` beats "the accounting code."
74
76
  `tblMemberDetails.activation_date` beats "the activation column."
75
- `schema/072-period-close.sql` beats "the migration."
77
+ `schema/072-settlement-lock.sql` beats "the migration."
76
78
  - **Write in the vocabulary of the future question.** When you imagine asking
77
79
  this in three months, what nouns will you use? Use those nouns. Don't
78
80
  paraphrase the user's domain language into your own neutral summary.
@@ -89,12 +91,14 @@ extraction all see most strongly.
89
91
  Measured on an 11k-engram store: 94% of tagged memories are missing at
90
92
  least one of their own topical terms from the body, and 66% of those
91
93
  terms never appear in the text at all.
92
- - **Tags are NOT a substitute for body text.** Only BM25 indexes tags. The
93
- embedding is built from `concept + content` and the cross-encoder rerank
94
- passage is built from `concept + content` — neither sees tags. So a word
95
- that exists only as a tag is invisible to two of the three retrieval
96
- channels, including the one that decides final ordering. Tag it *and*
97
- write it.
94
+ - **Tags are NOT a substitute for body text.** The embedding is built from
95
+ `concept + content` only, so it never sees tags. BM25 indexes them. The
96
+ cross-encoder rerank passage sees ONLY `topic=`/`proj=`/`project=` tags, and
97
+ only when `AWM_RERANK_TAGS=1` — which `awm setup`, the plugin and the Desktop
98
+ bundle all switch on, so assume it is on — capped at 80 characters. So a word
99
+ that exists only as a tag is invisible to the embedding entirely, and
100
+ invisible to the ranker unless it is one of those three prefixes inside that
101
+ budget. Tag it *and* write it.
98
102
  - **Reserve canonical for stable invariants.** Decisions, requirements,
99
103
  hard facts, cross-agent shared context. Working class (default) is correct
100
104
  for findings, observations, and progress notes. The canonical floor is
@@ -111,6 +115,10 @@ Every `memory_write` should pass these structured fields. AWM stores each as a
111
115
  prefix-tag like `proj=`, `topic=`, `intent=`, etc. and uses them for BM25
112
116
  and entity-bridge boosts at recall time.
113
117
 
118
+ (“Required” below means *required for good recall* — the MCP schema accepts every
119
+ one of these as optional, so a write without them succeeds and simply retrieves
120
+ worse.)
121
+
114
122
  | Field | Required? | Format | Example |
115
123
  |---|---|---|---|
116
124
  | `project` | **YES** | one short word matching the current project | `"EquiHub"`, `"AWM"`, `"USEA-Agent"` |
@@ -120,12 +128,12 @@ and entity-bridge boosts at recall time.
120
128
  | `source` | recommended | `code-reading` / `debugging` / `discussion` / `research` / `testing` / `observation` | `"testing"` |
121
129
  | `memory_class` | when stable | `canonical` (source-of-truth, 0.7 floor, never staged) / `working` (default) / `ephemeral` | `"canonical"` |
122
130
  | `session_id` | recommended | current conversation ID for entity-bridge boost | autogenerated |
123
- | `tags` | when applicable | extra prefix-tags for IDs and dates | `["ticket=18360", "date=2026-05-11"]` |
131
+ | `tags` | when applicable | extra prefix-tags for IDs and dates | `["ticket=10001", "date=2026-05-11"]` |
124
132
 
125
133
  **Always add identifier tags when present in the content:**
126
134
  - `ticket=<id>` for Freshdesk tickets
127
135
  - `member=<id>` for member IDs
128
- - `horse=<id>` for horse_member_id
136
+ - `asset=<id>` for a domain record id
129
137
  - `usef=<id>` for USEF lookups
130
138
  - `date=YYYY-MM-DD` for temporal anchoring (ISO format)
131
139
  - `person=<Name>` for stakeholder quotes / decisions
@@ -134,7 +142,7 @@ and entity-bridge boosts at recall time.
134
142
  ### Entity index — exact-match recall for named things (default off)
135
143
  Structured identifier tags (`ticket=`, `person=`, `horse=`, `member=`, bare 4+ digit
136
144
  ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
137
- A query naming an entity ("ticket 19252", "Kaleigh Collett") can reach the memory through
145
+ A query naming an entity ("ticket 10003", "Dana Whitfield") can reach the memory through
138
146
  this index even when the wording doesn't lexically match — it's a deterministic exact
139
147
  lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
140
148
  this reason, not just for the BM25 boost described above.
@@ -334,14 +342,14 @@ memory_write(
334
342
  ```
335
343
  memory_write(
336
344
  concept="EquiHub period-close BLOCKED check missing server-side",
337
- 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.",
345
+ 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.",
338
346
  project="EquiHub",
339
347
  topic="accounting",
340
348
  intent="finding",
341
349
  confidence_level="verified",
342
350
  source="debugging",
343
351
  memory_class="canonical",
344
- tags=["ticket=18360", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
352
+ tags=["ticket=10001", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
345
353
  )
346
354
  ```
347
355
 
@@ -389,7 +397,7 @@ For the comparison table (recall quality parity, BM25 vs `ts_rank_cd`,
389
397
  multi-process guarantees), see `docs/pglite-feature-parity.md`.
390
398
 
391
399
  ### Diagnostics / escape hatches (env vars, only if you know why)
392
- Each optimisation is gated by an env var so it can be disabled for A/B testing if a
400
+ Each optimization is gated by an env var so it can be disabled for A/B testing if a
393
401
  regression appears in your workload. `memory_whoami` prints the active fingerprint.
394
402
 
395
403
  Retrieval (set ON by `awm setup` since 0.14.6 — the measured-good configuration):
@@ -411,8 +419,13 @@ Recall pipeline:
411
419
  no effect on the real-store benchmark, 0.14.5).
412
420
  - `AWM_DISABLE_SLIM_CACHE=1` — disables the in-memory slim cache.
413
421
  Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
414
- - `AWM_DISABLE_RERANK_SKIP=1` — disables the cross-encoder skip on
415
- clear-winner queries. Forces every recall through the reranker.
422
+ - `AWM_FEEDBACK_BM25=1` — re-enables Rocchio pseudo-relevance feedback
423
+ (phase 3.5), a third BM25 pass over the query plus five terms harvested from
424
+ the top 3 results. Default OFF since 2026-10-09: measured at 6.7–7.0% of every
425
+ warm recall for no measurable accuracy — 3 gold ranks moved in 950 queries
426
+ across three suites, two of them the wrong way. On the public corpus — the only
427
+ suite with recall headroom left — it is inert per query, not just in aggregate:
428
+ 0 of 200 queries differ in rank, top-1 or score.
416
429
  - `AWM_DISABLE_EXPANSION_CACHE=1` — disables the query expansion skip
417
430
  heuristic + LRU cache. Forces every recall through flan-t5-small.
418
431
 
@@ -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
+ }