agent-working-memory 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +19 -9
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.d.ts.map +1 -0
  4. package/dist/adapters/claude-code.js +218 -0
  5. package/dist/adapters/claude-code.js.map +1 -0
  6. package/dist/adapters/codex.d.ts +4 -0
  7. package/dist/adapters/codex.d.ts.map +1 -0
  8. package/dist/adapters/codex.js +226 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/common.d.ts +34 -0
  11. package/dist/adapters/common.d.ts.map +1 -0
  12. package/dist/adapters/common.js +145 -0
  13. package/dist/adapters/common.js.map +1 -0
  14. package/dist/adapters/cursor.d.ts +4 -0
  15. package/dist/adapters/cursor.d.ts.map +1 -0
  16. package/dist/adapters/cursor.js +138 -0
  17. package/dist/adapters/cursor.js.map +1 -0
  18. package/dist/adapters/http.d.ts +4 -0
  19. package/dist/adapters/http.d.ts.map +1 -0
  20. package/dist/adapters/http.js +88 -0
  21. package/dist/adapters/http.js.map +1 -0
  22. package/dist/adapters/index.d.ts +7 -0
  23. package/dist/adapters/index.d.ts.map +1 -0
  24. package/dist/adapters/index.js +21 -0
  25. package/dist/adapters/index.js.map +1 -0
  26. package/dist/adapters/types.d.ts +65 -0
  27. package/dist/adapters/types.d.ts.map +1 -0
  28. package/dist/adapters/types.js +4 -0
  29. package/dist/adapters/types.js.map +1 -0
  30. package/dist/api/routes.d.ts.map +1 -1
  31. package/dist/api/routes.js +1 -0
  32. package/dist/api/routes.js.map +1 -1
  33. package/dist/cli.js +104 -230
  34. package/dist/cli.js.map +1 -1
  35. package/dist/coordination/events.d.ts +59 -0
  36. package/dist/coordination/events.d.ts.map +1 -0
  37. package/dist/coordination/events.js +28 -0
  38. package/dist/coordination/events.js.map +1 -0
  39. package/dist/coordination/index.d.ts +10 -1
  40. package/dist/coordination/index.d.ts.map +1 -1
  41. package/dist/coordination/index.js +87 -3
  42. package/dist/coordination/index.js.map +1 -1
  43. package/dist/coordination/peer-decisions.d.ts +40 -0
  44. package/dist/coordination/peer-decisions.d.ts.map +1 -0
  45. package/dist/coordination/peer-decisions.js +82 -0
  46. package/dist/coordination/peer-decisions.js.map +1 -0
  47. package/dist/coordination/plugin-loader.d.ts +18 -0
  48. package/dist/coordination/plugin-loader.d.ts.map +1 -0
  49. package/dist/coordination/plugin-loader.js +55 -0
  50. package/dist/coordination/plugin-loader.js.map +1 -0
  51. package/dist/coordination/plugin.d.ts +40 -0
  52. package/dist/coordination/plugin.d.ts.map +1 -0
  53. package/dist/coordination/plugin.js +22 -0
  54. package/dist/coordination/plugin.js.map +1 -0
  55. package/dist/coordination/routes.d.ts +2 -1
  56. package/dist/coordination/routes.d.ts.map +1 -1
  57. package/dist/coordination/routes.js +899 -76
  58. package/dist/coordination/routes.js.map +1 -1
  59. package/dist/coordination/schema.d.ts.map +1 -1
  60. package/dist/coordination/schema.js +72 -14
  61. package/dist/coordination/schema.js.map +1 -1
  62. package/dist/coordination/schemas.d.ts +84 -3
  63. package/dist/coordination/schemas.d.ts.map +1 -1
  64. package/dist/coordination/schemas.js +71 -1
  65. package/dist/coordination/schemas.js.map +1 -1
  66. package/dist/coordination/stale.d.ts.map +1 -1
  67. package/dist/coordination/stale.js +2 -1
  68. package/dist/coordination/stale.js.map +1 -1
  69. package/dist/coordination/types.d.ts +252 -0
  70. package/dist/coordination/types.d.ts.map +1 -0
  71. package/dist/coordination/types.js +8 -0
  72. package/dist/coordination/types.js.map +1 -0
  73. package/dist/coordination/write-mutex.d.ts +26 -0
  74. package/dist/coordination/write-mutex.d.ts.map +1 -0
  75. package/dist/coordination/write-mutex.js +63 -0
  76. package/dist/coordination/write-mutex.js.map +1 -0
  77. package/dist/core/embeddings.d.ts +2 -0
  78. package/dist/core/embeddings.d.ts.map +1 -1
  79. package/dist/core/embeddings.js +4 -0
  80. package/dist/core/embeddings.js.map +1 -1
  81. package/dist/core/hebbian.d.ts +25 -1
  82. package/dist/core/hebbian.d.ts.map +1 -1
  83. package/dist/core/hebbian.js +71 -3
  84. package/dist/core/hebbian.js.map +1 -1
  85. package/dist/engine/activation.d.ts +22 -4
  86. package/dist/engine/activation.d.ts.map +1 -1
  87. package/dist/engine/activation.js +152 -76
  88. package/dist/engine/activation.js.map +1 -1
  89. package/dist/engine/consolidation.d.ts.map +1 -1
  90. package/dist/engine/consolidation.js +15 -6
  91. package/dist/engine/consolidation.js.map +1 -1
  92. package/dist/engine/retraction.d.ts +3 -1
  93. package/dist/engine/retraction.d.ts.map +1 -1
  94. package/dist/engine/retraction.js +19 -6
  95. package/dist/engine/retraction.js.map +1 -1
  96. package/dist/index.js +6 -18
  97. package/dist/index.js.map +1 -1
  98. package/dist/mcp.js +61 -4
  99. package/dist/mcp.js.map +1 -1
  100. package/dist/storage/sqlite.d.ts +23 -1
  101. package/dist/storage/sqlite.d.ts.map +1 -1
  102. package/dist/storage/sqlite.js +112 -3
  103. package/dist/storage/sqlite.js.map +1 -1
  104. package/dist/types/engram.d.ts +1 -0
  105. package/dist/types/engram.d.ts.map +1 -1
  106. package/package.json +1 -1
  107. package/src/adapters/claude-code.ts +234 -0
  108. package/src/adapters/codex.ts +262 -0
  109. package/src/adapters/common.ts +172 -0
  110. package/src/adapters/cursor.ts +150 -0
  111. package/src/adapters/http.ts +100 -0
  112. package/src/adapters/index.ts +31 -0
  113. package/src/adapters/types.ts +75 -0
  114. package/src/api/routes.ts +2 -0
  115. package/src/cli.ts +107 -238
  116. package/src/coordination/events.ts +90 -0
  117. package/src/coordination/index.ts +102 -3
  118. package/src/coordination/peer-decisions.ts +105 -0
  119. package/src/coordination/plugin-loader.ts +60 -0
  120. package/src/coordination/plugin.ts +44 -0
  121. package/src/coordination/routes.ts +1176 -105
  122. package/src/coordination/schema.ts +67 -14
  123. package/src/coordination/schemas.ts +85 -1
  124. package/src/coordination/stale.ts +3 -2
  125. package/src/coordination/types.ts +311 -0
  126. package/src/coordination/write-mutex.ts +69 -0
  127. package/src/core/embeddings.ts +5 -0
  128. package/src/core/hebbian.ts +84 -3
  129. package/src/engine/activation.ts +149 -73
  130. package/src/engine/consolidation.ts +15 -6
  131. package/src/engine/retraction.ts +22 -6
  132. package/src/index.ts +6 -15
  133. package/src/mcp.ts +77 -4
  134. package/src/storage/sqlite.ts +116 -3
  135. package/src/types/engram.ts +1 -0
@@ -30,14 +30,25 @@ export function strengthenAssociation(
30
30
 
31
31
  /**
32
32
  * Weaken an association weight due to lack of co-activation.
33
- * Called periodically by the connection engine.
33
+ * Uses power-law decay (DASH model) instead of exponential.
34
+ * Power law has a longer tail — old but valuable associations
35
+ * don't vanish as aggressively as exponential decay.
36
+ *
37
+ * DASH: weight = initial × (1 + t/scale)^(-exponent)
38
+ * vs exponential: weight = initial × 0.5^(t/halfLife)
39
+ *
40
+ * At 7 days: power-law retains ~58% vs exponential 50%
41
+ * At 30 days: power-law retains ~32% vs exponential 6%
42
+ * At 90 days: power-law retains ~20% vs exponential 0.02%
34
43
  */
35
44
  export function decayAssociation(
36
45
  currentWeight: number,
37
46
  daysSinceActivation: number,
38
- halfLife: number = 7.0 // days
47
+ halfLife: number = 7.0 // scale parameter (days)
39
48
  ): number {
40
- const decayFactor = Math.pow(0.5, daysSinceActivation / halfLife);
49
+ // Power-law decay: (1 + t/scale)^(-exponent)
50
+ const exponent = 0.8; // Controls steepness — 0.8 gives a good balance
51
+ const decayFactor = Math.pow(1 + daysSinceActivation / halfLife, -exponent);
41
52
  return Math.max(currentWeight * decayFactor, MIN_WEIGHT);
42
53
  }
43
54
 
@@ -91,3 +102,73 @@ export class CoActivationBuffer {
91
102
  this.buffer = [];
92
103
  }
93
104
  }
105
+
106
+ /**
107
+ * Validation-gated Hebbian buffer (Kairos-inspired).
108
+ *
109
+ * Instead of strengthening associations immediately on co-activation,
110
+ * pairs are held pending until feedback arrives. This prevents hub toxicity
111
+ * from noisy co-retrieval — edges only strengthen when the retrieval was
112
+ * actually useful.
113
+ *
114
+ * - Positive feedback → strengthen the pending pairs
115
+ * - Negative feedback → slightly weaken them
116
+ * - No feedback within GATE_TIMEOUT_MS → discard (neutral)
117
+ */
118
+ const GATE_TIMEOUT_MS = 60_000; // 60 seconds to receive feedback
119
+
120
+ interface PendingHebbianUpdate {
121
+ pairs: [string, string][];
122
+ engramIds: string[];
123
+ timestamp: number;
124
+ }
125
+
126
+ export class ValidationGatedBuffer {
127
+ private pending: PendingHebbianUpdate[] = [];
128
+
129
+ /** Record co-activated pairs as pending (awaiting feedback validation) */
130
+ addPending(engramIds: string[], pairs: [string, string][]): void {
131
+ this.pending.push({ pairs, engramIds, timestamp: Date.now() });
132
+ // Evict expired entries
133
+ const cutoff = Date.now() - GATE_TIMEOUT_MS;
134
+ this.pending = this.pending.filter(p => p.timestamp > cutoff);
135
+ }
136
+
137
+ /**
138
+ * Resolve pending updates for an engram that received feedback.
139
+ * Returns pairs to strengthen (positive) or weaken (negative).
140
+ */
141
+ resolveFeedback(engramId: string, useful: boolean): { pairs: [string, string][]; signal: number } {
142
+ const cutoff = Date.now() - GATE_TIMEOUT_MS;
143
+ const matching: [string, string][] = [];
144
+
145
+ // Find all pending updates that include this engram and are still within the gate window
146
+ this.pending = this.pending.filter(p => {
147
+ if (p.timestamp < cutoff) return false; // expired
148
+ if (p.engramIds.includes(engramId)) {
149
+ matching.push(...p.pairs);
150
+ return false; // consumed
151
+ }
152
+ return true; // keep
153
+ });
154
+
155
+ // Deduplicate pairs
156
+ const seen = new Set<string>();
157
+ const unique: [string, string][] = [];
158
+ for (const [a, b] of matching) {
159
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
160
+ if (!seen.has(key)) {
161
+ seen.add(key);
162
+ unique.push([a, b]);
163
+ }
164
+ }
165
+
166
+ return {
167
+ pairs: unique,
168
+ signal: useful ? 1.0 : -0.3, // positive = full strengthen, negative = slight weaken
169
+ };
170
+ }
171
+
172
+ /** Get count of pending updates (for stats) */
173
+ get pendingCount(): number { return this.pending.length; }
174
+ }
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { randomUUID } from 'node:crypto';
23
23
  import { baseLevelActivation, softplus } from '../core/decay.js';
24
- import { strengthenAssociation, CoActivationBuffer } from '../core/hebbian.js';
24
+ import { strengthenAssociation, CoActivationBuffer, ValidationGatedBuffer } from '../core/hebbian.js';
25
25
  import { embed, cosineSimilarity } from '../core/embeddings.js';
26
26
  import { rerank } from '../core/reranker.js';
27
27
  import { expandQuery } from '../core/query-expander.js';
@@ -163,10 +163,12 @@ function jaccard(a: Set<string>, b: Set<string>): number {
163
163
  export class ActivationEngine {
164
164
  private store: EngramStore;
165
165
  private coActivationBuffer: CoActivationBuffer;
166
+ readonly validationGate: ValidationGatedBuffer;
166
167
 
167
168
  constructor(store: EngramStore) {
168
169
  this.store = store;
169
170
  this.coActivationBuffer = new CoActivationBuffer(50);
171
+ this.validationGate = new ValidationGatedBuffer();
170
172
  }
171
173
 
172
174
  /**
@@ -181,14 +183,18 @@ export class ActivationEngine {
181
183
  const abstentionThreshold = query.abstentionThreshold ?? 0;
182
184
  const adaptive = resolveAdaptiveParams(query);
183
185
 
186
+ // Resolve workspace scope: if workspace is set, search across all agents in that workspace
187
+ const agentIds = query.workspace
188
+ ? this.store.getWorkspaceAgentIds(query.agentId, query.workspace)
189
+ : [query.agentId];
190
+ const isWorkspaceScoped = agentIds.length > 1;
191
+
184
192
  // Phase -1: Coref expansion — if query has pronouns, append recent entity names
185
- // Helps conversational recall where "she/he/they/it" refers to a named entity.
186
193
  let queryContext = query.context;
187
194
  const pronounPattern = /\b(she|he|they|her|his|him|their|it|that|this|there)\b/i;
188
195
  if (pronounPattern.test(queryContext)) {
189
- // Pull recent entity names from the agent's most-accessed memories
190
196
  try {
191
- const recentEntities = this.store.getEngramsByAgent(query.agentId, 'active')
197
+ const recentEntities = this.store.getEngramsByAgents(agentIds, 'active')
192
198
  .sort((a, b) => b.accessCount - a.accessCount)
193
199
  .slice(0, 10)
194
200
  .flatMap(e => e.tags.filter(t => t.length >= 3 && !/^(session-|low-|D\d)/.test(t)))
@@ -203,10 +209,16 @@ export class ActivationEngine {
203
209
  // Phase 0: Query expansion — add related terms to improve BM25 recall
204
210
  let searchContext = queryContext;
205
211
  if (useExpansion) {
212
+ let timer: ReturnType<typeof setTimeout> | undefined;
206
213
  try {
207
- searchContext = await expandQuery(query.context);
214
+ searchContext = await Promise.race([
215
+ expandQuery(query.context),
216
+ new Promise<string>((_, reject) => { timer = setTimeout(() => reject(new Error('expansion timeout')), 5000); }),
217
+ ]);
208
218
  } catch {
209
- // Expansion unavailable — use original query
219
+ // Expansion unavailable or timed out — use original query
220
+ } finally {
221
+ if (timer) clearTimeout(timer);
210
222
  }
211
223
  }
212
224
 
@@ -222,9 +234,9 @@ export class ActivationEngine {
222
234
  // Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
223
235
  const keywordQuery = Array.from(tokenize(query.context)).join(' ');
224
236
  const bm25Keyword = keywordQuery.length > 2
225
- ? this.store.searchBM25WithRank(query.agentId, keywordQuery, limit * 3)
237
+ ? this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
226
238
  : [];
227
- const bm25Expanded = this.store.searchBM25WithRank(query.agentId, searchContext, limit * 3);
239
+ const bm25Expanded = this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
228
240
 
229
241
  // Merge: take the best BM25 score per engram from either pass
230
242
  const bm25ScoreMap = new Map<string, number>();
@@ -240,8 +252,8 @@ export class ActivationEngine {
240
252
  engram, bm25Score: bm25ScoreMap.get(id) ?? 0,
241
253
  }));
242
254
 
243
- const allActive = this.store.getEngramsByAgent(
244
- query.agentId,
255
+ const allActive = this.store.getEngramsByAgents(
256
+ agentIds,
245
257
  query.includeStaging ? undefined : 'active',
246
258
  query.includeRetracted ?? false
247
259
  );
@@ -396,7 +408,7 @@ export class ActivationEngine {
396
408
  // Take top 5 feedback terms and re-search
397
409
  const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
398
410
  if (extraTerms) {
399
- const feedbackBM25 = this.store.searchBM25WithRank(query.agentId, `${searchContext} ${extraTerms}`, limit * 2);
411
+ const feedbackBM25 = this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
400
412
  for (const r of feedbackBM25) {
401
413
  if (!candidateMap.has(r.engram.id)) {
402
414
  candidateMap.set(r.engram.id, r.engram);
@@ -522,7 +534,11 @@ export class ActivationEngine {
522
534
  const passages = rerankPool.map(r =>
523
535
  `${r.engram.concept}: ${r.engram.content}`
524
536
  );
525
- const rerankResults = await rerank(query.context, passages);
537
+ let rerankTimer: ReturnType<typeof setTimeout> | undefined;
538
+ const rerankResults = await Promise.race([
539
+ rerank(query.context, passages),
540
+ new Promise<never>((_, reject) => { rerankTimer = setTimeout(() => reject(new Error('reranker timeout')), 10000); }),
541
+ ]).finally(() => { if (rerankTimer) clearTimeout(rerankTimer); });
526
542
 
527
543
  // Adaptive reranker blend (Codex recommendation):
528
544
  // When BM25/text signals are strong, trust them more; when weak, lean on reranker.
@@ -622,13 +638,21 @@ export class ActivationEngine {
622
638
 
623
639
  const activatedIds = results.map(r => r.engram.id);
624
640
 
625
- // Side effects: touch, co-activate, Hebbian update (skip for internal/system calls)
641
+ // Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
626
642
  if (!query.internal) {
627
643
  for (const id of activatedIds) {
628
644
  this.store.touchEngram(id);
629
645
  }
630
646
  this.coActivationBuffer.pushBatch(activatedIds);
631
- this.updateHebbianWeights();
647
+ // Validation-gated Hebbian: defer strengthening until feedback arrives
648
+ const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
649
+ const seen = new Set<string>();
650
+ const uniquePairs: [string, string][] = [];
651
+ for (const [a, b] of pairs) {
652
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
653
+ if (!seen.has(key)) { seen.add(key); uniquePairs.push([a, b]); }
654
+ }
655
+ this.validationGate.addPending(activatedIds, uniquePairs);
632
656
 
633
657
  // Log activation event for eval
634
658
  const latencyMs = performance.now() - startTime;
@@ -648,10 +672,27 @@ export class ActivationEngine {
648
672
  }
649
673
 
650
674
  /**
651
- * Beam search graph walk — replaces naive BFS.
652
- * Scores paths (not just nodes), uses query-dependent edge filtering,
653
- * and supports deeper exploration with focused beams.
675
+ * Multi-graph traversal (MAGMA-inspired).
676
+ *
677
+ * Instead of one beam search over all edge types, runs independent traversals
678
+ * per graph type with specialized scoring, then fuses the boosts.
679
+ *
680
+ * Four sub-graphs:
681
+ * - Semantic (connection + hebbian edges) → standard weight-based walk
682
+ * - Temporal (temporal edges) → recency-weighted (favor recent connections)
683
+ * - Causal (causal edges) → full weight walk (causal links are high-value)
684
+ * - Entity (bridge edges) → entity-tag-weighted walk
685
+ *
686
+ * Each sub-graph contributes independently to the final graph boost,
687
+ * weighted by configurable per-graph weights.
654
688
  */
689
+ private static readonly GRAPH_WEIGHTS = {
690
+ semantic: 0.40, // connection + hebbian
691
+ temporal: 0.20, // temporal edges
692
+ causal: 0.25, // causal edges (high-value signal)
693
+ entity: 0.15, // bridge edges
694
+ };
695
+
655
696
  private graphWalk(
656
697
  scored: { engram: Engram; score: number; phaseScores: PhaseScores; associations: Association[] }[],
657
698
  maxDepth: number,
@@ -660,85 +701,120 @@ export class ActivationEngine {
660
701
  ): void {
661
702
  const scoreMap = new Map(scored.map(s => [s.engram.id, s]));
662
703
  const MAX_TOTAL_BOOST = 0.25;
663
- const BEAM_WIDTH = beamWidth;
664
704
 
665
- // Seed the beam with high-scoring, text-relevant items
666
- const beam = scored
667
- .filter(item => item.phaseScores.textMatch >= 0.15)
668
- .sort((a, b) => b.score - a.score)
669
- .slice(0, BEAM_WIDTH);
705
+ // Define which edge types belong to each sub-graph
706
+ const graphTypes: Record<string, string[]> = {
707
+ semantic: ['connection', 'hebbian'],
708
+ temporal: ['temporal'],
709
+ causal: ['causal'],
710
+ entity: ['bridge'],
711
+ };
712
+
713
+ // Run independent traversals per sub-graph, accumulate boosts
714
+ const boostAccum = new Map<string, number>(); // engramId → total boost
670
715
 
671
- // Track which engrams have been explored (avoid cycles)
672
- const explored = new Set<string>();
716
+ for (const [graphName, edgeTypes] of Object.entries(graphTypes)) {
717
+ const graphWeight = ActivationEngine.GRAPH_WEIGHTS[graphName as keyof typeof ActivationEngine.GRAPH_WEIGHTS];
718
+ const subBeamWidth = Math.max(3, Math.ceil(beamWidth * graphWeight));
673
719
 
674
- for (let depth = 0; depth < maxDepth; depth++) {
675
- const nextBeam: typeof beam = [];
720
+ // Seed beam
721
+ const beam = scored
722
+ .filter(item => item.phaseScores.textMatch >= 0.15)
723
+ .sort((a, b) => b.score - a.score)
724
+ .slice(0, subBeamWidth);
725
+
726
+ const explored = new Set<string>();
727
+
728
+ for (let depth = 0; depth < maxDepth; depth++) {
729
+ const nextBeam: typeof beam = [];
676
730
 
677
- for (const item of beam) {
678
- if (explored.has(item.engram.id)) continue;
679
- explored.add(item.engram.id);
731
+ for (const item of beam) {
732
+ if (explored.has(item.engram.id)) continue;
733
+ explored.add(item.engram.id);
680
734
 
681
- // Get associations for depth > 0, fetch from store if not in scored set
682
- const associations = item.associations.length > 0
683
- ? item.associations
684
- : this.store.getAssociationsFor(item.engram.id);
735
+ const associations = item.associations.length > 0
736
+ ? item.associations
737
+ : this.store.getAssociationsFor(item.engram.id);
685
738
 
686
- for (const assoc of associations) {
687
- const neighborId = assoc.fromEngramId === item.engram.id
688
- ? assoc.toEngramId
689
- : assoc.fromEngramId;
739
+ // Filter to only edges of this sub-graph type
740
+ const relevantEdges = associations.filter(a => edgeTypes.includes(a.type));
690
741
 
691
- if (explored.has(neighborId)) continue;
742
+ for (const assoc of relevantEdges) {
743
+ const neighborId = assoc.fromEngramId === item.engram.id
744
+ ? assoc.toEngramId
745
+ : assoc.fromEngramId;
692
746
 
693
- const neighbor = scoreMap.get(neighborId);
694
- if (!neighbor) continue;
747
+ if (explored.has(neighborId)) continue;
748
+ const neighbor = scoreMap.get(neighborId);
749
+ if (!neighbor) continue;
695
750
 
696
- // Query-dependent edge filtering: neighbor must have SOME relevance
697
- // (textMatch > 0.05 for deeper hops, relaxed from 0.1)
698
- const relevanceFloor = depth === 0 ? 0.1 : 0.05;
699
- if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
751
+ const relevanceFloor = depth === 0 ? 0.1 : 0.05;
752
+ if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
700
753
 
701
- // Skip if neighbor already at boost cap
702
- if (neighbor.phaseScores.graphBoost >= MAX_TOTAL_BOOST) continue;
754
+ // Path score with graph-type-specific weighting
755
+ const normalizedWeight = Math.min(assoc.weight, 5.0) / 5.0;
756
+ let pathScore = item.score * normalizedWeight * Math.pow(hopPenalty, depth + 1);
703
757
 
704
- // Path score: source score * edge weight * hop penalty^(depth+1)
705
- const normalizedWeight = Math.min(assoc.weight, 5.0) / 5.0;
706
- const pathScore = item.score * normalizedWeight * Math.pow(hopPenalty, depth + 1);
758
+ // Causal edges get a 2x boost they represent verified reasoning chains
759
+ if (graphName === 'causal') pathScore *= 2.0;
707
760
 
708
- const boost = Math.min(pathScore, 0.15, MAX_TOTAL_BOOST - neighbor.phaseScores.graphBoost);
709
- if (boost > 0.001) {
710
- neighbor.score += boost;
711
- neighbor.phaseScores.graphBoost += boost;
712
- nextBeam.push(neighbor);
761
+ // Weight by sub-graph importance
762
+ const boost = Math.min(pathScore * graphWeight, 0.15);
763
+ if (boost > 0.001) {
764
+ boostAccum.set(neighborId, (boostAccum.get(neighborId) ?? 0) + boost);
765
+ nextBeam.push(neighbor);
766
+ }
713
767
  }
714
768
  }
769
+
770
+ if (nextBeam.length === 0) break;
771
+ beam.length = 0;
772
+ beam.push(...nextBeam
773
+ .sort((a, b) => b.score - a.score)
774
+ .slice(0, subBeamWidth)
775
+ );
715
776
  }
777
+ }
716
778
 
717
- // Prune beam for next depth level
718
- if (nextBeam.length === 0) break;
719
- beam.length = 0;
720
- beam.push(...nextBeam
721
- .sort((a, b) => b.score - a.score)
722
- .slice(0, BEAM_WIDTH)
723
- );
779
+ // Apply fused boosts to scored items
780
+ for (const [engramId, totalBoost] of boostAccum) {
781
+ const item = scoreMap.get(engramId);
782
+ if (!item) continue;
783
+ const capped = Math.min(totalBoost, MAX_TOTAL_BOOST - item.phaseScores.graphBoost);
784
+ if (capped > 0.001) {
785
+ item.score += capped;
786
+ item.phaseScores.graphBoost += capped;
787
+ }
724
788
  }
725
789
  }
726
790
 
727
- private updateHebbianWeights(): void {
728
- const pairs = this.coActivationBuffer.getCoActivatedPairs(10_000);
729
- // Deduplicate pairs to prevent repeated strengthening
730
- const seen = new Set<string>();
731
- for (const [a, b] of pairs) {
732
- const key = a < b ? `${a}:${b}` : `${b}:${a}`;
733
- if (seen.has(key)) continue;
734
- seen.add(key);
791
+ /**
792
+ * Resolve validation-gated Hebbian update for a specific engram.
793
+ * Called by memory_feedback only strengthens when retrieval was useful.
794
+ * This prevents hub toxicity from noisy co-retrieval (Kairos-inspired).
795
+ */
796
+ resolveHebbianFeedback(engramId: string, useful: boolean): number {
797
+ const { pairs, signal } = this.validationGate.resolveFeedback(engramId, useful);
798
+ let updated = 0;
735
799
 
800
+ for (const [a, b] of pairs) {
736
801
  const existing = this.store.getAssociation(a, b) ?? this.store.getAssociation(b, a);
737
802
  const currentWeight = existing?.weight ?? 0.1;
738
- const newWeight = strengthenAssociation(currentWeight);
739
- this.store.upsertAssociation(a, b, newWeight, 'hebbian');
740
- this.store.upsertAssociation(b, a, newWeight, 'hebbian');
803
+
804
+ if (signal > 0) {
805
+ // Positive feedback → strengthen
806
+ const newWeight = strengthenAssociation(currentWeight, signal);
807
+ this.store.upsertAssociation(a, b, newWeight, 'hebbian');
808
+ this.store.upsertAssociation(b, a, newWeight, 'hebbian');
809
+ } else {
810
+ // Negative feedback → slight weakening (decay by signal magnitude)
811
+ const newWeight = Math.max(0.001, currentWeight * (1 + signal)); // signal is -0.3
812
+ this.store.upsertAssociation(a, b, newWeight, 'hebbian');
813
+ this.store.upsertAssociation(b, a, newWeight, 'hebbian');
814
+ }
815
+ updated++;
741
816
  }
817
+ return updated;
742
818
  }
743
819
 
744
820
  private explain(phases: PhaseScores, engram: Engram, associations: Association[]): string {
@@ -144,13 +144,22 @@ export class ConsolidationEngine {
144
144
  const needsEmbedding = allActive.filter(e => !e.embedding || e.embedding.length === 0);
145
145
  if (needsEmbedding.length > 0) {
146
146
  try {
147
- const { embed } = await import('../core/embeddings.js');
148
- for (const e of needsEmbedding) {
147
+ const { embedBatch, getModelId } = await import('../core/embeddings.js');
148
+ const BATCH_SIZE = 32;
149
+ const modelId = getModelId?.() ?? 'unknown';
150
+ for (let b = 0; b < needsEmbedding.length; b += BATCH_SIZE) {
151
+ const batch = needsEmbedding.slice(b, b + BATCH_SIZE);
149
152
  try {
150
- const vec = await embed(`${e.concept} ${e.content}`);
151
- this.store.updateEmbedding(e.id, vec);
152
- e.embedding = vec;
153
- } catch { /* non-fatal */ }
153
+ const texts = batch.map(e => `${e.concept} ${e.content}`);
154
+ const vecs = await embedBatch(texts);
155
+ for (let j = 0; j < batch.length; j++) {
156
+ this.store.updateEmbedding(batch[j].id, vecs[j], modelId);
157
+ batch[j].embedding = vecs[j];
158
+ }
159
+ } catch { /* batch failed, skip — non-fatal */ }
160
+ }
161
+ if (needsEmbedding.length > 0) {
162
+ console.log(`[consolidation] Backfilled ${needsEmbedding.filter(e => e.embedding?.length).length}/${needsEmbedding.length} embeddings (model: ${modelId})`);
154
163
  }
155
164
  } catch { /* embeddings module unavailable */ }
156
165
  }
@@ -60,40 +60,56 @@ export class RetractionEngine {
60
60
  this.store.retractEngram(target.id, correction.id);
61
61
  }
62
62
 
63
- // Reduce confidence of closely associated engrams (contamination spread)
64
- const associatesAffected = this.propagateConfidenceReduction(target.id, 0.1, 1);
63
+ // Reduce confidence of associated engrams (contamination spread)
64
+ // Depth 2 with 50% decay per hop, capped at 20 total affected nodes
65
+ const associatesAffected = this.propagateConfidenceReduction(target.id, 0.1, 2);
65
66
 
66
67
  return { retractedId: target.id, correctionId, associatesAffected };
67
68
  }
68
69
 
69
70
  /**
70
71
  * Reduce confidence of engrams associated with a retracted engram.
71
- * Shallow propagation (depth 1) to avoid over-penalizing.
72
+ * Propagates up to maxDepth hops with decaying penalty (50% per hop).
73
+ * Capped at MAX_AFFECTED to prevent cascading through the graph.
72
74
  */
75
+ private static readonly MAX_AFFECTED = 20;
76
+
73
77
  private propagateConfidenceReduction(
74
78
  engramId: string,
75
79
  penalty: number,
76
80
  maxDepth: number,
77
- currentDepth: number = 0
81
+ currentDepth: number = 0,
82
+ visited: Set<string> = new Set(),
78
83
  ): number {
79
84
  if (currentDepth >= maxDepth) return 0;
85
+ if (visited.size >= RetractionEngine.MAX_AFFECTED) return 0;
86
+ visited.add(engramId);
80
87
 
81
88
  let affected = 0;
82
89
  const associations = this.store.getAssociationsFor(engramId);
83
90
  for (const assoc of associations) {
84
91
  if (assoc.type === 'invalidation') continue; // Don't penalize corrections
92
+ if (visited.size >= RetractionEngine.MAX_AFFECTED) break;
85
93
 
86
94
  const neighborId = assoc.fromEngramId === engramId
87
95
  ? assoc.toEngramId
88
96
  : assoc.fromEngramId;
97
+ if (visited.has(neighborId)) continue;
98
+
89
99
  const neighbor = this.store.getEngram(neighborId);
90
100
  if (!neighbor || neighbor.retracted) continue;
91
101
 
92
- // Scale penalty by association weight
93
- const scaledPenalty = penalty * assoc.weight;
102
+ // Scale penalty by association weight and decay per hop (50% per depth level)
103
+ const depthDecay = Math.pow(0.5, currentDepth);
104
+ const scaledPenalty = penalty * assoc.weight * depthDecay;
94
105
  const newConfidence = Math.max(0.1, neighbor.confidence - scaledPenalty);
95
106
  this.store.updateConfidence(neighborId, newConfidence);
96
107
  affected++;
108
+
109
+ // Recurse to next depth
110
+ affected += this.propagateConfidenceReduction(
111
+ neighborId, penalty, maxDepth, currentDepth + 1, visited
112
+ );
97
113
  }
98
114
  return affected;
99
115
  }
package/src/index.ts CHANGED
@@ -101,7 +101,8 @@ async function main() {
101
101
  const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
102
102
 
103
103
  // API — disable Fastify's default request logging (too noisy for hive polling)
104
- const app = Fastify({ logger: false });
104
+ // bodyLimit: 512KB to prevent Content-Length mismatch errors with large task payloads
105
+ const app = Fastify({ logger: false, bodyLimit: 512_000 });
105
106
 
106
107
  // Bearer token auth — only enforced when AWM_API_KEY is explicitly set and non-empty
107
108
  if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
@@ -122,19 +123,9 @@ async function main() {
122
123
  });
123
124
 
124
125
  // Coordination module (opt-in via AWM_COORDINATION=true)
125
- let heartbeatPruneTimer: ReturnType<typeof setInterval> | null = null;
126
- const { isCoordinationEnabled, initCoordination } = await import('./coordination/index.js');
126
+ const { isCoordinationEnabled, initCoordination, stopCoordinationCleanup } = await import('./coordination/index.js');
127
127
  if (isCoordinationEnabled()) {
128
- initCoordination(app, store.getDb());
129
- // Prune stale heartbeat events every 30s (keeps assignment/command events permanently)
130
- // Purge dead agents older than 24h every 30s to prevent table bloat
131
- const { pruneOldHeartbeats, purgeDeadAgents } = await import('./coordination/stale.js');
132
- heartbeatPruneTimer = setInterval(() => {
133
- const pruned = pruneOldHeartbeats(store.getDb());
134
- if (pruned > 0) console.log(`[coordination] pruned ${pruned} old heartbeat event(s)`);
135
- const purged = purgeDeadAgents(store.getDb());
136
- if (purged > 0) console.log(`[coordination] purged ${purged} dead agent(s) older than 24h`);
137
- }, 30_000);
128
+ initCoordination(app, store.getDb(), store);
138
129
  } else {
139
130
  console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
140
131
  }
@@ -189,9 +180,9 @@ async function main() {
189
180
  console.log(`AgentWorkingMemory v0.6.0 listening on port ${PORT}`);
190
181
 
191
182
  // Graceful shutdown
192
- const shutdown = () => {
183
+ const shutdown = async () => {
193
184
  clearInterval(backupTimer);
194
- if (heartbeatPruneTimer) clearInterval(heartbeatPruneTimer);
185
+ await stopCoordinationCleanup();
195
186
  consolidationScheduler.stop();
196
187
  stagingBuffer.stop();
197
188
  try { store.walCheckpoint(); } catch { /* non-fatal */ }