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.
- package/AGENTS.md +36 -0
- package/README.md +23 -12
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +33 -20
- package/dist/adapters/common.js.map +1 -1
- package/dist/core/embeddings.d.ts +1 -1
- package/dist/core/embeddings.js +1 -1
- package/dist/core/entity-extract.js +1 -1
- package/dist/core/query-expander.d.ts +4 -2
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +4 -2
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/recall-config.d.ts +1 -1
- package/dist/core/recall-config.d.ts.map +1 -1
- package/dist/core/recall-config.js +6 -1
- package/dist/core/recall-config.js.map +1 -1
- package/dist/core/recall-telemetry.d.ts +15 -0
- package/dist/core/recall-telemetry.d.ts.map +1 -0
- package/dist/core/recall-telemetry.js +108 -0
- package/dist/core/recall-telemetry.js.map +1 -0
- package/dist/core/rerank-window.d.ts +5 -4
- package/dist/core/rerank-window.d.ts.map +1 -1
- package/dist/core/rerank-window.js +5 -4
- package/dist/core/rerank-window.js.map +1 -1
- package/dist/core/reranker.d.ts +2 -1
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +23 -3
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.js +1 -1
- package/dist/core/salience.js.map +1 -1
- package/dist/core/token-budget.d.ts +2 -2
- package/dist/core/token-budget.js +2 -2
- package/dist/core/whoami.d.ts +1 -1
- package/dist/engine/activation.d.ts +30 -13
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +159 -50
- package/dist/engine/activation.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +98 -98
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite.d.ts +28 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +55 -0
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +28 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +55 -0
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +28 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +56 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/llms.txt +437 -0
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -1
- package/plugin/bin/awm-mcp-launcher.cjs +2 -2
- package/plugin/skills/awm-memory/SKILL.md +33 -20
- package/src/adapters/common.ts +33 -20
- package/src/core/embeddings.ts +1 -1
- package/src/core/entity-extract.ts +1 -1
- package/src/core/query-expander.ts +4 -2
- package/src/core/recall-config.ts +6 -1
- package/src/core/recall-telemetry.ts +131 -0
- package/src/core/rerank-window.ts +159 -158
- package/src/core/reranker.ts +24 -3
- package/src/core/salience.ts +1 -1
- package/src/core/token-budget.ts +160 -160
- package/src/core/whoami.ts +120 -120
- package/src/engine/activation.ts +159 -51
- package/src/index.ts +1 -1
- package/src/mcp.ts +1475 -1475
- package/src/storage/pglite.ts +49 -0
- package/src/storage/postgres.ts +49 -0
- package/src/storage/sqlite.ts +51 -0
|
@@ -6,25 +6,42 @@
|
|
|
6
6
|
* Cognitive retrieval pipeline — phases as shipped (D4 honesty pass 2026-07-30;
|
|
7
7
|
* default-OFF phases are marked, since operators tune from this header):
|
|
8
8
|
* -1. Coreference expansion (conditional: query contains pronouns)
|
|
9
|
-
* 0. Query expansion (flan-t5-small; caller-gated
|
|
9
|
+
* 0. Query expansion (flan-t5-small; caller-gated. The ENGINE default is OFF and
|
|
10
|
+
* AWM_DEFAULT_EXPANSION=1 flips it — but MCP memory_recall defaults
|
|
11
|
+
* use_expansion to TRUE, so it DOES run on the path most callers use.
|
|
12
|
+
* HTTP /memory/activate and the prime hook pass nothing, so they get OFF.)
|
|
10
13
|
* 1. Vector embedding (bge-small 384d)
|
|
11
14
|
* 2. Parallel retrieval (dual FTS5/BM25 + native vector top-K)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* 3a. Candidate fetch — and entity-index candidate injection splices in HERE,
|
|
16
|
+
* before any scoring (DEFAULT OFF; AWM_ENTITY_INDEX_FETCH=1 — D11
|
|
17
|
+
* 2026-07-30: D9 inverted-index lookup of query-named entities; injected
|
|
18
|
+
* candidates get no boost but a guaranteed rerank audition)
|
|
19
|
+
* 3b. Per-candidate scoring (BM25, Jaccard, cosine floor, ACT-R decay,
|
|
20
|
+
* Hebbian boost, confidence gate — all computed TOGETHER here, not as
|
|
21
|
+
* separate later passes)
|
|
22
|
+
* 3.5 Rocchio pseudo-relevance feedback (DEFAULT OFF since 2026-10-09;
|
|
23
|
+
* AWM_FEEDBACK_BM25=1 — measured at 7% of a recall for nothing, see phase 3.5)
|
|
24
|
+
* 3.7 Entity-bridge boost (default ON. AWM_DISABLE_ENTITY_BRIDGE disables it —
|
|
25
|
+
* the check is for PRESENCE, not value, so =0 disables it too; unset to
|
|
26
|
+
* re-enable.)
|
|
19
27
|
* 3.75 Query-conditioned entity bridge (DEFAULT OFF; AWM_QUERY_BRIDGE=1)
|
|
20
|
-
* 4/5
|
|
21
|
-
*
|
|
28
|
+
* 4/5 Graph walk. A plain beam walk over Hebbian + temporal edges is the
|
|
29
|
+
* DEFAULT; the iterative SPREADING-activation variant is the opt-in
|
|
30
|
+
* (AWM_SPREAD=1 — parked after displacing-gold regressions, see D11)
|
|
22
31
|
* 6. Filter + sort into rerank pool (wide pool since 0.9.0)
|
|
23
|
-
* 7. Cross-encoder rerank (ms-marco;
|
|
24
|
-
*
|
|
25
|
-
* 8. Multi-channel OOD detection + agreement gate; supersession penalty;
|
|
32
|
+
* 7. Cross-encoder rerank (ms-marco; every pooled candidate — the
|
|
33
|
+
* 0.7.10 clear-winner skip was removed 2026-10-09, see phase 7a)
|
|
34
|
+
* 8. Multi-channel OOD detection + agreement gate; supersession penalty (8c);
|
|
26
35
|
* abstention enforced only when caller passes require_confidence
|
|
27
36
|
* 9. Final sort, granularity, confidence attach
|
|
37
|
+
* 9b. rerank2 second-pass reorder (feedback / edge-strength / class bonuses).
|
|
38
|
+
* Engine default OFF (AWM_RERANK2=1), but awm setup, the plugin and the
|
|
39
|
+
* Desktop bundle all switch it on, so a normal install HAS it.
|
|
40
|
+
*
|
|
41
|
+
* Phase numbers are the ones used in the code below, so they can be grepped.
|
|
42
|
+
* Corrected 2026-10-09: the entity-index injection was listed as a second
|
|
43
|
+
* "3.5" (colliding with Rocchio) and placed after 3.7 though it runs inside
|
|
44
|
+
* 3a; 9b was missing entirely.
|
|
28
45
|
*
|
|
29
46
|
* Logs activation events for eval metrics.
|
|
30
47
|
*/
|
|
@@ -34,6 +51,7 @@ import { strengthenAssociation, CoActivationBuffer, ValidationGatedBuffer } from
|
|
|
34
51
|
import { embed, cosineSimilarity } from '../core/embeddings.js';
|
|
35
52
|
import { rerank } from '../core/reranker.js';
|
|
36
53
|
import { expandQuery } from '../core/query-expander.js';
|
|
54
|
+
import { startRecallProfile } from '../core/recall-telemetry.js';
|
|
37
55
|
import { computeRecallConfidence } from './confidence.js';
|
|
38
56
|
import { reorderByReranker, rerank2Enabled, rerank2WindowSize } from '../core/rerank2.js';
|
|
39
57
|
import { buildRerankPassage, rerankTruncation, rerankWindowMode } from '../core/rerank-window.js';
|
|
@@ -176,6 +194,7 @@ export class ActivationEngine {
|
|
|
176
194
|
*/
|
|
177
195
|
async activate(query) {
|
|
178
196
|
const startTime = performance.now();
|
|
197
|
+
const prof = startRecallProfile();
|
|
179
198
|
const limit = query.limit ?? 10;
|
|
180
199
|
const minScore = query.minScore ?? 0.01; // Default: filter out zero-relevance results
|
|
181
200
|
const useReranker = query.useReranker ?? true;
|
|
@@ -189,9 +208,11 @@ export class ActivationEngine {
|
|
|
189
208
|
const requireConfidence = query.requireConfidence ?? 0;
|
|
190
209
|
const adaptive = resolveAdaptiveParams(query);
|
|
191
210
|
// Resolve workspace scope: if workspace is set, search across all agents in that workspace
|
|
211
|
+
prof.begin('agents');
|
|
192
212
|
const agentIds = query.workspace
|
|
193
213
|
? await this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
|
|
194
214
|
: [query.agentId];
|
|
215
|
+
prof.end('agents');
|
|
195
216
|
const isWorkspaceScoped = agentIds.length > 1;
|
|
196
217
|
// ── Phase -2: temporal expression ──
|
|
197
218
|
// Nothing downstream parses dates, so "from last Thursday" was being spent
|
|
@@ -214,11 +235,14 @@ export class ActivationEngine {
|
|
|
214
235
|
}
|
|
215
236
|
const pronounPattern = /\b(she|he|they|her|his|him|their|it|that|this|there)\b/i;
|
|
216
237
|
if (pronounPattern.test(queryContext)) {
|
|
238
|
+
prof.begin('pronounEntities');
|
|
217
239
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
240
|
+
// Bounded in SQL. This used to load EVERY active engram to pick five
|
|
241
|
+
// tag words — 280ms median on 2.1% of real prompts. See
|
|
242
|
+
// EngramStore.getTopAccessedTags and docs/recall-latency.md.
|
|
243
|
+
const recentEntities = (await this.store.getTopAccessedTags(agentIds, 10, 'active'))
|
|
244
|
+
.flat()
|
|
245
|
+
.filter(t => t.length >= 3 && !/^(session-|low-|D\d)/.test(t))
|
|
222
246
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
223
247
|
.slice(0, 5);
|
|
224
248
|
if (recentEntities.length > 0) {
|
|
@@ -227,6 +251,7 @@ export class ActivationEngine {
|
|
|
227
251
|
}
|
|
228
252
|
catch { /* non-fatal */ }
|
|
229
253
|
}
|
|
254
|
+
prof.end('pronounEntities');
|
|
230
255
|
// Phase 0: Query expansion — add related terms to improve BM25 recall
|
|
231
256
|
let searchContext = queryContext;
|
|
232
257
|
// Project-dialect aliases. Added to the BM25 SEARCH STRING ONLY — never to
|
|
@@ -239,6 +264,7 @@ export class ActivationEngine {
|
|
|
239
264
|
const aliasAdded = aliasTermsFor(queryContext);
|
|
240
265
|
if (aliasAdded.length > 0)
|
|
241
266
|
searchContext = `${searchContext} ${aliasAdded.join(' ')}`;
|
|
267
|
+
prof.begin('expansion');
|
|
242
268
|
if (useExpansion) {
|
|
243
269
|
let timer;
|
|
244
270
|
try {
|
|
@@ -255,22 +281,27 @@ export class ActivationEngine {
|
|
|
255
281
|
clearTimeout(timer);
|
|
256
282
|
}
|
|
257
283
|
}
|
|
284
|
+
prof.end('expansion');
|
|
258
285
|
// Phase 1: Embed query for vector similarity (uses coref-expanded context)
|
|
259
286
|
let queryEmbedding = null;
|
|
287
|
+
prof.begin('embed');
|
|
260
288
|
try {
|
|
261
289
|
queryEmbedding = await embed(queryContext);
|
|
262
290
|
}
|
|
263
291
|
catch {
|
|
264
292
|
// Embedding unavailable — fall back to text-only matching
|
|
265
293
|
}
|
|
294
|
+
prof.end('embed');
|
|
266
295
|
// Phase 2: Parallel retrieval — dual BM25 + all active engrams
|
|
267
296
|
// Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
|
|
268
297
|
// Uses queryContext, not query.context: temporal words must not reach BM25.
|
|
298
|
+
prof.begin('bm25');
|
|
269
299
|
const keywordQuery = Array.from(tokenize(queryContext)).join(' ');
|
|
270
300
|
const bm25Keyword = keywordQuery.length > 2
|
|
271
301
|
? await this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
272
302
|
: [];
|
|
273
303
|
const bm25Expanded = await this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
304
|
+
prof.end('bm25');
|
|
274
305
|
// Merge: take the best BM25 score per engram from either pass
|
|
275
306
|
const bm25ScoreMap = new Map();
|
|
276
307
|
const bm25EngramMap = new Map();
|
|
@@ -326,7 +357,9 @@ export class ActivationEngine {
|
|
|
326
357
|
if (queryEmbedding) {
|
|
327
358
|
for (const aid of agentIds) {
|
|
328
359
|
try {
|
|
360
|
+
prof.begin('vector');
|
|
329
361
|
const hits = await this.store.searchByVector(aid, queryEmbedding, VECTOR_TOP_K);
|
|
362
|
+
prof.end('vector');
|
|
330
363
|
for (const h of hits) {
|
|
331
364
|
// pgvector cosine distance: 0 = identical, 2 = opposite.
|
|
332
365
|
// For unit-norm BGE vectors, distance ≈ 1 - cosineSimilarity.
|
|
@@ -401,7 +434,9 @@ export class ActivationEngine {
|
|
|
401
434
|
}
|
|
402
435
|
if (ents.size > 0) {
|
|
403
436
|
try {
|
|
437
|
+
prof.begin('entityBridge');
|
|
404
438
|
const entHits = await this.store.searchBM25WithRankMultiAgent(agentIds, Array.from(ents).slice(0, 8).join(' '), ENT_CAP);
|
|
439
|
+
prof.end('entityBridge');
|
|
405
440
|
let added = 0;
|
|
406
441
|
for (const h of entHits) {
|
|
407
442
|
if (added >= ENT_CAP)
|
|
@@ -424,11 +459,13 @@ export class ActivationEngine {
|
|
|
424
459
|
}
|
|
425
460
|
}
|
|
426
461
|
// ── D11 (2026-07-30): ENTITY-INDEX CANDIDATE INJECTION (default-OFF, AWM_ENTITY_INDEX_FETCH=1) ──
|
|
427
|
-
// The D9 inverted index resolves query-NAMED entities ("
|
|
462
|
+
// The D9 inverted index resolves query-NAMED entities ("Avery", "ticket 10002") to every
|
|
428
463
|
// engram indexed under them — deterministic exact lookup, immune to embedding/BM25 vocabulary
|
|
429
464
|
// mismatch. Injected candidates get NO score boost; instead they are GUARANTEED a rerank
|
|
430
|
-
// audition (exempt from the topN cut, the minScore pool filter
|
|
431
|
-
//
|
|
465
|
+
// audition (exempt from the topN cut, the minScore pool filter and the rerank-pool slice)
|
|
466
|
+
// and the cross-encoder alone decides whether they surface. The fourth exemption, from the
|
|
467
|
+
// clear-winner rerank skip, went away with that branch on 2026-10-09 — the reranker is now
|
|
468
|
+
// unconditional, so an injected candidate is always auditioned.
|
|
432
469
|
// This is the guarded successor to AWM_ENTITY_FETCH above, whose failure mode was measured:
|
|
433
470
|
// injected gold entered the pool but was cut before the reranker ever scored it. Bounded
|
|
434
471
|
// (AWM_ENTITY_INDEX_CAP, default 12) so the audition costs at most one extra rerank batch.
|
|
@@ -451,7 +488,9 @@ export class ActivationEngine {
|
|
|
451
488
|
for (const term of Array.from(terms).slice(0, 4)) {
|
|
452
489
|
if (added >= IDX_CAP)
|
|
453
490
|
break;
|
|
491
|
+
prof.begin('entitySearch');
|
|
454
492
|
const entities = await this.store.searchEntities(term, 6);
|
|
493
|
+
prof.end('entitySearch');
|
|
455
494
|
for (const entity of entities) {
|
|
456
495
|
if (added >= IDX_CAP)
|
|
457
496
|
break;
|
|
@@ -475,7 +514,9 @@ export class ActivationEngine {
|
|
|
475
514
|
}
|
|
476
515
|
continue;
|
|
477
516
|
}
|
|
517
|
+
prof.begin('entityFetch');
|
|
478
518
|
const n = await this.store.getEngram(id);
|
|
519
|
+
prof.end('entityFetch');
|
|
479
520
|
if (!n || n.stage !== 'active' || n.retracted || n.supersededBy)
|
|
480
521
|
continue;
|
|
481
522
|
if (query.memoryType && n.memoryType !== query.memoryType)
|
|
@@ -505,10 +546,14 @@ export class ActivationEngine {
|
|
|
505
546
|
// Graph walk still needs full Association objects, but it operates on the
|
|
506
547
|
// top-N (~30 candidates) — its on-demand `getAssociationsFor` lookups are
|
|
507
548
|
// cheap (<5ms total).
|
|
549
|
+
prof.note('candidates', candidates.length);
|
|
550
|
+
prof.begin('assocStats');
|
|
508
551
|
const assocStats = await this.store.getAssociationStatsForBatch(candidates.map(e => e.id));
|
|
552
|
+
prof.end('assocStats');
|
|
509
553
|
// 0.14.4: decay clock is overridable (query.now) so evals against a frozen
|
|
510
554
|
// snapshot are reproducible across days. See ActivationQuery.now.
|
|
511
555
|
const nowMs = query.now ?? Date.now();
|
|
556
|
+
prof.begin('scoring');
|
|
512
557
|
const scored = candidates.map(engram => {
|
|
513
558
|
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
514
559
|
const stats = assocStats.get(engram.id) ?? { count: 0, sumWeight: 0 };
|
|
@@ -607,10 +652,39 @@ export class ActivationEngine {
|
|
|
607
652
|
// associations: empty in 0.7.12+ — graph walk lazy-fetches per engram on demand
|
|
608
653
|
return { engram, score: composite, phaseScores, associations: [] };
|
|
609
654
|
});
|
|
655
|
+
prof.end('scoring');
|
|
610
656
|
// Phase 3.5: Rocchio pseudo-relevance feedback — expand query with top result terms
|
|
611
|
-
// then re-search BM25 to find candidates that keyword search missed
|
|
657
|
+
// then re-search BM25 to find candidates that keyword search missed.
|
|
658
|
+
//
|
|
659
|
+
// DEFAULT OFF since 2026-10-09. `AWM_FEEDBACK_BM25=1` restores it.
|
|
660
|
+
//
|
|
661
|
+
// It ran on 100% of queries and cost 6.7-7.0% of every warm recall (35-41ms
|
|
662
|
+
// mean, plus a getAssociationsFor per newly discovered candidate on 71-79%
|
|
663
|
+
// of them). What it bought had never been measured. Across three suites and
|
|
664
|
+
// 950 queries it is (arrow reads ON -> OFF):
|
|
665
|
+
//
|
|
666
|
+
// identifier n=300 s@1 92.7 -> 93.0 s@5 96.7 -> 97.0 silence 90.0 -> 90.0
|
|
667
|
+
// topic n=450 s@1 92.0 -> 92.2 s@5 96.7 -> 96.9 silence 90.0 -> 90.0
|
|
668
|
+
// public n=200 s@1 56.0 -> 56.0 s@5 61.5 -> 61.5 silence 100 -> 100
|
|
669
|
+
//
|
|
670
|
+
// Three gold ranks move in 950 queries, and two of the three move in the
|
|
671
|
+
// wrong direction for the pass — which is the shape you would expect, since
|
|
672
|
+
// it can only ADD candidates and an added candidate can only displace. The
|
|
673
|
+
// public corpus is the decisive row: it is the only suite with real recall
|
|
674
|
+
// headroom (s@5 61.5%), and there the pass is INERT per query, not merely
|
|
675
|
+
// equal in aggregate — 0 of 200 queries differ in gold rank, top-1 identity
|
|
676
|
+
// or top-1 score, against an ON-vs-ON determinism check that is also 0.
|
|
677
|
+
//
|
|
678
|
+
// Scope that to THIS implementation before concluding anything about the
|
|
679
|
+
// technique: the expansion terms are the first five novel tokens in DOCUMENT
|
|
680
|
+
// ORDER (a Set populated by iterating the top-3 contents), not the top five
|
|
681
|
+
// by weight, and they come mostly from the top-1 result. A weighted term
|
|
682
|
+
// selection is a different experiment and might well pay.
|
|
612
683
|
const preSorted = scored.sort((a, b) => b.score - a.score);
|
|
613
|
-
const
|
|
684
|
+
const feedbackEnabled = process.env.AWM_FEEDBACK_BM25 === '1';
|
|
685
|
+
const topForFeedback = feedbackEnabled
|
|
686
|
+
? preSorted.slice(0, 3).filter(r => r.phaseScores.textMatch > 0.1)
|
|
687
|
+
: [];
|
|
614
688
|
if (topForFeedback.length > 0) {
|
|
615
689
|
const feedbackTerms = new Set();
|
|
616
690
|
for (const item of topForFeedback) {
|
|
@@ -623,14 +697,18 @@ export class ActivationEngine {
|
|
|
623
697
|
// Take top 5 feedback terms and re-search
|
|
624
698
|
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
625
699
|
if (extraTerms) {
|
|
700
|
+
prof.begin('feedbackBM25');
|
|
626
701
|
const feedbackBM25 = await this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
702
|
+
prof.end('feedbackBM25');
|
|
627
703
|
for (const r of feedbackBM25) {
|
|
628
704
|
if (!candidateMap.has(r.engram.id)) {
|
|
629
705
|
candidateMap.set(r.engram.id, r.engram);
|
|
630
706
|
// Score the new candidate
|
|
631
707
|
const engram = r.engram;
|
|
632
708
|
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
709
|
+
prof.begin('feedbackAssoc');
|
|
633
710
|
const associations = await this.store.getAssociationsFor(engram.id);
|
|
711
|
+
prof.end('feedbackAssoc');
|
|
634
712
|
const cTokens = tokenize(engram.concept);
|
|
635
713
|
const ctTokens = tokenize(engram.content);
|
|
636
714
|
const cJac = jaccard(queryTokens, cTokens);
|
|
@@ -840,12 +918,14 @@ export class ActivationEngine {
|
|
|
840
918
|
topN.push(item);
|
|
841
919
|
}
|
|
842
920
|
}
|
|
921
|
+
prof.begin('graph');
|
|
843
922
|
if (process.env.AWM_SPREAD === '1' && query.spread !== false) {
|
|
844
923
|
await this.spreadActivation(topN);
|
|
845
924
|
}
|
|
846
925
|
else {
|
|
847
926
|
await this.graphWalk(topN, 2, adaptive.hopPenalty, adaptive.beamWidth);
|
|
848
927
|
}
|
|
928
|
+
prof.end('graph');
|
|
849
929
|
// Phase 6: Initial filter and sort for re-ranking pool
|
|
850
930
|
// (D11 guard 2/4: injected entity-index candidates are exempt from the minScore floor.)
|
|
851
931
|
const pool = topN
|
|
@@ -869,39 +949,51 @@ export class ActivationEngine {
|
|
|
869
949
|
rerankPool.push(item);
|
|
870
950
|
}
|
|
871
951
|
}
|
|
872
|
-
//
|
|
873
|
-
// strong absolute score AND a meaningful gap to the runner-up, the cross-encoder
|
|
874
|
-
// is unlikely to change the top result. Skipping saves ~300ms of wall-clock per
|
|
875
|
-
// recall on simple queries (40% of post-0.7.9 floor was reranker).
|
|
952
|
+
// Phase 7a: every candidate in the pool is judged by the cross-encoder.
|
|
876
953
|
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
// - rerankPool size <= limit*2 (small pool — reranker has less to do)
|
|
954
|
+
// There used to be a skip here (0.7.10): when the composite already had a
|
|
955
|
+
// "clean winner" — top-1 textMatch >= 0.8 AND top-1 >= 1.5x top-2 — and the
|
|
956
|
+
// pool was small, the cross-encoder was skipped to save its ~400ms.
|
|
881
957
|
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
958
|
+
// It is gone, measured 2026-10-09 (`npm run profile:recall` at d7417c4,
|
|
959
|
+
// which carries `--skip-pool off` for exactly this):
|
|
960
|
+
//
|
|
961
|
+
// - It never fired at any store size this repository benchmarks: 0/300
|
|
962
|
+
// identifier, 0/200 public, 0/870 in the earlier profile. Its bound was
|
|
963
|
+
// max(limit*2, 20) = 20 against a pool of 24 at the shipped k=3. The
|
|
964
|
+
// bound was in step when written — 0.7.13 had cut the pool to
|
|
965
|
+
// max(limit*2, 15) — and 0.9.0 widened the pool to max(limit*4, 40) for
|
|
966
|
+
// recall without revisiting it.
|
|
967
|
+
//
|
|
968
|
+
// - Forced on, it cost success@1 92.7% -> 91.7%, success@5 96.7% -> 96.3%,
|
|
969
|
+
// MRR 94.6% -> 93.9% on the identifier suite, to save at most 13.6ms of a
|
|
970
|
+
// 528ms recall — under this machine's run-to-run drift. A per-query diff
|
|
971
|
+
// moved exactly the 10 queries it fired on: 3 of those 10 lost rank-1 and
|
|
972
|
+
// one lost the gold out of the top 5 altogether. Its premise, that the
|
|
973
|
+
// cross-encoder rarely changes the top result on a clean winner, does not
|
|
974
|
+
// hold on the queries it selected for itself.
|
|
975
|
+
//
|
|
976
|
+
// - Where it DID fire — a store with under ~20 candidates clearing
|
|
977
|
+
// minScore, which is to say a new install — it left every pool item at
|
|
978
|
+
// rerankerScore = 0, and phase 8 reads that in three places with no idea
|
|
979
|
+
// the stage was skipped: the reranker agreement channel dies; `margin` is
|
|
980
|
+
// 0 so the thin-margin branch ALWAYS fires (x0.4 on every score, which
|
|
981
|
+
// drops `floor` in computeRecallConfidence and so depresses confidence on
|
|
982
|
+
// exactly the clearest queries); and a caller passing abstentionThreshold
|
|
983
|
+
// needs 3 of 3 channels, so it abstains.
|
|
984
|
+
//
|
|
985
|
+
// That last hazard outlives the skip on the two paths where the reranker
|
|
986
|
+
// still does not run: `useReranker: false` (exposed as the MCP
|
|
987
|
+
// `use_reranker` parameter) and the catch below. Separate defect, needs its
|
|
988
|
+
// own measurement — AWM task 2026-10-09.
|
|
989
|
+
prof.note('rerankPool', rerankPool.length);
|
|
990
|
+
if (useReranker && rerankPool.length > 0) {
|
|
900
991
|
try {
|
|
901
992
|
// Passage selection for the cross-encoder. Truncation exists for a real
|
|
902
993
|
// reason: cross-encoders pad to the longest passage in the batch, so one
|
|
903
994
|
// 5,000-char memory in a 40-item pool drags everything to ~512 tokens and
|
|
904
|
-
// costs 3-4x — and the reranker is
|
|
995
|
+
// costs 3-4x — and the reranker is 83% of warm recall latency (measured,
|
|
996
|
+
// `npm run profile:recall`; this comment claimed ~90% until it was).
|
|
905
997
|
//
|
|
906
998
|
// But a PREFIX is the wrong budget to spend. On the live store, canonical
|
|
907
999
|
// memories are median 1,965 chars and 98.7% exceed 400, so the reranker
|
|
@@ -914,15 +1006,21 @@ export class ActivationEngine {
|
|
|
914
1006
|
// AWM_RERANK_WINDOW=query spends the SAME budget on the window that
|
|
915
1007
|
// actually contains the query terms. Cost is unchanged. See
|
|
916
1008
|
// src/core/rerank-window.ts.
|
|
1009
|
+
prof.begin('passages');
|
|
917
1010
|
const rrBudget = rerankTruncation();
|
|
918
1011
|
const rrMode = rerankWindowMode();
|
|
919
1012
|
const passages = rerankPool.map(r => buildRerankPassage(r.engram.concept, r.engram.content, queryContext, rrBudget, rrMode, r.engram.tags));
|
|
1013
|
+
prof.end('passages');
|
|
1014
|
+
prof.note('passageChars', passages.reduce((s, p) => s + p.length, 0));
|
|
1015
|
+
prof.note('passageMaxChars', passages.reduce((m, p) => Math.max(m, p.length), 0));
|
|
920
1016
|
let rerankTimer;
|
|
1017
|
+
prof.begin('rerank');
|
|
921
1018
|
const rerankResults = await Promise.race([
|
|
922
1019
|
rerank(queryContext, passages),
|
|
923
1020
|
new Promise((_, reject) => { rerankTimer = setTimeout(() => reject(new Error('reranker timeout')), 10000); }),
|
|
924
1021
|
]).finally(() => { if (rerankTimer)
|
|
925
1022
|
clearTimeout(rerankTimer); });
|
|
1023
|
+
prof.end('rerank');
|
|
926
1024
|
// Adaptive reranker blend (Codex recommendation):
|
|
927
1025
|
// When BM25/text signals are strong, trust them more; when weak, lean on reranker.
|
|
928
1026
|
const bm25Max = Math.max(...rerankPool.map(r => r.phaseScores.textMatch));
|
|
@@ -987,11 +1085,13 @@ export class ActivationEngine {
|
|
|
987
1085
|
// on raw cosine against the mode floor (targeted=0.50, exploratory=0.35).
|
|
988
1086
|
const semanticFloor = adaptive.zScoreGate > 0.5 ? 0.50 : 0.35;
|
|
989
1087
|
if (channelsAgreeing < requiredChannels && maxRawCosine < semanticFloor) {
|
|
1088
|
+
prof.finish({ results: 0, abstained: 'channels' });
|
|
990
1089
|
return [];
|
|
991
1090
|
}
|
|
992
1091
|
// Soft penalty: only 1 channel agrees or margin is thin
|
|
993
1092
|
if (channelsAgreeing < 2 || margin < 0.05) {
|
|
994
1093
|
if (abstentionThreshold > 0) {
|
|
1094
|
+
prof.finish({ results: 0, abstained: 'soft' });
|
|
995
1095
|
return [];
|
|
996
1096
|
}
|
|
997
1097
|
for (const item of rerankPool) {
|
|
@@ -1009,6 +1109,7 @@ export class ActivationEngine {
|
|
|
1009
1109
|
const meanScore = topRerankerScores.reduce((s, v) => s + v, 0) / topRerankerScores.length;
|
|
1010
1110
|
const variance = topRerankerScores.reduce((s, v) => s + (v - meanScore) ** 2, 0) / topRerankerScores.length;
|
|
1011
1111
|
if (maxScore < abstentionThreshold || (maxScore < 0.5 && variance < 0.01)) {
|
|
1112
|
+
prof.finish({ results: 0, abstained: 'legacy' });
|
|
1012
1113
|
return [];
|
|
1013
1114
|
}
|
|
1014
1115
|
}
|
|
@@ -1040,6 +1141,7 @@ export class ActivationEngine {
|
|
|
1040
1141
|
confidence,
|
|
1041
1142
|
threshold: requireConfidence,
|
|
1042
1143
|
});
|
|
1144
|
+
prof.finish({ results: 0, abstained: 'confidence' });
|
|
1043
1145
|
return [];
|
|
1044
1146
|
}
|
|
1045
1147
|
// Confidence-adaptive output granularity (Paper 3: cognitive teaming).
|
|
@@ -1076,7 +1178,7 @@ export class ActivationEngine {
|
|
|
1076
1178
|
// AWM_SNIPPET_WEIGHT=rarity weights a hit by 1/(occurrences of that token
|
|
1077
1179
|
// in THIS document), so one occurrence of a rare term outweighs many of a
|
|
1078
1180
|
// common one. Doc-local: no corpus statistics, no extra queries, no
|
|
1079
|
-
// latency. Default OFF preserves the shipped
|
|
1181
|
+
// latency. Default OFF preserves the shipped behavior exactly.
|
|
1080
1182
|
const rarityMode = process.env.AWM_SNIPPET_WEIGHT === 'rarity' || process.env.AWM_SNIPPET_WEIGHT === 'anchor';
|
|
1081
1183
|
const hits = [];
|
|
1082
1184
|
const hitW = [];
|
|
@@ -1186,8 +1288,11 @@ export class ActivationEngine {
|
|
|
1186
1288
|
// Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
|
|
1187
1289
|
if (!query.internal) {
|
|
1188
1290
|
for (const id of activatedIds) {
|
|
1291
|
+
prof.begin('touch');
|
|
1189
1292
|
await this.store.touchEngram(id);
|
|
1293
|
+
prof.end('touch');
|
|
1190
1294
|
}
|
|
1295
|
+
prof.begin('hebbian');
|
|
1191
1296
|
this.coActivationBuffer.pushBatch(activatedIds);
|
|
1192
1297
|
// Validation-gated Hebbian: defer strengthening until feedback arrives
|
|
1193
1298
|
const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
|
|
@@ -1201,8 +1306,10 @@ export class ActivationEngine {
|
|
|
1201
1306
|
}
|
|
1202
1307
|
}
|
|
1203
1308
|
this.validationGate.addPending(activatedIds, uniquePairs);
|
|
1309
|
+
prof.end('hebbian');
|
|
1204
1310
|
// Log activation event for eval
|
|
1205
1311
|
const latencyMs = performance.now() - startTime;
|
|
1312
|
+
prof.begin('logEvent');
|
|
1206
1313
|
const eventId = randomUUID();
|
|
1207
1314
|
await this.store.logActivationEvent({
|
|
1208
1315
|
id: eventId,
|
|
@@ -1214,6 +1321,7 @@ export class ActivationEngine {
|
|
|
1214
1321
|
latencyMs,
|
|
1215
1322
|
engramIds: activatedIds,
|
|
1216
1323
|
});
|
|
1324
|
+
prof.end('logEvent');
|
|
1217
1325
|
// 0.14.3: expose the event id so callers can hand it back on
|
|
1218
1326
|
// memory_feedback. Before this, the id was generated here and dropped —
|
|
1219
1327
|
// every one of the 872 retrieval_feedback rows in the live store has
|
|
@@ -1224,6 +1332,7 @@ export class ActivationEngine {
|
|
|
1224
1332
|
r.activationEventId = eventId;
|
|
1225
1333
|
this.lastActivationEventId = eventId;
|
|
1226
1334
|
}
|
|
1335
|
+
prof.finish({ results: results.length });
|
|
1227
1336
|
return results;
|
|
1228
1337
|
}
|
|
1229
1338
|
/** Id of the most recent non-internal activation this engine logged (0.14.3). */
|