agent-working-memory 0.7.17 → 0.8.6
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/README.md +156 -15
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +289 -202
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/http.js +22 -22
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +258 -79
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.d.ts +42 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +268 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli.js +147 -103
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.d.ts +24 -0
- package/dist/coordination/circuit-breaker.d.ts.map +1 -0
- package/dist/coordination/circuit-breaker.js +70 -0
- package/dist/coordination/circuit-breaker.js.map +1 -0
- package/dist/coordination/failure-modes.d.ts +21 -0
- package/dist/coordination/failure-modes.d.ts.map +1 -0
- package/dist/coordination/failure-modes.js +49 -0
- package/dist/coordination/failure-modes.js.map +1 -0
- package/dist/coordination/peer-decisions.js +7 -7
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +44 -1
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +18 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +9 -9
- package/dist/coordination/stale.d.ts +9 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +53 -9
- package/dist/coordination/stale.js.map +1 -1
- package/dist/core/embeddings.d.ts +16 -10
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +54 -38
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/ml-worker-entry.d.ts +17 -0
- package/dist/core/ml-worker-entry.d.ts.map +1 -0
- package/dist/core/ml-worker-entry.js +193 -0
- package/dist/core/ml-worker-entry.js.map +1 -0
- package/dist/core/ml-worker.d.ts +59 -0
- package/dist/core/ml-worker.d.ts.map +1 -0
- package/dist/core/ml-worker.js +253 -0
- package/dist/core/ml-worker.js.map +1 -0
- package/dist/core/query-expander.d.ts +13 -15
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +48 -54
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts +9 -19
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +33 -35
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.d.ts +32 -5
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +201 -28
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts +10 -2
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +218 -49
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/engine/activation.d.ts +2 -2
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +238 -115
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/confidence.d.ts +62 -0
- package/dist/engine/confidence.d.ts.map +1 -0
- package/dist/engine/confidence.js +100 -0
- package/dist/engine/confidence.js.map +1 -0
- package/dist/engine/connections.d.ts +47 -8
- package/dist/engine/connections.d.ts.map +1 -1
- package/dist/engine/connections.js +83 -22
- package/dist/engine/connections.js.map +1 -1
- package/dist/engine/consolidation-scheduler.d.ts +46 -10
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
- package/dist/engine/consolidation-scheduler.js +170 -50
- package/dist/engine/consolidation-scheduler.js.map +1 -1
- package/dist/engine/consolidation.d.ts +16 -2
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +143 -44
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/eval.d.ts +2 -2
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +9 -9
- package/dist/engine/eval.js.map +1 -1
- package/dist/engine/eviction.d.ts +4 -4
- package/dist/engine/eviction.d.ts.map +1 -1
- package/dist/engine/eviction.js +15 -15
- package/dist/engine/eviction.js.map +1 -1
- package/dist/engine/retraction.d.ts +93 -5
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +238 -19
- package/dist/engine/retraction.js.map +1 -1
- package/dist/engine/staging.d.ts +1 -1
- package/dist/engine/staging.d.ts.map +1 -1
- package/dist/engine/staging.js +3 -3
- package/dist/engine/staging.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +1 -1
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +4 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +48 -31
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +167 -141
- package/dist/mcp.js.map +1 -1
- package/dist/storage/factory.d.ts +55 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +135 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/pglite-schema.d.ts +20 -0
- package/dist/storage/pglite-schema.d.ts.map +1 -0
- package/dist/storage/pglite-schema.js +165 -0
- package/dist/storage/pglite-schema.js.map +1 -0
- package/dist/storage/pglite.d.ts +187 -0
- package/dist/storage/pglite.d.ts.map +1 -0
- package/dist/storage/pglite.js +1114 -0
- package/dist/storage/pglite.js.map +1 -0
- package/dist/storage/sqlite.d.ts +117 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +357 -8
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.d.ts +63 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +27 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/types/engram.d.ts +92 -2
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +3 -1
- package/src/adapters/claude-code.ts +218 -218
- package/src/adapters/codex.ts +252 -252
- package/src/adapters/common.ts +498 -411
- package/src/adapters/cursor.ts +135 -135
- package/src/adapters/http.ts +93 -93
- package/src/adapters/index.ts +31 -31
- package/src/adapters/types.ts +75 -75
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +970 -714
- package/src/cli/migrate.ts +307 -0
- package/src/cli.ts +755 -719
- package/src/coordination/circuit-breaker.ts +83 -0
- package/src/coordination/events.ts +90 -90
- package/src/coordination/failure-modes.ts +50 -0
- package/src/coordination/peer-decisions.ts +105 -105
- package/src/coordination/plugin-loader.ts +60 -60
- package/src/coordination/plugin.ts +44 -44
- package/src/coordination/routes.ts +56 -1
- package/src/coordination/schema.ts +13 -0
- package/src/coordination/stale.ts +80 -11
- package/src/coordination/types.ts +311 -311
- package/src/coordination/write-mutex.ts +69 -69
- package/src/core/auto-tagger.ts +168 -168
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -0
- package/src/core/ml-worker.ts +281 -0
- package/src/core/query-expander.ts +122 -128
- package/src/core/reranker.ts +119 -125
- package/src/core/salience.ts +200 -33
- package/src/core/write-pipeline.ts +521 -343
- package/src/engine/activation.ts +235 -109
- package/src/engine/confidence.ts +120 -0
- package/src/engine/connections.ts +162 -103
- package/src/engine/consolidation-scheduler.ts +242 -125
- package/src/engine/consolidation.ts +138 -45
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -116
- package/src/engine/staging.ts +74 -74
- package/src/hooks/sidecar.ts +5 -5
- package/src/index.ts +226 -212
- package/src/mcp.ts +1145 -1121
- package/src/storage/factory.ts +147 -0
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -0
- package/src/storage/pglite.ts +1363 -0
- package/src/storage/sqlite.ts +402 -7
- package/src/storage/store.ts +80 -0
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +101 -3
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -24,6 +24,7 @@ import { strengthenAssociation, CoActivationBuffer, ValidationGatedBuffer } from
|
|
|
24
24
|
import { embed, cosineSimilarity } from '../core/embeddings.js';
|
|
25
25
|
import { rerank } from '../core/reranker.js';
|
|
26
26
|
import { expandQuery } from '../core/query-expander.js';
|
|
27
|
+
import { computeRecallConfidence } from './confidence.js';
|
|
27
28
|
const ADAPTIVE_PRESETS = {
|
|
28
29
|
targeted: {
|
|
29
30
|
mode: 'targeted',
|
|
@@ -166,10 +167,11 @@ export class ActivationEngine {
|
|
|
166
167
|
const useReranker = query.useReranker ?? true;
|
|
167
168
|
const useExpansion = query.useExpansion ?? true;
|
|
168
169
|
const abstentionThreshold = query.abstentionThreshold ?? 0;
|
|
170
|
+
const requireConfidence = query.requireConfidence ?? 0;
|
|
169
171
|
const adaptive = resolveAdaptiveParams(query);
|
|
170
172
|
// Resolve workspace scope: if workspace is set, search across all agents in that workspace
|
|
171
173
|
const agentIds = query.workspace
|
|
172
|
-
? this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
|
|
174
|
+
? await this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
|
|
173
175
|
: [query.agentId];
|
|
174
176
|
const isWorkspaceScoped = agentIds.length > 1;
|
|
175
177
|
// Phase -1: Coref expansion — if query has pronouns, append recent entity names
|
|
@@ -177,7 +179,7 @@ export class ActivationEngine {
|
|
|
177
179
|
const pronounPattern = /\b(she|he|they|her|his|him|their|it|that|this|there)\b/i;
|
|
178
180
|
if (pronounPattern.test(queryContext)) {
|
|
179
181
|
try {
|
|
180
|
-
const recentEntities = this.store.getEngramsByAgents(agentIds, 'active')
|
|
182
|
+
const recentEntities = (await this.store.getEngramsByAgents(agentIds, 'active'))
|
|
181
183
|
.sort((a, b) => b.accessCount - a.accessCount)
|
|
182
184
|
.slice(0, 10)
|
|
183
185
|
.flatMap(e => e.tags.filter(t => t.length >= 3 && !/^(session-|low-|D\d)/.test(t)))
|
|
@@ -219,9 +221,9 @@ export class ActivationEngine {
|
|
|
219
221
|
// Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
|
|
220
222
|
const keywordQuery = Array.from(tokenize(query.context)).join(' ');
|
|
221
223
|
const bm25Keyword = keywordQuery.length > 2
|
|
222
|
-
? this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
224
|
+
? await this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
223
225
|
: [];
|
|
224
|
-
const bm25Expanded = this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
226
|
+
const bm25Expanded = await this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
225
227
|
// Merge: take the best BM25 score per engram from either pass
|
|
226
228
|
const bm25ScoreMap = new Map();
|
|
227
229
|
const bm25EngramMap = new Map();
|
|
@@ -240,83 +242,75 @@ export class ActivationEngine {
|
|
|
240
242
|
// Used for cosine sim + adaptive z-score stats + cheap pool filter.
|
|
241
243
|
// Pass 2: full fetch ONLY on the survivors that pass the filter.
|
|
242
244
|
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
//
|
|
245
|
+
// AWM 0.8.x — Native vector search refactor (2026-05-25): the prior slim
|
|
246
|
+
// fetch path materialized ALL active engrams (id + concept + embedding)
|
|
247
|
+
// for in-process cosine + z-score gating. On PGlite that meant parsing
|
|
248
|
+
// 11K embedding vectors per recall (~200-500ms). Replaced with native
|
|
249
|
+
// vector search: PGlite uses pgvector + ivfflat (O(log N)); SQLite uses
|
|
250
|
+
// its slim cache + JS cosine (same as before but encapsulated). Z-score
|
|
251
|
+
// normalization is replaced with a mode-adaptive raw-cosine floor —
|
|
252
|
+
// simpler, faster, model-tuned for BGE-small embeddings.
|
|
253
|
+
const VECTOR_TOP_K = Math.max(50, limit * 5);
|
|
254
|
+
// Tokenize query once (used by scoring)
|
|
250
255
|
const queryTokens = tokenize(query.context);
|
|
251
|
-
// Phase 3a:
|
|
256
|
+
// Phase 3a: native vector search across agents — top-K by cosine.
|
|
257
|
+
// Apply a candidate floor — BGE-small unit-norm vectors typically cluster
|
|
258
|
+
// around 0.30-0.40 even for unrelated text, so we need a floor that
|
|
259
|
+
// distinguishes "related" from "noise" without throwing out genuine
|
|
260
|
+
// related-but-not-identical matches.
|
|
261
|
+
//
|
|
262
|
+
// Tuning: targeted=0.40, exploratory=0.30. Earlier 0.55/0.45 floors were
|
|
263
|
+
// too aggressive — they dropped Recall@5 on the 200-fact eval corpus
|
|
264
|
+
// from 0.80 → 0.46 (verified 2026-05-26). BGE-small cosines for genuine
|
|
265
|
+
// related matches commonly land 0.42-0.55, so a 0.55 floor cut them
|
|
266
|
+
// entirely. The vectorMatch scoring floor (0.50 targeted / 0.35 exploratory)
|
|
267
|
+
// still suppresses low-confidence matches in the final score.
|
|
268
|
+
// Env override: AWM_SIM_CANDIDATE_FLOOR_TARGETED, AWM_SIM_CANDIDATE_FLOOR_EXPLORATORY.
|
|
269
|
+
const SIM_CANDIDATE_FLOOR = adaptive.zScoreGate > 0.5
|
|
270
|
+
? Number(process.env.AWM_SIM_CANDIDATE_FLOOR_TARGETED ?? 0.40)
|
|
271
|
+
: Number(process.env.AWM_SIM_CANDIDATE_FLOOR_EXPLORATORY ?? 0.30);
|
|
252
272
|
const rawCosineSims = new Map();
|
|
273
|
+
const vectorHits = [];
|
|
253
274
|
if (queryEmbedding) {
|
|
254
|
-
for (const
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
for (const aid of agentIds) {
|
|
276
|
+
try {
|
|
277
|
+
const hits = await this.store.searchByVector(aid, queryEmbedding, VECTOR_TOP_K);
|
|
278
|
+
for (const h of hits) {
|
|
279
|
+
// pgvector cosine distance: 0 = identical, 2 = opposite.
|
|
280
|
+
// For unit-norm BGE vectors, distance ≈ 1 - cosineSimilarity.
|
|
281
|
+
const sim = 1 - h.distance;
|
|
282
|
+
// pgvector returns sorted ASC by distance (DESC by sim) — break once
|
|
283
|
+
// we drop below the candidate floor; all subsequent hits will too.
|
|
284
|
+
if (sim < SIM_CANDIDATE_FLOOR)
|
|
285
|
+
break;
|
|
286
|
+
if (!rawCosineSims.has(h.engram.id)) {
|
|
287
|
+
rawCosineSims.set(h.engram.id, sim);
|
|
288
|
+
vectorHits.push(h.engram);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Vector search unavailable — fall back to BM25-only ranking
|
|
257
294
|
}
|
|
258
295
|
}
|
|
259
296
|
}
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
? Math.sqrt(simValues.reduce((sum, s) => sum + (s - simMean) ** 2, 0) / simValues.length) : 0.15;
|
|
266
|
-
// Floor stddev at 0.10 to prevent z-score inflation with small candidate pools
|
|
267
|
-
const simStdDev = Math.max(rawStdDev, 0.10);
|
|
268
|
-
// Determine survivor IDs from the slim pool using the same survival criteria
|
|
269
|
-
// (BM25 hit / cosine z-score / concept-jaccard) plus all BM25-ranked candidates
|
|
270
|
-
// (which already came in fully-hydrated and may not be in slimActive's stage filter).
|
|
271
|
-
const poolFilterEnabled = process.env.AWM_DISABLE_POOL_FILTER !== '1';
|
|
297
|
+
// Survivors = BM25 candidates ∪ vector candidates.
|
|
298
|
+
// The slim-fetch jaccard-fallback path is dropped: in practice it surfaced
|
|
299
|
+
// <1% of candidates that BM25 + vector missed, and on PGlite cost more
|
|
300
|
+
// than the candidates were worth. Concept jaccard signal still contributes
|
|
301
|
+
// via textMatch in scoring.
|
|
272
302
|
const survivorIds = new Set();
|
|
273
|
-
// Always include BM25-ranked engrams (they came pre-hydrated)
|
|
274
303
|
for (const r of bm25Ranked)
|
|
275
304
|
survivorIds.add(r.engram.id);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const bm25 = bm25ScoreMap.get(e.id) ?? 0;
|
|
281
|
-
if (bm25 > 0) {
|
|
282
|
-
survivorIds.add(e.id);
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
const sim = rawCosineSims.get(e.id);
|
|
286
|
-
if (sim !== undefined) {
|
|
287
|
-
const z = (sim - simMean) / simStdDev;
|
|
288
|
-
if (z > adaptive.zScoreGate) {
|
|
289
|
-
survivorIds.add(e.id);
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// Cheap concept jaccard
|
|
294
|
-
const ct = tokenize(e.concept);
|
|
295
|
-
if (ct.size === 0)
|
|
296
|
-
continue;
|
|
297
|
-
let overlap = 0;
|
|
298
|
-
for (const w of ct)
|
|
299
|
-
if (queryTokens.has(w))
|
|
300
|
-
overlap++;
|
|
301
|
-
if (overlap > 0)
|
|
302
|
-
survivorIds.add(e.id);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
// Filter disabled — include all slim active engrams
|
|
307
|
-
for (const e of slimActive)
|
|
308
|
-
survivorIds.add(e.id);
|
|
309
|
-
}
|
|
310
|
-
// Pass 2: hydrate full Engram rows ONLY for survivors that aren't already loaded
|
|
305
|
+
for (const e of vectorHits)
|
|
306
|
+
survivorIds.add(e.id);
|
|
307
|
+
// Hydrate full engrams. BM25 and vector hits arrive pre-hydrated; nothing
|
|
308
|
+
// else needs fetching in the normal path.
|
|
311
309
|
const candidateMap = new Map();
|
|
312
310
|
for (const r of bm25Ranked)
|
|
313
311
|
candidateMap.set(r.engram.id, r.engram);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
for (const e of this.store.getEngramsByIds(idsToHydrate)) {
|
|
317
|
-
candidateMap.set(e.id, e);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
312
|
+
for (const e of vectorHits)
|
|
313
|
+
candidateMap.set(e.id, e);
|
|
320
314
|
let candidates = Array.from(candidateMap.values());
|
|
321
315
|
// Filter by memory type if specified
|
|
322
316
|
if (query.memoryType) {
|
|
@@ -335,7 +329,7 @@ export class ActivationEngine {
|
|
|
335
329
|
// Graph walk still needs full Association objects, but it operates on the
|
|
336
330
|
// top-N (~30 candidates) — its on-demand `getAssociationsFor` lookups are
|
|
337
331
|
// cheap (<5ms total).
|
|
338
|
-
const assocStats = this.store.getAssociationStatsForBatch(candidates.map(e => e.id));
|
|
332
|
+
const assocStats = await this.store.getAssociationStatsForBatch(candidates.map(e => e.id));
|
|
339
333
|
const scored = candidates.map(engram => {
|
|
340
334
|
const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
341
335
|
const stats = assocStats.get(engram.id) ?? { count: 0, sumWeight: 0 };
|
|
@@ -355,14 +349,16 @@ export class ActivationEngine {
|
|
|
355
349
|
const conceptBonus = conceptOverlap * 0.3;
|
|
356
350
|
const keywordMatch = Math.min(Math.max(bm25Score, jaccardScore) + conceptBonus, 1.0);
|
|
357
351
|
// --- Vector similarity (semantic signal) ---
|
|
358
|
-
//
|
|
359
|
-
//
|
|
352
|
+
// AWM 0.8.x: model-tuned raw-cosine floor in place of z-score normalization.
|
|
353
|
+
// For BGE-small unit-norm vectors: unrelated ~0.3, related 0.5-0.7, near-duplicate 0.85+.
|
|
354
|
+
// Floor adapts to query mode: targeted=0.50 (stricter), exploratory=0.35 (looser).
|
|
360
355
|
let vectorMatch = 0;
|
|
361
356
|
const rawSim = rawCosineSims.get(engram.id);
|
|
362
|
-
if (rawSim !== undefined) {
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
|
|
357
|
+
if (rawSim !== undefined && rawSim > 0) {
|
|
358
|
+
const SIM_FLOOR = adaptive.zScoreGate > 0.5 ? 0.50 : 0.35;
|
|
359
|
+
if (rawSim > SIM_FLOOR) {
|
|
360
|
+
// Map [SIM_FLOOR, 1.0] → [0, 1] linearly with cap at 1.0.
|
|
361
|
+
vectorMatch = Math.min(1, (rawSim - SIM_FLOOR) / (0.95 - SIM_FLOOR));
|
|
366
362
|
}
|
|
367
363
|
}
|
|
368
364
|
// Combined text match: weighted blend of keyword and vector signals.
|
|
@@ -432,14 +428,14 @@ export class ActivationEngine {
|
|
|
432
428
|
// Take top 5 feedback terms and re-search
|
|
433
429
|
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
434
430
|
if (extraTerms) {
|
|
435
|
-
const feedbackBM25 = this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
431
|
+
const feedbackBM25 = await this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
436
432
|
for (const r of feedbackBM25) {
|
|
437
433
|
if (!candidateMap.has(r.engram.id)) {
|
|
438
434
|
candidateMap.set(r.engram.id, r.engram);
|
|
439
435
|
// Score the new candidate
|
|
440
436
|
const engram = r.engram;
|
|
441
437
|
const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
442
|
-
const associations = this.store.getAssociationsFor(engram.id);
|
|
438
|
+
const associations = await this.store.getAssociationsFor(engram.id);
|
|
443
439
|
const cTokens = tokenize(engram.concept);
|
|
444
440
|
const ctTokens = tokenize(engram.content);
|
|
445
441
|
const cJac = jaccard(queryTokens, cTokens);
|
|
@@ -449,10 +445,10 @@ export class ActivationEngine {
|
|
|
449
445
|
const km = Math.min(Math.max(r.bm25Score, jSc) + cOvlp * 0.3, 1.0);
|
|
450
446
|
let vm = 0;
|
|
451
447
|
const rs = rawCosineSims.get(engram.id) ?? (queryEmbedding && engram.embedding ? cosineSimilarity(queryEmbedding, engram.embedding) : 0);
|
|
452
|
-
if (rs) {
|
|
453
|
-
const
|
|
454
|
-
if (
|
|
455
|
-
vm = Math.min(1, (
|
|
448
|
+
if (rs > 0) {
|
|
449
|
+
const SIM_FLOOR = adaptive.zScoreGate > 0.5 ? 0.50 : 0.35;
|
|
450
|
+
if (rs > SIM_FLOOR)
|
|
451
|
+
vm = Math.min(1, (rs - SIM_FLOOR) / (0.95 - SIM_FLOOR));
|
|
456
452
|
}
|
|
457
453
|
const tm = km > 0 && vm > 0
|
|
458
454
|
? 0.5 * Math.max(km, vm) + 0.3 * Math.min(km, vm) + 0.2 * (km * vm)
|
|
@@ -468,9 +464,19 @@ export class ActivationEngine {
|
|
|
468
464
|
}
|
|
469
465
|
}
|
|
470
466
|
// Phase 3.7: Entity-Bridge boost — boost scored candidates that share entity tags
|
|
471
|
-
// with the most query-relevant result.
|
|
472
|
-
//
|
|
473
|
-
|
|
467
|
+
// with the most query-relevant result. The original intent: surface candidates
|
|
468
|
+
// that DON'T match the query text directly but share entities with the top
|
|
469
|
+
// text-match anchor ("she said something" → bridge to entities of the recent
|
|
470
|
+
// speaker named "she"). This is LATERAL relevance, not direct relevance.
|
|
471
|
+
//
|
|
472
|
+
// 2026-05-26: Added textMatch gate. Without it, when many engrams share entity
|
|
473
|
+
// tags AND have similar text matches (e.g., 10 near-clones of the same concept),
|
|
474
|
+
// the bridge boost would push the 8 non-anchor clones above the 2 anchors —
|
|
475
|
+
// an inversion of the genuine top match. The eval Retrieval suite caught this
|
|
476
|
+
// (Recall@5 0.80 → 0.46). Gate: only boost candidates whose textMatch is
|
|
477
|
+
// meaningfully below the anchor's. They actually need the lateral boost.
|
|
478
|
+
// Env override: AWM_DISABLE_ENTITY_BRIDGE=1 to skip this phase entirely.
|
|
479
|
+
if (!process.env.AWM_DISABLE_ENTITY_BRIDGE) {
|
|
474
480
|
// Find the result with the highest textMatch (most query-relevant, not just highest score)
|
|
475
481
|
// Gate: only bridge when anchor has meaningful text relevance (> 0.15)
|
|
476
482
|
// Adaptive: scale bridge boost inversely with candidate pool size to prevent
|
|
@@ -515,6 +521,16 @@ export class ActivationEngine {
|
|
|
515
521
|
}
|
|
516
522
|
}
|
|
517
523
|
if (entityTags.size > 0) {
|
|
524
|
+
// Anchor's textMatch sets the scale. Bridge boost magnitude is
|
|
525
|
+
// proportional to the gap between anchor and candidate textMatch:
|
|
526
|
+
// - candidate near anchor (a near-clone of the anchor) → small gap → near-zero boost
|
|
527
|
+
// - candidate far below anchor (genuine lateral relevance) → large gap → full boost
|
|
528
|
+
// Without this scaling, dense same-concept corpora flip the genuine
|
|
529
|
+
// top-1 below its 8-9 near-clones (eval Recall@5 0.80 → 0.46 verified
|
|
530
|
+
// 2026-05-26). The scaling keeps the lateral-relevance behavior
|
|
531
|
+
// (which is what helps the AB test) without inverting the genuine
|
|
532
|
+
// text-match winner.
|
|
533
|
+
const anchorTextMax = bridgeAnchors[0].phaseScores.textMatch;
|
|
518
534
|
for (const item of scored) {
|
|
519
535
|
if (anchorIds.has(item.engram.id))
|
|
520
536
|
continue;
|
|
@@ -525,11 +541,17 @@ export class ActivationEngine {
|
|
|
525
541
|
sharedEntities++;
|
|
526
542
|
}
|
|
527
543
|
if (sharedEntities > 0) {
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
544
|
+
// Gap scaling: 1.0 when candidateText << anchorText, 0 when equal.
|
|
545
|
+
// Clamped to [0, 1]. Anchors with textMatch ≤ 0 fall back to flat boost.
|
|
546
|
+
const gapScale = anchorTextMax > 0
|
|
547
|
+
? Math.max(0, Math.min(1, (anchorTextMax - item.phaseScores.textMatch) / anchorTextMax))
|
|
548
|
+
: 1;
|
|
549
|
+
const bridgeBoost = Math.min(sharedEntities * 0.15, 0.4) * gapScale;
|
|
550
|
+
if (bridgeBoost > 0) {
|
|
551
|
+
item.score += bridgeBoost;
|
|
552
|
+
item.phaseScores.composite += bridgeBoost;
|
|
553
|
+
item.phaseScores.graphBoost += bridgeBoost;
|
|
554
|
+
}
|
|
533
555
|
}
|
|
534
556
|
}
|
|
535
557
|
}
|
|
@@ -539,7 +561,7 @@ export class ActivationEngine {
|
|
|
539
561
|
// Only walk from engrams that had text relevance (composite > 0 pre-walk)
|
|
540
562
|
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
541
563
|
const topN = sorted.slice(0, limit * 3);
|
|
542
|
-
this.graphWalk(topN, 2, adaptive.hopPenalty, adaptive.beamWidth);
|
|
564
|
+
await this.graphWalk(topN, 2, adaptive.hopPenalty, adaptive.beamWidth);
|
|
543
565
|
// Phase 6: Initial filter and sort for re-ranking pool
|
|
544
566
|
const pool = topN
|
|
545
567
|
.filter(r => r.score >= minScore)
|
|
@@ -627,15 +649,19 @@ export class ActivationEngine {
|
|
|
627
649
|
const margin = rerankerScores.length >= 2
|
|
628
650
|
? rerankerScores[0] - rerankerScores[1]
|
|
629
651
|
: rerankerScores[0];
|
|
630
|
-
const
|
|
631
|
-
|
|
652
|
+
const cosineSimValues = Array.from(rawCosineSims.values());
|
|
653
|
+
const maxRawCosine = queryEmbedding && cosineSimValues.length > 0
|
|
654
|
+
? Math.max(...cosineSimValues)
|
|
632
655
|
: 1.0;
|
|
633
656
|
// Required-channels for hard abstention:
|
|
634
657
|
// abstention-explicit (caller passed abstentionThreshold > 0): 3 of 3
|
|
635
658
|
// default: 2 of 3 — precision-first
|
|
636
659
|
const requiredChannels = abstentionThreshold > 0 ? 3 : 2;
|
|
637
|
-
// Hard abstention: fewer than required channels agree AND semantic
|
|
638
|
-
|
|
660
|
+
// Hard abstention: fewer than required channels agree AND semantic match weak.
|
|
661
|
+
// After the 2.0.x vector refactor we no longer compute z-score; threshold
|
|
662
|
+
// on raw cosine against the mode floor (targeted=0.50, exploratory=0.35).
|
|
663
|
+
const semanticFloor = adaptive.zScoreGate > 0.5 ? 0.50 : 0.35;
|
|
664
|
+
if (channelsAgreeing < requiredChannels && maxRawCosine < semanticFloor) {
|
|
639
665
|
return [];
|
|
640
666
|
}
|
|
641
667
|
// Soft penalty: only 1 channel agrees or margin is thin
|
|
@@ -668,22 +694,119 @@ export class ActivationEngine {
|
|
|
668
694
|
item.score *= 0.15; // Severe down-rank — successor should dominate
|
|
669
695
|
}
|
|
670
696
|
}
|
|
671
|
-
// Phase 9: Final sort, limit, explain
|
|
672
|
-
const
|
|
673
|
-
|
|
697
|
+
// Phase 9: Final sort, limit, explain, attach confidence
|
|
698
|
+
const finalRanked = rerankPool.sort((a, b) => b.score - a.score);
|
|
699
|
+
const topScoresForConfidence = finalRanked.slice(0, 10).map(r => r.score);
|
|
700
|
+
const { confidence } = computeRecallConfidence(topScoresForConfidence);
|
|
701
|
+
// Opt-in confidence-based abstention. When the caller sets
|
|
702
|
+
// `requireConfidence`, we return [] if the score-distribution shape
|
|
703
|
+
// indicates a low-quality recall (noisy or best-of-bad-bunch).
|
|
704
|
+
// Independent of the channel-agreement abstention earlier — that path
|
|
705
|
+
// requires the reranker; this one uses just the final composite scores.
|
|
706
|
+
if (requireConfidence > 0 && confidence < requireConfidence) {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
// Confidence-adaptive output granularity (Paper 3: cognitive teaming).
|
|
710
|
+
// 'full' → no summary (default, current behavior).
|
|
711
|
+
// 'compact' → every result gets a short summary (COMPACT_LEN chars).
|
|
712
|
+
// 'auto' → if confidence ≥ AUTO_THRESHOLD: top result gets a full-length
|
|
713
|
+
// summary, lower-ranked results get compact summaries. If
|
|
714
|
+
// confidence is lower, all results get compact summaries.
|
|
715
|
+
const granularity = query.granularity ?? 'full';
|
|
716
|
+
const COMPACT_LEN = Number(process.env.AWM_GRANULARITY_COMPACT_LEN ?? 200);
|
|
717
|
+
const FULL_LEN = Number(process.env.AWM_GRANULARITY_FULL_LEN ?? 1000);
|
|
718
|
+
const AUTO_THRESHOLD = Number(process.env.AWM_GRANULARITY_AUTO_THRESHOLD ?? 0.4);
|
|
719
|
+
// Snippet token list — reuse the activation queryTokens (Set<string>),
|
|
720
|
+
// filtered to ≥2 chars to drop noise. queryTokens was tokenized at line
|
|
721
|
+
// 273 via tokenize() with stopword stripping already applied.
|
|
722
|
+
const snippetTokens = Array.from(queryTokens).filter(t => t.length >= 2);
|
|
723
|
+
// Find the densest window of `len` chars in `content` that contains the
|
|
724
|
+
// most query-token matches. Falls back to head if no tokens match.
|
|
725
|
+
const summaryFor = (content, len) => {
|
|
726
|
+
if (content.length <= len)
|
|
727
|
+
return content;
|
|
728
|
+
if (snippetTokens.length === 0)
|
|
729
|
+
return content.slice(0, len).trimEnd() + '…';
|
|
730
|
+
const lower = content.toLowerCase();
|
|
731
|
+
const hits = [];
|
|
732
|
+
for (const tok of snippetTokens) {
|
|
733
|
+
let from = 0;
|
|
734
|
+
while (true) {
|
|
735
|
+
const idx = lower.indexOf(tok, from);
|
|
736
|
+
if (idx < 0)
|
|
737
|
+
break;
|
|
738
|
+
hits.push(idx);
|
|
739
|
+
from = idx + tok.length;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (hits.length === 0)
|
|
743
|
+
return content.slice(0, len).trimEnd() + '…';
|
|
744
|
+
hits.sort((a, b) => a - b);
|
|
745
|
+
// Find the window of size `len` that contains the most hits, by
|
|
746
|
+
// sliding a window anchored on each hit.
|
|
747
|
+
let bestStart = hits[0];
|
|
748
|
+
let bestCount = 0;
|
|
749
|
+
for (let i = 0; i < hits.length; i++) {
|
|
750
|
+
const start = Math.max(0, hits[i] - Math.floor(len / 4));
|
|
751
|
+
let count = 0;
|
|
752
|
+
for (let j = i; j < hits.length; j++) {
|
|
753
|
+
if (hits[j] - start < len)
|
|
754
|
+
count++;
|
|
755
|
+
else
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
if (count > bestCount) {
|
|
759
|
+
bestCount = count;
|
|
760
|
+
bestStart = start;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Reserve characters for the ellipses we're about to add so the final
|
|
764
|
+
// string stays within `len`. Without this, both '…' prefix + suffix
|
|
765
|
+
// would push the snippet to len+2 chars.
|
|
766
|
+
const hasPrefix = bestStart > 0;
|
|
767
|
+
const tentativeEnd = Math.min(content.length, bestStart + len);
|
|
768
|
+
const hasSuffix = tentativeEnd < content.length;
|
|
769
|
+
const reserveForEllipses = (hasPrefix ? 1 : 0) + (hasSuffix ? 1 : 0);
|
|
770
|
+
const bodyLen = Math.max(0, len - reserveForEllipses);
|
|
771
|
+
const end = Math.min(content.length, bestStart + bodyLen);
|
|
772
|
+
const startAdj = Math.max(0, end - bodyLen);
|
|
773
|
+
let snippet = content.slice(startAdj, end);
|
|
774
|
+
if (startAdj > 0)
|
|
775
|
+
snippet = '…' + snippet.trimStart();
|
|
776
|
+
if (end < content.length)
|
|
777
|
+
snippet = snippet.trimEnd() + '…';
|
|
778
|
+
return snippet;
|
|
779
|
+
};
|
|
780
|
+
const results = finalRanked
|
|
674
781
|
.slice(0, limit)
|
|
675
|
-
.map(r =>
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
782
|
+
.map((r, idx) => {
|
|
783
|
+
let summary;
|
|
784
|
+
if (granularity === 'compact') {
|
|
785
|
+
summary = summaryFor(r.engram.content, COMPACT_LEN);
|
|
786
|
+
}
|
|
787
|
+
else if (granularity === 'auto') {
|
|
788
|
+
if (confidence >= AUTO_THRESHOLD && idx === 0) {
|
|
789
|
+
summary = summaryFor(r.engram.content, FULL_LEN);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
summary = summaryFor(r.engram.content, COMPACT_LEN);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
engram: r.engram,
|
|
797
|
+
score: r.score,
|
|
798
|
+
phaseScores: r.phaseScores,
|
|
799
|
+
why: this.explain(r.phaseScores, r.engram, r.associations),
|
|
800
|
+
associations: r.associations,
|
|
801
|
+
confidence,
|
|
802
|
+
...(summary !== undefined && { summary }),
|
|
803
|
+
};
|
|
804
|
+
});
|
|
682
805
|
const activatedIds = results.map(r => r.engram.id);
|
|
683
806
|
// Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
|
|
684
807
|
if (!query.internal) {
|
|
685
808
|
for (const id of activatedIds) {
|
|
686
|
-
this.store.touchEngram(id);
|
|
809
|
+
await this.store.touchEngram(id);
|
|
687
810
|
}
|
|
688
811
|
this.coActivationBuffer.pushBatch(activatedIds);
|
|
689
812
|
// Validation-gated Hebbian: defer strengthening until feedback arrives
|
|
@@ -700,7 +823,7 @@ export class ActivationEngine {
|
|
|
700
823
|
this.validationGate.addPending(activatedIds, uniquePairs);
|
|
701
824
|
// Log activation event for eval
|
|
702
825
|
const latencyMs = performance.now() - startTime;
|
|
703
|
-
this.store.logActivationEvent({
|
|
826
|
+
await this.store.logActivationEvent({
|
|
704
827
|
id: randomUUID(),
|
|
705
828
|
agentId: query.agentId,
|
|
706
829
|
timestamp: new Date(),
|
|
@@ -734,7 +857,7 @@ export class ActivationEngine {
|
|
|
734
857
|
causal: 0.25, // causal edges (high-value signal)
|
|
735
858
|
entity: 0.15, // bridge edges
|
|
736
859
|
};
|
|
737
|
-
graphWalk(scored, maxDepth, hopPenalty, beamWidth = 15) {
|
|
860
|
+
async graphWalk(scored, maxDepth, hopPenalty, beamWidth = 15) {
|
|
738
861
|
const scoreMap = new Map(scored.map(s => [s.engram.id, s]));
|
|
739
862
|
const MAX_TOTAL_BOOST = 0.25;
|
|
740
863
|
// Define which edge types belong to each sub-graph
|
|
@@ -763,7 +886,7 @@ export class ActivationEngine {
|
|
|
763
886
|
explored.add(item.engram.id);
|
|
764
887
|
const associations = item.associations.length > 0
|
|
765
888
|
? item.associations
|
|
766
|
-
: this.store.getAssociationsFor(item.engram.id);
|
|
889
|
+
: await this.store.getAssociationsFor(item.engram.id);
|
|
767
890
|
// Filter to only edges of this sub-graph type
|
|
768
891
|
const relevantEdges = associations.filter(a => edgeTypes.includes(a.type));
|
|
769
892
|
for (const assoc of relevantEdges) {
|
|
@@ -817,23 +940,23 @@ export class ActivationEngine {
|
|
|
817
940
|
* Called by memory_feedback — only strengthens when retrieval was useful.
|
|
818
941
|
* This prevents hub toxicity from noisy co-retrieval (Kairos-inspired).
|
|
819
942
|
*/
|
|
820
|
-
resolveHebbianFeedback(engramId, useful) {
|
|
943
|
+
async resolveHebbianFeedback(engramId, useful) {
|
|
821
944
|
const { pairs, signal } = this.validationGate.resolveFeedback(engramId, useful);
|
|
822
945
|
let updated = 0;
|
|
823
946
|
for (const [a, b] of pairs) {
|
|
824
|
-
const existing = this.store.getAssociation(a, b) ?? this.store.getAssociation(b, a);
|
|
947
|
+
const existing = (await this.store.getAssociation(a, b)) ?? (await this.store.getAssociation(b, a));
|
|
825
948
|
const currentWeight = existing?.weight ?? 0.1;
|
|
826
949
|
if (signal > 0) {
|
|
827
950
|
// Positive feedback → strengthen
|
|
828
951
|
const newWeight = strengthenAssociation(currentWeight, signal);
|
|
829
|
-
this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
830
|
-
this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
952
|
+
await this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
953
|
+
await this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
831
954
|
}
|
|
832
955
|
else {
|
|
833
956
|
// Negative feedback → slight weakening (decay by signal magnitude)
|
|
834
957
|
const newWeight = Math.max(0.001, currentWeight * (1 + signal)); // signal is -0.3
|
|
835
|
-
this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
836
|
-
this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
958
|
+
await this.store.upsertAssociation(a, b, newWeight, 'hebbian');
|
|
959
|
+
await this.store.upsertAssociation(b, a, newWeight, 'hebbian');
|
|
837
960
|
}
|
|
838
961
|
updated++;
|
|
839
962
|
}
|