agent-working-memory 0.6.1 → 0.7.1
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 +27 -8
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +9 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +108 -10
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.js +103 -103
- package/dist/core/auto-tagger.d.ts +29 -0
- package/dist/core/auto-tagger.d.ts.map +1 -0
- package/dist/core/auto-tagger.js +139 -0
- package/dist/core/auto-tagger.js.map +1 -0
- 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/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.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 +136 -73
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts +1 -0
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +149 -9
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp.js +123 -84
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +17 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +73 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +2 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/common.ts +9 -1
- package/src/api/routes.ts +723 -600
- package/src/cli.ts +719 -719
- package/src/core/auto-tagger.ts +168 -0
- package/src/core/hebbian.ts +84 -3
- package/src/core/query-expander.ts +0 -1
- package/src/core/reranker.ts +0 -1
- package/src/engine/activation.ts +136 -70
- package/src/engine/consolidation.ts +165 -9
- package/src/index.ts +199 -199
- package/src/mcp.ts +1134 -1099
- package/src/storage/sqlite.ts +77 -0
- package/src/types/engram.ts +2 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Auto-Tagger — generates meta-tags for memories at write time.
|
|
5
|
+
*
|
|
6
|
+
* Meta-tags serve as categorical signals that boost BM25 recall.
|
|
7
|
+
* They're indexed in FTS5 alongside concept/content/tags, so queries
|
|
8
|
+
* that match a category tag get better BM25 scores.
|
|
9
|
+
*
|
|
10
|
+
* Three sources of tags:
|
|
11
|
+
* 1. Content analysis — extract topics, entities, categories from the text
|
|
12
|
+
* 2. Context propagation — inherit relevant tags from related memories
|
|
13
|
+
* 3. Type classification — fact/experience/belief/entity markers
|
|
14
|
+
*
|
|
15
|
+
* Design: lightweight heuristics, no LLM calls. Tags are additive —
|
|
16
|
+
* they enrich the existing tag set without replacing user-provided tags.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract meta-tags from memory content using keyword patterns.
|
|
21
|
+
* Returns tags prefixed with 'cat:' to distinguish from user tags.
|
|
22
|
+
*/
|
|
23
|
+
export function extractMetaTags(concept: string, content: string): string[] {
|
|
24
|
+
const tags: string[] = [];
|
|
25
|
+
const text = `${concept} ${content}`.toLowerCase();
|
|
26
|
+
|
|
27
|
+
// --- Category tags (broad topic classification) ---
|
|
28
|
+
|
|
29
|
+
// People / personal
|
|
30
|
+
if (/\b(i |my |i'm |i've |we |our |me )\b/.test(text)) {
|
|
31
|
+
tags.push('cat:personal');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Work / professional
|
|
35
|
+
if (/\b(work|job|office|meeting|project|team|manager|colleague|career|salary|hired)\b/.test(text)) {
|
|
36
|
+
tags.push('cat:work');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Technology / computing
|
|
40
|
+
if (/\b(code|programming|software|database|api|server|deploy|bug|git|typescript|python|react|node)\b/.test(text)) {
|
|
41
|
+
tags.push('cat:tech');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Health / wellness
|
|
45
|
+
if (/\b(health|doctor|exercise|yoga|gym|diet|sleep|meditation|therapy|medicine|symptom)\b/.test(text)) {
|
|
46
|
+
tags.push('cat:health');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Finance / money
|
|
50
|
+
if (/\b(money|budget|savings|invest|salary|cost|price|payment|bank|credit|expense|coupon|store|bought|purchased)\b/.test(text)) {
|
|
51
|
+
tags.push('cat:finance');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Home / living
|
|
55
|
+
if (/\b(home|house|apartment|room|kitchen|bedroom|furniture|garden|repair|renovation|neighbor|moved)\b/.test(text)) {
|
|
56
|
+
tags.push('cat:home');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Travel / location
|
|
60
|
+
if (/\b(travel|trip|vacation|flight|hotel|restaurant|city|country|visited|downtown|park)\b/.test(text)) {
|
|
61
|
+
tags.push('cat:location');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Education / learning
|
|
65
|
+
if (/\b(school|university|college|degree|course|class|study|learn|graduate|student|teacher|exam)\b/.test(text)) {
|
|
66
|
+
tags.push('cat:education');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Social / relationships
|
|
70
|
+
if (/\b(friend|family|partner|spouse|child|parent|sibling|birthday|party|dinner|wedding|date)\b/.test(text)) {
|
|
71
|
+
tags.push('cat:social');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Hobbies / entertainment
|
|
75
|
+
if (/\b(music|movie|book|game|sport|hobby|play|concert|theater|playlist|podcast|guitar|tennis|yoga|painting)\b/.test(text)) {
|
|
76
|
+
tags.push('cat:hobby');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Shopping / consumer
|
|
80
|
+
if (/\b(bought|purchased|ordered|shop|store|amazon|target|walmart|coupon|sale|discount|delivery)\b/.test(text)) {
|
|
81
|
+
tags.push('cat:shopping');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Food / cooking
|
|
85
|
+
if (/\b(cook|recipe|restaurant|meal|food|dinner|lunch|breakfast|coffee|tea|bake|kitchen)\b/.test(text)) {
|
|
86
|
+
tags.push('cat:food');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pets / animals
|
|
90
|
+
if (/\b(pet|dog|cat|animal|vet|shelter|walk|breed)\b/.test(text)) {
|
|
91
|
+
tags.push('cat:pets');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Time markers
|
|
95
|
+
if (/\b(yesterday|today|last week|last month|tomorrow|next week|this morning|this evening|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/.test(text)) {
|
|
96
|
+
tags.push('cat:temporal');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Entity extraction (simple noun phrase patterns) ---
|
|
100
|
+
|
|
101
|
+
// Proper nouns (capitalized words that aren't sentence starters)
|
|
102
|
+
const properNouns = content.match(/(?:^|\.\s+)?([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/g);
|
|
103
|
+
if (properNouns) {
|
|
104
|
+
const unique = [...new Set(properNouns.map(n => n.trim()).filter(n => n.length > 2 && n.length < 30))];
|
|
105
|
+
for (const noun of unique.slice(0, 5)) {
|
|
106
|
+
tags.push(`entity:${noun}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Knowledge type tags ---
|
|
111
|
+
|
|
112
|
+
// Preference (I like/prefer/enjoy/love/hate)
|
|
113
|
+
if (/\b(i like|i prefer|i enjoy|i love|i hate|my favorite|i don't like)\b/.test(text)) {
|
|
114
|
+
tags.push('cat:preference');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fact (declarative statements about identity/attributes)
|
|
118
|
+
if (/\b(my name is|i am a|i work at|i live in|i graduated|my birthday|i was born)\b/.test(text)) {
|
|
119
|
+
tags.push('cat:identity');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Plan / intention
|
|
123
|
+
if (/\b(i plan to|i'm going to|i want to|i'm thinking of|i'm considering|next week i|planning to)\b/.test(text)) {
|
|
124
|
+
tags.push('cat:plan');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Experience / event
|
|
128
|
+
if (/\b(i went|i visited|i attended|i tried|i saw|i heard|i found|i discovered)\b/.test(text)) {
|
|
129
|
+
tags.push('cat:experience');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return tags;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Propagate relevant tags from related memories to a new memory.
|
|
137
|
+
* Called after connection engine links the new memory to existing ones.
|
|
138
|
+
*
|
|
139
|
+
* Strategy: inherit meta-tags from strongly connected neighbors,
|
|
140
|
+
* but only tags that appear in 2+ neighbors (consensus filtering).
|
|
141
|
+
*/
|
|
142
|
+
export function propagateTagsFromNeighbors(
|
|
143
|
+
existingTags: string[],
|
|
144
|
+
neighborTagSets: string[][],
|
|
145
|
+
): string[] {
|
|
146
|
+
if (neighborTagSets.length === 0) return [];
|
|
147
|
+
|
|
148
|
+
// Count meta-tag occurrences across neighbors
|
|
149
|
+
const tagCounts = new Map<string, number>();
|
|
150
|
+
for (const tags of neighborTagSets) {
|
|
151
|
+
for (const tag of tags) {
|
|
152
|
+
if (tag.startsWith('cat:') || tag.startsWith('entity:')) {
|
|
153
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Only propagate tags that appear in 2+ neighbors (consensus)
|
|
159
|
+
const propagated: string[] = [];
|
|
160
|
+
const existingSet = new Set(existingTags);
|
|
161
|
+
for (const [tag, count] of tagCounts) {
|
|
162
|
+
if (count >= 2 && !existingSet.has(tag)) {
|
|
163
|
+
propagated.push(tag);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return propagated.slice(0, 5); // Cap to avoid tag bloat
|
|
168
|
+
}
|
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
|
+
}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
|
|
16
16
|
|
|
17
17
|
const MODEL_ID = 'Xenova/flan-t5-small';
|
|
18
|
-
|
|
19
18
|
let instance: Text2TextGenerationPipeline | null = null;
|
|
20
19
|
let initPromise: Promise<Text2TextGenerationPipeline> | null = null;
|
|
21
20
|
|
package/src/core/reranker.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
25
25
|
const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
|
|
26
|
-
|
|
27
26
|
let tokenizer: PreTrainedTokenizer | null = null;
|
|
28
27
|
let model: PreTrainedModel | null = null;
|
|
29
28
|
let initPromise: Promise<void> | null = null;
|
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)))
|
|
@@ -228,9 +234,9 @@ export class ActivationEngine {
|
|
|
228
234
|
// Two-pass BM25: (1) keyword-stripped query for precision, (2) expanded query for recall.
|
|
229
235
|
const keywordQuery = Array.from(tokenize(query.context)).join(' ');
|
|
230
236
|
const bm25Keyword = keywordQuery.length > 2
|
|
231
|
-
? this.store.
|
|
237
|
+
? this.store.searchBM25WithRankMultiAgent(agentIds, keywordQuery, limit * 3)
|
|
232
238
|
: [];
|
|
233
|
-
const bm25Expanded = this.store.
|
|
239
|
+
const bm25Expanded = this.store.searchBM25WithRankMultiAgent(agentIds, searchContext, limit * 3);
|
|
234
240
|
|
|
235
241
|
// Merge: take the best BM25 score per engram from either pass
|
|
236
242
|
const bm25ScoreMap = new Map<string, number>();
|
|
@@ -246,8 +252,8 @@ export class ActivationEngine {
|
|
|
246
252
|
engram, bm25Score: bm25ScoreMap.get(id) ?? 0,
|
|
247
253
|
}));
|
|
248
254
|
|
|
249
|
-
const allActive = this.store.
|
|
250
|
-
|
|
255
|
+
const allActive = this.store.getEngramsByAgents(
|
|
256
|
+
agentIds,
|
|
251
257
|
query.includeStaging ? undefined : 'active',
|
|
252
258
|
query.includeRetracted ?? false
|
|
253
259
|
);
|
|
@@ -402,7 +408,7 @@ export class ActivationEngine {
|
|
|
402
408
|
// Take top 5 feedback terms and re-search
|
|
403
409
|
const extraTerms = Array.from(feedbackTerms).slice(0, 5).join(' ');
|
|
404
410
|
if (extraTerms) {
|
|
405
|
-
const feedbackBM25 = this.store.
|
|
411
|
+
const feedbackBM25 = this.store.searchBM25WithRankMultiAgent(agentIds, `${searchContext} ${extraTerms}`, limit * 2);
|
|
406
412
|
for (const r of feedbackBM25) {
|
|
407
413
|
if (!candidateMap.has(r.engram.id)) {
|
|
408
414
|
candidateMap.set(r.engram.id, r.engram);
|
|
@@ -632,13 +638,21 @@ export class ActivationEngine {
|
|
|
632
638
|
|
|
633
639
|
const activatedIds = results.map(r => r.engram.id);
|
|
634
640
|
|
|
635
|
-
// Side effects: touch, co-activate, Hebbian
|
|
641
|
+
// Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)
|
|
636
642
|
if (!query.internal) {
|
|
637
643
|
for (const id of activatedIds) {
|
|
638
644
|
this.store.touchEngram(id);
|
|
639
645
|
}
|
|
640
646
|
this.coActivationBuffer.pushBatch(activatedIds);
|
|
641
|
-
|
|
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);
|
|
642
656
|
|
|
643
657
|
// Log activation event for eval
|
|
644
658
|
const latencyMs = performance.now() - startTime;
|
|
@@ -658,10 +672,27 @@ export class ActivationEngine {
|
|
|
658
672
|
}
|
|
659
673
|
|
|
660
674
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
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.
|
|
664
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
|
+
|
|
665
696
|
private graphWalk(
|
|
666
697
|
scored: { engram: Engram; score: number; phaseScores: PhaseScores; associations: Association[] }[],
|
|
667
698
|
maxDepth: number,
|
|
@@ -670,85 +701,120 @@ export class ActivationEngine {
|
|
|
670
701
|
): void {
|
|
671
702
|
const scoreMap = new Map(scored.map(s => [s.engram.id, s]));
|
|
672
703
|
const MAX_TOTAL_BOOST = 0.25;
|
|
673
|
-
const BEAM_WIDTH = beamWidth;
|
|
674
704
|
|
|
675
|
-
//
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
680
715
|
|
|
681
|
-
|
|
682
|
-
|
|
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));
|
|
683
719
|
|
|
684
|
-
|
|
685
|
-
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 = [];
|
|
686
730
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
731
|
+
for (const item of beam) {
|
|
732
|
+
if (explored.has(item.engram.id)) continue;
|
|
733
|
+
explored.add(item.engram.id);
|
|
690
734
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: this.store.getAssociationsFor(item.engram.id);
|
|
735
|
+
const associations = item.associations.length > 0
|
|
736
|
+
? item.associations
|
|
737
|
+
: this.store.getAssociationsFor(item.engram.id);
|
|
695
738
|
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
? assoc.toEngramId
|
|
699
|
-
: assoc.fromEngramId;
|
|
739
|
+
// Filter to only edges of this sub-graph type
|
|
740
|
+
const relevantEdges = associations.filter(a => edgeTypes.includes(a.type));
|
|
700
741
|
|
|
701
|
-
|
|
742
|
+
for (const assoc of relevantEdges) {
|
|
743
|
+
const neighborId = assoc.fromEngramId === item.engram.id
|
|
744
|
+
? assoc.toEngramId
|
|
745
|
+
: assoc.fromEngramId;
|
|
702
746
|
|
|
703
|
-
|
|
704
|
-
|
|
747
|
+
if (explored.has(neighborId)) continue;
|
|
748
|
+
const neighbor = scoreMap.get(neighborId);
|
|
749
|
+
if (!neighbor) continue;
|
|
705
750
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const relevanceFloor = depth === 0 ? 0.1 : 0.05;
|
|
709
|
-
if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
|
|
751
|
+
const relevanceFloor = depth === 0 ? 0.1 : 0.05;
|
|
752
|
+
if (neighbor.phaseScores.textMatch < relevanceFloor) continue;
|
|
710
753
|
|
|
711
|
-
|
|
712
|
-
|
|
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);
|
|
713
757
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
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;
|
|
717
760
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
+
}
|
|
723
767
|
}
|
|
724
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
|
+
);
|
|
725
776
|
}
|
|
777
|
+
}
|
|
726
778
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
}
|
|
734
788
|
}
|
|
735
789
|
}
|
|
736
790
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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;
|
|
745
799
|
|
|
800
|
+
for (const [a, b] of pairs) {
|
|
746
801
|
const existing = this.store.getAssociation(a, b) ?? this.store.getAssociation(b, a);
|
|
747
802
|
const currentWeight = existing?.weight ?? 0.1;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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++;
|
|
751
816
|
}
|
|
817
|
+
return updated;
|
|
752
818
|
}
|
|
753
819
|
|
|
754
820
|
private explain(phases: PhaseScores, engram: Engram, associations: Association[]): string {
|