cascade-ai 0.12.24 → 0.13.2
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 +19 -2
- package/dist/cli.cjs +943 -211
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +938 -206
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1619 -927
- package/dist/index.cjs +894 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -2
- package/dist/index.d.ts +198 -2
- package/dist/index.js +871 -174
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var Database = require('better-sqlite3');
|
|
4
|
+
var path20 = require('path');
|
|
5
|
+
var fs19 = require('fs');
|
|
6
|
+
var os6 = require('os');
|
|
7
|
+
var crypto3 = require('crypto');
|
|
3
8
|
var EventEmitter = require('events');
|
|
4
|
-
var crypto = require('crypto');
|
|
5
9
|
var glob = require('glob');
|
|
6
10
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
7
11
|
var OpenAI = require('openai');
|
|
@@ -12,12 +16,9 @@ var https = require('https');
|
|
|
12
16
|
var zlib = require('zlib');
|
|
13
17
|
var stream = require('stream');
|
|
14
18
|
var fs4 = require('fs/promises');
|
|
15
|
-
var path18 = require('path');
|
|
16
|
-
var os4 = require('os');
|
|
17
19
|
var ignoreFactory = require('ignore');
|
|
18
20
|
var child_process = require('child_process');
|
|
19
21
|
var util = require('util');
|
|
20
|
-
var fs17 = require('fs');
|
|
21
22
|
var simpleGit = require('simple-git');
|
|
22
23
|
var PDFDocument = require('pdfkit');
|
|
23
24
|
var dns2 = require('dns/promises');
|
|
@@ -26,7 +27,6 @@ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
|
26
27
|
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
27
28
|
var zod = require('zod');
|
|
28
29
|
var worker_threads = require('worker_threads');
|
|
29
|
-
var Database = require('better-sqlite3');
|
|
30
30
|
var url = require('url');
|
|
31
31
|
var express = require('express');
|
|
32
32
|
var rateLimit = require('express-rate-limit');
|
|
@@ -57,8 +57,12 @@ function _interopNamespace(e) {
|
|
|
57
57
|
return Object.freeze(n);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
61
|
+
var path20__default = /*#__PURE__*/_interopDefault(path20);
|
|
62
|
+
var fs19__default = /*#__PURE__*/_interopDefault(fs19);
|
|
63
|
+
var os6__default = /*#__PURE__*/_interopDefault(os6);
|
|
64
|
+
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
60
65
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
61
|
-
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
62
66
|
var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
|
|
63
67
|
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
64
68
|
var dns__default = /*#__PURE__*/_interopDefault(dns);
|
|
@@ -66,14 +70,10 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
66
70
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
67
71
|
var zlib__default = /*#__PURE__*/_interopDefault(zlib);
|
|
68
72
|
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
|
|
69
|
-
var path18__default = /*#__PURE__*/_interopDefault(path18);
|
|
70
|
-
var os4__default = /*#__PURE__*/_interopDefault(os4);
|
|
71
73
|
var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
|
|
72
|
-
var fs17__default = /*#__PURE__*/_interopDefault(fs17);
|
|
73
74
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
74
75
|
var dns2__default = /*#__PURE__*/_interopDefault(dns2);
|
|
75
76
|
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
76
|
-
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
77
77
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
78
78
|
var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
79
79
|
var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
|
|
@@ -82,10 +82,161 @@ var jwt__default = /*#__PURE__*/_interopDefault(jwt);
|
|
|
82
82
|
var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
83
83
|
|
|
84
84
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
85
|
+
var __defProp = Object.defineProperty;
|
|
86
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
87
|
+
var __esm = (fn, res) => function __init() {
|
|
88
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
89
|
+
};
|
|
90
|
+
var __export = (target, all) => {
|
|
91
|
+
for (var name in all)
|
|
92
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
93
|
+
};
|
|
85
94
|
|
|
95
|
+
// src/core/audit/audit-logger.ts
|
|
96
|
+
var audit_logger_exports = {};
|
|
97
|
+
__export(audit_logger_exports, {
|
|
98
|
+
AuditLogger: () => AuditLogger2
|
|
99
|
+
});
|
|
100
|
+
var AuditLogger2;
|
|
101
|
+
var init_audit_logger = __esm({
|
|
102
|
+
"src/core/audit/audit-logger.ts"() {
|
|
103
|
+
AuditLogger2 = class {
|
|
104
|
+
constructor(workspacePath, debugMode = false) {
|
|
105
|
+
this.workspacePath = workspacePath;
|
|
106
|
+
this.debugMode = debugMode;
|
|
107
|
+
const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
|
|
108
|
+
if (!fs19__default.default.existsSync(cascadeDir)) {
|
|
109
|
+
fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
this.keyPath = path20__default.default.join(cascadeDir, "audit_log.key");
|
|
112
|
+
this.dbPath = path20__default.default.join(cascadeDir, "audit_log.db");
|
|
113
|
+
this.initEncryptionKey();
|
|
114
|
+
this.db = new Database__default.default(this.dbPath);
|
|
115
|
+
this.db.pragma("journal_mode = WAL");
|
|
116
|
+
this.db.exec(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
118
|
+
id TEXT PRIMARY KEY,
|
|
119
|
+
timestamp TEXT NOT NULL,
|
|
120
|
+
event_type TEXT NOT NULL,
|
|
121
|
+
tier_id TEXT NOT NULL,
|
|
122
|
+
encrypted_payload TEXT NOT NULL,
|
|
123
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
124
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
125
|
+
)
|
|
126
|
+
`);
|
|
127
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
128
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
129
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
130
|
+
}
|
|
131
|
+
workspacePath;
|
|
132
|
+
debugMode;
|
|
133
|
+
db;
|
|
134
|
+
keyPath;
|
|
135
|
+
dbPath;
|
|
136
|
+
encryptionKey;
|
|
137
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
138
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
139
|
+
return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
initEncryptionKey() {
|
|
142
|
+
if (fs19__default.default.existsSync(this.keyPath)) {
|
|
143
|
+
this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
|
|
144
|
+
} else {
|
|
145
|
+
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
146
|
+
fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
encrypt(text) {
|
|
150
|
+
const iv = crypto3__default.default.randomBytes(16);
|
|
151
|
+
const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
152
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
153
|
+
encrypted += cipher.final("hex");
|
|
154
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
155
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
156
|
+
}
|
|
157
|
+
decrypt(text) {
|
|
158
|
+
const parts = text.split(":");
|
|
159
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
160
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
161
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
162
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
163
|
+
const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
164
|
+
decipher.setAuthTag(authTag);
|
|
165
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
166
|
+
decrypted += decipher.final("utf8");
|
|
167
|
+
return decrypted;
|
|
168
|
+
}
|
|
169
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
170
|
+
const id = crypto3__default.default.randomUUID();
|
|
171
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
172
|
+
const payload = JSON.stringify(payloadObj);
|
|
173
|
+
const encryptedPayload = this.encrypt(payload);
|
|
174
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
175
|
+
const prevHash = last?.hash ?? "";
|
|
176
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
177
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
178
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
179
|
+
this.dumpDebugIfNeeded();
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
184
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
185
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
186
|
+
* can only be claimed for chained history.
|
|
187
|
+
*/
|
|
188
|
+
verifyChain() {
|
|
189
|
+
const rows = this.db.prepare(
|
|
190
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
191
|
+
).all();
|
|
192
|
+
let prevHash = "";
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
195
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
196
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
197
|
+
}
|
|
198
|
+
prevHash = row.hash;
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, entries: rows.length };
|
|
201
|
+
}
|
|
202
|
+
getAllLogs() {
|
|
203
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
204
|
+
const rows = stmt.all();
|
|
205
|
+
return rows.map((row) => {
|
|
206
|
+
let payload = "";
|
|
207
|
+
try {
|
|
208
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: row.id,
|
|
214
|
+
timestamp: row.timestamp,
|
|
215
|
+
eventType: row.event_type,
|
|
216
|
+
tierId: row.tier_id,
|
|
217
|
+
payload
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
dumpDebugIfNeeded() {
|
|
222
|
+
if (!this.debugMode) return;
|
|
223
|
+
try {
|
|
224
|
+
const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
225
|
+
const entries = this.getAllLogs();
|
|
226
|
+
fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error("Failed to dump debug audit logs", err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
close() {
|
|
232
|
+
this.db.close();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
86
237
|
|
|
87
238
|
// src/constants.ts
|
|
88
|
-
var CASCADE_VERSION = "0.
|
|
239
|
+
var CASCADE_VERSION = "0.13.2";
|
|
89
240
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
90
241
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
91
242
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2017,7 +2168,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
2017
2168
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
2018
2169
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
2019
2170
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
2020
|
-
var DEFAULT_CACHE_FILE =
|
|
2171
|
+
var DEFAULT_CACHE_FILE = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
2021
2172
|
function normalizeModelId(id) {
|
|
2022
2173
|
let s = id.toLowerCase();
|
|
2023
2174
|
const slash = s.lastIndexOf("/");
|
|
@@ -2132,7 +2283,7 @@ var LiveDataProvider = class {
|
|
|
2132
2283
|
}
|
|
2133
2284
|
async saveCache() {
|
|
2134
2285
|
try {
|
|
2135
|
-
await fs4__default.default.mkdir(
|
|
2286
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
2136
2287
|
const cache = {
|
|
2137
2288
|
fetchedAt: this.fetchedAt,
|
|
2138
2289
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -2262,7 +2413,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2262
2413
|
costByTier: {},
|
|
2263
2414
|
tokensByTier: {},
|
|
2264
2415
|
inputTokensByTier: {},
|
|
2265
|
-
outputTokensByTier: {}
|
|
2416
|
+
outputTokensByTier: {},
|
|
2417
|
+
costByFeature: {}
|
|
2266
2418
|
};
|
|
2267
2419
|
tierModels = /* @__PURE__ */ new Map();
|
|
2268
2420
|
config;
|
|
@@ -2284,6 +2436,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2284
2436
|
tpmLimiter;
|
|
2285
2437
|
localQueue;
|
|
2286
2438
|
taskAnalyzer;
|
|
2439
|
+
worldStateDB;
|
|
2440
|
+
privacyPaths;
|
|
2441
|
+
guidanceQueue;
|
|
2287
2442
|
liveData;
|
|
2288
2443
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
2289
2444
|
originalTierModels;
|
|
@@ -2441,7 +2596,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2441
2596
|
if (options.model && !requireVision) {
|
|
2442
2597
|
this.ensureProvider(options.model, this.config.providers);
|
|
2443
2598
|
}
|
|
2444
|
-
|
|
2599
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
2600
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
2601
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
2602
|
+
if (!localModel) {
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
"privacy.paths: this subtask touches a local-only path but no LOCAL model is available. Configure Ollama or an OpenAI-compatible endpoint on a loopback/private host, or remove the privacy policy."
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
2608
|
+
model = localModel;
|
|
2609
|
+
}
|
|
2445
2610
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
2446
2611
|
const provider = this.getProvider(model);
|
|
2447
2612
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -2513,7 +2678,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2513
2678
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
2514
2679
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
2515
2680
|
}
|
|
2516
|
-
this.recordStats(tier, model, result.usage);
|
|
2681
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
2517
2682
|
this.failover.recordSuccess(model.provider);
|
|
2518
2683
|
return result;
|
|
2519
2684
|
} catch (err) {
|
|
@@ -2618,6 +2783,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2618
2783
|
setTaskAnalyzer(analyzer) {
|
|
2619
2784
|
this.taskAnalyzer = analyzer;
|
|
2620
2785
|
}
|
|
2786
|
+
setWorldStateDB(db) {
|
|
2787
|
+
this.worldStateDB = db;
|
|
2788
|
+
}
|
|
2789
|
+
getWorldStateDB() {
|
|
2790
|
+
return this.worldStateDB;
|
|
2791
|
+
}
|
|
2792
|
+
setPrivacyPaths(paths) {
|
|
2793
|
+
this.privacyPaths = paths;
|
|
2794
|
+
}
|
|
2795
|
+
getPrivacyPaths() {
|
|
2796
|
+
return this.privacyPaths;
|
|
2797
|
+
}
|
|
2798
|
+
setGuidanceQueue(queue) {
|
|
2799
|
+
this.guidanceQueue = queue;
|
|
2800
|
+
}
|
|
2801
|
+
getGuidanceQueue() {
|
|
2802
|
+
return this.guidanceQueue;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
2806
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
2807
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
2808
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
2809
|
+
*/
|
|
2810
|
+
isPrivateModel(model) {
|
|
2811
|
+
if (model.isLocal) return true;
|
|
2812
|
+
if (model.provider !== "openai-compatible") return false;
|
|
2813
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
2814
|
+
if (!baseUrl) return false;
|
|
2815
|
+
try {
|
|
2816
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
2817
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith(".local");
|
|
2818
|
+
} catch {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2621
2822
|
/**
|
|
2622
2823
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
2623
2824
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -2641,7 +2842,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2641
2842
|
costByTier: { ...this.stats.costByTier },
|
|
2642
2843
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
2643
2844
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
2644
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
2845
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
2846
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
2645
2847
|
};
|
|
2646
2848
|
}
|
|
2647
2849
|
/**
|
|
@@ -2691,7 +2893,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2691
2893
|
costByTier: {},
|
|
2692
2894
|
tokensByTier: {},
|
|
2693
2895
|
inputTokensByTier: {},
|
|
2694
|
-
outputTokensByTier: {}
|
|
2896
|
+
outputTokensByTier: {},
|
|
2897
|
+
costByFeature: {}
|
|
2695
2898
|
};
|
|
2696
2899
|
this.sessionCostUsd = 0;
|
|
2697
2900
|
this.budgetState = "ok";
|
|
@@ -2857,7 +3060,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2857
3060
|
}
|
|
2858
3061
|
return void 0;
|
|
2859
3062
|
}
|
|
2860
|
-
recordStats(tier, model, usage) {
|
|
3063
|
+
recordStats(tier, model, usage, featureTag) {
|
|
2861
3064
|
this.stats.totalTokens += usage.totalTokens;
|
|
2862
3065
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
2863
3066
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -2867,6 +3070,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2867
3070
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
2868
3071
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
2869
3072
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
3073
|
+
if (featureTag) {
|
|
3074
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
3075
|
+
}
|
|
2870
3076
|
this.runTokens += usage.totalTokens;
|
|
2871
3077
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
2872
3078
|
this.updateBudgetState();
|
|
@@ -2963,13 +3169,24 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
2963
3169
|
hierarchyContext = "";
|
|
2964
3170
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
2965
3171
|
signal;
|
|
3172
|
+
/**
|
|
3173
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
3174
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
3175
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
3176
|
+
* which would interleave, are not tagged.
|
|
3177
|
+
*/
|
|
3178
|
+
isPresenter = false;
|
|
2966
3179
|
constructor(role, id, parentId) {
|
|
2967
3180
|
super();
|
|
2968
3181
|
this.role = role;
|
|
2969
|
-
this.id = id ?? `${role}_${
|
|
3182
|
+
this.id = id ?? `${role}_${crypto3.randomUUID().slice(0, 8)}`;
|
|
2970
3183
|
this.parentId = parentId;
|
|
2971
3184
|
this.label = this.id;
|
|
2972
3185
|
}
|
|
3186
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
3187
|
+
setPresenter(on = true) {
|
|
3188
|
+
this.isPresenter = on;
|
|
3189
|
+
}
|
|
2973
3190
|
getStatus() {
|
|
2974
3191
|
return this.status;
|
|
2975
3192
|
}
|
|
@@ -3217,7 +3434,7 @@ var AuditLogger = class {
|
|
|
3217
3434
|
logAt(level, tierId, action, details) {
|
|
3218
3435
|
if (!this.shouldLog(level)) return;
|
|
3219
3436
|
const entry = {
|
|
3220
|
-
id:
|
|
3437
|
+
id: crypto3.randomUUID(),
|
|
3221
3438
|
sessionId: this.sessionId,
|
|
3222
3439
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3223
3440
|
tierId,
|
|
@@ -3403,6 +3620,8 @@ var T3Worker = class extends BaseTier {
|
|
|
3403
3620
|
toolRegistry;
|
|
3404
3621
|
context;
|
|
3405
3622
|
assignment;
|
|
3623
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
3624
|
+
localOnlyMatch = false;
|
|
3406
3625
|
peerSyncBuffer = [];
|
|
3407
3626
|
store;
|
|
3408
3627
|
audit;
|
|
@@ -3458,6 +3677,11 @@ var T3Worker = class extends BaseTier {
|
|
|
3458
3677
|
this.taskId = taskId;
|
|
3459
3678
|
this.setLabel(assignment.subtaskTitle);
|
|
3460
3679
|
this.setStatus("ACTIVE");
|
|
3680
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
3681
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
3682
|
+
if (this.localOnlyMatch) {
|
|
3683
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
3684
|
+
}
|
|
3461
3685
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
3462
3686
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
3463
3687
|
this.tools = [...this.tools, {
|
|
@@ -3588,9 +3812,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
3588
3812
|
}
|
|
3589
3813
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
3590
3814
|
if (reflectCfg.enabled) {
|
|
3591
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
3815
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
3592
3816
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
3593
3817
|
}
|
|
3818
|
+
const db = this.router.getWorldStateDB?.();
|
|
3819
|
+
if (db) {
|
|
3820
|
+
try {
|
|
3821
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
3822
|
+
} catch (e) {
|
|
3823
|
+
this.log("Failed to write to World State DB");
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3594
3826
|
this.setStatus("COMPLETED", output);
|
|
3595
3827
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
3596
3828
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -3663,6 +3895,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
3663
3895
|
while (iterations < MAX_ITERATIONS) {
|
|
3664
3896
|
iterations++;
|
|
3665
3897
|
this.throwIfCancelled();
|
|
3898
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
3899
|
+
for (const g of guidance) {
|
|
3900
|
+
this.log(`User intervention received: ${g.text}`);
|
|
3901
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
3902
|
+
await this.context.addMessage({
|
|
3903
|
+
role: "user",
|
|
3904
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
3905
|
+
${g.text}`
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3666
3908
|
const options = {
|
|
3667
3909
|
messages: this.context.getMessages(),
|
|
3668
3910
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -3671,13 +3913,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3671
3913
|
// Don't pass tools array when model can't use them natively
|
|
3672
3914
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
3673
3915
|
maxTokens: 4096,
|
|
3674
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
3916
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
3917
|
+
featureTag: this.assignment?.sectionTitle,
|
|
3918
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
3675
3919
|
};
|
|
3676
3920
|
const result = await this.router.generate(
|
|
3677
3921
|
"T3",
|
|
3678
3922
|
options,
|
|
3679
3923
|
(chunk) => {
|
|
3680
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
3924
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
3681
3925
|
}
|
|
3682
3926
|
);
|
|
3683
3927
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -3835,8 +4079,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3835
4079
|
tierId: this.id,
|
|
3836
4080
|
sessionId: this.taskId,
|
|
3837
4081
|
requireApproval: false,
|
|
3838
|
-
saveSnapshot: async (
|
|
3839
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4082
|
+
saveSnapshot: async (path21, content) => {
|
|
4083
|
+
this.store?.addFileSnapshot(this.taskId, path21, content);
|
|
3840
4084
|
},
|
|
3841
4085
|
sendPeerSync: (to, syncType, content) => {
|
|
3842
4086
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -4006,7 +4250,7 @@ ${assignment.expectedOutput}`;
|
|
|
4006
4250
|
const { promisify: promisify4 } = await import('util');
|
|
4007
4251
|
const execAsync2 = promisify4(exec2);
|
|
4008
4252
|
for (const artifactPath of artifactPaths) {
|
|
4009
|
-
const absolutePath =
|
|
4253
|
+
const absolutePath = path20__default.default.resolve(process.cwd(), artifactPath);
|
|
4010
4254
|
try {
|
|
4011
4255
|
const stat = await fs4__default.default.stat(absolutePath);
|
|
4012
4256
|
if (!stat.isFile()) {
|
|
@@ -4027,7 +4271,7 @@ ${assignment.expectedOutput}`;
|
|
|
4027
4271
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
4028
4272
|
continue;
|
|
4029
4273
|
}
|
|
4030
|
-
const ext =
|
|
4274
|
+
const ext = path20__default.default.extname(absolutePath).toLowerCase();
|
|
4031
4275
|
try {
|
|
4032
4276
|
if (ext === ".ts" || ext === ".tsx") {
|
|
4033
4277
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -4056,30 +4300,35 @@ ${stdout}`);
|
|
|
4056
4300
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
4057
4301
|
*/
|
|
4058
4302
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
4059
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4060
|
-
|
|
4061
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
4062
4303
|
let current = output;
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
const
|
|
4304
|
+
try {
|
|
4305
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
4306
|
+
const verdictResult = await this.router.generate("T2", {
|
|
4066
4307
|
messages: [{
|
|
4067
4308
|
role: "user",
|
|
4068
|
-
content: `
|
|
4309
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
4069
4310
|
|
|
4070
|
-
Goal
|
|
4311
|
+
Goal: ${assignment.expectedOutput}
|
|
4071
4312
|
Subtask: ${assignment.description}
|
|
4072
|
-
|
|
4073
|
-
Output:
|
|
4313
|
+
Current Output:
|
|
4074
4314
|
${current}
|
|
4075
4315
|
|
|
4076
|
-
|
|
4316
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
4317
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
4077
4318
|
}],
|
|
4078
|
-
systemPrompt:
|
|
4079
|
-
maxTokens: 400
|
|
4319
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
4320
|
+
maxTokens: 400,
|
|
4321
|
+
signal: this.signal,
|
|
4322
|
+
featureTag: assignment.sectionTitle,
|
|
4323
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4080
4324
|
});
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4325
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
4326
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
4327
|
+
if (parsed.sufficient !== false) {
|
|
4328
|
+
this.log("T2-Critic approved output.");
|
|
4329
|
+
break;
|
|
4330
|
+
}
|
|
4331
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
4083
4332
|
const improved = await this.router.generate("T3", {
|
|
4084
4333
|
messages: [{
|
|
4085
4334
|
role: "user",
|
|
@@ -4091,16 +4340,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
4091
4340
|
Current output:
|
|
4092
4341
|
${current}`
|
|
4093
4342
|
}],
|
|
4094
|
-
systemPrompt:
|
|
4095
|
-
|
|
4343
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4344
|
+
|
|
4345
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4346
|
+
maxTokens: 4096,
|
|
4347
|
+
featureTag: assignment.sectionTitle,
|
|
4348
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4096
4349
|
});
|
|
4097
4350
|
const next = (improved.content ?? "").trim();
|
|
4098
4351
|
if (!next) break;
|
|
4099
4352
|
current = next;
|
|
4100
4353
|
this.log("Reflection: revised output for better goal alignment.");
|
|
4101
|
-
} catch {
|
|
4102
|
-
break;
|
|
4103
4354
|
}
|
|
4355
|
+
} catch (e) {
|
|
4356
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
4104
4357
|
}
|
|
4105
4358
|
return current;
|
|
4106
4359
|
}
|
|
@@ -4121,7 +4374,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
4121
4374
|
maxTokens: 500,
|
|
4122
4375
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4123
4376
|
|
|
4124
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
4377
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4378
|
+
featureTag: assignment.sectionTitle,
|
|
4379
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4125
4380
|
});
|
|
4126
4381
|
try {
|
|
4127
4382
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -4216,6 +4471,7 @@ Begin execution now.`;
|
|
|
4216
4471
|
issues,
|
|
4217
4472
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
4218
4473
|
correctionAttempts,
|
|
4474
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
4219
4475
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
4220
4476
|
};
|
|
4221
4477
|
}
|
|
@@ -4507,6 +4763,42 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
4507
4763
|
}
|
|
4508
4764
|
};
|
|
4509
4765
|
|
|
4766
|
+
// src/core/audit/redaction.ts
|
|
4767
|
+
var RedactionLayer = class {
|
|
4768
|
+
// Regexes for common secrets/PII
|
|
4769
|
+
static RULES = [
|
|
4770
|
+
// IPv4 addresses (basic approximation)
|
|
4771
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
4772
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
4773
|
+
{ pattern: /(?:\b(?:api_key|apikey|secret|token|password|bearer|auth|authorization)\b[^a-zA-Z0-9_]{1,4})([a-zA-Z0-9_\-\.]{16,})/gi, replacement: "$1[REDACTED_SECRET]" },
|
|
4774
|
+
// Email addresses
|
|
4775
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
4776
|
+
// Phone numbers (simplistic)
|
|
4777
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
4778
|
+
// AWS Access Key ID
|
|
4779
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
4780
|
+
];
|
|
4781
|
+
/**
|
|
4782
|
+
* Applies all redaction rules to the input string.
|
|
4783
|
+
*/
|
|
4784
|
+
static redact(text) {
|
|
4785
|
+
if (!text) return text;
|
|
4786
|
+
let redacted = text;
|
|
4787
|
+
for (const rule of this.RULES) {
|
|
4788
|
+
if (rule.pattern.test(redacted)) {
|
|
4789
|
+
rule.pattern.lastIndex = 0;
|
|
4790
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str) => {
|
|
4791
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
4792
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
4793
|
+
}
|
|
4794
|
+
return rule.replacement;
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
return redacted;
|
|
4799
|
+
}
|
|
4800
|
+
};
|
|
4801
|
+
|
|
4510
4802
|
// src/core/tiers/t2-manager.ts
|
|
4511
4803
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
4512
4804
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -4735,16 +5027,19 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4735
5027
|
try {
|
|
4736
5028
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
4737
5029
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
4738
|
-
|
|
5030
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
5031
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
4739
5032
|
} catch {
|
|
4740
5033
|
return [{
|
|
4741
|
-
subtaskId:
|
|
5034
|
+
subtaskId: crypto3.randomUUID(),
|
|
4742
5035
|
subtaskTitle: assignment.sectionTitle,
|
|
4743
5036
|
description: assignment.description,
|
|
4744
5037
|
expectedOutput: assignment.expectedOutput,
|
|
4745
5038
|
constraints: assignment.constraints,
|
|
4746
5039
|
peerT3Ids: [],
|
|
4747
5040
|
parentT2: this.id,
|
|
5041
|
+
sectionTitle: assignment.sectionTitle,
|
|
5042
|
+
dependsOn: [],
|
|
4748
5043
|
executionMode: "parallel"
|
|
4749
5044
|
}];
|
|
4750
5045
|
}
|
|
@@ -4864,6 +5159,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4864
5159
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
4865
5160
|
const worker = workerMap.get(id);
|
|
4866
5161
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
5162
|
+
if (result.localOnly) {
|
|
5163
|
+
result.output = `[local-only path \u2014 output withheld by privacy policy; status: ${result.status}; checks passed: ${result.testResults.passed.length}/${result.testResults.checksRun.length || 0}]`;
|
|
5164
|
+
} else {
|
|
5165
|
+
if (typeof result.output === "string" && result.output) {
|
|
5166
|
+
result.output = RedactionLayer.redact(result.output);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
4867
5170
|
resultMap.set(id, result);
|
|
4868
5171
|
return result;
|
|
4869
5172
|
};
|
|
@@ -5079,6 +5382,7 @@ ${peerOutputs}` : "";
|
|
|
5079
5382
|
chunkEnd++;
|
|
5080
5383
|
}
|
|
5081
5384
|
i = chunkEnd;
|
|
5385
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
5082
5386
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
5083
5387
|
${currentSummary ? `
|
|
5084
5388
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -5088,6 +5392,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
5088
5392
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
5089
5393
|
const messages = [{ role: "user", content: prompt }];
|
|
5090
5394
|
try {
|
|
5395
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
5091
5396
|
const result = await this.router.generate("T2", {
|
|
5092
5397
|
messages,
|
|
5093
5398
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -5095,7 +5400,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
5095
5400
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
5096
5401
|
maxTokens: 500,
|
|
5097
5402
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
5098
|
-
});
|
|
5403
|
+
}, streamFinal);
|
|
5099
5404
|
currentSummary = result.content;
|
|
5100
5405
|
} catch (err) {
|
|
5101
5406
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -5147,14 +5452,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
5147
5452
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
5148
5453
|
});
|
|
5149
5454
|
const answer = result.content.trim().toUpperCase();
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
}
|
|
5153
|
-
if (answer.includes("NO")) {
|
|
5154
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
5155
|
-
}
|
|
5455
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
5456
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
5156
5457
|
return null;
|
|
5157
5458
|
} catch {
|
|
5459
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
5158
5460
|
return null;
|
|
5159
5461
|
}
|
|
5160
5462
|
}
|
|
@@ -5302,7 +5604,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
5302
5604
|
}
|
|
5303
5605
|
async execute(userPrompt, images, systemContext, signal) {
|
|
5304
5606
|
this.signal = signal;
|
|
5305
|
-
this.taskId =
|
|
5607
|
+
this.taskId = crypto3.randomUUID();
|
|
5306
5608
|
this.setLabel("Administrator");
|
|
5307
5609
|
this.setStatus("ACTIVE");
|
|
5308
5610
|
this.taskGoal = userPrompt;
|
|
@@ -5493,10 +5795,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
5493
5795
|
}
|
|
5494
5796
|
}
|
|
5495
5797
|
async decomposeTask(prompt, systemContext) {
|
|
5798
|
+
const db = this.router.getWorldStateDB?.();
|
|
5799
|
+
const worldStateContext = db ? `
|
|
5800
|
+
|
|
5801
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
5802
|
+
${db.getFormattedState()}` : "";
|
|
5496
5803
|
const contextSection = systemContext ? `
|
|
5497
5804
|
Project context:
|
|
5498
5805
|
${systemContext}` : "";
|
|
5499
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
5806
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
5500
5807
|
|
|
5501
5808
|
Task: ${prompt}
|
|
5502
5809
|
|
|
@@ -5828,7 +6135,7 @@ Instructions:
|
|
|
5828
6135
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
5829
6136
|
maxTokens: 8e3
|
|
5830
6137
|
}, (chunk) => {
|
|
5831
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
6138
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5832
6139
|
});
|
|
5833
6140
|
return result.content;
|
|
5834
6141
|
}
|
|
@@ -5857,14 +6164,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
5857
6164
|
temperature: 0
|
|
5858
6165
|
});
|
|
5859
6166
|
const answer = result.content.trim().toUpperCase();
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
}
|
|
5863
|
-
if (answer.includes("NO")) {
|
|
5864
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
5865
|
-
}
|
|
6167
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
6168
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
5866
6169
|
return null;
|
|
5867
6170
|
} catch {
|
|
6171
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
5868
6172
|
return null;
|
|
5869
6173
|
}
|
|
5870
6174
|
}
|
|
@@ -5978,16 +6282,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
5978
6282
|
if (typeof input !== "string" || input.length === 0) {
|
|
5979
6283
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
5980
6284
|
}
|
|
5981
|
-
const root =
|
|
5982
|
-
const abs =
|
|
5983
|
-
const rel =
|
|
5984
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6285
|
+
const root = path20__default.default.resolve(workspaceRoot);
|
|
6286
|
+
const abs = path20__default.default.isAbsolute(input) ? path20__default.default.resolve(input) : path20__default.default.resolve(root, input);
|
|
6287
|
+
const rel = path20__default.default.relative(root, abs);
|
|
6288
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20__default.default.isAbsolute(rel)) {
|
|
5985
6289
|
throw new WorkspaceSandboxError(input, root);
|
|
5986
6290
|
}
|
|
5987
6291
|
try {
|
|
5988
|
-
const real =
|
|
5989
|
-
const realRel =
|
|
5990
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6292
|
+
const real = fs19__default.default.realpathSync(abs);
|
|
6293
|
+
const realRel = path20__default.default.relative(root, real);
|
|
6294
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20__default.default.isAbsolute(realRel))) {
|
|
5991
6295
|
throw new WorkspaceSandboxError(input, root);
|
|
5992
6296
|
}
|
|
5993
6297
|
} catch (e) {
|
|
@@ -6048,7 +6352,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6048
6352
|
} catch {
|
|
6049
6353
|
}
|
|
6050
6354
|
}
|
|
6051
|
-
await fs4__default.default.mkdir(
|
|
6355
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(absPath), { recursive: true });
|
|
6052
6356
|
await fs4__default.default.writeFile(absPath, content, "utf-8");
|
|
6053
6357
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6054
6358
|
}
|
|
@@ -6546,7 +6850,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
6546
6850
|
};
|
|
6547
6851
|
async function fileToImageAttachment(filePath) {
|
|
6548
6852
|
const data = await fs4__default.default.readFile(filePath);
|
|
6549
|
-
const ext =
|
|
6853
|
+
const ext = path20__default.default.extname(filePath).toLowerCase();
|
|
6550
6854
|
const mimeMap = {
|
|
6551
6855
|
".jpg": "image/jpeg",
|
|
6552
6856
|
".jpeg": "image/jpeg",
|
|
@@ -6580,14 +6884,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
6580
6884
|
const filePath = input["path"];
|
|
6581
6885
|
const content = input["content"];
|
|
6582
6886
|
const title = input["title"];
|
|
6583
|
-
const dir =
|
|
6584
|
-
if (!
|
|
6585
|
-
|
|
6887
|
+
const dir = path20__default.default.dirname(filePath);
|
|
6888
|
+
if (!fs19__default.default.existsSync(dir)) {
|
|
6889
|
+
fs19__default.default.mkdirSync(dir, { recursive: true });
|
|
6586
6890
|
}
|
|
6587
6891
|
return new Promise((resolve, reject) => {
|
|
6588
6892
|
try {
|
|
6589
6893
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
6590
|
-
const stream =
|
|
6894
|
+
const stream = fs19__default.default.createWriteStream(filePath);
|
|
6591
6895
|
doc.pipe(stream);
|
|
6592
6896
|
if (title) {
|
|
6593
6897
|
doc.info["Title"] = title;
|
|
@@ -6665,22 +6969,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
6665
6969
|
}
|
|
6666
6970
|
cmdPrefix = NODE_CMD;
|
|
6667
6971
|
}
|
|
6668
|
-
const tmpDir =
|
|
6669
|
-
if (!
|
|
6670
|
-
|
|
6972
|
+
const tmpDir = path20__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
6973
|
+
if (!fs19__default.default.existsSync(tmpDir)) {
|
|
6974
|
+
fs19__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
6671
6975
|
}
|
|
6672
6976
|
const extension = language === "python" ? "py" : "js";
|
|
6673
|
-
const fileName = `intp_${
|
|
6674
|
-
const filePath =
|
|
6675
|
-
|
|
6977
|
+
const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
|
|
6978
|
+
const filePath = path20__default.default.join(tmpDir, fileName);
|
|
6979
|
+
fs19__default.default.writeFileSync(filePath, code, "utf-8");
|
|
6676
6980
|
const execArgs = [filePath, ...args];
|
|
6677
6981
|
return new Promise((resolve) => {
|
|
6678
6982
|
const startMs = Date.now();
|
|
6679
6983
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
6680
6984
|
const duration = Date.now() - startMs;
|
|
6681
6985
|
try {
|
|
6682
|
-
if (
|
|
6683
|
-
|
|
6986
|
+
if (fs19__default.default.existsSync(filePath)) {
|
|
6987
|
+
fs19__default.default.unlinkSync(filePath);
|
|
6684
6988
|
}
|
|
6685
6989
|
} catch (cleanupErr) {
|
|
6686
6990
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -6959,7 +7263,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6959
7263
|
};
|
|
6960
7264
|
async execute(input, _options) {
|
|
6961
7265
|
const pattern = input["pattern"];
|
|
6962
|
-
const searchPath = input["path"] ?
|
|
7266
|
+
const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
6963
7267
|
const matches = await glob.glob(pattern, {
|
|
6964
7268
|
cwd: searchPath,
|
|
6965
7269
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -6972,7 +7276,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6972
7276
|
const withMtime = await Promise.all(
|
|
6973
7277
|
matches.map(async (rel) => {
|
|
6974
7278
|
try {
|
|
6975
|
-
const stat = await fs4__default.default.stat(
|
|
7279
|
+
const stat = await fs4__default.default.stat(path20__default.default.join(searchPath, rel));
|
|
6976
7280
|
return { rel, mtime: stat.mtimeMs };
|
|
6977
7281
|
} catch {
|
|
6978
7282
|
return { rel, mtime: 0 };
|
|
@@ -7021,7 +7325,7 @@ var GrepTool = class extends BaseTool {
|
|
|
7021
7325
|
};
|
|
7022
7326
|
async execute(input, _options) {
|
|
7023
7327
|
const pattern = input["pattern"];
|
|
7024
|
-
const searchPath = input["path"] ?
|
|
7328
|
+
const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7025
7329
|
const globPattern = input["glob"];
|
|
7026
7330
|
const outputMode = input["output_mode"] ?? "content";
|
|
7027
7331
|
const context = input["context"] ?? 0;
|
|
@@ -7075,12 +7379,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7075
7379
|
nodir: true
|
|
7076
7380
|
});
|
|
7077
7381
|
} catch {
|
|
7078
|
-
files = [
|
|
7382
|
+
files = [path20__default.default.relative(searchPath, searchPath) || "."];
|
|
7079
7383
|
}
|
|
7080
7384
|
const results = [];
|
|
7081
7385
|
let totalCount = 0;
|
|
7082
7386
|
for (const rel of files) {
|
|
7083
|
-
const abs =
|
|
7387
|
+
const abs = path20__default.default.join(searchPath, rel);
|
|
7084
7388
|
let content;
|
|
7085
7389
|
try {
|
|
7086
7390
|
content = await fs4__default.default.readFile(abs, "utf-8");
|
|
@@ -7442,10 +7746,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
7442
7746
|
}
|
|
7443
7747
|
isIgnored(filePath) {
|
|
7444
7748
|
if (!filePath) return false;
|
|
7445
|
-
const abs =
|
|
7446
|
-
const rel =
|
|
7447
|
-
if (!rel || rel.startsWith("..") ||
|
|
7448
|
-
const posixRel = rel.split(
|
|
7749
|
+
const abs = path20__default.default.resolve(this.workspaceRoot, filePath);
|
|
7750
|
+
const rel = path20__default.default.relative(this.workspaceRoot, abs);
|
|
7751
|
+
if (!rel || rel.startsWith("..") || path20__default.default.isAbsolute(rel)) return true;
|
|
7752
|
+
const posixRel = rel.split(path20__default.default.sep).join("/");
|
|
7449
7753
|
return this.ignoreMatcher.ignores(posixRel);
|
|
7450
7754
|
}
|
|
7451
7755
|
};
|
|
@@ -7562,6 +7866,9 @@ var McpClient = class _McpClient {
|
|
|
7562
7866
|
return this.clients.has(serverName);
|
|
7563
7867
|
}
|
|
7564
7868
|
};
|
|
7869
|
+
|
|
7870
|
+
// src/core/cascade.ts
|
|
7871
|
+
init_audit_logger();
|
|
7565
7872
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
7566
7873
|
"file_read",
|
|
7567
7874
|
"file_list",
|
|
@@ -7814,7 +8121,8 @@ var WorkspaceConfigSchema = zod.z.object({
|
|
|
7814
8121
|
cascadeMdPath: zod.z.string().default("CASCADE.md"),
|
|
7815
8122
|
configPath: zod.z.string().default(".cascade/config.json"),
|
|
7816
8123
|
keystorePath: zod.z.string().default(".cascade/keystore.enc"),
|
|
7817
|
-
auditLogPath: zod.z.string().default(".cascade/audit.log")
|
|
8124
|
+
auditLogPath: zod.z.string().default(".cascade/audit.log"),
|
|
8125
|
+
debugWorldState: zod.z.boolean().default(false)
|
|
7818
8126
|
});
|
|
7819
8127
|
var CascadeConfigSchema = zod.z.object({
|
|
7820
8128
|
version: zod.z.literal("1.0").default("1.0"),
|
|
@@ -7959,6 +8267,16 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
7959
8267
|
* 'parallel' / 'sequential' — force it.
|
|
7960
8268
|
*/
|
|
7961
8269
|
t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
8270
|
+
/**
|
|
8271
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
8272
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
8273
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
8274
|
+
*/
|
|
8275
|
+
privacy: zod.z.object({
|
|
8276
|
+
paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
|
|
8277
|
+
}).optional(),
|
|
8278
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
8279
|
+
routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
7962
8280
|
/**
|
|
7963
8281
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
7964
8282
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -8227,9 +8545,10 @@ var TaskAnalyzer = class {
|
|
|
8227
8545
|
analysisCache.clear();
|
|
8228
8546
|
}
|
|
8229
8547
|
};
|
|
8230
|
-
var DEFAULT_STATS_FILE =
|
|
8548
|
+
var DEFAULT_STATS_FILE = path20__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
|
|
8231
8549
|
var ModelPerformanceTracker = class {
|
|
8232
8550
|
stats = /* @__PURE__ */ new Map();
|
|
8551
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
8233
8552
|
statsFile;
|
|
8234
8553
|
loaded = false;
|
|
8235
8554
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -8241,18 +8560,29 @@ var ModelPerformanceTracker = class {
|
|
|
8241
8560
|
try {
|
|
8242
8561
|
const raw = await fs4__default.default.readFile(this.statsFile, "utf-8");
|
|
8243
8562
|
const parsed = JSON.parse(raw);
|
|
8244
|
-
|
|
8245
|
-
this.stats.set(key, stat);
|
|
8563
|
+
if (parsed.models) {
|
|
8564
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
8565
|
+
} else {
|
|
8566
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
8567
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
8568
|
+
this.stats.set(key, stat);
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
if (parsed.features) {
|
|
8573
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
8246
8574
|
}
|
|
8247
8575
|
} catch {
|
|
8248
8576
|
}
|
|
8249
8577
|
}
|
|
8250
8578
|
async save() {
|
|
8251
8579
|
try {
|
|
8252
|
-
await fs4__default.default.mkdir(
|
|
8253
|
-
const
|
|
8254
|
-
|
|
8255
|
-
|
|
8580
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(this.statsFile), { recursive: true });
|
|
8581
|
+
const modelsObj = {};
|
|
8582
|
+
const featuresObj = {};
|
|
8583
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
8584
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
8585
|
+
await fs4__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
8256
8586
|
} catch {
|
|
8257
8587
|
}
|
|
8258
8588
|
}
|
|
@@ -8273,6 +8603,13 @@ var ModelPerformanceTracker = class {
|
|
|
8273
8603
|
sampleCount: s.sampleCount + 1
|
|
8274
8604
|
});
|
|
8275
8605
|
}
|
|
8606
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
8607
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
8608
|
+
this.featureStats.set(featureTag, {
|
|
8609
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
8610
|
+
runCount: s.runCount + 1
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8276
8613
|
/**
|
|
8277
8614
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
8278
8615
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -8287,6 +8624,9 @@ var ModelPerformanceTracker = class {
|
|
|
8287
8624
|
getAll() {
|
|
8288
8625
|
return new Map(this.stats);
|
|
8289
8626
|
}
|
|
8627
|
+
getAllFeatures() {
|
|
8628
|
+
return new Map(this.featureStats);
|
|
8629
|
+
}
|
|
8290
8630
|
/**
|
|
8291
8631
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
8292
8632
|
* High retry counts penalise the score.
|
|
@@ -8651,7 +8991,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8651
8991
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
8652
8992
|
async loadPersistedTools() {
|
|
8653
8993
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8654
|
-
const file =
|
|
8994
|
+
const file = path20__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
8655
8995
|
try {
|
|
8656
8996
|
const raw = await fs4__default.default.readFile(file, "utf-8");
|
|
8657
8997
|
const specs = JSON.parse(raw);
|
|
@@ -8675,8 +9015,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8675
9015
|
}
|
|
8676
9016
|
async persist() {
|
|
8677
9017
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8678
|
-
const dir =
|
|
8679
|
-
const file =
|
|
9018
|
+
const dir = path20__default.default.join(this.workspacePath, ".cascade");
|
|
9019
|
+
const file = path20__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
8680
9020
|
try {
|
|
8681
9021
|
await fs4__default.default.mkdir(dir, { recursive: true });
|
|
8682
9022
|
await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -8689,6 +9029,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8689
9029
|
return Array.from(this.specs.keys());
|
|
8690
9030
|
}
|
|
8691
9031
|
};
|
|
9032
|
+
var WorldStateDB = class {
|
|
9033
|
+
constructor(workspacePath, debugMode = false) {
|
|
9034
|
+
this.workspacePath = workspacePath;
|
|
9035
|
+
this.debugMode = debugMode;
|
|
9036
|
+
const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
|
|
9037
|
+
if (!fs19__default.default.existsSync(cascadeDir)) {
|
|
9038
|
+
fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
9039
|
+
}
|
|
9040
|
+
this.keyPath = path20__default.default.join(cascadeDir, "world_state.key");
|
|
9041
|
+
this.dbPath = path20__default.default.join(cascadeDir, "world_state.db");
|
|
9042
|
+
this.initEncryptionKey();
|
|
9043
|
+
this.db = new Database__default.default(this.dbPath);
|
|
9044
|
+
this.db.pragma("journal_mode = WAL");
|
|
9045
|
+
this.db.exec(`
|
|
9046
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
9047
|
+
id TEXT PRIMARY KEY,
|
|
9048
|
+
timestamp TEXT NOT NULL,
|
|
9049
|
+
worker_id TEXT NOT NULL,
|
|
9050
|
+
encrypted_payload TEXT NOT NULL
|
|
9051
|
+
)
|
|
9052
|
+
`);
|
|
9053
|
+
}
|
|
9054
|
+
workspacePath;
|
|
9055
|
+
debugMode;
|
|
9056
|
+
db;
|
|
9057
|
+
keyPath;
|
|
9058
|
+
dbPath;
|
|
9059
|
+
encryptionKey;
|
|
9060
|
+
initEncryptionKey() {
|
|
9061
|
+
if (fs19__default.default.existsSync(this.keyPath)) {
|
|
9062
|
+
this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
|
|
9063
|
+
} else {
|
|
9064
|
+
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
9065
|
+
fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9066
|
+
}
|
|
9067
|
+
}
|
|
9068
|
+
encrypt(text) {
|
|
9069
|
+
const iv = crypto3__default.default.randomBytes(16);
|
|
9070
|
+
const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9071
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
9072
|
+
encrypted += cipher.final("hex");
|
|
9073
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
9074
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
9075
|
+
}
|
|
9076
|
+
decrypt(text) {
|
|
9077
|
+
const parts = text.split(":");
|
|
9078
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
9079
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
9080
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
9081
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
9082
|
+
const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9083
|
+
decipher.setAuthTag(authTag);
|
|
9084
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
9085
|
+
decrypted += decipher.final("utf8");
|
|
9086
|
+
return decrypted;
|
|
9087
|
+
}
|
|
9088
|
+
addEntry(workerId, summary) {
|
|
9089
|
+
const id = crypto3__default.default.randomUUID();
|
|
9090
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9091
|
+
const payload = JSON.stringify({ summary });
|
|
9092
|
+
const encryptedPayload = this.encrypt(payload);
|
|
9093
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
9094
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
9095
|
+
this.dumpDebugIfNeeded();
|
|
9096
|
+
return id;
|
|
9097
|
+
}
|
|
9098
|
+
getAllEntries() {
|
|
9099
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
9100
|
+
const rows = stmt.all();
|
|
9101
|
+
return rows.map((row) => {
|
|
9102
|
+
try {
|
|
9103
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
9104
|
+
const parsed = JSON.parse(decrypted);
|
|
9105
|
+
return {
|
|
9106
|
+
id: row.id,
|
|
9107
|
+
timestamp: row.timestamp,
|
|
9108
|
+
workerId: row.worker_id,
|
|
9109
|
+
summary: parsed.summary
|
|
9110
|
+
};
|
|
9111
|
+
} catch (err) {
|
|
9112
|
+
return {
|
|
9113
|
+
id: row.id,
|
|
9114
|
+
timestamp: row.timestamp,
|
|
9115
|
+
workerId: row.worker_id,
|
|
9116
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
9117
|
+
};
|
|
9118
|
+
}
|
|
9119
|
+
});
|
|
9120
|
+
}
|
|
9121
|
+
getFormattedState() {
|
|
9122
|
+
const entries = this.getAllEntries();
|
|
9123
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
9124
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
9125
|
+
}
|
|
9126
|
+
dumpDebugIfNeeded() {
|
|
9127
|
+
if (!this.debugMode) return;
|
|
9128
|
+
try {
|
|
9129
|
+
const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
9130
|
+
const entries = this.getAllEntries();
|
|
9131
|
+
fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9132
|
+
} catch (err) {
|
|
9133
|
+
console.error("Failed to dump debug world state", err);
|
|
9134
|
+
}
|
|
9135
|
+
}
|
|
9136
|
+
close() {
|
|
9137
|
+
this.db.close();
|
|
9138
|
+
}
|
|
9139
|
+
};
|
|
9140
|
+
var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
|
|
9141
|
+
var PrivacyPaths = class {
|
|
9142
|
+
localOnly;
|
|
9143
|
+
hasRules;
|
|
9144
|
+
constructor(policies = []) {
|
|
9145
|
+
this.localOnly = ignore2();
|
|
9146
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
9147
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
9148
|
+
this.hasRules = patterns.length > 0;
|
|
9149
|
+
}
|
|
9150
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
9151
|
+
hasPolicies() {
|
|
9152
|
+
return this.hasRules;
|
|
9153
|
+
}
|
|
9154
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
9155
|
+
isLocalOnly(relativePath) {
|
|
9156
|
+
if (!this.hasRules || !relativePath) return false;
|
|
9157
|
+
try {
|
|
9158
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
9159
|
+
} catch {
|
|
9160
|
+
return false;
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
9164
|
+
anyLocalOnly(relativePaths) {
|
|
9165
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
9166
|
+
}
|
|
9167
|
+
};
|
|
9168
|
+
|
|
9169
|
+
// src/core/steering/guidance.ts
|
|
9170
|
+
var GuidanceQueue = class {
|
|
9171
|
+
entries = [];
|
|
9172
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
9173
|
+
cursors = /* @__PURE__ */ new Map();
|
|
9174
|
+
push(text, nodeId) {
|
|
9175
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9176
|
+
this.entries.push(entry);
|
|
9177
|
+
return entry;
|
|
9178
|
+
}
|
|
9179
|
+
/**
|
|
9180
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
9181
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
9182
|
+
*/
|
|
9183
|
+
drain(consumerId) {
|
|
9184
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
9185
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
9186
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
9187
|
+
}
|
|
9188
|
+
get size() {
|
|
9189
|
+
return this.entries.length;
|
|
9190
|
+
}
|
|
9191
|
+
};
|
|
8692
9192
|
|
|
8693
9193
|
// src/core/cascade.ts
|
|
8694
9194
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
@@ -8708,6 +9208,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
8708
9208
|
taskAnalyzer;
|
|
8709
9209
|
perfTracker;
|
|
8710
9210
|
toolCreator;
|
|
9211
|
+
worldStateDB;
|
|
9212
|
+
encryptedAuditLogger;
|
|
9213
|
+
guidanceQueue;
|
|
8711
9214
|
workspacePath;
|
|
8712
9215
|
constructor(config, workspacePath, store) {
|
|
8713
9216
|
super();
|
|
@@ -8729,6 +9232,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
8729
9232
|
});
|
|
8730
9233
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
8731
9234
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
9235
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9236
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
9237
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9238
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
9239
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
9240
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
9241
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
9242
|
+
}
|
|
9243
|
+
/**
|
|
9244
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
9245
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
9246
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
9247
|
+
*/
|
|
9248
|
+
injectGuidance(text, nodeId) {
|
|
9249
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
9250
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
9251
|
+
this.emit("guidance:injected", entry);
|
|
8732
9252
|
}
|
|
8733
9253
|
initOptionalFeatures() {
|
|
8734
9254
|
if (this.config.cascadeAuto === true) {
|
|
@@ -8891,6 +9411,12 @@ ${last.partialOutput}` : "");
|
|
|
8891
9411
|
if (!prompt) return null;
|
|
8892
9412
|
return this.run({ prompt });
|
|
8893
9413
|
}
|
|
9414
|
+
getWorkspacePath() {
|
|
9415
|
+
return this.workspacePath;
|
|
9416
|
+
}
|
|
9417
|
+
getWorldStateDB() {
|
|
9418
|
+
return this.worldStateDB;
|
|
9419
|
+
}
|
|
8894
9420
|
/**
|
|
8895
9421
|
* Record an explicit user rating for the last completed run.
|
|
8896
9422
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -8977,6 +9503,11 @@ ${last.partialOutput}` : "");
|
|
|
8977
9503
|
}
|
|
8978
9504
|
this.initOptionalFeatures();
|
|
8979
9505
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
9506
|
+
if (this.encryptedAuditLogger) {
|
|
9507
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
9508
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
9509
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
9510
|
+
}
|
|
8980
9511
|
this.initialized = true;
|
|
8981
9512
|
})();
|
|
8982
9513
|
try {
|
|
@@ -9020,6 +9551,21 @@ ${last.partialOutput}` : "");
|
|
|
9020
9551
|
const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
|
|
9021
9552
|
return inquiry && !producesArtifact;
|
|
9022
9553
|
}
|
|
9554
|
+
/**
|
|
9555
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
9556
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
9557
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
9558
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
9559
|
+
* Simple/Moderate) don't get over-escalated.
|
|
9560
|
+
*/
|
|
9561
|
+
looksClearlyComplex(prompt) {
|
|
9562
|
+
const p = prompt.trim();
|
|
9563
|
+
if (p.length < 24) return false;
|
|
9564
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
9565
|
+
const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
|
|
9566
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
9567
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
9568
|
+
}
|
|
9023
9569
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
9024
9570
|
static globCache = /* @__PURE__ */ new Map();
|
|
9025
9571
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -9103,10 +9649,16 @@ ${prompt}` : prompt;
|
|
|
9103
9649
|
let verdict;
|
|
9104
9650
|
if (match) {
|
|
9105
9651
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
9106
|
-
|
|
9652
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
9653
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
9654
|
+
verdict = "Complex";
|
|
9655
|
+
} else {
|
|
9656
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
9657
|
+
}
|
|
9107
9658
|
} else {
|
|
9108
|
-
|
|
9109
|
-
|
|
9659
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
9660
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
9661
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
9110
9662
|
}
|
|
9111
9663
|
return verdict;
|
|
9112
9664
|
} catch {
|
|
@@ -9124,7 +9676,7 @@ ${prompt}` : prompt;
|
|
|
9124
9676
|
this.router.beginRun();
|
|
9125
9677
|
this.router.setRunSignal(options.signal);
|
|
9126
9678
|
const startMs = Date.now();
|
|
9127
|
-
const taskId =
|
|
9679
|
+
const taskId = crypto3.randomUUID();
|
|
9128
9680
|
this.decisionLog = [];
|
|
9129
9681
|
const escalator = new PermissionEscalator(this.config.approvalTimeoutMs ?? 6e5, this.config.autonomy === "auto");
|
|
9130
9682
|
escalator.on("permission:user-required", async (req) => {
|
|
@@ -9158,7 +9710,15 @@ ${prompt}` : prompt;
|
|
|
9158
9710
|
}
|
|
9159
9711
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
9160
9712
|
});
|
|
9161
|
-
const
|
|
9713
|
+
const forceTier = this.config.routing?.forceTier;
|
|
9714
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
9715
|
+
let complexity;
|
|
9716
|
+
if (forced) {
|
|
9717
|
+
complexity = forced;
|
|
9718
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
9719
|
+
} else {
|
|
9720
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
9721
|
+
}
|
|
9162
9722
|
this.telemetry.capture("cascade:session_start", {
|
|
9163
9723
|
complexity,
|
|
9164
9724
|
providerCount: this.config.providers.length,
|
|
@@ -9238,6 +9798,7 @@ ${prompt}` : prompt;
|
|
|
9238
9798
|
try {
|
|
9239
9799
|
if (complexity === "Simple") {
|
|
9240
9800
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
9801
|
+
t3.setPresenter(true);
|
|
9241
9802
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
9242
9803
|
if (identityPrompt) {
|
|
9243
9804
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -9262,6 +9823,7 @@ ${prompt}` : prompt;
|
|
|
9262
9823
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
9263
9824
|
} else if (complexity === "Moderate") {
|
|
9264
9825
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
9826
|
+
t2.setPresenter(true);
|
|
9265
9827
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
9266
9828
|
if (identityPrompt) {
|
|
9267
9829
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -9311,6 +9873,7 @@ ${prompt}` : prompt;
|
|
|
9311
9873
|
}
|
|
9312
9874
|
} else {
|
|
9313
9875
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
9876
|
+
t1.setPresenter(true);
|
|
9314
9877
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
9315
9878
|
if (identityPrompt) {
|
|
9316
9879
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -9402,6 +9965,7 @@ ${prompt}` : prompt;
|
|
|
9402
9965
|
durationMs,
|
|
9403
9966
|
costByTier: stats.costByTier,
|
|
9404
9967
|
tokensByTier: stats.tokensByTier,
|
|
9968
|
+
costByFeature: stats.costByFeature,
|
|
9405
9969
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
9406
9970
|
};
|
|
9407
9971
|
}
|
|
@@ -9469,7 +10033,7 @@ var Keystore = class {
|
|
|
9469
10033
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
9470
10034
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
9471
10035
|
this.backend = "keytar";
|
|
9472
|
-
if (password &&
|
|
10036
|
+
if (password && fs19__default.default.existsSync(this.storePath)) {
|
|
9473
10037
|
try {
|
|
9474
10038
|
const fileEntries = this.decryptFile(password);
|
|
9475
10039
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -9488,8 +10052,8 @@ var Keystore = class {
|
|
|
9488
10052
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
9489
10053
|
);
|
|
9490
10054
|
}
|
|
9491
|
-
if (!
|
|
9492
|
-
const salt =
|
|
10055
|
+
if (!fs19__default.default.existsSync(this.storePath)) {
|
|
10056
|
+
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
9493
10057
|
this.masterKey = this.deriveKey(password, salt);
|
|
9494
10058
|
this.writeWithSalt({}, salt);
|
|
9495
10059
|
this.cache = {};
|
|
@@ -9502,8 +10066,8 @@ var Keystore = class {
|
|
|
9502
10066
|
}
|
|
9503
10067
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
9504
10068
|
unlockSync(password) {
|
|
9505
|
-
if (!
|
|
9506
|
-
const salt =
|
|
10069
|
+
if (!fs19__default.default.existsSync(this.storePath)) {
|
|
10070
|
+
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
9507
10071
|
this.masterKey = this.deriveKey(password, salt);
|
|
9508
10072
|
this.writeWithSalt({}, salt);
|
|
9509
10073
|
this.cache = {};
|
|
@@ -9560,12 +10124,12 @@ var Keystore = class {
|
|
|
9560
10124
|
}
|
|
9561
10125
|
}
|
|
9562
10126
|
decryptFile(password, knownSalt) {
|
|
9563
|
-
if (!
|
|
10127
|
+
if (!fs19__default.default.existsSync(this.storePath)) return {};
|
|
9564
10128
|
try {
|
|
9565
10129
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
9566
10130
|
const useSalt = knownSalt ?? salt;
|
|
9567
10131
|
const key = this.masterKey ?? this.deriveKey(password, useSalt);
|
|
9568
|
-
const decipher =
|
|
10132
|
+
const decipher = crypto3__default.default.createDecipheriv(ALGORITHM, key, iv);
|
|
9569
10133
|
decipher.setAuthTag(tag);
|
|
9570
10134
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
9571
10135
|
return JSON.parse(decrypted.toString("utf-8"));
|
|
@@ -9576,28 +10140,28 @@ var Keystore = class {
|
|
|
9576
10140
|
saveAll(data) {
|
|
9577
10141
|
if (!this.masterKey) return;
|
|
9578
10142
|
const raw = this.readRaw();
|
|
9579
|
-
const iv =
|
|
9580
|
-
const cipher =
|
|
10143
|
+
const iv = crypto3__default.default.randomBytes(IV_LEN);
|
|
10144
|
+
const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9581
10145
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9582
10146
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9583
10147
|
const tag = cipher.getAuthTag();
|
|
9584
10148
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
9585
|
-
|
|
9586
|
-
|
|
10149
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
|
|
10150
|
+
fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9587
10151
|
}
|
|
9588
10152
|
writeWithSalt(data, salt) {
|
|
9589
10153
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
9590
|
-
const iv =
|
|
9591
|
-
const cipher =
|
|
10154
|
+
const iv = crypto3__default.default.randomBytes(IV_LEN);
|
|
10155
|
+
const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9592
10156
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9593
10157
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9594
10158
|
const tag = cipher.getAuthTag();
|
|
9595
10159
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
9596
|
-
|
|
9597
|
-
|
|
10160
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
|
|
10161
|
+
fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9598
10162
|
}
|
|
9599
10163
|
readRaw() {
|
|
9600
|
-
const buf =
|
|
10164
|
+
const buf = fs19__default.default.readFileSync(this.storePath);
|
|
9601
10165
|
let offset = 0;
|
|
9602
10166
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
9603
10167
|
offset += SALT_LEN;
|
|
@@ -9609,15 +10173,15 @@ var Keystore = class {
|
|
|
9609
10173
|
return { salt, iv, tag, ciphertext };
|
|
9610
10174
|
}
|
|
9611
10175
|
deriveKey(password, salt) {
|
|
9612
|
-
return
|
|
10176
|
+
return crypto3__default.default.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
|
|
9613
10177
|
}
|
|
9614
10178
|
};
|
|
9615
|
-
var
|
|
10179
|
+
var ignore3 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
|
|
9616
10180
|
var CascadeIgnore = class {
|
|
9617
10181
|
ig;
|
|
9618
10182
|
loaded = false;
|
|
9619
10183
|
constructor() {
|
|
9620
|
-
this.ig =
|
|
10184
|
+
this.ig = ignore3();
|
|
9621
10185
|
this.ig.add([
|
|
9622
10186
|
".cascade/keystore.enc",
|
|
9623
10187
|
".cascade/memory.db",
|
|
@@ -9630,7 +10194,7 @@ var CascadeIgnore = class {
|
|
|
9630
10194
|
]);
|
|
9631
10195
|
}
|
|
9632
10196
|
async load(workspacePath) {
|
|
9633
|
-
const filePath =
|
|
10197
|
+
const filePath = path20__default.default.join(workspacePath, ".cascadeignore");
|
|
9634
10198
|
try {
|
|
9635
10199
|
const content = await fs4__default.default.readFile(filePath, "utf-8");
|
|
9636
10200
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -9641,7 +10205,7 @@ var CascadeIgnore = class {
|
|
|
9641
10205
|
}
|
|
9642
10206
|
isIgnored(filePath, workspacePath) {
|
|
9643
10207
|
try {
|
|
9644
|
-
const relative = workspacePath ?
|
|
10208
|
+
const relative = workspacePath ? path20__default.default.relative(workspacePath, filePath) : filePath;
|
|
9645
10209
|
return this.ig.ignores(relative);
|
|
9646
10210
|
} catch {
|
|
9647
10211
|
return false;
|
|
@@ -9652,7 +10216,7 @@ var CascadeIgnore = class {
|
|
|
9652
10216
|
}
|
|
9653
10217
|
};
|
|
9654
10218
|
async function loadCascadeMd(workspacePath) {
|
|
9655
|
-
const filePath =
|
|
10219
|
+
const filePath = path20__default.default.join(workspacePath, "CASCADE.md");
|
|
9656
10220
|
try {
|
|
9657
10221
|
const raw = await fs4__default.default.readFile(filePath, "utf-8");
|
|
9658
10222
|
return parseCascadeMd(raw);
|
|
@@ -9683,7 +10247,7 @@ ${raw.trim()}`;
|
|
|
9683
10247
|
var MemoryStore = class _MemoryStore {
|
|
9684
10248
|
db;
|
|
9685
10249
|
constructor(dbPath) {
|
|
9686
|
-
|
|
10250
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(dbPath), { recursive: true });
|
|
9687
10251
|
try {
|
|
9688
10252
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
9689
10253
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -9807,7 +10371,7 @@ Original error: ${err.message}`
|
|
|
9807
10371
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
9808
10372
|
`);
|
|
9809
10373
|
for (const msg of messages) {
|
|
9810
|
-
stmt.run(
|
|
10374
|
+
stmt.run(crypto3.randomUUID(), newId, msg.role, msg.content, msg.timestamp, msg.tokens, msg.agent_messages);
|
|
9811
10375
|
}
|
|
9812
10376
|
const snapshots = this.db.prepare("SELECT * FROM file_snapshots WHERE session_id = ?").all(originalId);
|
|
9813
10377
|
const snapStmt = this.db.prepare(`
|
|
@@ -9815,7 +10379,7 @@ Original error: ${err.message}`
|
|
|
9815
10379
|
VALUES (?, ?, ?, ?, ?)
|
|
9816
10380
|
`);
|
|
9817
10381
|
for (const snap of snapshots) {
|
|
9818
|
-
snapStmt.run(
|
|
10382
|
+
snapStmt.run(crypto3.randomUUID(), newId, snap.file_path, snap.content, snap.timestamp);
|
|
9819
10383
|
}
|
|
9820
10384
|
}
|
|
9821
10385
|
// ── Runtime Sessions / Nodes ─────────────────
|
|
@@ -10113,7 +10677,7 @@ Original error: ${err.message}`
|
|
|
10113
10677
|
this.db.prepare(`
|
|
10114
10678
|
INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
|
|
10115
10679
|
VALUES (?, ?, ?, ?, ?)
|
|
10116
|
-
`).run(
|
|
10680
|
+
`).run(crypto3.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
|
|
10117
10681
|
});
|
|
10118
10682
|
}
|
|
10119
10683
|
getLatestFileSnapshots(sessionId) {
|
|
@@ -10441,15 +11005,15 @@ var ConfigManager = class {
|
|
|
10441
11005
|
globalDir;
|
|
10442
11006
|
constructor(workspacePath = process.cwd()) {
|
|
10443
11007
|
this.workspacePath = workspacePath;
|
|
10444
|
-
this.globalDir =
|
|
11008
|
+
this.globalDir = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
10445
11009
|
}
|
|
10446
11010
|
async load() {
|
|
10447
11011
|
this.config = await this.loadConfig();
|
|
10448
11012
|
this.ignore = new CascadeIgnore();
|
|
10449
11013
|
await this.ignore.load(this.workspacePath);
|
|
10450
11014
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
10451
|
-
this.keystore = new Keystore(
|
|
10452
|
-
this.store = new MemoryStore(
|
|
11015
|
+
this.keystore = new Keystore(path20__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
11016
|
+
this.store = new MemoryStore(path20__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
10453
11017
|
await this.injectEnvKeys();
|
|
10454
11018
|
await this.ensureDefaultIdentity();
|
|
10455
11019
|
}
|
|
@@ -10472,8 +11036,8 @@ var ConfigManager = class {
|
|
|
10472
11036
|
return this.workspacePath;
|
|
10473
11037
|
}
|
|
10474
11038
|
async save() {
|
|
10475
|
-
const configPath =
|
|
10476
|
-
await fs4__default.default.mkdir(
|
|
11039
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11040
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(configPath), { recursive: true });
|
|
10477
11041
|
await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10478
11042
|
}
|
|
10479
11043
|
async updateConfig(updates) {
|
|
@@ -10497,7 +11061,7 @@ var ConfigManager = class {
|
|
|
10497
11061
|
return configProvider?.apiKey;
|
|
10498
11062
|
}
|
|
10499
11063
|
async loadConfig() {
|
|
10500
|
-
const configPath =
|
|
11064
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10501
11065
|
try {
|
|
10502
11066
|
const raw = await fs4__default.default.readFile(configPath, "utf-8");
|
|
10503
11067
|
return validateConfig(JSON.parse(raw));
|
|
@@ -10534,7 +11098,7 @@ var ConfigManager = class {
|
|
|
10534
11098
|
const existing = this.store.getDefaultIdentity();
|
|
10535
11099
|
if (existing) return;
|
|
10536
11100
|
const identity = {
|
|
10537
|
-
id:
|
|
11101
|
+
id: crypto3.randomUUID(),
|
|
10538
11102
|
name: "Default",
|
|
10539
11103
|
description: "Default Cascade identity",
|
|
10540
11104
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -10727,6 +11291,24 @@ var DashboardSocket = class {
|
|
|
10727
11291
|
});
|
|
10728
11292
|
});
|
|
10729
11293
|
}
|
|
11294
|
+
onSessionHalt(callback) {
|
|
11295
|
+
this.io.on("connection", (socket) => {
|
|
11296
|
+
socket.on("session:halt", (payload) => {
|
|
11297
|
+
if (typeof payload?.sessionId === "string") {
|
|
11298
|
+
callback(payload.sessionId);
|
|
11299
|
+
}
|
|
11300
|
+
});
|
|
11301
|
+
});
|
|
11302
|
+
}
|
|
11303
|
+
onSessionSteer(callback) {
|
|
11304
|
+
this.io.on("connection", (socket) => {
|
|
11305
|
+
socket.on("session:steer", (payload) => {
|
|
11306
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
11307
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
11308
|
+
}
|
|
11309
|
+
});
|
|
11310
|
+
});
|
|
11311
|
+
}
|
|
10730
11312
|
onConfigUpdate(callback) {
|
|
10731
11313
|
this.io.on("connection", (socket) => {
|
|
10732
11314
|
socket.on("config:update", (payload) => {
|
|
@@ -10749,7 +11331,8 @@ var DashboardSocket = class {
|
|
|
10749
11331
|
socket.on("cascade:run", (payload) => {
|
|
10750
11332
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
10751
11333
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
10752
|
-
|
|
11334
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
11335
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
10753
11336
|
}
|
|
10754
11337
|
});
|
|
10755
11338
|
});
|
|
@@ -10758,7 +11341,7 @@ var DashboardSocket = class {
|
|
|
10758
11341
|
this.io.close();
|
|
10759
11342
|
}
|
|
10760
11343
|
};
|
|
10761
|
-
var __dirname$1 =
|
|
11344
|
+
var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
10762
11345
|
var DashboardServer = class {
|
|
10763
11346
|
app;
|
|
10764
11347
|
httpServer;
|
|
@@ -10769,6 +11352,20 @@ var DashboardServer = class {
|
|
|
10769
11352
|
globalStore = null;
|
|
10770
11353
|
broadcastTimer = null;
|
|
10771
11354
|
activeSessions = /* @__PURE__ */ new Map();
|
|
11355
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
11356
|
+
/**
|
|
11357
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
11358
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
11359
|
+
* of runs the session performed in this server's lifetime.
|
|
11360
|
+
*/
|
|
11361
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
11362
|
+
/**
|
|
11363
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
11364
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
11365
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
11366
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
11367
|
+
*/
|
|
11368
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
10772
11369
|
port;
|
|
10773
11370
|
host;
|
|
10774
11371
|
workspacePath;
|
|
@@ -10827,15 +11424,18 @@ var DashboardServer = class {
|
|
|
10827
11424
|
}
|
|
10828
11425
|
this.persistConfig();
|
|
10829
11426
|
});
|
|
10830
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
10831
|
-
const sessionId = requestedSessionId ??
|
|
11427
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
11428
|
+
const sessionId = requestedSessionId ?? crypto3.randomUUID();
|
|
11429
|
+
const abortController = new AbortController();
|
|
11430
|
+
this.activeControllers.set(sessionId, abortController);
|
|
10832
11431
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
10833
11432
|
const title = this.persistRunStart(sessionId, prompt);
|
|
10834
|
-
|
|
11433
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
11434
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
10835
11435
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
10836
11436
|
this.activeSessions.set(sessionId, cascade);
|
|
10837
11437
|
cascade.on("stream:token", (e) => {
|
|
10838
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
11438
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
10839
11439
|
});
|
|
10840
11440
|
cascade.on("tier:status", (e) => {
|
|
10841
11441
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -10847,7 +11447,12 @@ var DashboardServer = class {
|
|
|
10847
11447
|
this.socket.emitPeerMessage(e);
|
|
10848
11448
|
});
|
|
10849
11449
|
try {
|
|
10850
|
-
const result = await cascade.run({
|
|
11450
|
+
const result = await cascade.run({
|
|
11451
|
+
prompt: runPrompt,
|
|
11452
|
+
signal: abortController.signal,
|
|
11453
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
11454
|
+
});
|
|
11455
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
10851
11456
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
10852
11457
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
10853
11458
|
this.socket.broadcast("cost:update", {
|
|
@@ -10864,6 +11469,24 @@ var DashboardServer = class {
|
|
|
10864
11469
|
});
|
|
10865
11470
|
} finally {
|
|
10866
11471
|
this.activeSessions.delete(sessionId);
|
|
11472
|
+
this.activeControllers.delete(sessionId);
|
|
11473
|
+
this.denyPendingApprovals(sessionId);
|
|
11474
|
+
}
|
|
11475
|
+
});
|
|
11476
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
11477
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
11478
|
+
this.denyPendingApprovals(sessionId);
|
|
11479
|
+
});
|
|
11480
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
11481
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
11482
|
+
if (!pending) return;
|
|
11483
|
+
this.pendingApprovals.delete(requestId);
|
|
11484
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
11485
|
+
});
|
|
11486
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
11487
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11488
|
+
if (steered > 0) {
|
|
11489
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10867
11490
|
}
|
|
10868
11491
|
});
|
|
10869
11492
|
}
|
|
@@ -10911,9 +11534,9 @@ var DashboardServer = class {
|
|
|
10911
11534
|
*/
|
|
10912
11535
|
persistConfig() {
|
|
10913
11536
|
try {
|
|
10914
|
-
const configPath =
|
|
10915
|
-
|
|
10916
|
-
|
|
11537
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11538
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(configPath), { recursive: true });
|
|
11539
|
+
fs19__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10917
11540
|
} catch (err) {
|
|
10918
11541
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
10919
11542
|
}
|
|
@@ -10928,15 +11551,15 @@ var DashboardServer = class {
|
|
|
10928
11551
|
resolveDashboardSecret() {
|
|
10929
11552
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
10930
11553
|
if (fromConfig) return fromConfig;
|
|
10931
|
-
const secretPath =
|
|
11554
|
+
const secretPath = path20__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
10932
11555
|
try {
|
|
10933
|
-
if (
|
|
10934
|
-
const existing =
|
|
11556
|
+
if (fs19__default.default.existsSync(secretPath)) {
|
|
11557
|
+
const existing = fs19__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
10935
11558
|
if (existing.length >= 16) return existing;
|
|
10936
11559
|
}
|
|
10937
|
-
const generated =
|
|
10938
|
-
|
|
10939
|
-
|
|
11560
|
+
const generated = crypto3.randomUUID();
|
|
11561
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(secretPath), { recursive: true });
|
|
11562
|
+
fs19__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
10940
11563
|
if (this.config.dashboard.auth) {
|
|
10941
11564
|
console.warn(
|
|
10942
11565
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -10945,7 +11568,7 @@ var DashboardServer = class {
|
|
|
10945
11568
|
return generated;
|
|
10946
11569
|
} catch {
|
|
10947
11570
|
console.warn("Unable to persist dashboard secret; falling back to a process-ephemeral secret.");
|
|
10948
|
-
return
|
|
11571
|
+
return crypto3.randomUUID();
|
|
10949
11572
|
}
|
|
10950
11573
|
}
|
|
10951
11574
|
/**
|
|
@@ -10963,7 +11586,7 @@ var DashboardServer = class {
|
|
|
10963
11586
|
// ── Setup ─────────────────────────────────────
|
|
10964
11587
|
getGlobalStore() {
|
|
10965
11588
|
if (!this.globalStore) {
|
|
10966
|
-
const globalDbPath =
|
|
11589
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
10967
11590
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
10968
11591
|
}
|
|
10969
11592
|
return this.globalStore;
|
|
@@ -10993,7 +11616,7 @@ var DashboardServer = class {
|
|
|
10993
11616
|
metadata: { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 }
|
|
10994
11617
|
});
|
|
10995
11618
|
}
|
|
10996
|
-
this.store.addMessage({ id:
|
|
11619
|
+
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "user", content: prompt, timestamp: now });
|
|
10997
11620
|
} catch (err) {
|
|
10998
11621
|
console.warn("[dashboard] failed to persist run start:", err);
|
|
10999
11622
|
}
|
|
@@ -11004,13 +11627,51 @@ var DashboardServer = class {
|
|
|
11004
11627
|
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
11005
11628
|
try {
|
|
11006
11629
|
if (reply && reply.trim()) {
|
|
11007
|
-
this.store.addMessage({ id:
|
|
11630
|
+
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
11008
11631
|
}
|
|
11009
11632
|
} catch (err) {
|
|
11010
11633
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
11011
11634
|
}
|
|
11012
11635
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
11013
11636
|
}
|
|
11637
|
+
/**
|
|
11638
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
11639
|
+
* session, or every active session when none is specified (the desktop
|
|
11640
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
11641
|
+
*/
|
|
11642
|
+
steerSessions(message, sessionId, nodeId) {
|
|
11643
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
11644
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
11645
|
+
return targets.length;
|
|
11646
|
+
}
|
|
11647
|
+
recordSessionTask(sessionId, taskId) {
|
|
11648
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11649
|
+
list.push(taskId);
|
|
11650
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
11651
|
+
}
|
|
11652
|
+
/**
|
|
11653
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
11654
|
+
* user. The request itself was already pushed to the client via the
|
|
11655
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
11656
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
11657
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
11658
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
11659
|
+
* timeout denies it.
|
|
11660
|
+
*/
|
|
11661
|
+
makeApprovalCallback(sessionId) {
|
|
11662
|
+
return (request) => new Promise((resolve) => {
|
|
11663
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
11664
|
+
});
|
|
11665
|
+
}
|
|
11666
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
11667
|
+
denyPendingApprovals(sessionId) {
|
|
11668
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
11669
|
+
if (pending.sessionId === sessionId) {
|
|
11670
|
+
this.pendingApprovals.delete(id);
|
|
11671
|
+
pending.resolve({ approved: false, always: false });
|
|
11672
|
+
}
|
|
11673
|
+
}
|
|
11674
|
+
}
|
|
11014
11675
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
11015
11676
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11016
11677
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -11104,12 +11765,12 @@ ${prompt}`;
|
|
|
11104
11765
|
}
|
|
11105
11766
|
}
|
|
11106
11767
|
watchRuntimeChanges() {
|
|
11107
|
-
const workspaceDbPath =
|
|
11108
|
-
const globalDbPath =
|
|
11768
|
+
const workspaceDbPath = path20__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
11769
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11109
11770
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
11110
11771
|
for (const watchPath of watchPaths) {
|
|
11111
|
-
if (!
|
|
11112
|
-
|
|
11772
|
+
if (!fs19__default.default.existsSync(watchPath)) continue;
|
|
11773
|
+
fs19__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
11113
11774
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
11114
11775
|
});
|
|
11115
11776
|
}
|
|
@@ -11179,7 +11840,7 @@ ${prompt}`;
|
|
|
11179
11840
|
const truthy = Buffer.from("1");
|
|
11180
11841
|
const falsy = Buffer.from("0");
|
|
11181
11842
|
const probe = ok ? truthy : falsy;
|
|
11182
|
-
const authorized =
|
|
11843
|
+
const authorized = crypto3.timingSafeEqual(probe, truthy);
|
|
11183
11844
|
if (authorized) {
|
|
11184
11845
|
const token = createToken(
|
|
11185
11846
|
{ id: username, username, role: "admin" },
|
|
@@ -11199,15 +11860,6 @@ ${prompt}`;
|
|
|
11199
11860
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
11200
11861
|
res.json({ success: true, ...payload });
|
|
11201
11862
|
});
|
|
11202
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
11203
|
-
const body = req.body;
|
|
11204
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
11205
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
11206
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11207
|
-
this.socket.broadcast("session:approve", payload);
|
|
11208
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
11209
|
-
res.json({ success: true, ...payload });
|
|
11210
|
-
});
|
|
11211
11863
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
11212
11864
|
const body = req.body;
|
|
11213
11865
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -11217,7 +11869,8 @@ ${prompt}`;
|
|
|
11217
11869
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
11218
11870
|
return;
|
|
11219
11871
|
}
|
|
11220
|
-
const
|
|
11872
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11873
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11221
11874
|
this.socket.broadcast("session:message-injected", payload);
|
|
11222
11875
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
11223
11876
|
res.json({ success: true, ...payload });
|
|
@@ -11239,7 +11892,7 @@ ${prompt}`;
|
|
|
11239
11892
|
const sessionId = req.params.id;
|
|
11240
11893
|
this.store.deleteSession(sessionId);
|
|
11241
11894
|
this.store.deleteRuntimeSession(sessionId);
|
|
11242
|
-
const globalDbPath =
|
|
11895
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11243
11896
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11244
11897
|
try {
|
|
11245
11898
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -11251,9 +11904,34 @@ ${prompt}`;
|
|
|
11251
11904
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
11252
11905
|
res.json({ ok: true });
|
|
11253
11906
|
});
|
|
11907
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
11908
|
+
const sessionId = req.params.id;
|
|
11909
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11910
|
+
if (!taskIds.length) {
|
|
11911
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
11912
|
+
return;
|
|
11913
|
+
}
|
|
11914
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
11915
|
+
for (const taskId of taskIds) {
|
|
11916
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
11917
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
11918
|
+
}
|
|
11919
|
+
}
|
|
11920
|
+
const { writeFile } = await import('fs/promises');
|
|
11921
|
+
let restored = 0;
|
|
11922
|
+
for (const [filePath, content] of toRestore) {
|
|
11923
|
+
try {
|
|
11924
|
+
await writeFile(filePath, content, "utf-8");
|
|
11925
|
+
restored++;
|
|
11926
|
+
} catch (err) {
|
|
11927
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
res.json({ ok: true, restored });
|
|
11931
|
+
});
|
|
11254
11932
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
11255
11933
|
const body = req.body;
|
|
11256
|
-
const globalDbPath =
|
|
11934
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11257
11935
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
11258
11936
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11259
11937
|
try {
|
|
@@ -11276,7 +11954,7 @@ ${prompt}`;
|
|
|
11276
11954
|
});
|
|
11277
11955
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
11278
11956
|
this.store.deleteAllRuntimeNodes();
|
|
11279
|
-
const globalDbPath =
|
|
11957
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11280
11958
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11281
11959
|
try {
|
|
11282
11960
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -11300,7 +11978,7 @@ ${prompt}`;
|
|
|
11300
11978
|
const existing = this.store.getDefaultIdentity();
|
|
11301
11979
|
if (existing) this.store.updateIdentity(existing.id, { isDefault: false });
|
|
11302
11980
|
}
|
|
11303
|
-
const id =
|
|
11981
|
+
const id = crypto3.randomUUID();
|
|
11304
11982
|
const identity = {
|
|
11305
11983
|
id,
|
|
11306
11984
|
name: body.name,
|
|
@@ -11327,6 +12005,19 @@ ${prompt}`;
|
|
|
11327
12005
|
this.store.deleteIdentity(req.params.id);
|
|
11328
12006
|
res.json({ ok: true });
|
|
11329
12007
|
});
|
|
12008
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
12009
|
+
try {
|
|
12010
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
12011
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
12012
|
+
try {
|
|
12013
|
+
res.json(logger.verifyChain());
|
|
12014
|
+
} finally {
|
|
12015
|
+
logger.close();
|
|
12016
|
+
}
|
|
12017
|
+
} catch (err) {
|
|
12018
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
12019
|
+
}
|
|
12020
|
+
});
|
|
11330
12021
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
11331
12022
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
11332
12023
|
res.json(log);
|
|
@@ -11349,12 +12040,12 @@ ${prompt}`;
|
|
|
11349
12040
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
11350
12041
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
11351
12042
|
try {
|
|
11352
|
-
const configPath =
|
|
11353
|
-
const existing =
|
|
12043
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
12044
|
+
const existing = fs19__default.default.existsSync(configPath) ? JSON.parse(fs19__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
11354
12045
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
11355
12046
|
const tmp = configPath + ".tmp";
|
|
11356
|
-
|
|
11357
|
-
|
|
12047
|
+
fs19__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
12048
|
+
fs19__default.default.renameSync(tmp, configPath);
|
|
11358
12049
|
res.json({ ok: true });
|
|
11359
12050
|
} catch (err) {
|
|
11360
12051
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -11382,7 +12073,7 @@ ${prompt}`;
|
|
|
11382
12073
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
11383
12074
|
const scope = req.query["scope"] ?? "workspace";
|
|
11384
12075
|
if (scope === "global") {
|
|
11385
|
-
const globalDbPath =
|
|
12076
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11386
12077
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11387
12078
|
try {
|
|
11388
12079
|
res.json({
|
|
@@ -11410,7 +12101,7 @@ ${prompt}`;
|
|
|
11410
12101
|
return;
|
|
11411
12102
|
}
|
|
11412
12103
|
const requestedSessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId.trim() : void 0;
|
|
11413
|
-
const sessionId = requestedSessionId ??
|
|
12104
|
+
const sessionId = requestedSessionId ?? crypto3.randomUUID();
|
|
11414
12105
|
res.json({ sessionId, status: "ACTIVE" });
|
|
11415
12106
|
const prompt = body.prompt;
|
|
11416
12107
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
@@ -11419,7 +12110,7 @@ ${prompt}`;
|
|
|
11419
12110
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
11420
12111
|
this.activeSessions.set(sessionId, cascade);
|
|
11421
12112
|
cascade.on("stream:token", (e) => {
|
|
11422
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
12113
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
11423
12114
|
});
|
|
11424
12115
|
cascade.on("tier:status", (e) => {
|
|
11425
12116
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -11431,7 +12122,12 @@ ${prompt}`;
|
|
|
11431
12122
|
this.socket.emitPeerMessage(e);
|
|
11432
12123
|
});
|
|
11433
12124
|
try {
|
|
11434
|
-
const result = await cascade.run({
|
|
12125
|
+
const result = await cascade.run({
|
|
12126
|
+
prompt: runPrompt,
|
|
12127
|
+
identityId: body.identityId,
|
|
12128
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12129
|
+
});
|
|
12130
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
11435
12131
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
11436
12132
|
this.socket.broadcast("cost:update", {
|
|
11437
12133
|
sessionId,
|
|
@@ -11448,6 +12144,7 @@ ${prompt}`;
|
|
|
11448
12144
|
});
|
|
11449
12145
|
} finally {
|
|
11450
12146
|
this.activeSessions.delete(sessionId);
|
|
12147
|
+
this.denyPendingApprovals(sessionId);
|
|
11451
12148
|
}
|
|
11452
12149
|
})();
|
|
11453
12150
|
});
|
|
@@ -11462,13 +12159,13 @@ ${prompt}`;
|
|
|
11462
12159
|
}))
|
|
11463
12160
|
});
|
|
11464
12161
|
});
|
|
11465
|
-
const prodPath =
|
|
11466
|
-
const devPath =
|
|
11467
|
-
const webDistPath =
|
|
11468
|
-
if (
|
|
12162
|
+
const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
|
|
12163
|
+
const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
|
|
12164
|
+
const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
12165
|
+
if (fs19__default.default.existsSync(webDistPath)) {
|
|
11469
12166
|
this.app.use(express__default.default.static(webDistPath));
|
|
11470
12167
|
this.app.get("*", (_req, res) => {
|
|
11471
|
-
res.sendFile(
|
|
12168
|
+
res.sendFile(path20__default.default.join(webDistPath, "index.html"));
|
|
11472
12169
|
});
|
|
11473
12170
|
} else {
|
|
11474
12171
|
this.app.get("/", (_req, res) => {
|