agent-working-memory 0.5.5 → 0.6.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 +428 -399
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +60 -5
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.js +468 -68
- package/dist/cli.js.map +1 -1
- package/dist/coordination/index.d.ts +11 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +39 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/mcp-tools.d.ts +8 -0
- package/dist/coordination/mcp-tools.d.ts.map +1 -0
- package/dist/coordination/mcp-tools.js +221 -0
- package/dist/coordination/mcp-tools.js.map +1 -0
- package/dist/coordination/routes.d.ts +9 -0
- package/dist/coordination/routes.d.ts.map +1 -0
- package/dist/coordination/routes.js +573 -0
- package/dist/coordination/routes.js.map +1 -0
- package/dist/coordination/schema.d.ts +12 -0
- package/dist/coordination/schema.d.ts.map +1 -0
- package/dist/coordination/schema.js +125 -0
- package/dist/coordination/schema.js.map +1 -0
- package/dist/coordination/schemas.d.ts +227 -0
- package/dist/coordination/schemas.d.ts.map +1 -0
- package/dist/coordination/schemas.js +125 -0
- package/dist/coordination/schemas.js.map +1 -0
- package/dist/coordination/stale.d.ts +27 -0
- package/dist/coordination/stale.d.ts.map +1 -0
- package/dist/coordination/stale.js +58 -0
- package/dist/coordination/stale.js.map +1 -0
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +119 -23
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +27 -6
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/index.js +100 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +149 -80
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +21 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +331 -282
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +24 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +57 -55
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +600 -536
- package/src/cli.ts +850 -397
- package/src/coordination/index.ts +47 -0
- package/src/coordination/mcp-tools.ts +318 -0
- package/src/coordination/routes.ts +846 -0
- package/src/coordination/schema.ts +120 -0
- package/src/coordination/schemas.ts +155 -0
- package/src/coordination/stale.ts +97 -0
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +88 -88
- package/src/core/hebbian.ts +93 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/query-expander.ts +66 -66
- package/src/core/reranker.ts +101 -101
- package/src/engine/activation.ts +758 -656
- package/src/engine/connections.ts +103 -103
- package/src/engine/consolidation-scheduler.ts +125 -125
- package/src/engine/consolidation.ts +29 -6
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +100 -100
- package/src/engine/staging.ts +74 -74
- package/src/index.ts +208 -121
- package/src/mcp.ts +1093 -1013
- package/src/storage/index.ts +3 -3
- package/src/storage/sqlite.ts +1017 -963
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +245 -217
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Connection Engine — discovers links between memories.
|
|
5
|
-
*
|
|
6
|
-
* Runs asynchronously. When a new engram is written, the connection
|
|
7
|
-
* engine checks it against existing memories and forms association
|
|
8
|
-
* edges where resonance exceeds a threshold.
|
|
9
|
-
*
|
|
10
|
-
* Connection memories are first-class engrams — they can themselves
|
|
11
|
-
* activate and form higher-order connections, producing emergent
|
|
12
|
-
* associative structure over time.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { EngramStore } from '../storage/sqlite.js';
|
|
16
|
-
import type { ActivationEngine } from './activation.js';
|
|
17
|
-
import type { Engram } from '../types/index.js';
|
|
18
|
-
|
|
19
|
-
export class ConnectionEngine {
|
|
20
|
-
private store: EngramStore;
|
|
21
|
-
private engine: ActivationEngine;
|
|
22
|
-
private threshold: number;
|
|
23
|
-
private queue: string[] = [];
|
|
24
|
-
private processing = false;
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
store: EngramStore,
|
|
28
|
-
engine: ActivationEngine,
|
|
29
|
-
threshold: number = 0.7
|
|
30
|
-
) {
|
|
31
|
-
this.store = store;
|
|
32
|
-
this.engine = engine;
|
|
33
|
-
this.threshold = threshold;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Queue a newly written engram for connection discovery.
|
|
38
|
-
*/
|
|
39
|
-
enqueue(engramId: string): void {
|
|
40
|
-
this.queue.push(engramId);
|
|
41
|
-
if (!this.processing) {
|
|
42
|
-
this.processQueue();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private async processQueue(): Promise<void> {
|
|
47
|
-
this.processing = true;
|
|
48
|
-
|
|
49
|
-
while (this.queue.length > 0) {
|
|
50
|
-
const engramId = this.queue.shift()!;
|
|
51
|
-
const engram = this.store.getEngram(engramId);
|
|
52
|
-
if (!engram || engram.stage !== 'active') continue;
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await this.findConnections(engram);
|
|
56
|
-
} catch {
|
|
57
|
-
// Connection discovery is best-effort — don't crash the server
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.processing = false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Find and create connections for a given engram.
|
|
66
|
-
*/
|
|
67
|
-
private async findConnections(engram: Engram): Promise<void> {
|
|
68
|
-
const results = await this.engine.activate({
|
|
69
|
-
agentId: engram.agentId,
|
|
70
|
-
context: `${engram.concept} ${engram.content}`,
|
|
71
|
-
limit: 5,
|
|
72
|
-
minScore: this.threshold,
|
|
73
|
-
internal: true,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Filter out self and already-connected engrams
|
|
77
|
-
const existing = this.store.getAssociationsFor(engram.id);
|
|
78
|
-
const existingIds = new Set(existing.map(a =>
|
|
79
|
-
a.fromEngramId === engram.id ? a.toEngramId : a.fromEngramId
|
|
80
|
-
));
|
|
81
|
-
|
|
82
|
-
for (const result of results) {
|
|
83
|
-
if (result.engram.id === engram.id) continue;
|
|
84
|
-
if (existingIds.has(result.engram.id)) continue;
|
|
85
|
-
|
|
86
|
-
// Create a connection association
|
|
87
|
-
this.store.upsertAssociation(
|
|
88
|
-
engram.id,
|
|
89
|
-
result.engram.id,
|
|
90
|
-
result.score,
|
|
91
|
-
'connection'
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Bidirectional
|
|
95
|
-
this.store.upsertAssociation(
|
|
96
|
-
result.engram.id,
|
|
97
|
-
engram.id,
|
|
98
|
-
result.score,
|
|
99
|
-
'connection'
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Connection Engine — discovers links between memories.
|
|
5
|
+
*
|
|
6
|
+
* Runs asynchronously. When a new engram is written, the connection
|
|
7
|
+
* engine checks it against existing memories and forms association
|
|
8
|
+
* edges where resonance exceeds a threshold.
|
|
9
|
+
*
|
|
10
|
+
* Connection memories are first-class engrams — they can themselves
|
|
11
|
+
* activate and form higher-order connections, producing emergent
|
|
12
|
+
* associative structure over time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EngramStore } from '../storage/sqlite.js';
|
|
16
|
+
import type { ActivationEngine } from './activation.js';
|
|
17
|
+
import type { Engram } from '../types/index.js';
|
|
18
|
+
|
|
19
|
+
export class ConnectionEngine {
|
|
20
|
+
private store: EngramStore;
|
|
21
|
+
private engine: ActivationEngine;
|
|
22
|
+
private threshold: number;
|
|
23
|
+
private queue: string[] = [];
|
|
24
|
+
private processing = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
store: EngramStore,
|
|
28
|
+
engine: ActivationEngine,
|
|
29
|
+
threshold: number = 0.7
|
|
30
|
+
) {
|
|
31
|
+
this.store = store;
|
|
32
|
+
this.engine = engine;
|
|
33
|
+
this.threshold = threshold;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Queue a newly written engram for connection discovery.
|
|
38
|
+
*/
|
|
39
|
+
enqueue(engramId: string): void {
|
|
40
|
+
this.queue.push(engramId);
|
|
41
|
+
if (!this.processing) {
|
|
42
|
+
this.processQueue();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async processQueue(): Promise<void> {
|
|
47
|
+
this.processing = true;
|
|
48
|
+
|
|
49
|
+
while (this.queue.length > 0) {
|
|
50
|
+
const engramId = this.queue.shift()!;
|
|
51
|
+
const engram = this.store.getEngram(engramId);
|
|
52
|
+
if (!engram || engram.stage !== 'active') continue;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await this.findConnections(engram);
|
|
56
|
+
} catch {
|
|
57
|
+
// Connection discovery is best-effort — don't crash the server
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.processing = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find and create connections for a given engram.
|
|
66
|
+
*/
|
|
67
|
+
private async findConnections(engram: Engram): Promise<void> {
|
|
68
|
+
const results = await this.engine.activate({
|
|
69
|
+
agentId: engram.agentId,
|
|
70
|
+
context: `${engram.concept} ${engram.content}`,
|
|
71
|
+
limit: 5,
|
|
72
|
+
minScore: this.threshold,
|
|
73
|
+
internal: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Filter out self and already-connected engrams
|
|
77
|
+
const existing = this.store.getAssociationsFor(engram.id);
|
|
78
|
+
const existingIds = new Set(existing.map(a =>
|
|
79
|
+
a.fromEngramId === engram.id ? a.toEngramId : a.fromEngramId
|
|
80
|
+
));
|
|
81
|
+
|
|
82
|
+
for (const result of results) {
|
|
83
|
+
if (result.engram.id === engram.id) continue;
|
|
84
|
+
if (existingIds.has(result.engram.id)) continue;
|
|
85
|
+
|
|
86
|
+
// Create a connection association
|
|
87
|
+
this.store.upsertAssociation(
|
|
88
|
+
engram.id,
|
|
89
|
+
result.engram.id,
|
|
90
|
+
result.score,
|
|
91
|
+
'connection'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Bidirectional
|
|
95
|
+
this.store.upsertAssociation(
|
|
96
|
+
result.engram.id,
|
|
97
|
+
engram.id,
|
|
98
|
+
result.score,
|
|
99
|
+
'connection'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,125 +1,125 @@
|
|
|
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
|
-
await 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 async runFullConsolidation(agentId: string, reason: string): Promise<void> {
|
|
113
|
-
this.running = true;
|
|
114
|
-
try {
|
|
115
|
-
console.log(`[scheduler] full consolidation for ${agentId} — trigger: ${reason}`);
|
|
116
|
-
const result = await 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
|
+
// 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
|
+
await 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 async runFullConsolidation(agentId: string, reason: string): Promise<void> {
|
|
113
|
+
this.running = true;
|
|
114
|
+
try {
|
|
115
|
+
console.log(`[scheduler] full consolidation for ${agentId} — trigger: ${reason}`);
|
|
116
|
+
const result = await 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
|
+
}
|
|
@@ -68,11 +68,15 @@ const FORGET_ARCHIVE_DAYS = 30;
|
|
|
68
68
|
/** Age at which archived, never-retrieved, unconnected memories get deleted (days) */
|
|
69
69
|
const FORGET_DELETE_DAYS = 90;
|
|
70
70
|
|
|
71
|
-
/** Cosine similarity above which two low-confidence memories are considered redundant
|
|
72
|
-
|
|
71
|
+
/** Cosine similarity above which two low-confidence memories are considered redundant.
|
|
72
|
+
* MiniLM-L6 paraphrases typically score 0.75-0.88 cosine; 0.85 misses most of them.
|
|
73
|
+
* 0.75 catches paraphrases while keeping precision above 0.60 for unrelated facts. */
|
|
74
|
+
const REDUNDANCY_THRESHOLD = 0.75;
|
|
73
75
|
|
|
74
|
-
/** Max redundant memories to prune per cycle (gradual, not sudden)
|
|
75
|
-
|
|
76
|
+
/** Max redundant memories to prune per cycle (gradual, not sudden).
|
|
77
|
+
* Raised from 10 to 25 — the eval harness runs multiple cycles anyway,
|
|
78
|
+
* but faster convergence reduces consolidation time for larger pools. */
|
|
79
|
+
const MAX_REDUNDANCY_PRUNE_PER_CYCLE = 25;
|
|
76
80
|
|
|
77
81
|
/** Max confidence drift per consolidation cycle (prevents runaway) */
|
|
78
82
|
const CONFIDENCE_DRIFT_CAP = 0.03;
|
|
@@ -404,9 +408,28 @@ export class ConsolidationEngine {
|
|
|
404
408
|
|
|
405
409
|
const sim = cosineSimilarity(sortedLow[i].embedding!, sortedLow[j].embedding!);
|
|
406
410
|
if (sim >= REDUNDANCY_THRESHOLD) {
|
|
411
|
+
const survivorId = sortedLow[i].id;
|
|
412
|
+
const prunedId = sortedLow[j].id;
|
|
413
|
+
|
|
414
|
+
// Transfer associations from pruned memory to survivor
|
|
415
|
+
const prunedEdges = this.store.getAssociationsFor(prunedId);
|
|
416
|
+
for (const edge of prunedEdges) {
|
|
417
|
+
const peerId = edge.fromEngramId === prunedId ? edge.toEngramId : edge.fromEngramId;
|
|
418
|
+
if (peerId === survivorId) continue; // Skip self-loops
|
|
419
|
+
this.store.upsertAssociation(survivorId, peerId, edge.weight, edge.type, edge.confidence);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Merge tags from pruned to survivor
|
|
423
|
+
const survivor = sortedLow[i];
|
|
424
|
+
const prunedMem = sortedLow[j];
|
|
425
|
+
const mergedTags = [...new Set([...survivor.tags, ...prunedMem.tags])];
|
|
426
|
+
if (mergedTags.length > survivor.tags.length) {
|
|
427
|
+
this.store.updateTags(survivorId, mergedTags);
|
|
428
|
+
}
|
|
429
|
+
|
|
407
430
|
// Archive the lower-quality duplicate
|
|
408
|
-
this.store.updateStage(
|
|
409
|
-
pruned.add(
|
|
431
|
+
this.store.updateStage(prunedId, 'archived');
|
|
432
|
+
pruned.add(prunedId);
|
|
410
433
|
redundancyCount++;
|
|
411
434
|
}
|
|
412
435
|
}
|