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.
Files changed (127) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +21 -3
  3. package/dist/api/index.d.ts.map +1 -1
  4. package/dist/api/index.js +2 -0
  5. package/dist/api/index.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +7 -0
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/cli.d.ts +0 -9
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +69 -67
  12. package/dist/cli.js.map +1 -1
  13. package/dist/core/decay.d.ts.map +1 -1
  14. package/dist/core/decay.js +2 -0
  15. package/dist/core/decay.js.map +1 -1
  16. package/dist/core/embeddings.d.ts.map +1 -1
  17. package/dist/core/embeddings.js +2 -0
  18. package/dist/core/embeddings.js.map +1 -1
  19. package/dist/core/hebbian.d.ts.map +1 -1
  20. package/dist/core/hebbian.js +2 -0
  21. package/dist/core/hebbian.js.map +1 -1
  22. package/dist/core/index.d.ts.map +1 -1
  23. package/dist/core/index.js +2 -0
  24. package/dist/core/index.js.map +1 -1
  25. package/dist/core/logger.d.ts.map +1 -1
  26. package/dist/core/logger.js +2 -0
  27. package/dist/core/logger.js.map +1 -1
  28. package/dist/core/query-expander.d.ts.map +1 -1
  29. package/dist/core/query-expander.js +2 -0
  30. package/dist/core/query-expander.js.map +1 -1
  31. package/dist/core/reranker.d.ts.map +1 -1
  32. package/dist/core/reranker.js +2 -0
  33. package/dist/core/reranker.js.map +1 -1
  34. package/dist/core/salience.d.ts +3 -1
  35. package/dist/core/salience.d.ts.map +1 -1
  36. package/dist/core/salience.js +29 -10
  37. package/dist/core/salience.js.map +1 -1
  38. package/dist/engine/activation.d.ts.map +1 -1
  39. package/dist/engine/activation.js +9 -0
  40. package/dist/engine/activation.js.map +1 -1
  41. package/dist/engine/connections.d.ts.map +1 -1
  42. package/dist/engine/connections.js +2 -0
  43. package/dist/engine/connections.js.map +1 -1
  44. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  45. package/dist/engine/consolidation-scheduler.js +2 -0
  46. package/dist/engine/consolidation-scheduler.js.map +1 -1
  47. package/dist/engine/consolidation.d.ts.map +1 -1
  48. package/dist/engine/consolidation.js +5 -3
  49. package/dist/engine/consolidation.js.map +1 -1
  50. package/dist/engine/eval.d.ts.map +1 -1
  51. package/dist/engine/eval.js +2 -0
  52. package/dist/engine/eval.js.map +1 -1
  53. package/dist/engine/eviction.d.ts.map +1 -1
  54. package/dist/engine/eviction.js +2 -0
  55. package/dist/engine/eviction.js.map +1 -1
  56. package/dist/engine/index.d.ts.map +1 -1
  57. package/dist/engine/index.js +2 -0
  58. package/dist/engine/index.js.map +1 -1
  59. package/dist/engine/retraction.d.ts.map +1 -1
  60. package/dist/engine/retraction.js +2 -0
  61. package/dist/engine/retraction.js.map +1 -1
  62. package/dist/engine/staging.d.ts.map +1 -1
  63. package/dist/engine/staging.js +2 -0
  64. package/dist/engine/staging.js.map +1 -1
  65. package/dist/hooks/sidecar.d.ts.map +1 -1
  66. package/dist/hooks/sidecar.js +2 -0
  67. package/dist/hooks/sidecar.js.map +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/mcp.d.ts +2 -1
  71. package/dist/mcp.d.ts.map +1 -1
  72. package/dist/mcp.js +69 -3
  73. package/dist/mcp.js.map +1 -1
  74. package/dist/storage/index.d.ts.map +1 -1
  75. package/dist/storage/index.js +2 -0
  76. package/dist/storage/index.js.map +1 -1
  77. package/dist/storage/sqlite.d.ts +12 -1
  78. package/dist/storage/sqlite.d.ts.map +1 -1
  79. package/dist/storage/sqlite.js +52 -5
  80. package/dist/storage/sqlite.js.map +1 -1
  81. package/dist/types/agent.d.ts.map +1 -1
  82. package/dist/types/agent.js +2 -0
  83. package/dist/types/agent.js.map +1 -1
  84. package/dist/types/checkpoint.d.ts.map +1 -1
  85. package/dist/types/checkpoint.js +2 -0
  86. package/dist/types/checkpoint.js.map +1 -1
  87. package/dist/types/engram.d.ts +16 -0
  88. package/dist/types/engram.d.ts.map +1 -1
  89. package/dist/types/engram.js +2 -0
  90. package/dist/types/engram.js.map +1 -1
  91. package/dist/types/eval.d.ts.map +1 -1
  92. package/dist/types/eval.js +2 -0
  93. package/dist/types/eval.js.map +1 -1
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/index.js +2 -0
  96. package/dist/types/index.js.map +1 -1
  97. package/package.json +2 -2
  98. package/src/api/index.ts +2 -0
  99. package/src/api/routes.ts +8 -0
  100. package/src/cli.ts +385 -383
  101. package/src/core/decay.ts +2 -0
  102. package/src/core/embeddings.ts +2 -0
  103. package/src/core/hebbian.ts +2 -0
  104. package/src/core/index.ts +2 -0
  105. package/src/core/logger.ts +2 -0
  106. package/src/core/query-expander.ts +2 -0
  107. package/src/core/reranker.ts +2 -0
  108. package/src/core/salience.ts +34 -13
  109. package/src/engine/activation.ts +10 -0
  110. package/src/engine/connections.ts +2 -0
  111. package/src/engine/consolidation-scheduler.ts +125 -123
  112. package/src/engine/consolidation.ts +5 -3
  113. package/src/engine/eval.ts +2 -0
  114. package/src/engine/eviction.ts +2 -0
  115. package/src/engine/index.ts +2 -0
  116. package/src/engine/retraction.ts +2 -0
  117. package/src/engine/staging.ts +2 -0
  118. package/src/hooks/sidecar.ts +2 -0
  119. package/src/index.ts +2 -0
  120. package/src/mcp.ts +82 -3
  121. package/src/storage/index.ts +2 -0
  122. package/src/storage/sqlite.ts +61 -5
  123. package/src/types/agent.ts +2 -0
  124. package/src/types/checkpoint.ts +46 -44
  125. package/src/types/engram.ts +23 -0
  126. package/src/types/eval.ts +2 -0
  127. package/src/types/index.ts +2 -0
package/src/core/decay.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * ACT-R Base-Level Activation
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Embedding Engine — local vector embeddings via transformers.js
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Hebbian Learning — "neurons that fire together wire together"
3
5
  *
package/src/core/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  export * from './decay.js';
2
4
  export * from './hebbian.js';
3
5
  export * from './salience.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Simple file logger for AWM activity.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Query Expander — rewrites queries with synonyms and related terms.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Cross-Encoder Re-Ranker — scores (query, passage) pairs for relevance.
3
5
  *
@@ -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
- const score = Math.min(surpriseScore + decisionScore + causalScore + effortScore + noveltyScore + typeBonus, 1.0);
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 (score >= activeThreshold) {
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
- // BM25 scores are unbounded. Normalize the top score relative to a "strong match" threshold.
129
- // A BM25 score > 15 typically indicates very high overlap. > 25 is near-duplicate.
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 > 25) return 0.1; // Near-duplicate
133
- if (topScore > 15) return 0.3; // High overlap
134
- if (topScore > 8) return 0.5; // Moderate overlap
135
- if (topScore > 3) return 0.7; // Some overlap
136
- return 0.9; // Minimal overlap — mostly novel
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;
@@ -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,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Connection Engine — discovers links between memories.
3
5
  *
@@ -1,123 +1,125 @@
1
- /**
2
- * Consolidation Scheduler — automatically triggers sleep cycles.
3
- *
4
- * Four triggers:
5
- * 1. Idle — agent inactive >10min → full consolidation
6
- * 2. Volume — 50+ writes since last consolidation → full consolidation
7
- * 3. Time30min since last consolidation → full consolidation
8
- * 4. Adaptiveretrieval precision <0.4 → full consolidation
9
- *
10
- * Also provides mini-consolidation for restore (fire-and-forget, lightweight).
11
- * Checks every 30 seconds across all active agents.
12
- */
13
-
14
- import type { EngramStore } from '../storage/sqlite.js';
15
- import type { ConsolidationEngine } from './consolidation.js';
16
-
17
- const TICK_INTERVAL_MS = 30_000; // Check every 30s
18
- const IDLE_THRESHOLD_MS = 10 * 60_000; // 10 minutes
19
- const VOLUME_THRESHOLD = 50; // 50 writes
20
- const TIME_THRESHOLD_MS = 30 * 60_000; // 30 minutes
21
- const PRECISION_THRESHOLD = 0.4; // Below this triggers consolidation
22
-
23
- export class ConsolidationScheduler {
24
- private timer: ReturnType<typeof setInterval> | null = null;
25
- private running = false;
26
-
27
- constructor(
28
- private store: EngramStore,
29
- private consolidationEngine: ConsolidationEngine,
30
- ) {}
31
-
32
- start(): void {
33
- if (this.timer) return;
34
- this.timer = setInterval(() => this.tick(), TICK_INTERVAL_MS);
35
- console.log('ConsolidationScheduler started (30s tick)');
36
- }
37
-
38
- stop(): void {
39
- if (this.timer) {
40
- clearInterval(this.timer);
41
- this.timer = null;
42
- }
43
- console.log('ConsolidationScheduler stopped');
44
- }
45
-
46
- /**
47
- * Mini-consolidation — lightweight, called from restore path.
48
- * Only runs replay + strengthen (phases 1-2), skips heavy phases.
49
- */
50
- async runMiniConsolidation(agentId: string): Promise<void> {
51
- if (this.running) return;
52
- this.running = true;
53
- try {
54
- console.log(`[scheduler] mini-consolidation for ${agentId}`);
55
- this.consolidationEngine.consolidate(agentId);
56
- this.store.markConsolidation(agentId, true);
57
- } catch (err) {
58
- console.error(`[scheduler] mini-consolidation failed for ${agentId}:`, err);
59
- } finally {
60
- this.running = false;
61
- }
62
- }
63
-
64
- private tick(): void {
65
- if (this.running) return;
66
-
67
- const agents = this.store.getActiveAgents();
68
- const now = Date.now();
69
-
70
- for (const agent of agents) {
71
- const idleMs = now - agent.lastActivityAt.getTime();
72
- const sinceConsolidation = agent.lastConsolidationAt
73
- ? now - agent.lastConsolidationAt.getTime()
74
- : Infinity;
75
-
76
- let trigger: string | null = null;
77
-
78
- // 1. Idle trigger agent stopped writing/recalling >10min ago
79
- if (idleMs > IDLE_THRESHOLD_MS && sinceConsolidation > IDLE_THRESHOLD_MS) {
80
- trigger = `idle (${Math.round(idleMs / 60_000)}min)`;
81
- }
82
-
83
- // 2. Volume trigger — many writes accumulated
84
- if (!trigger && agent.writeCount >= VOLUME_THRESHOLD) {
85
- trigger = `volume (${agent.writeCount} writes)`;
86
- }
87
-
88
- // 3. Time trigger — been too long since last consolidation
89
- if (!trigger && sinceConsolidation > TIME_THRESHOLD_MS) {
90
- trigger = `time (${Math.round(sinceConsolidation / 60_000)}min)`;
91
- }
92
-
93
- // 4. Adaptive trigger — precision is low
94
- if (!trigger) {
95
- try {
96
- const precision = this.store.getRetrievalPrecision(agent.agentId, 1);
97
- if (precision > 0 && precision < PRECISION_THRESHOLD) {
98
- trigger = `adaptive (precision ${(precision * 100).toFixed(0)}%)`;
99
- }
100
- } catch { /* precision check is non-fatal */ }
101
- }
102
-
103
- if (trigger) {
104
- this.runFullConsolidation(agent.agentId, trigger);
105
- return; // One consolidation per tick to avoid overload
106
- }
107
- }
108
- }
109
-
110
- private runFullConsolidation(agentId: string, reason: string): void {
111
- this.running = true;
112
- try {
113
- console.log(`[scheduler] full consolidation for ${agentId} — trigger: ${reason}`);
114
- const result = this.consolidationEngine.consolidate(agentId);
115
- this.store.markConsolidation(agentId, false);
116
- console.log(`[scheduler] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
117
- } catch (err) {
118
- console.error(`[scheduler] consolidation failed for ${agentId}:`, err);
119
- } finally {
120
- this.running = false;
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. Idleagent inactive >10min → full consolidation
8
+ * 2. Volume50+ 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 edges = this.store.getAssociationsFor(id);
232
- const totalWeight = edges.reduce((sum, a) => sum + a.weight, 0);
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 edges) {
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);
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Evaluation Engine — measures whether memory actually helps.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Eviction Engine — capacity enforcement and edge pruning.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  export * from './activation.js';
2
4
  export * from './staging.js';
3
5
  export * from './connections.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Retraction Engine — negative memory / invalidation.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Staging Buffer — weak signal handler.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Hook Sidecar — lightweight HTTP server that runs alongside the MCP process.
3
5
  *
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { readFileSync } from 'node:fs';
2
4
  import { resolve } from 'node:path';
3
5
  import Fastify from 'fastify';
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 (11):
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
  }
@@ -1 +1,3 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  export * from './sqlite.js';