mindforge-cc 10.7.0 → 11.0.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 (50) hide show
  1. package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
  2. package/.mindforge/config.json +6 -1
  3. package/CHANGELOG.md +64 -0
  4. package/MINDFORGE.md +3 -3
  5. package/README.md +49 -4
  6. package/RELEASENOTES.md +80 -0
  7. package/SECURITY.md +20 -8
  8. package/bin/autonomous/audit-writer.js +13 -0
  9. package/bin/autonomous/auto-runner.js +74 -16
  10. package/bin/autonomous/context-refactorer.js +26 -11
  11. package/bin/autonomous/state-manager.js +62 -6
  12. package/bin/autonomous/stuck-monitor.js +46 -7
  13. package/bin/autonomous/wave-executor.js +66 -25
  14. package/bin/dashboard/api-router.js +43 -0
  15. package/bin/dashboard/metrics-aggregator.js +28 -1
  16. package/bin/dashboard/server.js +67 -4
  17. package/bin/dashboard/sse-bridge.js +4 -4
  18. package/bin/engine/feedback-loop.js +8 -0
  19. package/bin/engine/intelligence-interlock.js +32 -15
  20. package/bin/engine/logic-drift-detector.js +2 -1
  21. package/bin/engine/nexus-tracer.js +3 -2
  22. package/bin/engine/remediation-engine.js +155 -32
  23. package/bin/engine/self-corrective-synthesizer.js +84 -10
  24. package/bin/engine/sre-manager.js +12 -4
  25. package/bin/engine/temporal-hub.js +131 -34
  26. package/bin/governance/approve.js +41 -5
  27. package/bin/governance/impact-analyzer.js +28 -0
  28. package/bin/governance/policy-engine.js +10 -3
  29. package/bin/governance/quantum-crypto.js +32 -19
  30. package/bin/governance/rbac-manager.js +74 -2
  31. package/bin/governance/ztai-manager.js +49 -7
  32. package/bin/hindsight-injector.js +3 -3
  33. package/bin/memory/eis-client.js +71 -34
  34. package/bin/memory/embedding-engine.js +61 -0
  35. package/bin/memory/knowledge-graph.js +58 -5
  36. package/bin/memory/knowledge-indexer.js +53 -6
  37. package/bin/memory/knowledge-store.js +22 -0
  38. package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
  39. package/bin/migrations/schema-versions.js +13 -0
  40. package/bin/models/anthropic-provider.js +45 -0
  41. package/bin/models/cloud-broker.js +68 -20
  42. package/bin/models/gemini-provider.js +51 -0
  43. package/bin/models/model-client.js +20 -0
  44. package/bin/models/model-router.js +28 -8
  45. package/bin/models/openai-provider.js +44 -0
  46. package/bin/utils/file-io.js +63 -1
  47. package/bin/utils/index.js +58 -0
  48. package/docs/getting-started.md +1 -1
  49. package/docs/user-guide.md +2 -2
  50. package/package.json +2 -2
@@ -109,6 +109,7 @@ function addEdge(edge) {
109
109
  record.checksum = crypto.createHash('sha256').update(payload).digest('hex');
110
110
 
111
111
  fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(record) + '\n');
112
+ invalidateAdjacencyCache();
112
113
  return id;
113
114
  }
114
115
 
@@ -155,6 +156,7 @@ function deprecateEdge(edgeId, reason) {
155
156
  };
156
157
 
157
158
  fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(deprecated) + '\n');
159
+ invalidateAdjacencyCache();
158
160
  }
159
161
 
160
162
  /**
@@ -181,18 +183,68 @@ function reinforceEdge(edgeId) {
181
183
  fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(reinforced) + '\n');
182
184
  }
183
185
 
184
- // ── Adjacency Index ───────────────────────────────────────────────────────────
186
+ // ── Adjacency Index (with persistent cache) ─────────────────────────────────
187
+
188
+ function getAdjacencyCachePath() {
189
+ const paths = getPaths();
190
+ return path.join(paths.MEMORY_DIR, '.adjacency-cache.json');
191
+ }
192
+
193
+ function invalidateAdjacencyCache() {
194
+ const cachePath = getAdjacencyCachePath();
195
+ if (fs.existsSync(cachePath)) {
196
+ fs.unlinkSync(cachePath);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Load adjacency index from cache if edges file hasn't changed,
202
+ * otherwise rebuild and persist.
203
+ * @param {object[]} edges - All active edges (used for rebuild)
204
+ * @returns {Map<string, object[]>} nodeId → [{ edge, neighborId, direction }]
205
+ */
206
+ function loadOrBuildAdjacencyIndex(edges) {
207
+ const paths = getPaths();
208
+ const cachePath = getAdjacencyCachePath();
209
+ const edgesStat = fs.statSync(paths.EDGES_PATH, { throwIfNoEntry: false });
210
+
211
+ if (edgesStat && fs.existsSync(cachePath)) {
212
+ try {
213
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
214
+ if (cache.mtime === edgesStat.mtimeMs) {
215
+ const index = new Map();
216
+ for (const [nodeId, neighbors] of Object.entries(cache.adjacency)) {
217
+ index.set(nodeId, neighbors);
218
+ }
219
+ return index;
220
+ }
221
+ } catch (e) { /* cache corrupt, rebuild */ }
222
+ }
223
+
224
+ const index = buildAdjacencyIndex(edges);
225
+
226
+ if (edgesStat) {
227
+ const serialized = {};
228
+ for (const [nodeId, neighbors] of index) {
229
+ serialized[nodeId] = neighbors;
230
+ }
231
+ const cacheData = { mtime: edgesStat.mtimeMs, adjacency: serialized };
232
+ ensureDir(paths.MEMORY_DIR);
233
+ fs.writeFileSync(cachePath, JSON.stringify(cacheData));
234
+ }
235
+
236
+ return index;
237
+ }
185
238
 
186
239
  /**
187
240
  * Build an in-memory adjacency index for O(1) neighbor lookups.
188
241
  * @param {object[]} edges - All active edges
189
- * @returns {Map<string, object[]>} nodeId → [{ edge, neighborId }]
242
+ * @returns {Map<string, object[]>} nodeId → [{ edge, neighborId, direction }]
190
243
  */
191
244
  function buildAdjacencyIndex(edges) {
192
245
  const index = new Map();
193
246
 
194
247
  for (const edge of edges) {
195
- // Forward direction
196
248
  if (!index.has(edge.sourceId)) index.set(edge.sourceId, []);
197
249
  index.get(edge.sourceId).push({
198
250
  edge,
@@ -200,7 +252,6 @@ function buildAdjacencyIndex(edges) {
200
252
  direction: 'outgoing',
201
253
  });
202
254
 
203
- // Reverse direction (for bidirectional traversal)
204
255
  if (!index.has(edge.targetId)) index.set(edge.targetId, []);
205
256
  index.get(edge.targetId).push({
206
257
  edge,
@@ -262,7 +313,7 @@ function addFederatedEdge(edge) {
262
313
  function traverse(startId, maxDepth = 2, opts = {}) {
263
314
  const { edgeTypes, minWeight = 0 } = opts;
264
315
  const edges = readAllEdges();
265
- const adjacency = buildAdjacencyIndex(edges);
316
+ const adjacency = loadOrBuildAdjacencyIndex(edges);
266
317
 
267
318
  const visited = new Set();
268
319
  const results = [];
@@ -598,6 +649,8 @@ module.exports = {
598
649
  deprecateEdge,
599
650
  reinforceEdge,
600
651
  buildAdjacencyIndex,
652
+ loadOrBuildAdjacencyIndex,
653
+ invalidateAdjacencyCache,
601
654
  traverse,
602
655
  findRelated,
603
656
  getNodeEdges,
@@ -9,7 +9,10 @@
9
9
  */
10
10
  'use strict';
11
11
 
12
+ const fs = require('fs');
13
+ const path = require('path');
12
14
  const Store = require('./knowledge-store');
15
+ const { buildBM25Index, bm25Score } = require('./embedding-engine');
13
16
 
14
17
  // ── Stopwords (excluded from TF-IDF scoring) ──────────────────────────────────
15
18
  const STOPWORDS = new Set([
@@ -79,6 +82,48 @@ function tfidfScore(queryTokens, entryId, index, docTokenCounts, N) {
79
82
  return score;
80
83
  }
81
84
 
85
+ // ── Persistent BM25 Index Cache ──────────────────────────────────────────────
86
+
87
+ function getKbPath() {
88
+ const memoryDir = path.join(process.cwd(), '.mindforge', 'memory');
89
+ return path.join(memoryDir, 'knowledge.jsonl');
90
+ }
91
+
92
+ function getCachePath() {
93
+ const memoryDir = path.join(process.cwd(), '.mindforge', 'memory');
94
+ return path.join(memoryDir, '.index-cache.json');
95
+ }
96
+
97
+ /**
98
+ * Load BM25 index from cache if source file hasn't changed,
99
+ * otherwise rebuild and persist.
100
+ */
101
+ function loadOrBuildIndex(entries) {
102
+ const kbPath = getKbPath();
103
+ const cachePath = getCachePath();
104
+ const stat = fs.statSync(kbPath, { throwIfNoEntry: false });
105
+
106
+ if (stat && fs.existsSync(cachePath)) {
107
+ try {
108
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
109
+ if (cache.mtime === stat.mtimeMs && cache.entryCount === entries.length) {
110
+ return cache.index;
111
+ }
112
+ } catch (e) { /* cache corrupt, rebuild */ }
113
+ }
114
+
115
+ const index = buildBM25Index(entries);
116
+
117
+ if (stat) {
118
+ const dir = path.dirname(cachePath);
119
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
120
+ const cacheData = { mtime: stat.mtimeMs, entryCount: entries.length, index };
121
+ fs.writeFileSync(cachePath, JSON.stringify(cacheData));
122
+ }
123
+
124
+ return index;
125
+ }
126
+
82
127
  // ── Main search function ──────────────────────────────────────────────────────
83
128
  /**
84
129
  * Search knowledge base with TF-IDF scoring.
@@ -106,18 +151,20 @@ function search(queryText, filters = {}, limit = 10) {
106
151
 
107
152
  const queryTokens = tokenize(queryText);
108
153
  if (queryTokens.length === 0) {
109
- // No meaningful query tokens — return by confidence
110
154
  return candidates
111
155
  .sort((a, b) => b.confidence - a.confidence)
112
156
  .slice(0, limit);
113
157
  }
114
158
 
115
- const { index, docTokenCounts, N } = buildIndex(candidates);
159
+ // Use cached BM25 index for scoring
160
+ const bm25Index = loadOrBuildIndex(candidates);
161
+ const { docFrequency, avgDocLength, tokenizedDocs } = bm25Index;
162
+ const totalDocs = tokenizedDocs.length;
163
+ const docMap = new Map(tokenizedDocs.map(d => [d.id, d.tokens]));
116
164
 
117
- // Score each candidate
118
165
  const scored = candidates.map(entry => {
119
- const textScore = tfidfScore(queryTokens, entry.id, index, docTokenCounts, N);
120
- // Combine TF-IDF score with confidence, but only if there's a text match
166
+ const docTokens = docMap.get(entry.id) || [];
167
+ const textScore = bm25Score(queryTokens, docTokens, docFrequency, totalDocs, avgDocLength);
121
168
  const finalScore = textScore > 0
122
169
  ? textScore * 0.7 + entry.confidence * 0.3
123
170
  : 0;
@@ -169,4 +216,4 @@ function loadSessionContext(context = {}) {
169
216
  return { preferences, decisions, bugPatterns, codePatterns, domain };
170
217
  }
171
218
 
172
- module.exports = { search, loadSessionContext, buildIndex, tfidfScore, tokenize };
219
+ module.exports = { search, loadSessionContext, buildIndex, tfidfScore, tokenize, loadOrBuildIndex };
@@ -126,6 +126,22 @@ function getFilePath(type) {
126
126
  }
127
127
  }
128
128
 
129
+ // ── File Integrity ────────────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Ensures a JSONL file doesn't end with a partial/truncated line.
133
+ * Appends a trailing newline if missing — prevents corruption from propagating.
134
+ */
135
+ function verifyFileIntegrity(filePath) {
136
+ if (!fs.existsSync(filePath)) return true;
137
+ const content = fs.readFileSync(filePath, 'utf8');
138
+ if (content.length === 0) return true;
139
+ if (!content.endsWith('\n')) {
140
+ fs.appendFileSync(filePath, '\n');
141
+ }
142
+ return true;
143
+ }
144
+
129
145
  // ── Write operations ──────────────────────────────────────────────────────────
130
146
 
131
147
  /**
@@ -184,10 +200,12 @@ function add(entry) {
184
200
  };
185
201
 
186
202
  const filePath = getFilePath(entry.type);
203
+ verifyFileIntegrity(filePath);
187
204
  fs.appendFileSync(filePath, JSON.stringify(full) + '\n');
188
205
 
189
206
  // Also append to unified knowledge-base.jsonl for cross-type queries
190
207
  if (filePath !== paths.KB_PATH) {
208
+ verifyFileIntegrity(paths.KB_PATH);
191
209
  fs.appendFileSync(paths.KB_PATH, JSON.stringify(full) + '\n');
192
210
  }
193
211
 
@@ -217,8 +235,10 @@ function deprecate(id, reason, supersededBy = null) {
217
235
  deprecated_at: new Date().toISOString(),
218
236
  };
219
237
 
238
+ verifyFileIntegrity(filePath);
220
239
  fs.appendFileSync(filePath, JSON.stringify(deprecated) + '\n');
221
240
  if (filePath !== paths.KB_PATH) {
241
+ verifyFileIntegrity(paths.KB_PATH);
222
242
  fs.appendFileSync(paths.KB_PATH, JSON.stringify(deprecated) + '\n');
223
243
  }
224
244
 
@@ -246,8 +266,10 @@ function reinforce(id) {
246
266
  };
247
267
 
248
268
  const filePath = getFilePath(entry.type);
269
+ verifyFileIntegrity(filePath);
249
270
  fs.appendFileSync(filePath, JSON.stringify(reinforced) + '\n');
250
271
  if (filePath !== paths.KB_PATH) {
272
+ verifyFileIntegrity(paths.KB_PATH);
251
273
  fs.appendFileSync(paths.KB_PATH, JSON.stringify(reinforced) + '\n');
252
274
  }
253
275
 
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const MIGRATION_ID = '10.7.0-to-11.0.0';
7
+ const TARGET_VERSION = '11.0.0';
8
+
9
+ async function migrate(projectRoot) {
10
+ const results = { steps: [], success: true };
11
+
12
+ // Step 1: Backup config.json
13
+ const configPath = path.join(projectRoot, '.mindforge', 'config.json');
14
+ if (fs.existsSync(configPath)) {
15
+ const backupPath = configPath + '.v10-backup';
16
+ fs.copyFileSync(configPath, backupPath);
17
+ results.steps.push({ step: 'backup_config', status: 'done', path: backupPath });
18
+
19
+ // Step 2: Add new config sections
20
+ try {
21
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
+
23
+ if (!config.temporal) {
24
+ config.temporal = { max_snapshots: 50, max_age_days: 7 };
25
+ }
26
+ if (!config.rate_limiting) {
27
+ config.rate_limiting = { dashboard_rpm: 100, model_rpm: {} };
28
+ }
29
+ if (!config.session) {
30
+ config.session = { token_expiry_hours: 24 };
31
+ }
32
+ if (!config.wave_execution) {
33
+ config.wave_execution = { max_concurrency: 3 };
34
+ }
35
+
36
+ config.version = TARGET_VERSION;
37
+
38
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
39
+ results.steps.push({ step: 'update_config', status: 'done' });
40
+ } catch (e) {
41
+ results.steps.push({ step: 'update_config', status: 'warning', error: e.message });
42
+ }
43
+ }
44
+
45
+ // Step 3: Archive old AUDIT lines if > 5000
46
+ const auditPath = path.join(projectRoot, '.planning', 'AUDIT.jsonl');
47
+ if (fs.existsSync(auditPath)) {
48
+ try {
49
+ const content = fs.readFileSync(auditPath, 'utf8');
50
+ const lines = content.split('\n').filter(l => l.trim());
51
+ if (lines.length > 5000) {
52
+ const archiveDir = path.join(projectRoot, '.planning', 'audit-archive');
53
+ if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
54
+
55
+ const zlib = require('zlib');
56
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
57
+ const archivePath = path.join(archiveDir, `AUDIT-pre-v11-${timestamp}.jsonl.gz`);
58
+ const toArchive = lines.slice(0, -500).join('\n') + '\n';
59
+ fs.writeFileSync(archivePath, zlib.gzipSync(toArchive));
60
+
61
+ const remaining = lines.slice(-500).join('\n') + '\n';
62
+ fs.writeFileSync(auditPath, remaining);
63
+ results.steps.push({ step: 'archive_audit', status: 'done', archived: lines.length - 500 });
64
+ } else {
65
+ results.steps.push({ step: 'archive_audit', status: 'skipped', reason: 'under_threshold' });
66
+ }
67
+ } catch (e) {
68
+ results.steps.push({ step: 'archive_audit', status: 'warning', error: e.message });
69
+ }
70
+ }
71
+
72
+ // Step 4: GC old snapshots
73
+ try {
74
+ const TemporalHub = require('../engine/temporal-hub');
75
+ const gcResult = await TemporalHub.gc({ maxSnapshots: 50, maxAgeDays: 30 });
76
+ results.steps.push({ step: 'snapshot_gc', status: 'done', deleted: gcResult.deleted });
77
+ } catch (e) {
78
+ results.steps.push({ step: 'snapshot_gc', status: 'warning', error: e.message });
79
+ }
80
+
81
+ // Step 5: Bump schema_version in HANDOFF.json
82
+ const handoffPath = path.join(projectRoot, '.planning', 'HANDOFF.json');
83
+ if (fs.existsSync(handoffPath)) {
84
+ try {
85
+ const handoff = JSON.parse(fs.readFileSync(handoffPath, 'utf8'));
86
+ handoff.schema_version = TARGET_VERSION;
87
+ fs.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2) + '\n');
88
+ results.steps.push({ step: 'bump_handoff_version', status: 'done' });
89
+ } catch (e) {
90
+ results.steps.push({ step: 'bump_handoff_version', status: 'warning', error: e.message });
91
+ }
92
+ }
93
+
94
+ // Step 6: Update MINDFORGE.md VERSION
95
+ const mindforgeFile = path.join(projectRoot, 'MINDFORGE.md');
96
+ if (fs.existsSync(mindforgeFile)) {
97
+ try {
98
+ let content = fs.readFileSync(mindforgeFile, 'utf8');
99
+ content = content.replace(/VERSION\s*=\s*[\d.]+/, `VERSION = ${TARGET_VERSION}`);
100
+ fs.writeFileSync(mindforgeFile, content);
101
+ results.steps.push({ step: 'bump_mindforge_version', status: 'done' });
102
+ } catch (e) {
103
+ results.steps.push({ step: 'bump_mindforge_version', status: 'warning', error: e.message });
104
+ }
105
+ }
106
+
107
+ return results;
108
+ }
109
+
110
+ module.exports = { MIGRATION_ID, TARGET_VERSION, migrate };
@@ -71,6 +71,19 @@ const SCHEMA_HISTORY = [
71
71
  'Plugin API version upgraded to 2.0.0',
72
72
  ],
73
73
  },
74
+ {
75
+ version: '11.0.0',
76
+ date: '2026-05-28',
77
+ description: 'v11.0.0 - Persona Expansion: temporal config, rate limiting, wave execution, audit archival',
78
+ handoff_fields_added: [],
79
+ handoff_fields_removed: [],
80
+ audit_fields_added: [],
81
+ breaking: [
82
+ 'config.json gains temporal, rate_limiting, session, wave_execution sections',
83
+ 'AUDIT.jsonl auto-archived if exceeding 5000 lines',
84
+ 'MINDFORGE.md VERSION format drops suffix (was X.Y.Z-SUFFIX, now X.Y.Z)',
85
+ ],
86
+ },
74
87
  ];
75
88
 
76
89
  module.exports = { SCHEMA_HISTORY };
@@ -72,6 +72,51 @@ class AnthropicProvider {
72
72
  req.end();
73
73
  });
74
74
  }
75
+
76
+ async streamComplete(messages, options = {}) {
77
+ const model = options.model || 'claude-sonnet-4-6';
78
+ const maxTokens = options.maxTokens || 4096;
79
+
80
+ const data = JSON.stringify({
81
+ model,
82
+ messages,
83
+ max_tokens: maxTokens,
84
+ stream: true,
85
+ });
86
+
87
+ return new Promise((resolve, reject) => {
88
+ const req = https.request({
89
+ hostname: 'api.anthropic.com',
90
+ path: '/v1/messages',
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ 'x-api-key': this.apiKey,
95
+ 'anthropic-version': '2023-06-01',
96
+ 'Content-Length': Buffer.byteLength(data),
97
+ },
98
+ timeout: 300_000,
99
+ }, res => {
100
+ if (res.statusCode !== 200) {
101
+ let body = '';
102
+ res.on('data', chunk => body += chunk);
103
+ res.on('end', () => {
104
+ reject(new Error(`Anthropic streaming failed: ${res.statusCode}`));
105
+ });
106
+ return;
107
+ }
108
+ resolve(res);
109
+ });
110
+
111
+ req.on('error', reject);
112
+ req.on('timeout', () => {
113
+ req.destroy();
114
+ reject(new Error('Anthropic stream timeout'));
115
+ });
116
+ req.write(data);
117
+ req.end();
118
+ });
119
+ }
75
120
  }
76
121
 
77
122
  module.exports = AnthropicProvider;
@@ -7,17 +7,37 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
 
10
+ // Per-provider latency ring buffer (last 10 calls)
11
+ const latencyHistory = new Map();
12
+
13
+ function recordLatency(provider, durationMs) {
14
+ if (!latencyHistory.has(provider)) {
15
+ latencyHistory.set(provider, []);
16
+ }
17
+ const history = latencyHistory.get(provider);
18
+ history.push(durationMs);
19
+ if (history.length > 10) history.shift();
20
+ }
21
+
22
+ function getP95Latency(provider) {
23
+ const history = latencyHistory.get(provider);
24
+ if (!history || history.length === 0) return 500;
25
+ const sorted = [...history].sort((a, b) => a - b);
26
+ const idx = Math.ceil(sorted.length * 0.95) - 1;
27
+ return sorted[Math.min(idx, sorted.length - 1)];
28
+ }
29
+
10
30
  class CloudBroker {
11
31
  constructor(config = {}) {
12
32
  this.providers = config.providers || ['anthropic', 'google', 'aws', 'azure'];
13
33
  this.statsPath = config.statsPath || path.join(__dirname, 'performance-stats.json');
14
- this.blacklist = new Map(); // provider -> expiry (Date)
15
- this.failureWindow = new Map(); // provider:taskType -> count
34
+ this.blacklist = new Map();
35
+ this.failureWindow = new Map();
16
36
  this.state = {
17
- 'anthropic': { latency: 450, costMultiplier: 1.0, healthy: true },
18
- 'google': { latency: 600, costMultiplier: 0.85, healthy: true },
19
- 'aws': { latency: 550, costMultiplier: 0.95, healthy: true },
20
- 'azure': { latency: 650, costMultiplier: 1.1, healthy: true }
37
+ 'anthropic': { costMultiplier: 1.0, healthy: true },
38
+ 'google': { costMultiplier: 0.85, healthy: true },
39
+ 'aws': { costMultiplier: 0.95, healthy: true },
40
+ 'azure': { costMultiplier: 1.1, healthy: true }
21
41
  };
22
42
  this.reloadStats();
23
43
  }
@@ -71,21 +91,20 @@ class CloudBroker {
71
91
  return true;
72
92
  })
73
93
  .map(([id, data]) => {
74
- // Calculate Success Probability for this task
75
94
  const stats = this.performanceStats[id]?.[taskType] || { success: 1, failure: 0 };
76
95
  const totalTasks = stats.success + stats.failure;
77
96
  const successProb = totalTasks > 0 ? (stats.success / totalTasks) : 0.5;
78
97
 
79
- // Score Calculation (The "Affinity" Algorithm)
80
98
  const latencyWeight = 0.2;
81
99
  const costWeight = 0.3;
82
- const affinityWeight = 0.5;
100
+ const affinityWeight = 0.5;
83
101
 
84
- const score = (data.latency * latencyWeight) +
85
- (data.costMultiplier * 1000 * costWeight) +
102
+ const providerLatency = getP95Latency(id);
103
+ const score = (providerLatency * latencyWeight) +
104
+ (data.costMultiplier * 1000 * costWeight) +
86
105
  ((1.0 - successProb) * 2000 * affinityWeight);
87
106
 
88
- return { id, score, successProb: successProb.toFixed(2) };
107
+ return { id, score, successProb: successProb.toFixed(2), p95: providerLatency };
89
108
  });
90
109
 
91
110
  scored.sort((a, b) => a.score - b.score);
@@ -110,7 +129,7 @@ class CloudBroker {
110
129
 
111
130
  const fallback = Object.entries(this.state)
112
131
  .filter(([id, data]) => id !== failedProvider && data.healthy)
113
- .sort((a, b) => a[1].latency - b[1].latency)[0];
132
+ .sort((a, b) => getP95Latency(a[0]) - getP95Latency(b[0]))[0];
114
133
 
115
134
  return fallback ? fallback[0] : 'google'; // Default fallback
116
135
  }
@@ -130,33 +149,62 @@ class CloudBroker {
130
149
  return mappings[provider]?.[modelGroup] || mappings[provider]?.['sonnet'];
131
150
  }
132
151
 
152
+ /**
153
+ * Removes failure entries whose sliding window (5 min) has expired.
154
+ */
155
+ _pruneStaleFailures() {
156
+ const now = Date.now();
157
+ const WINDOW_MS = 5 * 60 * 1000;
158
+
159
+ for (const [key, entry] of this.failureWindow.entries()) {
160
+ if (now - entry.firstFailureAt > WINDOW_MS) {
161
+ this.failureWindow.delete(key);
162
+ }
163
+ }
164
+ }
165
+
133
166
  /**
134
167
  * Records a task failure and manages the circuit breaker.
135
168
  */
136
169
  recordFailure(provider, taskType = 'default') {
137
- const key = `${provider}:${taskType}`;
138
- const failures = (this.failureWindow.get(key) || 0) + 1;
139
- this.failureWindow.set(key, failures);
170
+ this._pruneStaleFailures();
140
171
 
141
- if (failures >= 3) {
142
- const expiry = new Date(Date.now() + 10 * 60 * 1000); // 10 min blacklist
172
+ const key = `${provider}:${taskType}`;
173
+ const existing = this.failureWindow.get(key);
174
+ const entry = existing
175
+ ? { count: existing.count + 1, firstFailureAt: existing.firstFailureAt }
176
+ : { count: 1, firstFailureAt: Date.now() };
177
+ this.failureWindow.set(key, entry);
178
+
179
+ if (entry.count >= 3) {
180
+ const expiry = new Date(Date.now() + 10 * 60 * 1000);
143
181
  this.blacklist.set(provider, expiry);
144
182
  console.warn(`[MCA-CIRCUIT-OPEN] Provider '${provider}' blacklisted for 10 min due to consecutive failures on '${taskType}'.`);
145
- this.failureWindow.set(key, 0); // Reset window upon blacklisting
183
+ this.failureWindow.delete(key);
146
184
  }
147
185
  }
148
186
 
149
187
  /**
150
188
  * Hardening: Simulate provider failures to verify Fallback Protocol.
151
189
  */
190
+ recordLatency(provider, durationMs) {
191
+ recordLatency(provider, durationMs);
192
+ }
193
+
194
+ getP95Latency(provider) {
195
+ return getP95Latency(provider);
196
+ }
197
+
152
198
  startChaosMode() {
153
199
  console.log('[ENTERPRISE-RESILIENCE] CloudBroker Chaos Mode ACTIVE. Simulating jitter and provider dropouts...');
154
200
  setInterval(() => {
155
201
  const providers = Object.keys(this.state);
156
202
  const randomProvider = providers[Math.floor(Math.random() * providers.length)];
157
- this.state[randomProvider].latency = Math.random() > 0.7 ? 5000 : 100;
203
+ recordLatency(randomProvider, Math.random() > 0.7 ? 5000 : 100);
158
204
  }, 10000);
159
205
  }
160
206
  }
161
207
 
162
208
  module.exports = CloudBroker;
209
+ module.exports.recordLatency = recordLatency;
210
+ module.exports.getP95Latency = getP95Latency;
@@ -74,6 +74,57 @@ class GeminiProvider {
74
74
  req.end();
75
75
  });
76
76
  }
77
+
78
+ async streamComplete(messages, options = {}) {
79
+ const model = options.model || 'gemini-2.5-pro';
80
+ const maxTokens = options.maxTokens || 8192;
81
+
82
+ const contents = messages.map(msg => ({
83
+ role: msg.role === 'assistant' ? 'model' : 'user',
84
+ parts: [{ text: msg.content }],
85
+ }));
86
+
87
+ const data = JSON.stringify({
88
+ contents,
89
+ generationConfig: {
90
+ maxOutputTokens: maxTokens,
91
+ },
92
+ });
93
+
94
+ const modelId = model.startsWith('models/') ? model : `models/${model}`;
95
+
96
+ return new Promise((resolve, reject) => {
97
+ const req = https.request({
98
+ hostname: 'generativelanguage.googleapis.com',
99
+ path: `/v1beta/${modelId}:streamGenerateContent?alt=sse`,
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'x-goog-api-key': this.apiKey,
104
+ 'Content-Length': Buffer.byteLength(data),
105
+ },
106
+ timeout: 300_000,
107
+ }, res => {
108
+ if (res.statusCode !== 200) {
109
+ let body = '';
110
+ res.on('data', chunk => body += chunk);
111
+ res.on('end', () => {
112
+ reject(new Error(`Gemini streaming failed: ${res.statusCode}`));
113
+ });
114
+ return;
115
+ }
116
+ resolve(res);
117
+ });
118
+
119
+ req.on('error', reject);
120
+ req.on('timeout', () => {
121
+ req.destroy();
122
+ reject(new Error('Gemini stream timeout'));
123
+ });
124
+ req.write(data);
125
+ req.end();
126
+ });
127
+ }
77
128
  }
78
129
 
79
130
  module.exports = GeminiProvider;
@@ -82,6 +82,26 @@ class ModelClient {
82
82
  }
83
83
  }
84
84
 
85
+ static async streamComplete(params) {
86
+ const {
87
+ persona = 'developer',
88
+ tier = 1,
89
+ messages,
90
+ maxTokens,
91
+ taskName = 'unknown',
92
+ } = params;
93
+
94
+ const routing = Router.route(persona, tier);
95
+ const modelId = routing.model;
96
+ const provider = this._getProvider(modelId);
97
+
98
+ if (!provider || !provider.streamComplete) {
99
+ throw new Error(`Streaming not supported for model: ${modelId}`);
100
+ }
101
+
102
+ return provider.streamComplete(messages, { ...params, model: modelId });
103
+ }
104
+
85
105
  static _getProvider(modelId) {
86
106
  if (modelId.startsWith('claude') || modelId.startsWith('anthropic.claude')) {
87
107
  if (!process.env.ANTHROPIC_API_KEY) return null;