agent-working-memory 0.4.2 → 0.5.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/LICENSE +190 -21
- package/README.md +21 -3
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +7 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +69 -67
- package/dist/cli.js.map +1 -1
- package/dist/core/decay.d.ts.map +1 -1
- package/dist/core/decay.js +2 -0
- package/dist/core/decay.js.map +1 -1
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +2 -0
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/hebbian.d.ts.map +1 -1
- package/dist/core/hebbian.js +2 -0
- package/dist/core/hebbian.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +2 -0
- package/dist/core/logger.js.map +1 -1
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +2 -0
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +2 -0
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.d.ts +3 -1
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +29 -10
- package/dist/core/salience.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +9 -0
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/connections.d.ts.map +1 -1
- package/dist/engine/connections.js +2 -0
- package/dist/engine/connections.js.map +1 -1
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
- package/dist/engine/consolidation-scheduler.js +2 -0
- package/dist/engine/consolidation-scheduler.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +5 -3
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +2 -0
- package/dist/engine/eval.js.map +1 -1
- package/dist/engine/eviction.d.ts.map +1 -1
- package/dist/engine/eviction.js +2 -0
- package/dist/engine/eviction.js.map +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +2 -0
- package/dist/engine/retraction.js.map +1 -1
- package/dist/engine/staging.d.ts.map +1 -1
- package/dist/engine/staging.js +2 -0
- package/dist/engine/staging.js.map +1 -1
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +2 -0
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +69 -3
- package/dist/mcp.js.map +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/sqlite.d.ts +12 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +52 -5
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js +2 -0
- package/dist/types/agent.js.map +1 -1
- package/dist/types/checkpoint.d.ts.map +1 -1
- package/dist/types/checkpoint.js +2 -0
- package/dist/types/checkpoint.js.map +1 -1
- package/dist/types/engram.d.ts +16 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js +2 -0
- package/dist/types/engram.js.map +1 -1
- package/dist/types/eval.d.ts.map +1 -1
- package/dist/types/eval.js +2 -0
- package/dist/types/eval.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +2 -2
- package/src/api/index.ts +2 -0
- package/src/api/routes.ts +8 -0
- package/src/cli.ts +385 -383
- package/src/core/decay.ts +2 -0
- package/src/core/embeddings.ts +2 -0
- package/src/core/hebbian.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/logger.ts +2 -0
- package/src/core/query-expander.ts +2 -0
- package/src/core/reranker.ts +2 -0
- package/src/core/salience.ts +34 -13
- package/src/engine/activation.ts +10 -0
- package/src/engine/connections.ts +2 -0
- package/src/engine/consolidation-scheduler.ts +125 -123
- package/src/engine/consolidation.ts +5 -3
- package/src/engine/eval.ts +2 -0
- package/src/engine/eviction.ts +2 -0
- package/src/engine/index.ts +2 -0
- package/src/engine/retraction.ts +2 -0
- package/src/engine/staging.ts +2 -0
- package/src/hooks/sidecar.ts +2 -0
- package/src/index.ts +2 -0
- package/src/mcp.ts +82 -3
- package/src/storage/index.ts +2 -0
- package/src/storage/sqlite.ts +61 -5
- package/src/types/agent.ts +2 -0
- package/src/types/checkpoint.ts +46 -44
- package/src/types/engram.ts +23 -0
- package/src/types/eval.ts +2 -0
- package/src/types/index.ts +2 -0
package/src/core/decay.ts
CHANGED
package/src/core/embeddings.ts
CHANGED
package/src/core/hebbian.ts
CHANGED
package/src/core/index.ts
CHANGED
package/src/core/logger.ts
CHANGED
package/src/core/reranker.ts
CHANGED
package/src/core/salience.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Salience Filter — decides what's worth remembering.
|
|
3
5
|
*
|
|
@@ -8,7 +10,7 @@
|
|
|
8
10
|
* - Deterministic heuristics first, LLM augmentation optional
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import type { SalienceFeatures } from '../types/index.js';
|
|
13
|
+
import type { SalienceFeatures, MemoryClass } from '../types/index.js';
|
|
12
14
|
import type { EngramStore } from '../storage/sqlite.js';
|
|
13
15
|
|
|
14
16
|
export type SalienceEventType = 'decision' | 'friction' | 'surprise' | 'causal' | 'observation';
|
|
@@ -22,6 +24,8 @@ export interface SalienceInput {
|
|
|
22
24
|
resolutionEffort?: number;
|
|
23
25
|
/** 0 = exact duplicate exists, 1 = completely novel. Computed by caller via BM25 similarity check. */
|
|
24
26
|
novelty?: number;
|
|
27
|
+
/** Memory class — canonical memories get salience floor of 0.7 and never stage. */
|
|
28
|
+
memoryClass?: MemoryClass;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface SalienceResult {
|
|
@@ -90,10 +94,25 @@ export function evaluateSalience(
|
|
|
90
94
|
case 'observation': break;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
let score = Math.min(surpriseScore + decisionScore + causalScore + effortScore + noveltyScore + typeBonus, 1.0);
|
|
98
|
+
|
|
99
|
+
// Memory class overrides
|
|
100
|
+
const memoryClass = input.memoryClass ?? 'working';
|
|
101
|
+
|
|
102
|
+
if (memoryClass === 'canonical') {
|
|
103
|
+
// Canonical memories: salience floor of 0.7, never go to staging
|
|
104
|
+
score = Math.max(score, 0.7);
|
|
105
|
+
reasonCodes.push('class:canonical');
|
|
106
|
+
} else if (memoryClass === 'ephemeral') {
|
|
107
|
+
reasonCodes.push('class:ephemeral');
|
|
108
|
+
}
|
|
94
109
|
|
|
95
110
|
let disposition: 'active' | 'staging' | 'discard';
|
|
96
|
-
if (
|
|
111
|
+
if (memoryClass === 'canonical') {
|
|
112
|
+
// Canonical always goes active — they represent current truth
|
|
113
|
+
disposition = 'active';
|
|
114
|
+
reasonCodes.push('disposition:active');
|
|
115
|
+
} else if (score >= activeThreshold) {
|
|
97
116
|
disposition = 'active';
|
|
98
117
|
reasonCodes.push('disposition:active');
|
|
99
118
|
} else if (score >= stagingThreshold) {
|
|
@@ -118,22 +137,24 @@ export function evaluateSalience(
|
|
|
118
137
|
* The check is cheap (~1ms) because BM25 is synchronous SQLite FTS5.
|
|
119
138
|
*/
|
|
120
139
|
export function computeNovelty(store: EngramStore, agentId: string, concept: string, content: string): number {
|
|
121
|
-
// Search using concept + first 100 chars of content (enough to detect duplicates, fast)
|
|
122
|
-
const searchText = `${concept} ${content.slice(0, 100)}`;
|
|
123
|
-
|
|
124
140
|
try {
|
|
141
|
+
// Search using concept + first 100 chars of content (enough to detect duplicates, fast)
|
|
142
|
+
const contentStr = typeof content === 'string' ? content : '';
|
|
143
|
+
const conceptStr = typeof concept === 'string' ? concept : '';
|
|
144
|
+
const searchText = `${conceptStr} ${contentStr.slice(0, 100)}`;
|
|
145
|
+
|
|
125
146
|
const results = store.searchBM25WithRank(agentId, searchText, 3);
|
|
126
147
|
if (results.length === 0) return 1.0; // Nothing similar — fully novel
|
|
127
148
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
149
|
+
// searchBM25WithRank normalizes scores to 0..1 via |rank|/(1+|rank|).
|
|
150
|
+
// Higher score = stronger match = less novel.
|
|
130
151
|
const topScore = results[0].bm25Score;
|
|
131
152
|
|
|
132
|
-
if (topScore >
|
|
133
|
-
if (topScore >
|
|
134
|
-
if (topScore >
|
|
135
|
-
if (topScore >
|
|
136
|
-
return 0.9;
|
|
153
|
+
if (topScore > 0.95) return 0.1; // Near-duplicate
|
|
154
|
+
if (topScore > 0.85) return 0.3; // High overlap
|
|
155
|
+
if (topScore > 0.70) return 0.5; // Moderate overlap
|
|
156
|
+
if (topScore > 0.50) return 0.7; // Some overlap
|
|
157
|
+
return 0.9; // Minimal overlap — mostly novel
|
|
137
158
|
} catch {
|
|
138
159
|
// If BM25 search fails (e.g., FTS not ready), assume novel
|
|
139
160
|
return 0.8;
|
package/src/engine/activation.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Activation Pipeline — the core retrieval engine.
|
|
3
5
|
*
|
|
@@ -428,6 +430,14 @@ export class ActivationEngine {
|
|
|
428
430
|
}
|
|
429
431
|
}
|
|
430
432
|
|
|
433
|
+
// Phase 8c: Supersession penalty — superseded memories are deprioritized.
|
|
434
|
+
// They aren't wrong (that's retraction), just outdated.
|
|
435
|
+
for (const item of rerankPool) {
|
|
436
|
+
if (item.engram.supersededBy) {
|
|
437
|
+
item.score *= 0.15; // Severe down-rank — successor should dominate
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
431
441
|
// Phase 9: Final sort, limit, explain
|
|
432
442
|
const results: ActivationResult[] = rerankPool
|
|
433
443
|
.sort((a, b) => b.score - a.score)
|
|
@@ -1,123 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Consolidation Scheduler — automatically triggers sleep cycles.
|
|
5
|
+
*
|
|
6
|
+
* Four triggers:
|
|
7
|
+
* 1. Idle — agent inactive >10min → full consolidation
|
|
8
|
+
* 2. Volume — 50+ writes since last consolidation → full consolidation
|
|
9
|
+
* 3. Time — 30min since last consolidation → full consolidation
|
|
10
|
+
* 4. Adaptive — retrieval precision <0.4 → full consolidation
|
|
11
|
+
*
|
|
12
|
+
* Also provides mini-consolidation for restore (fire-and-forget, lightweight).
|
|
13
|
+
* Checks every 30 seconds across all active agents.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { EngramStore } from '../storage/sqlite.js';
|
|
17
|
+
import type { ConsolidationEngine } from './consolidation.js';
|
|
18
|
+
|
|
19
|
+
const TICK_INTERVAL_MS = 30_000; // Check every 30s
|
|
20
|
+
const IDLE_THRESHOLD_MS = 10 * 60_000; // 10 minutes
|
|
21
|
+
const VOLUME_THRESHOLD = 50; // 50 writes
|
|
22
|
+
const TIME_THRESHOLD_MS = 30 * 60_000; // 30 minutes
|
|
23
|
+
const PRECISION_THRESHOLD = 0.4; // Below this triggers consolidation
|
|
24
|
+
|
|
25
|
+
export class ConsolidationScheduler {
|
|
26
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
private running = false;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private store: EngramStore,
|
|
31
|
+
private consolidationEngine: ConsolidationEngine,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
start(): void {
|
|
35
|
+
if (this.timer) return;
|
|
36
|
+
this.timer = setInterval(() => this.tick(), TICK_INTERVAL_MS);
|
|
37
|
+
console.log('ConsolidationScheduler started (30s tick)');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stop(): void {
|
|
41
|
+
if (this.timer) {
|
|
42
|
+
clearInterval(this.timer);
|
|
43
|
+
this.timer = null;
|
|
44
|
+
}
|
|
45
|
+
console.log('ConsolidationScheduler stopped');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mini-consolidation — lightweight, called from restore path.
|
|
50
|
+
* Only runs replay + strengthen (phases 1-2), skips heavy phases.
|
|
51
|
+
*/
|
|
52
|
+
async runMiniConsolidation(agentId: string): Promise<void> {
|
|
53
|
+
if (this.running) return;
|
|
54
|
+
this.running = true;
|
|
55
|
+
try {
|
|
56
|
+
console.log(`[scheduler] mini-consolidation for ${agentId}`);
|
|
57
|
+
this.consolidationEngine.consolidate(agentId);
|
|
58
|
+
this.store.markConsolidation(agentId, true);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[scheduler] mini-consolidation failed for ${agentId}:`, err);
|
|
61
|
+
} finally {
|
|
62
|
+
this.running = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private tick(): void {
|
|
67
|
+
if (this.running) return;
|
|
68
|
+
|
|
69
|
+
const agents = this.store.getActiveAgents();
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
|
|
72
|
+
for (const agent of agents) {
|
|
73
|
+
const idleMs = now - agent.lastActivityAt.getTime();
|
|
74
|
+
const sinceConsolidation = agent.lastConsolidationAt
|
|
75
|
+
? now - agent.lastConsolidationAt.getTime()
|
|
76
|
+
: Infinity;
|
|
77
|
+
|
|
78
|
+
let trigger: string | null = null;
|
|
79
|
+
|
|
80
|
+
// 1. Idle trigger — agent stopped writing/recalling >10min ago
|
|
81
|
+
if (idleMs > IDLE_THRESHOLD_MS && sinceConsolidation > IDLE_THRESHOLD_MS) {
|
|
82
|
+
trigger = `idle (${Math.round(idleMs / 60_000)}min)`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Volume trigger — many writes accumulated
|
|
86
|
+
if (!trigger && agent.writeCount >= VOLUME_THRESHOLD) {
|
|
87
|
+
trigger = `volume (${agent.writeCount} writes)`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Time trigger — been too long since last consolidation
|
|
91
|
+
if (!trigger && sinceConsolidation > TIME_THRESHOLD_MS) {
|
|
92
|
+
trigger = `time (${Math.round(sinceConsolidation / 60_000)}min)`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 4. Adaptive trigger — precision is low
|
|
96
|
+
if (!trigger) {
|
|
97
|
+
try {
|
|
98
|
+
const precision = this.store.getRetrievalPrecision(agent.agentId, 1);
|
|
99
|
+
if (precision > 0 && precision < PRECISION_THRESHOLD) {
|
|
100
|
+
trigger = `adaptive (precision ${(precision * 100).toFixed(0)}%)`;
|
|
101
|
+
}
|
|
102
|
+
} catch { /* precision check is non-fatal */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (trigger) {
|
|
106
|
+
this.runFullConsolidation(agent.agentId, trigger);
|
|
107
|
+
return; // One consolidation per tick to avoid overload
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private runFullConsolidation(agentId: string, reason: string): void {
|
|
113
|
+
this.running = true;
|
|
114
|
+
try {
|
|
115
|
+
console.log(`[scheduler] full consolidation for ${agentId} — trigger: ${reason}`);
|
|
116
|
+
const result = this.consolidationEngine.consolidate(agentId);
|
|
117
|
+
this.store.markConsolidation(agentId, false);
|
|
118
|
+
console.log(`[scheduler] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`[scheduler] consolidation failed for ${agentId}:`, err);
|
|
121
|
+
} finally {
|
|
122
|
+
this.running = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Sleep Cycle — offline memory consolidation.
|
|
3
5
|
*
|
|
@@ -228,11 +230,11 @@ export class ConsolidationEngine {
|
|
|
228
230
|
// Nodes with many strong edges get scaled down so relative weights stay meaningful.
|
|
229
231
|
const engramIds = new Set(engrams.map(e => e.id));
|
|
230
232
|
for (const id of engramIds) {
|
|
231
|
-
const
|
|
232
|
-
const totalWeight =
|
|
233
|
+
const outgoing = this.store.getOutgoingAssociations(id);
|
|
234
|
+
const totalWeight = outgoing.reduce((sum, a) => sum + a.weight, 0);
|
|
233
235
|
if (totalWeight > HOMEOSTASIS_TARGET) {
|
|
234
236
|
const scale = HOMEOSTASIS_TARGET / totalWeight;
|
|
235
|
-
for (const edge of
|
|
237
|
+
for (const edge of outgoing) {
|
|
236
238
|
const newWeight = edge.weight * scale;
|
|
237
239
|
if (newWeight < PRUNE_THRESHOLD) {
|
|
238
240
|
this.store.deleteAssociation(edge.id);
|
package/src/engine/eval.ts
CHANGED
package/src/engine/eviction.ts
CHANGED
package/src/engine/index.ts
CHANGED
package/src/engine/retraction.ts
CHANGED
package/src/engine/staging.ts
CHANGED
package/src/hooks/sidecar.ts
CHANGED
package/src/index.ts
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* MCP Server — Model Context Protocol interface for AgentWorkingMemory.
|
|
3
5
|
*
|
|
4
6
|
* Runs as a stdio-based MCP server that Claude Code connects to directly.
|
|
5
7
|
* Uses the storage and engine layers in-process (no HTTP overhead).
|
|
6
8
|
*
|
|
7
|
-
* Tools exposed (
|
|
9
|
+
* Tools exposed (12):
|
|
8
10
|
* memory_write — store a memory (salience filter decides disposition)
|
|
9
11
|
* memory_recall — activate memories by context (cognitive retrieval)
|
|
10
12
|
* memory_feedback — report whether a recalled memory was useful
|
|
11
13
|
* memory_retract — invalidate a wrong memory with optional correction
|
|
14
|
+
* memory_supersede — replace an outdated memory with a current one
|
|
12
15
|
* memory_stats — get memory health metrics
|
|
13
16
|
* memory_checkpoint — save structured execution state (survives compaction)
|
|
14
17
|
* memory_restore — restore state + targeted recall after compaction
|
|
@@ -134,6 +137,10 @@ The concept should be a short label (3-8 words). The content should be the full
|
|
|
134
137
|
.describe('How deep is the causal understanding? 0=surface, 1=root cause'),
|
|
135
138
|
resolution_effort: z.number().min(0).max(1).optional().default(0.3)
|
|
136
139
|
.describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
|
|
140
|
+
memory_class: z.enum(['canonical', 'working', 'ephemeral']).optional().default('working')
|
|
141
|
+
.describe('Memory class: canonical (source-of-truth, never stages), working (default), ephemeral (temporary, decays faster)'),
|
|
142
|
+
supersedes: z.string().optional()
|
|
143
|
+
.describe('ID of an older memory this one replaces. The old memory is down-ranked, not deleted.'),
|
|
137
144
|
},
|
|
138
145
|
async (params) => {
|
|
139
146
|
// Check novelty — is this new information or a duplicate?
|
|
@@ -147,6 +154,7 @@ The concept should be a short label (3-8 words). The content should be the full
|
|
|
147
154
|
causalDepth: params.causal_depth,
|
|
148
155
|
resolutionEffort: params.resolution_effort,
|
|
149
156
|
novelty,
|
|
157
|
+
memoryClass: params.memory_class,
|
|
150
158
|
});
|
|
151
159
|
|
|
152
160
|
if (salience.disposition === 'discard') {
|
|
@@ -168,6 +176,8 @@ The concept should be a short label (3-8 words). The content should be the full
|
|
|
168
176
|
salienceFeatures: salience.features,
|
|
169
177
|
reasonCodes: salience.reasonCodes,
|
|
170
178
|
ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
|
|
179
|
+
memoryClass: params.memory_class,
|
|
180
|
+
supersedes: params.supersedes,
|
|
171
181
|
});
|
|
172
182
|
|
|
173
183
|
if (salience.disposition === 'staging') {
|
|
@@ -176,6 +186,16 @@ The concept should be a short label (3-8 words). The content should be the full
|
|
|
176
186
|
connectionEngine.enqueue(engram.id);
|
|
177
187
|
}
|
|
178
188
|
|
|
189
|
+
// Handle supersession: mark old memory as superseded
|
|
190
|
+
if (params.supersedes) {
|
|
191
|
+
const oldEngram = store.getEngram(params.supersedes);
|
|
192
|
+
if (oldEngram) {
|
|
193
|
+
store.supersedeEngram(params.supersedes, engram.id);
|
|
194
|
+
// Create supersession association
|
|
195
|
+
store.upsertAssociation(engram.id, oldEngram.id, 0.8, 'causal', 0.9);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
179
199
|
// Generate embedding asynchronously (don't block response)
|
|
180
200
|
embed(`${params.concept} ${params.content}`).then(vec => {
|
|
181
201
|
store.updateEmbedding(engram.id, vec);
|
|
@@ -329,6 +349,50 @@ Use this when you discover a memory contains incorrect information.`,
|
|
|
329
349
|
}
|
|
330
350
|
);
|
|
331
351
|
|
|
352
|
+
server.tool(
|
|
353
|
+
'memory_supersede',
|
|
354
|
+
`Replace an outdated memory with a newer one. Unlike retraction (which marks memories as wrong), supersession marks the old memory as outdated but historically correct.
|
|
355
|
+
|
|
356
|
+
Use this when:
|
|
357
|
+
- A status or count has changed (e.g., "5 reviews done" → "7 reviews done")
|
|
358
|
+
- Architecture or infrastructure evolved (e.g., "two-repo model" → "three-repo model")
|
|
359
|
+
- A schedule or plan was updated
|
|
360
|
+
|
|
361
|
+
The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`,
|
|
362
|
+
{
|
|
363
|
+
old_engram_id: z.string().describe('ID of the outdated memory'),
|
|
364
|
+
new_engram_id: z.string().describe('ID of the replacement memory'),
|
|
365
|
+
reason: z.string().optional().describe('Why the old memory is outdated'),
|
|
366
|
+
},
|
|
367
|
+
async (params) => {
|
|
368
|
+
const oldEngram = store.getEngram(params.old_engram_id);
|
|
369
|
+
if (!oldEngram) {
|
|
370
|
+
return { content: [{ type: 'text' as const, text: `Old memory not found: ${params.old_engram_id}` }] };
|
|
371
|
+
}
|
|
372
|
+
const newEngram = store.getEngram(params.new_engram_id);
|
|
373
|
+
if (!newEngram) {
|
|
374
|
+
return { content: [{ type: 'text' as const, text: `New memory not found: ${params.new_engram_id}` }] };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
store.supersedeEngram(params.old_engram_id, params.new_engram_id);
|
|
378
|
+
|
|
379
|
+
// Create supersession association (new → old)
|
|
380
|
+
store.upsertAssociation(params.new_engram_id, params.old_engram_id, 0.8, 'causal', 0.9);
|
|
381
|
+
|
|
382
|
+
// Reduce old memory's confidence (not to zero — it's historical, not wrong)
|
|
383
|
+
store.updateConfidence(params.old_engram_id, Math.max(0.2, oldEngram.confidence * 0.4));
|
|
384
|
+
|
|
385
|
+
log(AGENT_ID, 'supersede', `"${oldEngram.concept}" → "${newEngram.concept}"${params.reason ? ` (${params.reason})` : ''}`);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
content: [{
|
|
389
|
+
type: 'text' as const,
|
|
390
|
+
text: `Superseded: "${oldEngram.concept}" → "${newEngram.concept}"`,
|
|
391
|
+
}],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
332
396
|
server.tool(
|
|
333
397
|
'memory_stats',
|
|
334
398
|
`Get memory health stats — how many memories, confidence levels, association count, and system performance.
|
|
@@ -786,6 +850,8 @@ This captures what was accomplished so future sessions can recall it.`,
|
|
|
786
850
|
summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
|
|
787
851
|
tags: z.array(z.string()).optional().default([])
|
|
788
852
|
.describe('Tags for the summary memory'),
|
|
853
|
+
supersedes: z.array(z.string()).optional().default([])
|
|
854
|
+
.describe('IDs of older memories this task summary replaces (marks them as superseded)'),
|
|
789
855
|
},
|
|
790
856
|
async (params) => {
|
|
791
857
|
// 1. Write summary as a memory
|
|
@@ -810,6 +876,18 @@ This captures what was accomplished so future sessions can recall it.`,
|
|
|
810
876
|
|
|
811
877
|
connectionEngine.enqueue(engram.id);
|
|
812
878
|
|
|
879
|
+
// 2. Handle supersessions — mark old memories as outdated
|
|
880
|
+
let supersededCount = 0;
|
|
881
|
+
for (const oldId of params.supersedes) {
|
|
882
|
+
const oldEngram = store.getEngram(oldId);
|
|
883
|
+
if (oldEngram) {
|
|
884
|
+
store.supersedeEngram(oldId, engram.id);
|
|
885
|
+
store.upsertAssociation(engram.id, oldId, 0.8, 'causal', 0.9);
|
|
886
|
+
store.updateConfidence(oldId, Math.max(0.2, oldEngram.confidence * 0.4));
|
|
887
|
+
supersededCount++;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
813
891
|
// Generate embedding asynchronously
|
|
814
892
|
embed(`Task completed: ${params.summary}`).then(vec => {
|
|
815
893
|
store.updateEmbedding(engram.id, vec);
|
|
@@ -830,12 +908,13 @@ This captures what was accomplished so future sessions can recall it.`,
|
|
|
830
908
|
});
|
|
831
909
|
|
|
832
910
|
store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
|
|
833
|
-
log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)}`);
|
|
911
|
+
log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)} superseded=${supersededCount}`);
|
|
834
912
|
|
|
913
|
+
const supersededNote = supersededCount > 0 ? ` (${supersededCount} old memories superseded)` : '';
|
|
835
914
|
return {
|
|
836
915
|
content: [{
|
|
837
916
|
type: 'text' as const,
|
|
838
|
-
text: `Completed: "${completedTask}" [${salience.score.toFixed(2)}]`,
|
|
917
|
+
text: `Completed: "${completedTask}" [${salience.score.toFixed(2)}]${supersededNote}`,
|
|
839
918
|
}],
|
|
840
919
|
};
|
|
841
920
|
}
|
package/src/storage/index.ts
CHANGED