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.
- package/README.md +19 -9
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +218 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +4 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +226 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +145 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/cursor.d.ts +4 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +138 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/http.d.ts +4 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +88 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +4 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +1 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.js +104 -230
- package/dist/cli.js.map +1 -1
- package/dist/coordination/events.d.ts +59 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +28 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/index.d.ts +10 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +87 -3
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/peer-decisions.d.ts +40 -0
- package/dist/coordination/peer-decisions.d.ts.map +1 -0
- package/dist/coordination/peer-decisions.js +82 -0
- package/dist/coordination/peer-decisions.js.map +1 -0
- package/dist/coordination/plugin-loader.d.ts +18 -0
- package/dist/coordination/plugin-loader.d.ts.map +1 -0
- package/dist/coordination/plugin-loader.js +55 -0
- package/dist/coordination/plugin-loader.js.map +1 -0
- package/dist/coordination/plugin.d.ts +40 -0
- package/dist/coordination/plugin.d.ts.map +1 -0
- package/dist/coordination/plugin.js +22 -0
- package/dist/coordination/plugin.js.map +1 -0
- package/dist/coordination/routes.d.ts +2 -1
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +899 -76
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +72 -14
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +84 -3
- package/dist/coordination/schemas.d.ts.map +1 -1
- package/dist/coordination/schemas.js +71 -1
- package/dist/coordination/schemas.js.map +1 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +2 -1
- package/dist/coordination/stale.js.map +1 -1
- package/dist/coordination/types.d.ts +252 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +8 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/coordination/write-mutex.d.ts +26 -0
- package/dist/coordination/write-mutex.d.ts.map +1 -0
- package/dist/coordination/write-mutex.js +63 -0
- package/dist/coordination/write-mutex.js.map +1 -0
- package/dist/core/embeddings.d.ts +2 -0
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +4 -0
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/hebbian.d.ts +25 -1
- package/dist/core/hebbian.d.ts.map +1 -1
- package/dist/core/hebbian.js +71 -3
- package/dist/core/hebbian.js.map +1 -1
- package/dist/engine/activation.d.ts +22 -4
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +152 -76
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +15 -6
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/retraction.d.ts +3 -1
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +19 -6
- package/dist/engine/retraction.js.map +1 -1
- package/dist/index.js +6 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +61 -4
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +23 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +112 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +1 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +234 -0
- package/src/adapters/codex.ts +262 -0
- package/src/adapters/common.ts +172 -0
- package/src/adapters/cursor.ts +150 -0
- package/src/adapters/http.ts +100 -0
- package/src/adapters/index.ts +31 -0
- package/src/adapters/types.ts +75 -0
- package/src/api/routes.ts +2 -0
- package/src/cli.ts +107 -238
- package/src/coordination/events.ts +90 -0
- package/src/coordination/index.ts +102 -3
- package/src/coordination/peer-decisions.ts +105 -0
- package/src/coordination/plugin-loader.ts +60 -0
- package/src/coordination/plugin.ts +44 -0
- package/src/coordination/routes.ts +1176 -105
- package/src/coordination/schema.ts +67 -14
- package/src/coordination/schemas.ts +85 -1
- package/src/coordination/stale.ts +3 -2
- package/src/coordination/types.ts +311 -0
- package/src/coordination/write-mutex.ts +69 -0
- package/src/core/embeddings.ts +5 -0
- package/src/core/hebbian.ts +84 -3
- package/src/engine/activation.ts +149 -73
- package/src/engine/consolidation.ts +15 -6
- package/src/engine/retraction.ts +22 -6
- package/src/index.ts +6 -15
- package/src/mcp.ts +77 -4
- package/src/storage/sqlite.ts +116 -3
- package/src/types/engram.ts +1 -0
package/src/core/hebbian.ts
CHANGED
|
@@ -30,14 +30,25 @@ export function strengthenAssociation(
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Weaken an association weight due to lack of co-activation.
|
|
33
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|
package/src/engine/activation.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
237
|
+
? this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
226
238
|
: [];
|
|
227
|
-
const bm25Expanded = this.store.
|
|
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.
|
|
244
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
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
|
-
//
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
675
|
-
const
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
731
|
+
for (const item of beam) {
|
|
732
|
+
if (explored.has(item.engram.id)) continue;
|
|
733
|
+
explored.add(item.engram.id);
|
|
680
734
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
687
|
-
const
|
|
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
|
-
|
|
742
|
+
for (const assoc of relevantEdges) {
|
|
743
|
+
const neighborId = assoc.fromEngramId === item.engram.id
|
|
744
|
+
? assoc.toEngramId
|
|
745
|
+
: assoc.fromEngramId;
|
|
692
746
|
|
|
693
|
-
|
|
694
|
-
|
|
747
|
+
if (explored.has(neighborId)) continue;
|
|
748
|
+
const neighbor = scoreMap.get(neighborId);
|
|
749
|
+
if (!neighbor) continue;
|
|
695
750
|
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
702
|
-
|
|
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
|
-
|
|
705
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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 {
|
|
148
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
}
|
package/src/engine/retraction.ts
CHANGED
|
@@ -60,40 +60,56 @@ export class RetractionEngine {
|
|
|
60
60
|
this.store.retractEngram(target.id, correction.id);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// Reduce confidence of
|
|
64
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
await stopCoordinationCleanup();
|
|
195
186
|
consolidationScheduler.stop();
|
|
196
187
|
stagingBuffer.stop();
|
|
197
188
|
try { store.walCheckpoint(); } catch { /* non-fatal */ }
|