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
package/src/engine/activation.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -35,6 +52,7 @@ import { strengthenAssociation, CoActivationBuffer, ValidationGatedBuffer } from
|
|
|
35
52
|
import { embed, cosineSimilarity } from '../core/embeddings.js';
|
|
36
53
|
import { rerank } from '../core/reranker.js';
|
|
37
54
|
import { expandQuery } from '../core/query-expander.js';
|
|
55
|
+
import { startRecallProfile } from '../core/recall-telemetry.js';
|
|
38
56
|
import { computeRecallConfidence } from './confidence.js';
|
|
39
57
|
import type {
|
|
40
58
|
Engram, ActivationResult, ActivationQuery, Association, PhaseScores, QueryMode,
|
|
@@ -191,6 +209,7 @@ export class ActivationEngine {
|
|
|
191
209
|
*/
|
|
192
210
|
async activate(query: ActivationQuery): Promise<ActivationResult[]> {
|
|
193
211
|
const startTime = performance.now();
|
|
212
|
+
const prof = startRecallProfile();
|
|
194
213
|
const limit = query.limit ?? 10;
|
|
195
214
|
const minScore = query.minScore ?? 0.01; // Default: filter out zero-relevance results
|
|
196
215
|
const useReranker = query.useReranker ?? true;
|
|
@@ -205,9 +224,11 @@ export class ActivationEngine {
|
|
|
205
224
|
const adaptive = resolveAdaptiveParams(query);
|
|
206
225
|
|
|
207
226
|
// Resolve workspace scope: if workspace is set, search across all agents in that workspace
|
|
227
|
+
prof.begin('agents');
|
|
208
228
|
const agentIds = query.workspace
|
|
209
229
|
? await this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
|
|
210
230
|
: [query.agentId];
|
|
231
|
+
prof.end('agents');
|
|
211
232
|
const isWorkspaceScoped = agentIds.length > 1;
|
|
212
233
|
|
|
213
234
|
// ── Phase -2: temporal expression ──
|
|
@@ -230,11 +251,14 @@ export class ActivationEngine {
|
|
|
230
251
|
}
|
|
231
252
|
const pronounPattern = /\b(she|he|they|her|his|him|their|it|that|this|there)\b/i;
|
|
232
253
|
if (pronounPattern.test(queryContext)) {
|
|
254
|
+
prof.begin('pronounEntities');
|
|
233
255
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
// Bounded in SQL. This used to load EVERY active engram to pick five
|
|
257
|
+
// tag words — 280ms median on 2.1% of real prompts. See
|
|
258
|
+
// EngramStore.getTopAccessedTags and docs/recall-latency.md.
|
|
259
|
+
const recentEntities = (await this.store.getTopAccessedTags(agentIds, 10, 'active'))
|
|
260
|
+
.flat()
|
|
261
|
+
.filter(t => t.length >= 3 && !/^(session-|low-|D\d)/.test(t))
|
|
238
262
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
239
263
|
.slice(0, 5);
|
|
240
264
|
if (recentEntities.length > 0) {
|
|
@@ -243,6 +267,7 @@ export class ActivationEngine {
|
|
|
243
267
|
} catch { /* non-fatal */ }
|
|
244
268
|
}
|
|
245
269
|
|
|
270
|
+
prof.end('pronounEntities');
|
|
246
271
|
// Phase 0: Query expansion — add related terms to improve BM25 recall
|
|
247
272
|
let searchContext = queryContext;
|
|
248
273
|
|
|
@@ -255,6 +280,7 @@ export class ActivationEngine {
|
|
|
255
280
|
// RELEVANCE, so a hub alias cannot drag an irrelevant memory to the top.
|
|
256
281
|
const aliasAdded = aliasTermsFor(queryContext);
|
|
257
282
|
if (aliasAdded.length > 0) searchContext = `${searchContext} ${aliasAdded.join(' ')}`;
|
|
283
|
+
prof.begin('expansion');
|
|
258
284
|
if (useExpansion) {
|
|
259
285
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
260
286
|
try {
|
|
@@ -269,22 +295,27 @@ export class ActivationEngine {
|
|
|
269
295
|
}
|
|
270
296
|
}
|
|
271
297
|
|
|
298
|
+
prof.end('expansion');
|
|
272
299
|
// Phase 1: Embed query for vector similarity (uses coref-expanded context)
|
|
273
300
|
let queryEmbedding: number[] | null = null;
|
|
301
|
+
prof.begin('embed');
|
|
274
302
|
try {
|
|
275
303
|
queryEmbedding = await embed(queryContext);
|
|
276
304
|
} catch {
|
|
277
305
|
// Embedding unavailable — fall back to text-only matching
|
|
278
306
|
}
|
|
307
|
+
prof.end('embed');
|
|
279
308
|
|
|
280
309
|
// Phase 2: Parallel retrieval — dual BM25 + all active engrams
|
|
281
310
|
// Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
|
|
282
311
|
// Uses queryContext, not query.context: temporal words must not reach BM25.
|
|
312
|
+
prof.begin('bm25');
|
|
283
313
|
const keywordQuery = Array.from(tokenize(queryContext)).join(' ');
|
|
284
314
|
const bm25Keyword = keywordQuery.length > 2
|
|
285
315
|
? await this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
286
316
|
: [];
|
|
287
317
|
const bm25Expanded = await this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
318
|
+
prof.end('bm25');
|
|
288
319
|
|
|
289
320
|
// Merge: take the best BM25 score per engram from either pass
|
|
290
321
|
const bm25ScoreMap = new Map<string, number>();
|
|
@@ -344,7 +375,9 @@ export class ActivationEngine {
|
|
|
344
375
|
if (queryEmbedding) {
|
|
345
376
|
for (const aid of agentIds) {
|
|
346
377
|
try {
|
|
378
|
+
prof.begin('vector');
|
|
347
379
|
const hits = await this.store.searchByVector(aid, queryEmbedding, VECTOR_TOP_K);
|
|
380
|
+
prof.end('vector');
|
|
348
381
|
for (const h of hits) {
|
|
349
382
|
// pgvector cosine distance: 0 = identical, 2 = opposite.
|
|
350
383
|
// For unit-norm BGE vectors, distance ≈ 1 - cosineSimilarity.
|
|
@@ -415,7 +448,9 @@ export class ActivationEngine {
|
|
|
415
448
|
}
|
|
416
449
|
if (ents.size > 0) {
|
|
417
450
|
try {
|
|
451
|
+
prof.begin('entityBridge');
|
|
418
452
|
const entHits = await this.store.searchBM25WithRankMultiAgent(agentIds, Array.from(ents).slice(0, 8).join(' '), ENT_CAP);
|
|
453
|
+
prof.end('entityBridge');
|
|
419
454
|
let added = 0;
|
|
420
455
|
for (const h of entHits) {
|
|
421
456
|
if (added >= ENT_CAP) break;
|
|
@@ -433,11 +468,13 @@ export class ActivationEngine {
|
|
|
433
468
|
}
|
|
434
469
|
|
|
435
470
|
// ── D11 (2026-07-30): ENTITY-INDEX CANDIDATE INJECTION (default-OFF, AWM_ENTITY_INDEX_FETCH=1) ──
|
|
436
|
-
// The D9 inverted index resolves query-NAMED entities ("
|
|
471
|
+
// The D9 inverted index resolves query-NAMED entities ("Avery", "ticket 10002") to every
|
|
437
472
|
// engram indexed under them — deterministic exact lookup, immune to embedding/BM25 vocabulary
|
|
438
473
|
// mismatch. Injected candidates get NO score boost; instead they are GUARANTEED a rerank
|
|
439
|
-
// audition (exempt from the topN cut, the minScore pool filter
|
|
440
|
-
//
|
|
474
|
+
// audition (exempt from the topN cut, the minScore pool filter and the rerank-pool slice)
|
|
475
|
+
// and the cross-encoder alone decides whether they surface. The fourth exemption, from the
|
|
476
|
+
// clear-winner rerank skip, went away with that branch on 2026-10-09 — the reranker is now
|
|
477
|
+
// unconditional, so an injected candidate is always auditioned.
|
|
441
478
|
// This is the guarded successor to AWM_ENTITY_FETCH above, whose failure mode was measured:
|
|
442
479
|
// injected gold entered the pool but was cut before the reranker ever scored it. Bounded
|
|
443
480
|
// (AWM_ENTITY_INDEX_CAP, default 12) so the audition costs at most one extra rerank batch.
|
|
@@ -457,7 +494,9 @@ export class ActivationEngine {
|
|
|
457
494
|
let added = 0;
|
|
458
495
|
for (const term of Array.from(terms).slice(0, 4)) {
|
|
459
496
|
if (added >= IDX_CAP) break;
|
|
497
|
+
prof.begin('entitySearch');
|
|
460
498
|
const entities = await this.store.searchEntities(term, 6);
|
|
499
|
+
prof.end('entitySearch');
|
|
461
500
|
for (const entity of entities) {
|
|
462
501
|
if (added >= IDX_CAP) break;
|
|
463
502
|
for (const agentId of agentIds) {
|
|
@@ -474,7 +513,9 @@ export class ActivationEngine {
|
|
|
474
513
|
if (inPool.stage === 'active' && !(inPool as any).retracted && !inPool.supersededBy) { injectedIds.add(id); added++; }
|
|
475
514
|
continue;
|
|
476
515
|
}
|
|
516
|
+
prof.begin('entityFetch');
|
|
477
517
|
const n = await this.store.getEngram(id);
|
|
518
|
+
prof.end('entityFetch');
|
|
478
519
|
if (!n || n.stage !== 'active' || (n as any).retracted || n.supersededBy) continue;
|
|
479
520
|
if (query.memoryType && n.memoryType !== query.memoryType) continue;
|
|
480
521
|
candidateMap.set(id, n);
|
|
@@ -501,10 +542,14 @@ export class ActivationEngine {
|
|
|
501
542
|
// Graph walk still needs full Association objects, but it operates on the
|
|
502
543
|
// top-N (~30 candidates) — its on-demand `getAssociationsFor` lookups are
|
|
503
544
|
// cheap (<5ms total).
|
|
545
|
+
prof.note('candidates', candidates.length);
|
|
546
|
+
prof.begin('assocStats');
|
|
504
547
|
const assocStats = await this.store.getAssociationStatsForBatch(candidates.map(e => e.id));
|
|
548
|
+
prof.end('assocStats');
|
|
505
549
|
// 0.14.4: decay clock is overridable (query.now) so evals against a frozen
|
|
506
550
|
// snapshot are reproducible across days. See ActivationQuery.now.
|
|
507
551
|
const nowMs = query.now ?? Date.now();
|
|
552
|
+
prof.begin('scoring');
|
|
508
553
|
const scored = candidates.map(engram => {
|
|
509
554
|
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
510
555
|
const stats = assocStats.get(engram.id) ?? { count: 0, sumWeight: 0 };
|
|
@@ -619,11 +664,40 @@ export class ActivationEngine {
|
|
|
619
664
|
// associations: empty in 0.7.12+ — graph walk lazy-fetches per engram on demand
|
|
620
665
|
return { engram, score: composite, phaseScores, associations: [] as Association[] };
|
|
621
666
|
});
|
|
667
|
+
prof.end('scoring');
|
|
622
668
|
|
|
623
669
|
// Phase 3.5: Rocchio pseudo-relevance feedback — expand query with top result terms
|
|
624
|
-
// then re-search BM25 to find candidates that keyword search missed
|
|
670
|
+
// then re-search BM25 to find candidates that keyword search missed.
|
|
671
|
+
//
|
|
672
|
+
// DEFAULT OFF since 2026-10-09. `AWM_FEEDBACK_BM25=1` restores it.
|
|
673
|
+
//
|
|
674
|
+
// It ran on 100% of queries and cost 6.7-7.0% of every warm recall (35-41ms
|
|
675
|
+
// mean, plus a getAssociationsFor per newly discovered candidate on 71-79%
|
|
676
|
+
// of them). What it bought had never been measured. Across three suites and
|
|
677
|
+
// 950 queries it is (arrow reads ON -> OFF):
|
|
678
|
+
//
|
|
679
|
+
// identifier n=300 s@1 92.7 -> 93.0 s@5 96.7 -> 97.0 silence 90.0 -> 90.0
|
|
680
|
+
// topic n=450 s@1 92.0 -> 92.2 s@5 96.7 -> 96.9 silence 90.0 -> 90.0
|
|
681
|
+
// public n=200 s@1 56.0 -> 56.0 s@5 61.5 -> 61.5 silence 100 -> 100
|
|
682
|
+
//
|
|
683
|
+
// Three gold ranks move in 950 queries, and two of the three move in the
|
|
684
|
+
// wrong direction for the pass — which is the shape you would expect, since
|
|
685
|
+
// it can only ADD candidates and an added candidate can only displace. The
|
|
686
|
+
// public corpus is the decisive row: it is the only suite with real recall
|
|
687
|
+
// headroom (s@5 61.5%), and there the pass is INERT per query, not merely
|
|
688
|
+
// equal in aggregate — 0 of 200 queries differ in gold rank, top-1 identity
|
|
689
|
+
// or top-1 score, against an ON-vs-ON determinism check that is also 0.
|
|
690
|
+
//
|
|
691
|
+
// Scope that to THIS implementation before concluding anything about the
|
|
692
|
+
// technique: the expansion terms are the first five novel tokens in DOCUMENT
|
|
693
|
+
// ORDER (a Set populated by iterating the top-3 contents), not the top five
|
|
694
|
+
// by weight, and they come mostly from the top-1 result. A weighted term
|
|
695
|
+
// selection is a different experiment and might well pay.
|
|
625
696
|
const preSorted = scored.sort((a, b) => b.score - a.score);
|
|
626
|
-
const
|
|
697
|
+
const feedbackEnabled = process.env.AWM_FEEDBACK_BM25 === '1';
|
|
698
|
+
const topForFeedback = feedbackEnabled
|
|
699
|
+
? preSorted.slice(0, 3).filter(r => r.phaseScores.textMatch > 0.1)
|
|
700
|
+
: [];
|
|
627
701
|
if (topForFeedback.length > 0) {
|
|
628
702
|
const feedbackTerms = new Set<string>();
|
|
629
703
|
for (const item of topForFeedback) {
|
|
@@ -635,14 +709,18 @@ export class ActivationEngine {
|
|
|
635
709
|
// Take top 5 feedback terms and re-search
|
|
636
710
|
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
637
711
|
if (extraTerms) {
|
|
712
|
+
prof.begin('feedbackBM25');
|
|
638
713
|
const feedbackBM25 = await this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
714
|
+
prof.end('feedbackBM25');
|
|
639
715
|
for (const r of feedbackBM25) {
|
|
640
716
|
if (!candidateMap.has(r.engram.id)) {
|
|
641
717
|
candidateMap.set(r.engram.id, r.engram);
|
|
642
718
|
// Score the new candidate
|
|
643
719
|
const engram = r.engram;
|
|
644
720
|
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
721
|
+
prof.begin('feedbackAssoc');
|
|
645
722
|
const associations = await this.store.getAssociationsFor(engram.id);
|
|
723
|
+
prof.end('feedbackAssoc');
|
|
646
724
|
const cTokens = tokenize(engram.concept);
|
|
647
725
|
const ctTokens = tokenize(engram.content);
|
|
648
726
|
const cJac = jaccard(queryTokens, cTokens);
|
|
@@ -851,11 +929,13 @@ export class ActivationEngine {
|
|
|
851
929
|
if (injectedIds.has(item.engram.id)) topN.push(item);
|
|
852
930
|
}
|
|
853
931
|
}
|
|
932
|
+
prof.begin('graph');
|
|
854
933
|
if (process.env.AWM_SPREAD === '1' && query.spread !== false) {
|
|
855
934
|
await this.spreadActivation(topN);
|
|
856
935
|
} else {
|
|
857
936
|
await this.graphWalk(topN, 2, adaptive.hopPenalty, adaptive.beamWidth);
|
|
858
937
|
}
|
|
938
|
+
prof.end('graph');
|
|
859
939
|
|
|
860
940
|
// Phase 6: Initial filter and sort for re-ranking pool
|
|
861
941
|
// (D11 guard 2/4: injected entity-index candidates are exempt from the minScore floor.)
|
|
@@ -881,40 +961,51 @@ export class ActivationEngine {
|
|
|
881
961
|
}
|
|
882
962
|
}
|
|
883
963
|
|
|
884
|
-
//
|
|
885
|
-
// strong absolute score AND a meaningful gap to the runner-up, the cross-encoder
|
|
886
|
-
// is unlikely to change the top result. Skipping saves ~300ms of wall-clock per
|
|
887
|
-
// recall on simple queries (40% of post-0.7.9 floor was reranker).
|
|
964
|
+
// Phase 7a: every candidate in the pool is judged by the cross-encoder.
|
|
888
965
|
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
// - rerankPool size <= limit*2 (small pool — reranker has less to do)
|
|
966
|
+
// There used to be a skip here (0.7.10): when the composite already had a
|
|
967
|
+
// "clean winner" — top-1 textMatch >= 0.8 AND top-1 >= 1.5x top-2 — and the
|
|
968
|
+
// pool was small, the cross-encoder was skipped to save its ~400ms.
|
|
893
969
|
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
970
|
+
// It is gone, measured 2026-10-09 (`npm run profile:recall` at d7417c4,
|
|
971
|
+
// which carries `--skip-pool off` for exactly this):
|
|
972
|
+
//
|
|
973
|
+
// - It never fired at any store size this repository benchmarks: 0/300
|
|
974
|
+
// identifier, 0/200 public, 0/870 in the earlier profile. Its bound was
|
|
975
|
+
// max(limit*2, 20) = 20 against a pool of 24 at the shipped k=3. The
|
|
976
|
+
// bound was in step when written — 0.7.13 had cut the pool to
|
|
977
|
+
// max(limit*2, 15) — and 0.9.0 widened the pool to max(limit*4, 40) for
|
|
978
|
+
// recall without revisiting it.
|
|
979
|
+
//
|
|
980
|
+
// - Forced on, it cost success@1 92.7% -> 91.7%, success@5 96.7% -> 96.3%,
|
|
981
|
+
// MRR 94.6% -> 93.9% on the identifier suite, to save at most 13.6ms of a
|
|
982
|
+
// 528ms recall — under this machine's run-to-run drift. A per-query diff
|
|
983
|
+
// moved exactly the 10 queries it fired on: 3 of those 10 lost rank-1 and
|
|
984
|
+
// one lost the gold out of the top 5 altogether. Its premise, that the
|
|
985
|
+
// cross-encoder rarely changes the top result on a clean winner, does not
|
|
986
|
+
// hold on the queries it selected for itself.
|
|
987
|
+
//
|
|
988
|
+
// - Where it DID fire — a store with under ~20 candidates clearing
|
|
989
|
+
// minScore, which is to say a new install — it left every pool item at
|
|
990
|
+
// rerankerScore = 0, and phase 8 reads that in three places with no idea
|
|
991
|
+
// the stage was skipped: the reranker agreement channel dies; `margin` is
|
|
992
|
+
// 0 so the thin-margin branch ALWAYS fires (x0.4 on every score, which
|
|
993
|
+
// drops `floor` in computeRecallConfidence and so depresses confidence on
|
|
994
|
+
// exactly the clearest queries); and a caller passing abstentionThreshold
|
|
995
|
+
// needs 3 of 3 channels, so it abstains.
|
|
996
|
+
//
|
|
997
|
+
// That last hazard outlives the skip on the two paths where the reranker
|
|
998
|
+
// still does not run: `useReranker: false` (exposed as the MCP
|
|
999
|
+
// `use_reranker` parameter) and the catch below. Separate defect, needs its
|
|
1000
|
+
// own measurement — AWM task 2026-10-09.
|
|
1001
|
+
prof.note('rerankPool', rerankPool.length);
|
|
1002
|
+
if (useReranker && rerankPool.length > 0) {
|
|
913
1003
|
try {
|
|
914
1004
|
// Passage selection for the cross-encoder. Truncation exists for a real
|
|
915
1005
|
// reason: cross-encoders pad to the longest passage in the batch, so one
|
|
916
1006
|
// 5,000-char memory in a 40-item pool drags everything to ~512 tokens and
|
|
917
|
-
// costs 3-4x — and the reranker is
|
|
1007
|
+
// costs 3-4x — and the reranker is 83% of warm recall latency (measured,
|
|
1008
|
+
// `npm run profile:recall`; this comment claimed ~90% until it was).
|
|
918
1009
|
//
|
|
919
1010
|
// But a PREFIX is the wrong budget to spend. On the live store, canonical
|
|
920
1011
|
// memories are median 1,965 chars and 98.7% exceed 400, so the reranker
|
|
@@ -927,15 +1018,21 @@ export class ActivationEngine {
|
|
|
927
1018
|
// AWM_RERANK_WINDOW=query spends the SAME budget on the window that
|
|
928
1019
|
// actually contains the query terms. Cost is unchanged. See
|
|
929
1020
|
// src/core/rerank-window.ts.
|
|
1021
|
+
prof.begin('passages');
|
|
930
1022
|
const rrBudget = rerankTruncation();
|
|
931
1023
|
const rrMode = rerankWindowMode();
|
|
932
1024
|
const passages = rerankPool.map(r =>
|
|
933
1025
|
buildRerankPassage(r.engram.concept, r.engram.content, queryContext, rrBudget, rrMode, r.engram.tags));
|
|
1026
|
+
prof.end('passages');
|
|
1027
|
+
prof.note('passageChars', passages.reduce((s, p) => s + p.length, 0));
|
|
1028
|
+
prof.note('passageMaxChars', passages.reduce((m, p) => Math.max(m, p.length), 0));
|
|
934
1029
|
let rerankTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1030
|
+
prof.begin('rerank');
|
|
935
1031
|
const rerankResults = await Promise.race([
|
|
936
1032
|
rerank(queryContext, passages),
|
|
937
1033
|
new Promise<never>((_, reject) => { rerankTimer = setTimeout(() => reject(new Error('reranker timeout')), 10000); }),
|
|
938
1034
|
]).finally(() => { if (rerankTimer) clearTimeout(rerankTimer); });
|
|
1035
|
+
prof.end('rerank');
|
|
939
1036
|
|
|
940
1037
|
// Adaptive reranker blend (Codex recommendation):
|
|
941
1038
|
// When BM25/text signals are strong, trust them more; when weak, lean on reranker.
|
|
@@ -1008,12 +1105,14 @@ export class ActivationEngine {
|
|
|
1008
1105
|
// on raw cosine against the mode floor (targeted=0.50, exploratory=0.35).
|
|
1009
1106
|
const semanticFloor = adaptive.zScoreGate > 0.5 ? 0.50 : 0.35;
|
|
1010
1107
|
if (channelsAgreeing < requiredChannels && maxRawCosine < semanticFloor) {
|
|
1108
|
+
prof.finish({ results: 0, abstained: 'channels' });
|
|
1011
1109
|
return [];
|
|
1012
1110
|
}
|
|
1013
1111
|
|
|
1014
1112
|
// Soft penalty: only 1 channel agrees or margin is thin
|
|
1015
1113
|
if (channelsAgreeing < 2 || margin < 0.05) {
|
|
1016
1114
|
if (abstentionThreshold > 0) {
|
|
1115
|
+
prof.finish({ results: 0, abstained: 'soft' });
|
|
1017
1116
|
return [];
|
|
1018
1117
|
}
|
|
1019
1118
|
for (const item of rerankPool) {
|
|
@@ -1033,6 +1132,7 @@ export class ActivationEngine {
|
|
|
1033
1132
|
const variance = topRerankerScores.reduce((s, v) => s + (v - meanScore) ** 2, 0) / topRerankerScores.length;
|
|
1034
1133
|
|
|
1035
1134
|
if (maxScore < abstentionThreshold || (maxScore < 0.5 && variance < 0.01)) {
|
|
1135
|
+
prof.finish({ results: 0, abstained: 'legacy' });
|
|
1036
1136
|
return [];
|
|
1037
1137
|
}
|
|
1038
1138
|
}
|
|
@@ -1067,6 +1167,7 @@ export class ActivationEngine {
|
|
|
1067
1167
|
confidence,
|
|
1068
1168
|
threshold: requireConfidence,
|
|
1069
1169
|
});
|
|
1170
|
+
prof.finish({ results: 0, abstained: 'confidence' });
|
|
1070
1171
|
return [];
|
|
1071
1172
|
}
|
|
1072
1173
|
|
|
@@ -1105,7 +1206,7 @@ export class ActivationEngine {
|
|
|
1105
1206
|
// AWM_SNIPPET_WEIGHT=rarity weights a hit by 1/(occurrences of that token
|
|
1106
1207
|
// in THIS document), so one occurrence of a rare term outweighs many of a
|
|
1107
1208
|
// common one. Doc-local: no corpus statistics, no extra queries, no
|
|
1108
|
-
// latency. Default OFF preserves the shipped
|
|
1209
|
+
// latency. Default OFF preserves the shipped behavior exactly.
|
|
1109
1210
|
const rarityMode = process.env.AWM_SNIPPET_WEIGHT === 'rarity' || process.env.AWM_SNIPPET_WEIGHT === 'anchor';
|
|
1110
1211
|
const hits: number[] = [];
|
|
1111
1212
|
const hitW: number[] = [];
|
|
@@ -1210,8 +1311,11 @@ export class ActivationEngine {
|
|
|
1210
1311
|
// Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
|
|
1211
1312
|
if (!query.internal) {
|
|
1212
1313
|
for (const id of activatedIds) {
|
|
1314
|
+
prof.begin('touch');
|
|
1213
1315
|
await this.store.touchEngram(id);
|
|
1316
|
+
prof.end('touch');
|
|
1214
1317
|
}
|
|
1318
|
+
prof.begin('hebbian');
|
|
1215
1319
|
this.coActivationBuffer.pushBatch(activatedIds);
|
|
1216
1320
|
// Validation-gated Hebbian: defer strengthening until feedback arrives
|
|
1217
1321
|
const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
|
|
@@ -1222,9 +1326,11 @@ export class ActivationEngine {
|
|
|
1222
1326
|
if (!seen.has(key)) { seen.add(key); uniquePairs.push([a, b]); }
|
|
1223
1327
|
}
|
|
1224
1328
|
this.validationGate.addPending(activatedIds, uniquePairs);
|
|
1329
|
+
prof.end('hebbian');
|
|
1225
1330
|
|
|
1226
1331
|
// Log activation event for eval
|
|
1227
1332
|
const latencyMs = performance.now() - startTime;
|
|
1333
|
+
prof.begin('logEvent');
|
|
1228
1334
|
const eventId = randomUUID();
|
|
1229
1335
|
await this.store.logActivationEvent({
|
|
1230
1336
|
id: eventId,
|
|
@@ -1236,6 +1342,7 @@ export class ActivationEngine {
|
|
|
1236
1342
|
latencyMs,
|
|
1237
1343
|
engramIds: activatedIds,
|
|
1238
1344
|
});
|
|
1345
|
+
prof.end('logEvent');
|
|
1239
1346
|
// 0.14.3: expose the event id so callers can hand it back on
|
|
1240
1347
|
// memory_feedback. Before this, the id was generated here and dropped —
|
|
1241
1348
|
// every one of the 872 retrieval_feedback rows in the live store has
|
|
@@ -1246,6 +1353,7 @@ export class ActivationEngine {
|
|
|
1246
1353
|
this.lastActivationEventId = eventId;
|
|
1247
1354
|
}
|
|
1248
1355
|
|
|
1356
|
+
prof.finish({ results: results.length });
|
|
1249
1357
|
return results;
|
|
1250
1358
|
}
|
|
1251
1359
|
|
package/src/index.ts
CHANGED
|
@@ -204,7 +204,7 @@ async function main() {
|
|
|
204
204
|
}
|
|
205
205
|
}, 10 * 60_000) : null;
|
|
206
206
|
|
|
207
|
-
// Pre-load ML models (
|
|
207
|
+
// Pre-load ML models (first run downloads ~600MB: embeddings 134MB, reranker 92MB fp32, expander 377MB)
|
|
208
208
|
getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
|
|
209
209
|
getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
|
|
210
210
|
getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
|