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.
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +6 -1
- package/CHANGELOG.md +64 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +80 -0
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +13 -0
- package/bin/autonomous/auto-runner.js +74 -16
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +66 -25
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +67 -4
- package/bin/dashboard/sse-bridge.js +4 -4
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-hub.js +131 -34
- package/bin/governance/approve.js +41 -5
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +32 -19
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +49 -7
- package/bin/hindsight-injector.js +3 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +22 -0
- package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/models/anthropic-provider.js +45 -0
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/gemini-provider.js +51 -0
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +28 -8
- package/bin/models/openai-provider.js +44 -0
- package/bin/utils/file-io.js +63 -1
- package/bin/utils/index.js +58 -0
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- 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 =
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
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();
|
|
15
|
-
this.failureWindow = new Map();
|
|
34
|
+
this.blacklist = new Map();
|
|
35
|
+
this.failureWindow = new Map();
|
|
16
36
|
this.state = {
|
|
17
|
-
'anthropic': {
|
|
18
|
-
'google': {
|
|
19
|
-
'aws': {
|
|
20
|
-
'azure': {
|
|
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
|
|
85
|
-
|
|
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[
|
|
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
|
-
|
|
138
|
-
const failures = (this.failureWindow.get(key) || 0) + 1;
|
|
139
|
-
this.failureWindow.set(key, failures);
|
|
170
|
+
this._pruneStaleFailures();
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|