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.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path20 from 'path';
|
|
3
|
+
import fs19 from 'fs';
|
|
4
|
+
import os6 from 'os';
|
|
5
|
+
import crypto3, { randomUUID, timingSafeEqual } from 'crypto';
|
|
1
6
|
import EventEmitter from 'events';
|
|
2
|
-
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
3
7
|
import { glob } from 'glob';
|
|
4
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
9
|
import OpenAI, { AzureOpenAI } from 'openai';
|
|
@@ -10,13 +14,10 @@ import https from 'https';
|
|
|
10
14
|
import zlib from 'zlib';
|
|
11
15
|
import { Readable } from 'stream';
|
|
12
16
|
import fs4 from 'fs/promises';
|
|
13
|
-
import path18 from 'path';
|
|
14
|
-
import os4 from 'os';
|
|
15
17
|
import * as ignoreFactory from 'ignore';
|
|
16
18
|
import ignoreFactory__default from 'ignore';
|
|
17
19
|
import { exec, execFile, execSync } from 'child_process';
|
|
18
20
|
import { promisify } from 'util';
|
|
19
|
-
import fs17 from 'fs';
|
|
20
21
|
import { simpleGit } from 'simple-git';
|
|
21
22
|
import PDFDocument from 'pdfkit';
|
|
22
23
|
import dns2 from 'dns/promises';
|
|
@@ -25,7 +26,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
25
26
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
26
27
|
import { z } from 'zod';
|
|
27
28
|
import { Worker } from 'worker_threads';
|
|
28
|
-
import Database from 'better-sqlite3';
|
|
29
29
|
import { fileURLToPath } from 'url';
|
|
30
30
|
import express from 'express';
|
|
31
31
|
import rateLimit from 'express-rate-limit';
|
|
@@ -36,10 +36,161 @@ import jwt from 'jsonwebtoken';
|
|
|
36
36
|
import cron from 'node-cron';
|
|
37
37
|
|
|
38
38
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
39
|
+
var __defProp = Object.defineProperty;
|
|
40
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
41
|
+
var __esm = (fn, res) => function __init() {
|
|
42
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
43
|
+
};
|
|
44
|
+
var __export = (target, all) => {
|
|
45
|
+
for (var name in all)
|
|
46
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
47
|
+
};
|
|
39
48
|
|
|
49
|
+
// src/core/audit/audit-logger.ts
|
|
50
|
+
var audit_logger_exports = {};
|
|
51
|
+
__export(audit_logger_exports, {
|
|
52
|
+
AuditLogger: () => AuditLogger2
|
|
53
|
+
});
|
|
54
|
+
var AuditLogger2;
|
|
55
|
+
var init_audit_logger = __esm({
|
|
56
|
+
"src/core/audit/audit-logger.ts"() {
|
|
57
|
+
AuditLogger2 = class {
|
|
58
|
+
constructor(workspacePath, debugMode = false) {
|
|
59
|
+
this.workspacePath = workspacePath;
|
|
60
|
+
this.debugMode = debugMode;
|
|
61
|
+
const cascadeDir = path20.join(workspacePath, ".cascade");
|
|
62
|
+
if (!fs19.existsSync(cascadeDir)) {
|
|
63
|
+
fs19.mkdirSync(cascadeDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
this.keyPath = path20.join(cascadeDir, "audit_log.key");
|
|
66
|
+
this.dbPath = path20.join(cascadeDir, "audit_log.db");
|
|
67
|
+
this.initEncryptionKey();
|
|
68
|
+
this.db = new Database(this.dbPath);
|
|
69
|
+
this.db.pragma("journal_mode = WAL");
|
|
70
|
+
this.db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
timestamp TEXT NOT NULL,
|
|
74
|
+
event_type TEXT NOT NULL,
|
|
75
|
+
tier_id TEXT NOT NULL,
|
|
76
|
+
encrypted_payload TEXT NOT NULL,
|
|
77
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
78
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
82
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
83
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
84
|
+
}
|
|
85
|
+
workspacePath;
|
|
86
|
+
debugMode;
|
|
87
|
+
db;
|
|
88
|
+
keyPath;
|
|
89
|
+
dbPath;
|
|
90
|
+
encryptionKey;
|
|
91
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
92
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
93
|
+
return crypto3.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
94
|
+
}
|
|
95
|
+
initEncryptionKey() {
|
|
96
|
+
if (fs19.existsSync(this.keyPath)) {
|
|
97
|
+
this.encryptionKey = fs19.readFileSync(this.keyPath);
|
|
98
|
+
} else {
|
|
99
|
+
this.encryptionKey = crypto3.randomBytes(32);
|
|
100
|
+
fs19.writeFileSync(this.keyPath, this.encryptionKey);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
encrypt(text) {
|
|
104
|
+
const iv = crypto3.randomBytes(16);
|
|
105
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
106
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
107
|
+
encrypted += cipher.final("hex");
|
|
108
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
109
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
110
|
+
}
|
|
111
|
+
decrypt(text) {
|
|
112
|
+
const parts = text.split(":");
|
|
113
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
114
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
115
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
116
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
117
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
118
|
+
decipher.setAuthTag(authTag);
|
|
119
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
120
|
+
decrypted += decipher.final("utf8");
|
|
121
|
+
return decrypted;
|
|
122
|
+
}
|
|
123
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
124
|
+
const id = crypto3.randomUUID();
|
|
125
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
126
|
+
const payload = JSON.stringify(payloadObj);
|
|
127
|
+
const encryptedPayload = this.encrypt(payload);
|
|
128
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
129
|
+
const prevHash = last?.hash ?? "";
|
|
130
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
131
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
132
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
133
|
+
this.dumpDebugIfNeeded();
|
|
134
|
+
return id;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
138
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
139
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
140
|
+
* can only be claimed for chained history.
|
|
141
|
+
*/
|
|
142
|
+
verifyChain() {
|
|
143
|
+
const rows = this.db.prepare(
|
|
144
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
145
|
+
).all();
|
|
146
|
+
let prevHash = "";
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
149
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
150
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
151
|
+
}
|
|
152
|
+
prevHash = row.hash;
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, entries: rows.length };
|
|
155
|
+
}
|
|
156
|
+
getAllLogs() {
|
|
157
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
158
|
+
const rows = stmt.all();
|
|
159
|
+
return rows.map((row) => {
|
|
160
|
+
let payload = "";
|
|
161
|
+
try {
|
|
162
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
id: row.id,
|
|
168
|
+
timestamp: row.timestamp,
|
|
169
|
+
eventType: row.event_type,
|
|
170
|
+
tierId: row.tier_id,
|
|
171
|
+
payload
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
dumpDebugIfNeeded() {
|
|
176
|
+
if (!this.debugMode) return;
|
|
177
|
+
try {
|
|
178
|
+
const dumpPath = path20.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
|
|
179
|
+
const entries = this.getAllLogs();
|
|
180
|
+
fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error("Failed to dump debug audit logs", err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
close() {
|
|
186
|
+
this.db.close();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
});
|
|
40
191
|
|
|
41
192
|
// src/constants.ts
|
|
42
|
-
var CASCADE_VERSION = "0.
|
|
193
|
+
var CASCADE_VERSION = "0.13.2";
|
|
43
194
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
44
195
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
45
196
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -1971,7 +2122,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
1971
2122
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
1972
2123
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
1973
2124
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
1974
|
-
var DEFAULT_CACHE_FILE =
|
|
2125
|
+
var DEFAULT_CACHE_FILE = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
1975
2126
|
function normalizeModelId(id) {
|
|
1976
2127
|
let s = id.toLowerCase();
|
|
1977
2128
|
const slash = s.lastIndexOf("/");
|
|
@@ -2086,7 +2237,7 @@ var LiveDataProvider = class {
|
|
|
2086
2237
|
}
|
|
2087
2238
|
async saveCache() {
|
|
2088
2239
|
try {
|
|
2089
|
-
await fs4.mkdir(
|
|
2240
|
+
await fs4.mkdir(path20.dirname(this.opts.cacheFile), { recursive: true });
|
|
2090
2241
|
const cache = {
|
|
2091
2242
|
fetchedAt: this.fetchedAt,
|
|
2092
2243
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -2216,7 +2367,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2216
2367
|
costByTier: {},
|
|
2217
2368
|
tokensByTier: {},
|
|
2218
2369
|
inputTokensByTier: {},
|
|
2219
|
-
outputTokensByTier: {}
|
|
2370
|
+
outputTokensByTier: {},
|
|
2371
|
+
costByFeature: {}
|
|
2220
2372
|
};
|
|
2221
2373
|
tierModels = /* @__PURE__ */ new Map();
|
|
2222
2374
|
config;
|
|
@@ -2238,6 +2390,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2238
2390
|
tpmLimiter;
|
|
2239
2391
|
localQueue;
|
|
2240
2392
|
taskAnalyzer;
|
|
2393
|
+
worldStateDB;
|
|
2394
|
+
privacyPaths;
|
|
2395
|
+
guidanceQueue;
|
|
2241
2396
|
liveData;
|
|
2242
2397
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
2243
2398
|
originalTierModels;
|
|
@@ -2395,7 +2550,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2395
2550
|
if (options.model && !requireVision) {
|
|
2396
2551
|
this.ensureProvider(options.model, this.config.providers);
|
|
2397
2552
|
}
|
|
2398
|
-
|
|
2553
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
2554
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
2555
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
2556
|
+
if (!localModel) {
|
|
2557
|
+
throw new Error(
|
|
2558
|
+
"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."
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
2562
|
+
model = localModel;
|
|
2563
|
+
}
|
|
2399
2564
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
2400
2565
|
const provider = this.getProvider(model);
|
|
2401
2566
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -2467,7 +2632,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2467
2632
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
2468
2633
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
2469
2634
|
}
|
|
2470
|
-
this.recordStats(tier, model, result.usage);
|
|
2635
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
2471
2636
|
this.failover.recordSuccess(model.provider);
|
|
2472
2637
|
return result;
|
|
2473
2638
|
} catch (err) {
|
|
@@ -2572,6 +2737,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2572
2737
|
setTaskAnalyzer(analyzer) {
|
|
2573
2738
|
this.taskAnalyzer = analyzer;
|
|
2574
2739
|
}
|
|
2740
|
+
setWorldStateDB(db) {
|
|
2741
|
+
this.worldStateDB = db;
|
|
2742
|
+
}
|
|
2743
|
+
getWorldStateDB() {
|
|
2744
|
+
return this.worldStateDB;
|
|
2745
|
+
}
|
|
2746
|
+
setPrivacyPaths(paths) {
|
|
2747
|
+
this.privacyPaths = paths;
|
|
2748
|
+
}
|
|
2749
|
+
getPrivacyPaths() {
|
|
2750
|
+
return this.privacyPaths;
|
|
2751
|
+
}
|
|
2752
|
+
setGuidanceQueue(queue) {
|
|
2753
|
+
this.guidanceQueue = queue;
|
|
2754
|
+
}
|
|
2755
|
+
getGuidanceQueue() {
|
|
2756
|
+
return this.guidanceQueue;
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
2760
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
2761
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
2762
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
2763
|
+
*/
|
|
2764
|
+
isPrivateModel(model) {
|
|
2765
|
+
if (model.isLocal) return true;
|
|
2766
|
+
if (model.provider !== "openai-compatible") return false;
|
|
2767
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
2768
|
+
if (!baseUrl) return false;
|
|
2769
|
+
try {
|
|
2770
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
2771
|
+
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");
|
|
2772
|
+
} catch {
|
|
2773
|
+
return false;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2575
2776
|
/**
|
|
2576
2777
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
2577
2778
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -2595,7 +2796,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2595
2796
|
costByTier: { ...this.stats.costByTier },
|
|
2596
2797
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
2597
2798
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
2598
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
2799
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
2800
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
2599
2801
|
};
|
|
2600
2802
|
}
|
|
2601
2803
|
/**
|
|
@@ -2645,7 +2847,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2645
2847
|
costByTier: {},
|
|
2646
2848
|
tokensByTier: {},
|
|
2647
2849
|
inputTokensByTier: {},
|
|
2648
|
-
outputTokensByTier: {}
|
|
2850
|
+
outputTokensByTier: {},
|
|
2851
|
+
costByFeature: {}
|
|
2649
2852
|
};
|
|
2650
2853
|
this.sessionCostUsd = 0;
|
|
2651
2854
|
this.budgetState = "ok";
|
|
@@ -2811,7 +3014,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2811
3014
|
}
|
|
2812
3015
|
return void 0;
|
|
2813
3016
|
}
|
|
2814
|
-
recordStats(tier, model, usage) {
|
|
3017
|
+
recordStats(tier, model, usage, featureTag) {
|
|
2815
3018
|
this.stats.totalTokens += usage.totalTokens;
|
|
2816
3019
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
2817
3020
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -2821,6 +3024,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2821
3024
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
2822
3025
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
2823
3026
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
3027
|
+
if (featureTag) {
|
|
3028
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
3029
|
+
}
|
|
2824
3030
|
this.runTokens += usage.totalTokens;
|
|
2825
3031
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
2826
3032
|
this.updateBudgetState();
|
|
@@ -2917,6 +3123,13 @@ var BaseTier = class extends EventEmitter {
|
|
|
2917
3123
|
hierarchyContext = "";
|
|
2918
3124
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
2919
3125
|
signal;
|
|
3126
|
+
/**
|
|
3127
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
3128
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
3129
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
3130
|
+
* which would interleave, are not tagged.
|
|
3131
|
+
*/
|
|
3132
|
+
isPresenter = false;
|
|
2920
3133
|
constructor(role, id, parentId) {
|
|
2921
3134
|
super();
|
|
2922
3135
|
this.role = role;
|
|
@@ -2924,6 +3137,10 @@ var BaseTier = class extends EventEmitter {
|
|
|
2924
3137
|
this.parentId = parentId;
|
|
2925
3138
|
this.label = this.id;
|
|
2926
3139
|
}
|
|
3140
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
3141
|
+
setPresenter(on = true) {
|
|
3142
|
+
this.isPresenter = on;
|
|
3143
|
+
}
|
|
2927
3144
|
getStatus() {
|
|
2928
3145
|
return this.status;
|
|
2929
3146
|
}
|
|
@@ -3357,6 +3574,8 @@ var T3Worker = class extends BaseTier {
|
|
|
3357
3574
|
toolRegistry;
|
|
3358
3575
|
context;
|
|
3359
3576
|
assignment;
|
|
3577
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
3578
|
+
localOnlyMatch = false;
|
|
3360
3579
|
peerSyncBuffer = [];
|
|
3361
3580
|
store;
|
|
3362
3581
|
audit;
|
|
@@ -3412,6 +3631,11 @@ var T3Worker = class extends BaseTier {
|
|
|
3412
3631
|
this.taskId = taskId;
|
|
3413
3632
|
this.setLabel(assignment.subtaskTitle);
|
|
3414
3633
|
this.setStatus("ACTIVE");
|
|
3634
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
3635
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
3636
|
+
if (this.localOnlyMatch) {
|
|
3637
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
3638
|
+
}
|
|
3415
3639
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
3416
3640
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
3417
3641
|
this.tools = [...this.tools, {
|
|
@@ -3542,9 +3766,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
3542
3766
|
}
|
|
3543
3767
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
3544
3768
|
if (reflectCfg.enabled) {
|
|
3545
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
3769
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
3546
3770
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
3547
3771
|
}
|
|
3772
|
+
const db = this.router.getWorldStateDB?.();
|
|
3773
|
+
if (db) {
|
|
3774
|
+
try {
|
|
3775
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
3776
|
+
} catch (e) {
|
|
3777
|
+
this.log("Failed to write to World State DB");
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3548
3780
|
this.setStatus("COMPLETED", output);
|
|
3549
3781
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
3550
3782
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -3617,6 +3849,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
3617
3849
|
while (iterations < MAX_ITERATIONS) {
|
|
3618
3850
|
iterations++;
|
|
3619
3851
|
this.throwIfCancelled();
|
|
3852
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
3853
|
+
for (const g of guidance) {
|
|
3854
|
+
this.log(`User intervention received: ${g.text}`);
|
|
3855
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
3856
|
+
await this.context.addMessage({
|
|
3857
|
+
role: "user",
|
|
3858
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
3859
|
+
${g.text}`
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3620
3862
|
const options = {
|
|
3621
3863
|
messages: this.context.getMessages(),
|
|
3622
3864
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -3625,13 +3867,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3625
3867
|
// Don't pass tools array when model can't use them natively
|
|
3626
3868
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
3627
3869
|
maxTokens: 4096,
|
|
3628
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
3870
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
3871
|
+
featureTag: this.assignment?.sectionTitle,
|
|
3872
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
3629
3873
|
};
|
|
3630
3874
|
const result = await this.router.generate(
|
|
3631
3875
|
"T3",
|
|
3632
3876
|
options,
|
|
3633
3877
|
(chunk) => {
|
|
3634
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
3878
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
3635
3879
|
}
|
|
3636
3880
|
);
|
|
3637
3881
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -3789,8 +4033,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3789
4033
|
tierId: this.id,
|
|
3790
4034
|
sessionId: this.taskId,
|
|
3791
4035
|
requireApproval: false,
|
|
3792
|
-
saveSnapshot: async (
|
|
3793
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4036
|
+
saveSnapshot: async (path21, content) => {
|
|
4037
|
+
this.store?.addFileSnapshot(this.taskId, path21, content);
|
|
3794
4038
|
},
|
|
3795
4039
|
sendPeerSync: (to, syncType, content) => {
|
|
3796
4040
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -3960,7 +4204,7 @@ ${assignment.expectedOutput}`;
|
|
|
3960
4204
|
const { promisify: promisify4 } = await import('util');
|
|
3961
4205
|
const execAsync2 = promisify4(exec2);
|
|
3962
4206
|
for (const artifactPath of artifactPaths) {
|
|
3963
|
-
const absolutePath =
|
|
4207
|
+
const absolutePath = path20.resolve(process.cwd(), artifactPath);
|
|
3964
4208
|
try {
|
|
3965
4209
|
const stat = await fs4.stat(absolutePath);
|
|
3966
4210
|
if (!stat.isFile()) {
|
|
@@ -3981,7 +4225,7 @@ ${assignment.expectedOutput}`;
|
|
|
3981
4225
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
3982
4226
|
continue;
|
|
3983
4227
|
}
|
|
3984
|
-
const ext =
|
|
4228
|
+
const ext = path20.extname(absolutePath).toLowerCase();
|
|
3985
4229
|
try {
|
|
3986
4230
|
if (ext === ".ts" || ext === ".tsx") {
|
|
3987
4231
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -4010,30 +4254,35 @@ ${stdout}`);
|
|
|
4010
4254
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
4011
4255
|
*/
|
|
4012
4256
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
4013
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4014
|
-
|
|
4015
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
4016
4257
|
let current = output;
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
const
|
|
4258
|
+
try {
|
|
4259
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
4260
|
+
const verdictResult = await this.router.generate("T2", {
|
|
4020
4261
|
messages: [{
|
|
4021
4262
|
role: "user",
|
|
4022
|
-
content: `
|
|
4263
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
4023
4264
|
|
|
4024
|
-
Goal
|
|
4265
|
+
Goal: ${assignment.expectedOutput}
|
|
4025
4266
|
Subtask: ${assignment.description}
|
|
4026
|
-
|
|
4027
|
-
Output:
|
|
4267
|
+
Current Output:
|
|
4028
4268
|
${current}
|
|
4029
4269
|
|
|
4030
|
-
|
|
4270
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
4271
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
4031
4272
|
}],
|
|
4032
|
-
systemPrompt:
|
|
4033
|
-
maxTokens: 400
|
|
4273
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
4274
|
+
maxTokens: 400,
|
|
4275
|
+
signal: this.signal,
|
|
4276
|
+
featureTag: assignment.sectionTitle,
|
|
4277
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4034
4278
|
});
|
|
4035
|
-
const
|
|
4036
|
-
|
|
4279
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
4280
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
4281
|
+
if (parsed.sufficient !== false) {
|
|
4282
|
+
this.log("T2-Critic approved output.");
|
|
4283
|
+
break;
|
|
4284
|
+
}
|
|
4285
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
4037
4286
|
const improved = await this.router.generate("T3", {
|
|
4038
4287
|
messages: [{
|
|
4039
4288
|
role: "user",
|
|
@@ -4045,16 +4294,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
4045
4294
|
Current output:
|
|
4046
4295
|
${current}`
|
|
4047
4296
|
}],
|
|
4048
|
-
systemPrompt:
|
|
4049
|
-
|
|
4297
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4298
|
+
|
|
4299
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4300
|
+
maxTokens: 4096,
|
|
4301
|
+
featureTag: assignment.sectionTitle,
|
|
4302
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4050
4303
|
});
|
|
4051
4304
|
const next = (improved.content ?? "").trim();
|
|
4052
4305
|
if (!next) break;
|
|
4053
4306
|
current = next;
|
|
4054
4307
|
this.log("Reflection: revised output for better goal alignment.");
|
|
4055
|
-
} catch {
|
|
4056
|
-
break;
|
|
4057
4308
|
}
|
|
4309
|
+
} catch (e) {
|
|
4310
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
4058
4311
|
}
|
|
4059
4312
|
return current;
|
|
4060
4313
|
}
|
|
@@ -4075,7 +4328,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
4075
4328
|
maxTokens: 500,
|
|
4076
4329
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4077
4330
|
|
|
4078
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
4331
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4332
|
+
featureTag: assignment.sectionTitle,
|
|
4333
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4079
4334
|
});
|
|
4080
4335
|
try {
|
|
4081
4336
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -4170,6 +4425,7 @@ Begin execution now.`;
|
|
|
4170
4425
|
issues,
|
|
4171
4426
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
4172
4427
|
correctionAttempts,
|
|
4428
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
4173
4429
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
4174
4430
|
};
|
|
4175
4431
|
}
|
|
@@ -4461,6 +4717,42 @@ var PeerBus = class extends EventEmitter {
|
|
|
4461
4717
|
}
|
|
4462
4718
|
};
|
|
4463
4719
|
|
|
4720
|
+
// src/core/audit/redaction.ts
|
|
4721
|
+
var RedactionLayer = class {
|
|
4722
|
+
// Regexes for common secrets/PII
|
|
4723
|
+
static RULES = [
|
|
4724
|
+
// IPv4 addresses (basic approximation)
|
|
4725
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
4726
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
4727
|
+
{ 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]" },
|
|
4728
|
+
// Email addresses
|
|
4729
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
4730
|
+
// Phone numbers (simplistic)
|
|
4731
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
4732
|
+
// AWS Access Key ID
|
|
4733
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
4734
|
+
];
|
|
4735
|
+
/**
|
|
4736
|
+
* Applies all redaction rules to the input string.
|
|
4737
|
+
*/
|
|
4738
|
+
static redact(text) {
|
|
4739
|
+
if (!text) return text;
|
|
4740
|
+
let redacted = text;
|
|
4741
|
+
for (const rule of this.RULES) {
|
|
4742
|
+
if (rule.pattern.test(redacted)) {
|
|
4743
|
+
rule.pattern.lastIndex = 0;
|
|
4744
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str) => {
|
|
4745
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
4746
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
4747
|
+
}
|
|
4748
|
+
return rule.replacement;
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
return redacted;
|
|
4753
|
+
}
|
|
4754
|
+
};
|
|
4755
|
+
|
|
4464
4756
|
// src/core/tiers/t2-manager.ts
|
|
4465
4757
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
4466
4758
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -4689,7 +4981,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4689
4981
|
try {
|
|
4690
4982
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
4691
4983
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
4692
|
-
|
|
4984
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4985
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
4693
4986
|
} catch {
|
|
4694
4987
|
return [{
|
|
4695
4988
|
subtaskId: randomUUID(),
|
|
@@ -4699,6 +4992,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4699
4992
|
constraints: assignment.constraints,
|
|
4700
4993
|
peerT3Ids: [],
|
|
4701
4994
|
parentT2: this.id,
|
|
4995
|
+
sectionTitle: assignment.sectionTitle,
|
|
4996
|
+
dependsOn: [],
|
|
4702
4997
|
executionMode: "parallel"
|
|
4703
4998
|
}];
|
|
4704
4999
|
}
|
|
@@ -4818,6 +5113,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4818
5113
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
4819
5114
|
const worker = workerMap.get(id);
|
|
4820
5115
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
5116
|
+
if (result.localOnly) {
|
|
5117
|
+
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}]`;
|
|
5118
|
+
} else {
|
|
5119
|
+
if (typeof result.output === "string" && result.output) {
|
|
5120
|
+
result.output = RedactionLayer.redact(result.output);
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
4821
5124
|
resultMap.set(id, result);
|
|
4822
5125
|
return result;
|
|
4823
5126
|
};
|
|
@@ -5033,6 +5336,7 @@ ${peerOutputs}` : "";
|
|
|
5033
5336
|
chunkEnd++;
|
|
5034
5337
|
}
|
|
5035
5338
|
i = chunkEnd;
|
|
5339
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
5036
5340
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
5037
5341
|
${currentSummary ? `
|
|
5038
5342
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -5042,6 +5346,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
5042
5346
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
5043
5347
|
const messages = [{ role: "user", content: prompt }];
|
|
5044
5348
|
try {
|
|
5349
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
5045
5350
|
const result = await this.router.generate("T2", {
|
|
5046
5351
|
messages,
|
|
5047
5352
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -5049,7 +5354,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
5049
5354
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
5050
5355
|
maxTokens: 500,
|
|
5051
5356
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
5052
|
-
});
|
|
5357
|
+
}, streamFinal);
|
|
5053
5358
|
currentSummary = result.content;
|
|
5054
5359
|
} catch (err) {
|
|
5055
5360
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -5101,14 +5406,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
5101
5406
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
5102
5407
|
});
|
|
5103
5408
|
const answer = result.content.trim().toUpperCase();
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
}
|
|
5107
|
-
if (answer.includes("NO")) {
|
|
5108
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
5109
|
-
}
|
|
5409
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
5410
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
5110
5411
|
return null;
|
|
5111
5412
|
} catch {
|
|
5413
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
5112
5414
|
return null;
|
|
5113
5415
|
}
|
|
5114
5416
|
}
|
|
@@ -5447,10 +5749,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
5447
5749
|
}
|
|
5448
5750
|
}
|
|
5449
5751
|
async decomposeTask(prompt, systemContext) {
|
|
5752
|
+
const db = this.router.getWorldStateDB?.();
|
|
5753
|
+
const worldStateContext = db ? `
|
|
5754
|
+
|
|
5755
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
5756
|
+
${db.getFormattedState()}` : "";
|
|
5450
5757
|
const contextSection = systemContext ? `
|
|
5451
5758
|
Project context:
|
|
5452
5759
|
${systemContext}` : "";
|
|
5453
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
5760
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
5454
5761
|
|
|
5455
5762
|
Task: ${prompt}
|
|
5456
5763
|
|
|
@@ -5782,7 +6089,7 @@ Instructions:
|
|
|
5782
6089
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
5783
6090
|
maxTokens: 8e3
|
|
5784
6091
|
}, (chunk) => {
|
|
5785
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
6092
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5786
6093
|
});
|
|
5787
6094
|
return result.content;
|
|
5788
6095
|
}
|
|
@@ -5811,14 +6118,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
5811
6118
|
temperature: 0
|
|
5812
6119
|
});
|
|
5813
6120
|
const answer = result.content.trim().toUpperCase();
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
}
|
|
5817
|
-
if (answer.includes("NO")) {
|
|
5818
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
5819
|
-
}
|
|
6121
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
6122
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
5820
6123
|
return null;
|
|
5821
6124
|
} catch {
|
|
6125
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
5822
6126
|
return null;
|
|
5823
6127
|
}
|
|
5824
6128
|
}
|
|
@@ -5932,16 +6236,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
5932
6236
|
if (typeof input !== "string" || input.length === 0) {
|
|
5933
6237
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
5934
6238
|
}
|
|
5935
|
-
const root =
|
|
5936
|
-
const abs =
|
|
5937
|
-
const rel =
|
|
5938
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6239
|
+
const root = path20.resolve(workspaceRoot);
|
|
6240
|
+
const abs = path20.isAbsolute(input) ? path20.resolve(input) : path20.resolve(root, input);
|
|
6241
|
+
const rel = path20.relative(root, abs);
|
|
6242
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
5939
6243
|
throw new WorkspaceSandboxError(input, root);
|
|
5940
6244
|
}
|
|
5941
6245
|
try {
|
|
5942
|
-
const real =
|
|
5943
|
-
const realRel =
|
|
5944
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6246
|
+
const real = fs19.realpathSync(abs);
|
|
6247
|
+
const realRel = path20.relative(root, real);
|
|
6248
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20.isAbsolute(realRel))) {
|
|
5945
6249
|
throw new WorkspaceSandboxError(input, root);
|
|
5946
6250
|
}
|
|
5947
6251
|
} catch (e) {
|
|
@@ -6002,7 +6306,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6002
6306
|
} catch {
|
|
6003
6307
|
}
|
|
6004
6308
|
}
|
|
6005
|
-
await fs4.mkdir(
|
|
6309
|
+
await fs4.mkdir(path20.dirname(absPath), { recursive: true });
|
|
6006
6310
|
await fs4.writeFile(absPath, content, "utf-8");
|
|
6007
6311
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6008
6312
|
}
|
|
@@ -6500,7 +6804,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
6500
6804
|
};
|
|
6501
6805
|
async function fileToImageAttachment(filePath) {
|
|
6502
6806
|
const data = await fs4.readFile(filePath);
|
|
6503
|
-
const ext =
|
|
6807
|
+
const ext = path20.extname(filePath).toLowerCase();
|
|
6504
6808
|
const mimeMap = {
|
|
6505
6809
|
".jpg": "image/jpeg",
|
|
6506
6810
|
".jpeg": "image/jpeg",
|
|
@@ -6534,14 +6838,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
6534
6838
|
const filePath = input["path"];
|
|
6535
6839
|
const content = input["content"];
|
|
6536
6840
|
const title = input["title"];
|
|
6537
|
-
const dir =
|
|
6538
|
-
if (!
|
|
6539
|
-
|
|
6841
|
+
const dir = path20.dirname(filePath);
|
|
6842
|
+
if (!fs19.existsSync(dir)) {
|
|
6843
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
6540
6844
|
}
|
|
6541
6845
|
return new Promise((resolve, reject) => {
|
|
6542
6846
|
try {
|
|
6543
6847
|
const doc = new PDFDocument({ margin: 50 });
|
|
6544
|
-
const stream =
|
|
6848
|
+
const stream = fs19.createWriteStream(filePath);
|
|
6545
6849
|
doc.pipe(stream);
|
|
6546
6850
|
if (title) {
|
|
6547
6851
|
doc.info["Title"] = title;
|
|
@@ -6619,22 +6923,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
6619
6923
|
}
|
|
6620
6924
|
cmdPrefix = NODE_CMD;
|
|
6621
6925
|
}
|
|
6622
|
-
const tmpDir =
|
|
6623
|
-
if (!
|
|
6624
|
-
|
|
6926
|
+
const tmpDir = path20.join(this.workspaceRoot, ".cascade", "tmp");
|
|
6927
|
+
if (!fs19.existsSync(tmpDir)) {
|
|
6928
|
+
fs19.mkdirSync(tmpDir, { recursive: true });
|
|
6625
6929
|
}
|
|
6626
6930
|
const extension = language === "python" ? "py" : "js";
|
|
6627
6931
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
6628
|
-
const filePath =
|
|
6629
|
-
|
|
6932
|
+
const filePath = path20.join(tmpDir, fileName);
|
|
6933
|
+
fs19.writeFileSync(filePath, code, "utf-8");
|
|
6630
6934
|
const execArgs = [filePath, ...args];
|
|
6631
6935
|
return new Promise((resolve) => {
|
|
6632
6936
|
const startMs = Date.now();
|
|
6633
6937
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
6634
6938
|
const duration = Date.now() - startMs;
|
|
6635
6939
|
try {
|
|
6636
|
-
if (
|
|
6637
|
-
|
|
6940
|
+
if (fs19.existsSync(filePath)) {
|
|
6941
|
+
fs19.unlinkSync(filePath);
|
|
6638
6942
|
}
|
|
6639
6943
|
} catch (cleanupErr) {
|
|
6640
6944
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -6913,7 +7217,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6913
7217
|
};
|
|
6914
7218
|
async execute(input, _options) {
|
|
6915
7219
|
const pattern = input["pattern"];
|
|
6916
|
-
const searchPath = input["path"] ?
|
|
7220
|
+
const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
6917
7221
|
const matches = await glob(pattern, {
|
|
6918
7222
|
cwd: searchPath,
|
|
6919
7223
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -6926,7 +7230,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6926
7230
|
const withMtime = await Promise.all(
|
|
6927
7231
|
matches.map(async (rel) => {
|
|
6928
7232
|
try {
|
|
6929
|
-
const stat = await fs4.stat(
|
|
7233
|
+
const stat = await fs4.stat(path20.join(searchPath, rel));
|
|
6930
7234
|
return { rel, mtime: stat.mtimeMs };
|
|
6931
7235
|
} catch {
|
|
6932
7236
|
return { rel, mtime: 0 };
|
|
@@ -6975,7 +7279,7 @@ var GrepTool = class extends BaseTool {
|
|
|
6975
7279
|
};
|
|
6976
7280
|
async execute(input, _options) {
|
|
6977
7281
|
const pattern = input["pattern"];
|
|
6978
|
-
const searchPath = input["path"] ?
|
|
7282
|
+
const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
6979
7283
|
const globPattern = input["glob"];
|
|
6980
7284
|
const outputMode = input["output_mode"] ?? "content";
|
|
6981
7285
|
const context = input["context"] ?? 0;
|
|
@@ -7029,12 +7333,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7029
7333
|
nodir: true
|
|
7030
7334
|
});
|
|
7031
7335
|
} catch {
|
|
7032
|
-
files = [
|
|
7336
|
+
files = [path20.relative(searchPath, searchPath) || "."];
|
|
7033
7337
|
}
|
|
7034
7338
|
const results = [];
|
|
7035
7339
|
let totalCount = 0;
|
|
7036
7340
|
for (const rel of files) {
|
|
7037
|
-
const abs =
|
|
7341
|
+
const abs = path20.join(searchPath, rel);
|
|
7038
7342
|
let content;
|
|
7039
7343
|
try {
|
|
7040
7344
|
content = await fs4.readFile(abs, "utf-8");
|
|
@@ -7396,10 +7700,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
7396
7700
|
}
|
|
7397
7701
|
isIgnored(filePath) {
|
|
7398
7702
|
if (!filePath) return false;
|
|
7399
|
-
const abs =
|
|
7400
|
-
const rel =
|
|
7401
|
-
if (!rel || rel.startsWith("..") ||
|
|
7402
|
-
const posixRel = rel.split(
|
|
7703
|
+
const abs = path20.resolve(this.workspaceRoot, filePath);
|
|
7704
|
+
const rel = path20.relative(this.workspaceRoot, abs);
|
|
7705
|
+
if (!rel || rel.startsWith("..") || path20.isAbsolute(rel)) return true;
|
|
7706
|
+
const posixRel = rel.split(path20.sep).join("/");
|
|
7403
7707
|
return this.ignoreMatcher.ignores(posixRel);
|
|
7404
7708
|
}
|
|
7405
7709
|
};
|
|
@@ -7516,6 +7820,9 @@ var McpClient = class _McpClient {
|
|
|
7516
7820
|
return this.clients.has(serverName);
|
|
7517
7821
|
}
|
|
7518
7822
|
};
|
|
7823
|
+
|
|
7824
|
+
// src/core/cascade.ts
|
|
7825
|
+
init_audit_logger();
|
|
7519
7826
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
7520
7827
|
"file_read",
|
|
7521
7828
|
"file_list",
|
|
@@ -7768,7 +8075,8 @@ var WorkspaceConfigSchema = z.object({
|
|
|
7768
8075
|
cascadeMdPath: z.string().default("CASCADE.md"),
|
|
7769
8076
|
configPath: z.string().default(".cascade/config.json"),
|
|
7770
8077
|
keystorePath: z.string().default(".cascade/keystore.enc"),
|
|
7771
|
-
auditLogPath: z.string().default(".cascade/audit.log")
|
|
8078
|
+
auditLogPath: z.string().default(".cascade/audit.log"),
|
|
8079
|
+
debugWorldState: z.boolean().default(false)
|
|
7772
8080
|
});
|
|
7773
8081
|
var CascadeConfigSchema = z.object({
|
|
7774
8082
|
version: z.literal("1.0").default("1.0"),
|
|
@@ -7913,6 +8221,16 @@ var CascadeConfigSchema = z.object({
|
|
|
7913
8221
|
* 'parallel' / 'sequential' — force it.
|
|
7914
8222
|
*/
|
|
7915
8223
|
t3Execution: z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
8224
|
+
/**
|
|
8225
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
8226
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
8227
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
8228
|
+
*/
|
|
8229
|
+
privacy: z.object({
|
|
8230
|
+
paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
|
|
8231
|
+
}).optional(),
|
|
8232
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
8233
|
+
routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
7916
8234
|
/**
|
|
7917
8235
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
7918
8236
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -8181,9 +8499,10 @@ var TaskAnalyzer = class {
|
|
|
8181
8499
|
analysisCache.clear();
|
|
8182
8500
|
}
|
|
8183
8501
|
};
|
|
8184
|
-
var DEFAULT_STATS_FILE =
|
|
8502
|
+
var DEFAULT_STATS_FILE = path20.join(os6.homedir(), ".cascade", "model-perf.json");
|
|
8185
8503
|
var ModelPerformanceTracker = class {
|
|
8186
8504
|
stats = /* @__PURE__ */ new Map();
|
|
8505
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
8187
8506
|
statsFile;
|
|
8188
8507
|
loaded = false;
|
|
8189
8508
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -8195,18 +8514,29 @@ var ModelPerformanceTracker = class {
|
|
|
8195
8514
|
try {
|
|
8196
8515
|
const raw = await fs4.readFile(this.statsFile, "utf-8");
|
|
8197
8516
|
const parsed = JSON.parse(raw);
|
|
8198
|
-
|
|
8199
|
-
this.stats.set(key, stat);
|
|
8517
|
+
if (parsed.models) {
|
|
8518
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
8519
|
+
} else {
|
|
8520
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
8521
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
8522
|
+
this.stats.set(key, stat);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
if (parsed.features) {
|
|
8527
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
8200
8528
|
}
|
|
8201
8529
|
} catch {
|
|
8202
8530
|
}
|
|
8203
8531
|
}
|
|
8204
8532
|
async save() {
|
|
8205
8533
|
try {
|
|
8206
|
-
await fs4.mkdir(
|
|
8207
|
-
const
|
|
8208
|
-
|
|
8209
|
-
|
|
8534
|
+
await fs4.mkdir(path20.dirname(this.statsFile), { recursive: true });
|
|
8535
|
+
const modelsObj = {};
|
|
8536
|
+
const featuresObj = {};
|
|
8537
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
8538
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
8539
|
+
await fs4.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
8210
8540
|
} catch {
|
|
8211
8541
|
}
|
|
8212
8542
|
}
|
|
@@ -8227,6 +8557,13 @@ var ModelPerformanceTracker = class {
|
|
|
8227
8557
|
sampleCount: s.sampleCount + 1
|
|
8228
8558
|
});
|
|
8229
8559
|
}
|
|
8560
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
8561
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
8562
|
+
this.featureStats.set(featureTag, {
|
|
8563
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
8564
|
+
runCount: s.runCount + 1
|
|
8565
|
+
});
|
|
8566
|
+
}
|
|
8230
8567
|
/**
|
|
8231
8568
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
8232
8569
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -8241,6 +8578,9 @@ var ModelPerformanceTracker = class {
|
|
|
8241
8578
|
getAll() {
|
|
8242
8579
|
return new Map(this.stats);
|
|
8243
8580
|
}
|
|
8581
|
+
getAllFeatures() {
|
|
8582
|
+
return new Map(this.featureStats);
|
|
8583
|
+
}
|
|
8244
8584
|
/**
|
|
8245
8585
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
8246
8586
|
* High retry counts penalise the score.
|
|
@@ -8605,7 +8945,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8605
8945
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
8606
8946
|
async loadPersistedTools() {
|
|
8607
8947
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8608
|
-
const file =
|
|
8948
|
+
const file = path20.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
8609
8949
|
try {
|
|
8610
8950
|
const raw = await fs4.readFile(file, "utf-8");
|
|
8611
8951
|
const specs = JSON.parse(raw);
|
|
@@ -8629,8 +8969,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8629
8969
|
}
|
|
8630
8970
|
async persist() {
|
|
8631
8971
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8632
|
-
const dir =
|
|
8633
|
-
const file =
|
|
8972
|
+
const dir = path20.join(this.workspacePath, ".cascade");
|
|
8973
|
+
const file = path20.join(dir, DYNAMIC_TOOLS_FILE);
|
|
8634
8974
|
try {
|
|
8635
8975
|
await fs4.mkdir(dir, { recursive: true });
|
|
8636
8976
|
await fs4.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -8643,6 +8983,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8643
8983
|
return Array.from(this.specs.keys());
|
|
8644
8984
|
}
|
|
8645
8985
|
};
|
|
8986
|
+
var WorldStateDB = class {
|
|
8987
|
+
constructor(workspacePath, debugMode = false) {
|
|
8988
|
+
this.workspacePath = workspacePath;
|
|
8989
|
+
this.debugMode = debugMode;
|
|
8990
|
+
const cascadeDir = path20.join(workspacePath, ".cascade");
|
|
8991
|
+
if (!fs19.existsSync(cascadeDir)) {
|
|
8992
|
+
fs19.mkdirSync(cascadeDir, { recursive: true });
|
|
8993
|
+
}
|
|
8994
|
+
this.keyPath = path20.join(cascadeDir, "world_state.key");
|
|
8995
|
+
this.dbPath = path20.join(cascadeDir, "world_state.db");
|
|
8996
|
+
this.initEncryptionKey();
|
|
8997
|
+
this.db = new Database(this.dbPath);
|
|
8998
|
+
this.db.pragma("journal_mode = WAL");
|
|
8999
|
+
this.db.exec(`
|
|
9000
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
9001
|
+
id TEXT PRIMARY KEY,
|
|
9002
|
+
timestamp TEXT NOT NULL,
|
|
9003
|
+
worker_id TEXT NOT NULL,
|
|
9004
|
+
encrypted_payload TEXT NOT NULL
|
|
9005
|
+
)
|
|
9006
|
+
`);
|
|
9007
|
+
}
|
|
9008
|
+
workspacePath;
|
|
9009
|
+
debugMode;
|
|
9010
|
+
db;
|
|
9011
|
+
keyPath;
|
|
9012
|
+
dbPath;
|
|
9013
|
+
encryptionKey;
|
|
9014
|
+
initEncryptionKey() {
|
|
9015
|
+
if (fs19.existsSync(this.keyPath)) {
|
|
9016
|
+
this.encryptionKey = fs19.readFileSync(this.keyPath);
|
|
9017
|
+
} else {
|
|
9018
|
+
this.encryptionKey = crypto3.randomBytes(32);
|
|
9019
|
+
fs19.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9020
|
+
}
|
|
9021
|
+
}
|
|
9022
|
+
encrypt(text) {
|
|
9023
|
+
const iv = crypto3.randomBytes(16);
|
|
9024
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9025
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
9026
|
+
encrypted += cipher.final("hex");
|
|
9027
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
9028
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
9029
|
+
}
|
|
9030
|
+
decrypt(text) {
|
|
9031
|
+
const parts = text.split(":");
|
|
9032
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
9033
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
9034
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
9035
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
9036
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9037
|
+
decipher.setAuthTag(authTag);
|
|
9038
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
9039
|
+
decrypted += decipher.final("utf8");
|
|
9040
|
+
return decrypted;
|
|
9041
|
+
}
|
|
9042
|
+
addEntry(workerId, summary) {
|
|
9043
|
+
const id = crypto3.randomUUID();
|
|
9044
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9045
|
+
const payload = JSON.stringify({ summary });
|
|
9046
|
+
const encryptedPayload = this.encrypt(payload);
|
|
9047
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
9048
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
9049
|
+
this.dumpDebugIfNeeded();
|
|
9050
|
+
return id;
|
|
9051
|
+
}
|
|
9052
|
+
getAllEntries() {
|
|
9053
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
9054
|
+
const rows = stmt.all();
|
|
9055
|
+
return rows.map((row) => {
|
|
9056
|
+
try {
|
|
9057
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
9058
|
+
const parsed = JSON.parse(decrypted);
|
|
9059
|
+
return {
|
|
9060
|
+
id: row.id,
|
|
9061
|
+
timestamp: row.timestamp,
|
|
9062
|
+
workerId: row.worker_id,
|
|
9063
|
+
summary: parsed.summary
|
|
9064
|
+
};
|
|
9065
|
+
} catch (err) {
|
|
9066
|
+
return {
|
|
9067
|
+
id: row.id,
|
|
9068
|
+
timestamp: row.timestamp,
|
|
9069
|
+
workerId: row.worker_id,
|
|
9070
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
9071
|
+
};
|
|
9072
|
+
}
|
|
9073
|
+
});
|
|
9074
|
+
}
|
|
9075
|
+
getFormattedState() {
|
|
9076
|
+
const entries = this.getAllEntries();
|
|
9077
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
9078
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
9079
|
+
}
|
|
9080
|
+
dumpDebugIfNeeded() {
|
|
9081
|
+
if (!this.debugMode) return;
|
|
9082
|
+
try {
|
|
9083
|
+
const dumpPath = path20.join(os6.tmpdir(), "cascade_world_state_debug.json");
|
|
9084
|
+
const entries = this.getAllEntries();
|
|
9085
|
+
fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9086
|
+
} catch (err) {
|
|
9087
|
+
console.error("Failed to dump debug world state", err);
|
|
9088
|
+
}
|
|
9089
|
+
}
|
|
9090
|
+
close() {
|
|
9091
|
+
this.db.close();
|
|
9092
|
+
}
|
|
9093
|
+
};
|
|
9094
|
+
var ignore2 = ignoreFactory.default ?? ignoreFactory;
|
|
9095
|
+
var PrivacyPaths = class {
|
|
9096
|
+
localOnly;
|
|
9097
|
+
hasRules;
|
|
9098
|
+
constructor(policies = []) {
|
|
9099
|
+
this.localOnly = ignore2();
|
|
9100
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
9101
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
9102
|
+
this.hasRules = patterns.length > 0;
|
|
9103
|
+
}
|
|
9104
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
9105
|
+
hasPolicies() {
|
|
9106
|
+
return this.hasRules;
|
|
9107
|
+
}
|
|
9108
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
9109
|
+
isLocalOnly(relativePath) {
|
|
9110
|
+
if (!this.hasRules || !relativePath) return false;
|
|
9111
|
+
try {
|
|
9112
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
9113
|
+
} catch {
|
|
9114
|
+
return false;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
9118
|
+
anyLocalOnly(relativePaths) {
|
|
9119
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
9120
|
+
}
|
|
9121
|
+
};
|
|
9122
|
+
|
|
9123
|
+
// src/core/steering/guidance.ts
|
|
9124
|
+
var GuidanceQueue = class {
|
|
9125
|
+
entries = [];
|
|
9126
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
9127
|
+
cursors = /* @__PURE__ */ new Map();
|
|
9128
|
+
push(text, nodeId) {
|
|
9129
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9130
|
+
this.entries.push(entry);
|
|
9131
|
+
return entry;
|
|
9132
|
+
}
|
|
9133
|
+
/**
|
|
9134
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
9135
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
9136
|
+
*/
|
|
9137
|
+
drain(consumerId) {
|
|
9138
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
9139
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
9140
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
9141
|
+
}
|
|
9142
|
+
get size() {
|
|
9143
|
+
return this.entries.length;
|
|
9144
|
+
}
|
|
9145
|
+
};
|
|
8646
9146
|
|
|
8647
9147
|
// src/core/cascade.ts
|
|
8648
9148
|
var Cascade = class _Cascade extends EventEmitter {
|
|
@@ -8662,6 +9162,9 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
8662
9162
|
taskAnalyzer;
|
|
8663
9163
|
perfTracker;
|
|
8664
9164
|
toolCreator;
|
|
9165
|
+
worldStateDB;
|
|
9166
|
+
encryptedAuditLogger;
|
|
9167
|
+
guidanceQueue;
|
|
8665
9168
|
workspacePath;
|
|
8666
9169
|
constructor(config, workspacePath, store) {
|
|
8667
9170
|
super();
|
|
@@ -8683,6 +9186,23 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
8683
9186
|
});
|
|
8684
9187
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
8685
9188
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
9189
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9190
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
9191
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9192
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
9193
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
9194
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
9195
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
9196
|
+
}
|
|
9197
|
+
/**
|
|
9198
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
9199
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
9200
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
9201
|
+
*/
|
|
9202
|
+
injectGuidance(text, nodeId) {
|
|
9203
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
9204
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
9205
|
+
this.emit("guidance:injected", entry);
|
|
8686
9206
|
}
|
|
8687
9207
|
initOptionalFeatures() {
|
|
8688
9208
|
if (this.config.cascadeAuto === true) {
|
|
@@ -8845,6 +9365,12 @@ ${last.partialOutput}` : "");
|
|
|
8845
9365
|
if (!prompt) return null;
|
|
8846
9366
|
return this.run({ prompt });
|
|
8847
9367
|
}
|
|
9368
|
+
getWorkspacePath() {
|
|
9369
|
+
return this.workspacePath;
|
|
9370
|
+
}
|
|
9371
|
+
getWorldStateDB() {
|
|
9372
|
+
return this.worldStateDB;
|
|
9373
|
+
}
|
|
8848
9374
|
/**
|
|
8849
9375
|
* Record an explicit user rating for the last completed run.
|
|
8850
9376
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -8931,6 +9457,11 @@ ${last.partialOutput}` : "");
|
|
|
8931
9457
|
}
|
|
8932
9458
|
this.initOptionalFeatures();
|
|
8933
9459
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
9460
|
+
if (this.encryptedAuditLogger) {
|
|
9461
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
9462
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
9463
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
9464
|
+
}
|
|
8934
9465
|
this.initialized = true;
|
|
8935
9466
|
})();
|
|
8936
9467
|
try {
|
|
@@ -8974,6 +9505,21 @@ ${last.partialOutput}` : "");
|
|
|
8974
9505
|
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);
|
|
8975
9506
|
return inquiry && !producesArtifact;
|
|
8976
9507
|
}
|
|
9508
|
+
/**
|
|
9509
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
9510
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
9511
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
9512
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
9513
|
+
* Simple/Moderate) don't get over-escalated.
|
|
9514
|
+
*/
|
|
9515
|
+
looksClearlyComplex(prompt) {
|
|
9516
|
+
const p = prompt.trim();
|
|
9517
|
+
if (p.length < 24) return false;
|
|
9518
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
9519
|
+
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);
|
|
9520
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
9521
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
9522
|
+
}
|
|
8977
9523
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
8978
9524
|
static globCache = /* @__PURE__ */ new Map();
|
|
8979
9525
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -9057,10 +9603,16 @@ ${prompt}` : prompt;
|
|
|
9057
9603
|
let verdict;
|
|
9058
9604
|
if (match) {
|
|
9059
9605
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
9060
|
-
|
|
9606
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
9607
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
9608
|
+
verdict = "Complex";
|
|
9609
|
+
} else {
|
|
9610
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
9611
|
+
}
|
|
9061
9612
|
} else {
|
|
9062
|
-
|
|
9063
|
-
|
|
9613
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
9614
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
9615
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
9064
9616
|
}
|
|
9065
9617
|
return verdict;
|
|
9066
9618
|
} catch {
|
|
@@ -9112,7 +9664,15 @@ ${prompt}` : prompt;
|
|
|
9112
9664
|
}
|
|
9113
9665
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
9114
9666
|
});
|
|
9115
|
-
const
|
|
9667
|
+
const forceTier = this.config.routing?.forceTier;
|
|
9668
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
9669
|
+
let complexity;
|
|
9670
|
+
if (forced) {
|
|
9671
|
+
complexity = forced;
|
|
9672
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
9673
|
+
} else {
|
|
9674
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
9675
|
+
}
|
|
9116
9676
|
this.telemetry.capture("cascade:session_start", {
|
|
9117
9677
|
complexity,
|
|
9118
9678
|
providerCount: this.config.providers.length,
|
|
@@ -9192,6 +9752,7 @@ ${prompt}` : prompt;
|
|
|
9192
9752
|
try {
|
|
9193
9753
|
if (complexity === "Simple") {
|
|
9194
9754
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
9755
|
+
t3.setPresenter(true);
|
|
9195
9756
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
9196
9757
|
if (identityPrompt) {
|
|
9197
9758
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -9216,6 +9777,7 @@ ${prompt}` : prompt;
|
|
|
9216
9777
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
9217
9778
|
} else if (complexity === "Moderate") {
|
|
9218
9779
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
9780
|
+
t2.setPresenter(true);
|
|
9219
9781
|
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.");
|
|
9220
9782
|
if (identityPrompt) {
|
|
9221
9783
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -9265,6 +9827,7 @@ ${prompt}` : prompt;
|
|
|
9265
9827
|
}
|
|
9266
9828
|
} else {
|
|
9267
9829
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
9830
|
+
t1.setPresenter(true);
|
|
9268
9831
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
9269
9832
|
if (identityPrompt) {
|
|
9270
9833
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -9356,6 +9919,7 @@ ${prompt}` : prompt;
|
|
|
9356
9919
|
durationMs,
|
|
9357
9920
|
costByTier: stats.costByTier,
|
|
9358
9921
|
tokensByTier: stats.tokensByTier,
|
|
9922
|
+
costByFeature: stats.costByFeature,
|
|
9359
9923
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
9360
9924
|
};
|
|
9361
9925
|
}
|
|
@@ -9423,7 +9987,7 @@ var Keystore = class {
|
|
|
9423
9987
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
9424
9988
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
9425
9989
|
this.backend = "keytar";
|
|
9426
|
-
if (password &&
|
|
9990
|
+
if (password && fs19.existsSync(this.storePath)) {
|
|
9427
9991
|
try {
|
|
9428
9992
|
const fileEntries = this.decryptFile(password);
|
|
9429
9993
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -9442,8 +10006,8 @@ var Keystore = class {
|
|
|
9442
10006
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
9443
10007
|
);
|
|
9444
10008
|
}
|
|
9445
|
-
if (!
|
|
9446
|
-
const salt =
|
|
10009
|
+
if (!fs19.existsSync(this.storePath)) {
|
|
10010
|
+
const salt = crypto3.randomBytes(SALT_LEN);
|
|
9447
10011
|
this.masterKey = this.deriveKey(password, salt);
|
|
9448
10012
|
this.writeWithSalt({}, salt);
|
|
9449
10013
|
this.cache = {};
|
|
@@ -9456,8 +10020,8 @@ var Keystore = class {
|
|
|
9456
10020
|
}
|
|
9457
10021
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
9458
10022
|
unlockSync(password) {
|
|
9459
|
-
if (!
|
|
9460
|
-
const salt =
|
|
10023
|
+
if (!fs19.existsSync(this.storePath)) {
|
|
10024
|
+
const salt = crypto3.randomBytes(SALT_LEN);
|
|
9461
10025
|
this.masterKey = this.deriveKey(password, salt);
|
|
9462
10026
|
this.writeWithSalt({}, salt);
|
|
9463
10027
|
this.cache = {};
|
|
@@ -9514,12 +10078,12 @@ var Keystore = class {
|
|
|
9514
10078
|
}
|
|
9515
10079
|
}
|
|
9516
10080
|
decryptFile(password, knownSalt) {
|
|
9517
|
-
if (!
|
|
10081
|
+
if (!fs19.existsSync(this.storePath)) return {};
|
|
9518
10082
|
try {
|
|
9519
10083
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
9520
10084
|
const useSalt = knownSalt ?? salt;
|
|
9521
10085
|
const key = this.masterKey ?? this.deriveKey(password, useSalt);
|
|
9522
|
-
const decipher =
|
|
10086
|
+
const decipher = crypto3.createDecipheriv(ALGORITHM, key, iv);
|
|
9523
10087
|
decipher.setAuthTag(tag);
|
|
9524
10088
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
9525
10089
|
return JSON.parse(decrypted.toString("utf-8"));
|
|
@@ -9530,28 +10094,28 @@ var Keystore = class {
|
|
|
9530
10094
|
saveAll(data) {
|
|
9531
10095
|
if (!this.masterKey) return;
|
|
9532
10096
|
const raw = this.readRaw();
|
|
9533
|
-
const iv =
|
|
9534
|
-
const cipher =
|
|
10097
|
+
const iv = crypto3.randomBytes(IV_LEN);
|
|
10098
|
+
const cipher = crypto3.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9535
10099
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9536
10100
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9537
10101
|
const tag = cipher.getAuthTag();
|
|
9538
10102
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
9539
|
-
|
|
9540
|
-
|
|
10103
|
+
fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
|
|
10104
|
+
fs19.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9541
10105
|
}
|
|
9542
10106
|
writeWithSalt(data, salt) {
|
|
9543
10107
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
9544
|
-
const iv =
|
|
9545
|
-
const cipher =
|
|
10108
|
+
const iv = crypto3.randomBytes(IV_LEN);
|
|
10109
|
+
const cipher = crypto3.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9546
10110
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9547
10111
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9548
10112
|
const tag = cipher.getAuthTag();
|
|
9549
10113
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
9550
|
-
|
|
9551
|
-
|
|
10114
|
+
fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
|
|
10115
|
+
fs19.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9552
10116
|
}
|
|
9553
10117
|
readRaw() {
|
|
9554
|
-
const buf =
|
|
10118
|
+
const buf = fs19.readFileSync(this.storePath);
|
|
9555
10119
|
let offset = 0;
|
|
9556
10120
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
9557
10121
|
offset += SALT_LEN;
|
|
@@ -9563,15 +10127,15 @@ var Keystore = class {
|
|
|
9563
10127
|
return { salt, iv, tag, ciphertext };
|
|
9564
10128
|
}
|
|
9565
10129
|
deriveKey(password, salt) {
|
|
9566
|
-
return
|
|
10130
|
+
return crypto3.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
|
|
9567
10131
|
}
|
|
9568
10132
|
};
|
|
9569
|
-
var
|
|
10133
|
+
var ignore3 = ignoreFactory.default ?? ignoreFactory;
|
|
9570
10134
|
var CascadeIgnore = class {
|
|
9571
10135
|
ig;
|
|
9572
10136
|
loaded = false;
|
|
9573
10137
|
constructor() {
|
|
9574
|
-
this.ig =
|
|
10138
|
+
this.ig = ignore3();
|
|
9575
10139
|
this.ig.add([
|
|
9576
10140
|
".cascade/keystore.enc",
|
|
9577
10141
|
".cascade/memory.db",
|
|
@@ -9584,7 +10148,7 @@ var CascadeIgnore = class {
|
|
|
9584
10148
|
]);
|
|
9585
10149
|
}
|
|
9586
10150
|
async load(workspacePath) {
|
|
9587
|
-
const filePath =
|
|
10151
|
+
const filePath = path20.join(workspacePath, ".cascadeignore");
|
|
9588
10152
|
try {
|
|
9589
10153
|
const content = await fs4.readFile(filePath, "utf-8");
|
|
9590
10154
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -9595,7 +10159,7 @@ var CascadeIgnore = class {
|
|
|
9595
10159
|
}
|
|
9596
10160
|
isIgnored(filePath, workspacePath) {
|
|
9597
10161
|
try {
|
|
9598
|
-
const relative = workspacePath ?
|
|
10162
|
+
const relative = workspacePath ? path20.relative(workspacePath, filePath) : filePath;
|
|
9599
10163
|
return this.ig.ignores(relative);
|
|
9600
10164
|
} catch {
|
|
9601
10165
|
return false;
|
|
@@ -9606,7 +10170,7 @@ var CascadeIgnore = class {
|
|
|
9606
10170
|
}
|
|
9607
10171
|
};
|
|
9608
10172
|
async function loadCascadeMd(workspacePath) {
|
|
9609
|
-
const filePath =
|
|
10173
|
+
const filePath = path20.join(workspacePath, "CASCADE.md");
|
|
9610
10174
|
try {
|
|
9611
10175
|
const raw = await fs4.readFile(filePath, "utf-8");
|
|
9612
10176
|
return parseCascadeMd(raw);
|
|
@@ -9637,7 +10201,7 @@ ${raw.trim()}`;
|
|
|
9637
10201
|
var MemoryStore = class _MemoryStore {
|
|
9638
10202
|
db;
|
|
9639
10203
|
constructor(dbPath) {
|
|
9640
|
-
|
|
10204
|
+
fs19.mkdirSync(path20.dirname(dbPath), { recursive: true });
|
|
9641
10205
|
try {
|
|
9642
10206
|
this.db = new Database(dbPath, { timeout: 5e3 });
|
|
9643
10207
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -10395,15 +10959,15 @@ var ConfigManager = class {
|
|
|
10395
10959
|
globalDir;
|
|
10396
10960
|
constructor(workspacePath = process.cwd()) {
|
|
10397
10961
|
this.workspacePath = workspacePath;
|
|
10398
|
-
this.globalDir =
|
|
10962
|
+
this.globalDir = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
10399
10963
|
}
|
|
10400
10964
|
async load() {
|
|
10401
10965
|
this.config = await this.loadConfig();
|
|
10402
10966
|
this.ignore = new CascadeIgnore();
|
|
10403
10967
|
await this.ignore.load(this.workspacePath);
|
|
10404
10968
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
10405
|
-
this.keystore = new Keystore(
|
|
10406
|
-
this.store = new MemoryStore(
|
|
10969
|
+
this.keystore = new Keystore(path20.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
10970
|
+
this.store = new MemoryStore(path20.join(this.workspacePath, CASCADE_DB_FILE));
|
|
10407
10971
|
await this.injectEnvKeys();
|
|
10408
10972
|
await this.ensureDefaultIdentity();
|
|
10409
10973
|
}
|
|
@@ -10426,8 +10990,8 @@ var ConfigManager = class {
|
|
|
10426
10990
|
return this.workspacePath;
|
|
10427
10991
|
}
|
|
10428
10992
|
async save() {
|
|
10429
|
-
const configPath =
|
|
10430
|
-
await fs4.mkdir(
|
|
10993
|
+
const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10994
|
+
await fs4.mkdir(path20.dirname(configPath), { recursive: true });
|
|
10431
10995
|
await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10432
10996
|
}
|
|
10433
10997
|
async updateConfig(updates) {
|
|
@@ -10451,7 +11015,7 @@ var ConfigManager = class {
|
|
|
10451
11015
|
return configProvider?.apiKey;
|
|
10452
11016
|
}
|
|
10453
11017
|
async loadConfig() {
|
|
10454
|
-
const configPath =
|
|
11018
|
+
const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10455
11019
|
try {
|
|
10456
11020
|
const raw = await fs4.readFile(configPath, "utf-8");
|
|
10457
11021
|
return validateConfig(JSON.parse(raw));
|
|
@@ -10681,6 +11245,24 @@ var DashboardSocket = class {
|
|
|
10681
11245
|
});
|
|
10682
11246
|
});
|
|
10683
11247
|
}
|
|
11248
|
+
onSessionHalt(callback) {
|
|
11249
|
+
this.io.on("connection", (socket) => {
|
|
11250
|
+
socket.on("session:halt", (payload) => {
|
|
11251
|
+
if (typeof payload?.sessionId === "string") {
|
|
11252
|
+
callback(payload.sessionId);
|
|
11253
|
+
}
|
|
11254
|
+
});
|
|
11255
|
+
});
|
|
11256
|
+
}
|
|
11257
|
+
onSessionSteer(callback) {
|
|
11258
|
+
this.io.on("connection", (socket) => {
|
|
11259
|
+
socket.on("session:steer", (payload) => {
|
|
11260
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
11261
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
11262
|
+
}
|
|
11263
|
+
});
|
|
11264
|
+
});
|
|
11265
|
+
}
|
|
10684
11266
|
onConfigUpdate(callback) {
|
|
10685
11267
|
this.io.on("connection", (socket) => {
|
|
10686
11268
|
socket.on("config:update", (payload) => {
|
|
@@ -10703,7 +11285,8 @@ var DashboardSocket = class {
|
|
|
10703
11285
|
socket.on("cascade:run", (payload) => {
|
|
10704
11286
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
10705
11287
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
10706
|
-
|
|
11288
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
11289
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
10707
11290
|
}
|
|
10708
11291
|
});
|
|
10709
11292
|
});
|
|
@@ -10712,7 +11295,7 @@ var DashboardSocket = class {
|
|
|
10712
11295
|
this.io.close();
|
|
10713
11296
|
}
|
|
10714
11297
|
};
|
|
10715
|
-
var __dirname$1 =
|
|
11298
|
+
var __dirname$1 = path20.dirname(fileURLToPath(import.meta.url));
|
|
10716
11299
|
var DashboardServer = class {
|
|
10717
11300
|
app;
|
|
10718
11301
|
httpServer;
|
|
@@ -10723,6 +11306,20 @@ var DashboardServer = class {
|
|
|
10723
11306
|
globalStore = null;
|
|
10724
11307
|
broadcastTimer = null;
|
|
10725
11308
|
activeSessions = /* @__PURE__ */ new Map();
|
|
11309
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
11310
|
+
/**
|
|
11311
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
11312
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
11313
|
+
* of runs the session performed in this server's lifetime.
|
|
11314
|
+
*/
|
|
11315
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
11316
|
+
/**
|
|
11317
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
11318
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
11319
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
11320
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
11321
|
+
*/
|
|
11322
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
10726
11323
|
port;
|
|
10727
11324
|
host;
|
|
10728
11325
|
workspacePath;
|
|
@@ -10781,15 +11378,18 @@ var DashboardServer = class {
|
|
|
10781
11378
|
}
|
|
10782
11379
|
this.persistConfig();
|
|
10783
11380
|
});
|
|
10784
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
11381
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
10785
11382
|
const sessionId = requestedSessionId ?? randomUUID();
|
|
11383
|
+
const abortController = new AbortController();
|
|
11384
|
+
this.activeControllers.set(sessionId, abortController);
|
|
10786
11385
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
10787
11386
|
const title = this.persistRunStart(sessionId, prompt);
|
|
10788
|
-
|
|
11387
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
11388
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
10789
11389
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
10790
11390
|
this.activeSessions.set(sessionId, cascade);
|
|
10791
11391
|
cascade.on("stream:token", (e) => {
|
|
10792
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
11392
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
10793
11393
|
});
|
|
10794
11394
|
cascade.on("tier:status", (e) => {
|
|
10795
11395
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -10801,7 +11401,12 @@ var DashboardServer = class {
|
|
|
10801
11401
|
this.socket.emitPeerMessage(e);
|
|
10802
11402
|
});
|
|
10803
11403
|
try {
|
|
10804
|
-
const result = await cascade.run({
|
|
11404
|
+
const result = await cascade.run({
|
|
11405
|
+
prompt: runPrompt,
|
|
11406
|
+
signal: abortController.signal,
|
|
11407
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
11408
|
+
});
|
|
11409
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
10805
11410
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
10806
11411
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
10807
11412
|
this.socket.broadcast("cost:update", {
|
|
@@ -10818,6 +11423,24 @@ var DashboardServer = class {
|
|
|
10818
11423
|
});
|
|
10819
11424
|
} finally {
|
|
10820
11425
|
this.activeSessions.delete(sessionId);
|
|
11426
|
+
this.activeControllers.delete(sessionId);
|
|
11427
|
+
this.denyPendingApprovals(sessionId);
|
|
11428
|
+
}
|
|
11429
|
+
});
|
|
11430
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
11431
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
11432
|
+
this.denyPendingApprovals(sessionId);
|
|
11433
|
+
});
|
|
11434
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
11435
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
11436
|
+
if (!pending) return;
|
|
11437
|
+
this.pendingApprovals.delete(requestId);
|
|
11438
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
11439
|
+
});
|
|
11440
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
11441
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11442
|
+
if (steered > 0) {
|
|
11443
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10821
11444
|
}
|
|
10822
11445
|
});
|
|
10823
11446
|
}
|
|
@@ -10865,9 +11488,9 @@ var DashboardServer = class {
|
|
|
10865
11488
|
*/
|
|
10866
11489
|
persistConfig() {
|
|
10867
11490
|
try {
|
|
10868
|
-
const configPath =
|
|
10869
|
-
|
|
10870
|
-
|
|
11491
|
+
const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11492
|
+
fs19.mkdirSync(path20.dirname(configPath), { recursive: true });
|
|
11493
|
+
fs19.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10871
11494
|
} catch (err) {
|
|
10872
11495
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
10873
11496
|
}
|
|
@@ -10882,15 +11505,15 @@ var DashboardServer = class {
|
|
|
10882
11505
|
resolveDashboardSecret() {
|
|
10883
11506
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
10884
11507
|
if (fromConfig) return fromConfig;
|
|
10885
|
-
const secretPath =
|
|
11508
|
+
const secretPath = path20.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
10886
11509
|
try {
|
|
10887
|
-
if (
|
|
10888
|
-
const existing =
|
|
11510
|
+
if (fs19.existsSync(secretPath)) {
|
|
11511
|
+
const existing = fs19.readFileSync(secretPath, "utf-8").trim();
|
|
10889
11512
|
if (existing.length >= 16) return existing;
|
|
10890
11513
|
}
|
|
10891
11514
|
const generated = randomUUID();
|
|
10892
|
-
|
|
10893
|
-
|
|
11515
|
+
fs19.mkdirSync(path20.dirname(secretPath), { recursive: true });
|
|
11516
|
+
fs19.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
10894
11517
|
if (this.config.dashboard.auth) {
|
|
10895
11518
|
console.warn(
|
|
10896
11519
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -10917,7 +11540,7 @@ var DashboardServer = class {
|
|
|
10917
11540
|
// ── Setup ─────────────────────────────────────
|
|
10918
11541
|
getGlobalStore() {
|
|
10919
11542
|
if (!this.globalStore) {
|
|
10920
|
-
const globalDbPath =
|
|
11543
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
10921
11544
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
10922
11545
|
}
|
|
10923
11546
|
return this.globalStore;
|
|
@@ -10965,6 +11588,44 @@ var DashboardServer = class {
|
|
|
10965
11588
|
}
|
|
10966
11589
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
10967
11590
|
}
|
|
11591
|
+
/**
|
|
11592
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
11593
|
+
* session, or every active session when none is specified (the desktop
|
|
11594
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
11595
|
+
*/
|
|
11596
|
+
steerSessions(message, sessionId, nodeId) {
|
|
11597
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
11598
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
11599
|
+
return targets.length;
|
|
11600
|
+
}
|
|
11601
|
+
recordSessionTask(sessionId, taskId) {
|
|
11602
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11603
|
+
list.push(taskId);
|
|
11604
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
11605
|
+
}
|
|
11606
|
+
/**
|
|
11607
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
11608
|
+
* user. The request itself was already pushed to the client via the
|
|
11609
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
11610
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
11611
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
11612
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
11613
|
+
* timeout denies it.
|
|
11614
|
+
*/
|
|
11615
|
+
makeApprovalCallback(sessionId) {
|
|
11616
|
+
return (request) => new Promise((resolve) => {
|
|
11617
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
11620
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
11621
|
+
denyPendingApprovals(sessionId) {
|
|
11622
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
11623
|
+
if (pending.sessionId === sessionId) {
|
|
11624
|
+
this.pendingApprovals.delete(id);
|
|
11625
|
+
pending.resolve({ approved: false, always: false });
|
|
11626
|
+
}
|
|
11627
|
+
}
|
|
11628
|
+
}
|
|
10968
11629
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
10969
11630
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10970
11631
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -11058,12 +11719,12 @@ ${prompt}`;
|
|
|
11058
11719
|
}
|
|
11059
11720
|
}
|
|
11060
11721
|
watchRuntimeChanges() {
|
|
11061
|
-
const workspaceDbPath =
|
|
11062
|
-
const globalDbPath =
|
|
11722
|
+
const workspaceDbPath = path20.join(this.workspacePath, CASCADE_DB_FILE);
|
|
11723
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11063
11724
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
11064
11725
|
for (const watchPath of watchPaths) {
|
|
11065
|
-
if (!
|
|
11066
|
-
|
|
11726
|
+
if (!fs19.existsSync(watchPath)) continue;
|
|
11727
|
+
fs19.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
11067
11728
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
11068
11729
|
});
|
|
11069
11730
|
}
|
|
@@ -11153,15 +11814,6 @@ ${prompt}`;
|
|
|
11153
11814
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
11154
11815
|
res.json({ success: true, ...payload });
|
|
11155
11816
|
});
|
|
11156
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
11157
|
-
const body = req.body;
|
|
11158
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
11159
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
11160
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11161
|
-
this.socket.broadcast("session:approve", payload);
|
|
11162
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
11163
|
-
res.json({ success: true, ...payload });
|
|
11164
|
-
});
|
|
11165
11817
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
11166
11818
|
const body = req.body;
|
|
11167
11819
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -11171,7 +11823,8 @@ ${prompt}`;
|
|
|
11171
11823
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
11172
11824
|
return;
|
|
11173
11825
|
}
|
|
11174
|
-
const
|
|
11826
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11827
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11175
11828
|
this.socket.broadcast("session:message-injected", payload);
|
|
11176
11829
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
11177
11830
|
res.json({ success: true, ...payload });
|
|
@@ -11193,7 +11846,7 @@ ${prompt}`;
|
|
|
11193
11846
|
const sessionId = req.params.id;
|
|
11194
11847
|
this.store.deleteSession(sessionId);
|
|
11195
11848
|
this.store.deleteRuntimeSession(sessionId);
|
|
11196
|
-
const globalDbPath =
|
|
11849
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11197
11850
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11198
11851
|
try {
|
|
11199
11852
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -11205,9 +11858,34 @@ ${prompt}`;
|
|
|
11205
11858
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
11206
11859
|
res.json({ ok: true });
|
|
11207
11860
|
});
|
|
11861
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
11862
|
+
const sessionId = req.params.id;
|
|
11863
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11864
|
+
if (!taskIds.length) {
|
|
11865
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
11866
|
+
return;
|
|
11867
|
+
}
|
|
11868
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
11869
|
+
for (const taskId of taskIds) {
|
|
11870
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
11871
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
11872
|
+
}
|
|
11873
|
+
}
|
|
11874
|
+
const { writeFile } = await import('fs/promises');
|
|
11875
|
+
let restored = 0;
|
|
11876
|
+
for (const [filePath, content] of toRestore) {
|
|
11877
|
+
try {
|
|
11878
|
+
await writeFile(filePath, content, "utf-8");
|
|
11879
|
+
restored++;
|
|
11880
|
+
} catch (err) {
|
|
11881
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
res.json({ ok: true, restored });
|
|
11885
|
+
});
|
|
11208
11886
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
11209
11887
|
const body = req.body;
|
|
11210
|
-
const globalDbPath =
|
|
11888
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11211
11889
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
11212
11890
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11213
11891
|
try {
|
|
@@ -11230,7 +11908,7 @@ ${prompt}`;
|
|
|
11230
11908
|
});
|
|
11231
11909
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
11232
11910
|
this.store.deleteAllRuntimeNodes();
|
|
11233
|
-
const globalDbPath =
|
|
11911
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11234
11912
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11235
11913
|
try {
|
|
11236
11914
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -11281,6 +11959,19 @@ ${prompt}`;
|
|
|
11281
11959
|
this.store.deleteIdentity(req.params.id);
|
|
11282
11960
|
res.json({ ok: true });
|
|
11283
11961
|
});
|
|
11962
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
11963
|
+
try {
|
|
11964
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
11965
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
11966
|
+
try {
|
|
11967
|
+
res.json(logger.verifyChain());
|
|
11968
|
+
} finally {
|
|
11969
|
+
logger.close();
|
|
11970
|
+
}
|
|
11971
|
+
} catch (err) {
|
|
11972
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
11973
|
+
}
|
|
11974
|
+
});
|
|
11284
11975
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
11285
11976
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
11286
11977
|
res.json(log);
|
|
@@ -11303,12 +11994,12 @@ ${prompt}`;
|
|
|
11303
11994
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
11304
11995
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
11305
11996
|
try {
|
|
11306
|
-
const configPath =
|
|
11307
|
-
const existing =
|
|
11997
|
+
const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11998
|
+
const existing = fs19.existsSync(configPath) ? JSON.parse(fs19.readFileSync(configPath, "utf-8")) : {};
|
|
11308
11999
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
11309
12000
|
const tmp = configPath + ".tmp";
|
|
11310
|
-
|
|
11311
|
-
|
|
12001
|
+
fs19.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
12002
|
+
fs19.renameSync(tmp, configPath);
|
|
11312
12003
|
res.json({ ok: true });
|
|
11313
12004
|
} catch (err) {
|
|
11314
12005
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -11336,7 +12027,7 @@ ${prompt}`;
|
|
|
11336
12027
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
11337
12028
|
const scope = req.query["scope"] ?? "workspace";
|
|
11338
12029
|
if (scope === "global") {
|
|
11339
|
-
const globalDbPath =
|
|
12030
|
+
const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11340
12031
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11341
12032
|
try {
|
|
11342
12033
|
res.json({
|
|
@@ -11373,7 +12064,7 @@ ${prompt}`;
|
|
|
11373
12064
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
11374
12065
|
this.activeSessions.set(sessionId, cascade);
|
|
11375
12066
|
cascade.on("stream:token", (e) => {
|
|
11376
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
12067
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
11377
12068
|
});
|
|
11378
12069
|
cascade.on("tier:status", (e) => {
|
|
11379
12070
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -11385,7 +12076,12 @@ ${prompt}`;
|
|
|
11385
12076
|
this.socket.emitPeerMessage(e);
|
|
11386
12077
|
});
|
|
11387
12078
|
try {
|
|
11388
|
-
const result = await cascade.run({
|
|
12079
|
+
const result = await cascade.run({
|
|
12080
|
+
prompt: runPrompt,
|
|
12081
|
+
identityId: body.identityId,
|
|
12082
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12083
|
+
});
|
|
12084
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
11389
12085
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
11390
12086
|
this.socket.broadcast("cost:update", {
|
|
11391
12087
|
sessionId,
|
|
@@ -11402,6 +12098,7 @@ ${prompt}`;
|
|
|
11402
12098
|
});
|
|
11403
12099
|
} finally {
|
|
11404
12100
|
this.activeSessions.delete(sessionId);
|
|
12101
|
+
this.denyPendingApprovals(sessionId);
|
|
11405
12102
|
}
|
|
11406
12103
|
})();
|
|
11407
12104
|
});
|
|
@@ -11416,13 +12113,13 @@ ${prompt}`;
|
|
|
11416
12113
|
}))
|
|
11417
12114
|
});
|
|
11418
12115
|
});
|
|
11419
|
-
const prodPath =
|
|
11420
|
-
const devPath =
|
|
11421
|
-
const webDistPath =
|
|
11422
|
-
if (
|
|
12116
|
+
const prodPath = path20.resolve(__dirname$1, "../web/dist");
|
|
12117
|
+
const devPath = path20.resolve(__dirname$1, "../../web/dist");
|
|
12118
|
+
const webDistPath = fs19.existsSync(prodPath) ? prodPath : devPath;
|
|
12119
|
+
if (fs19.existsSync(webDistPath)) {
|
|
11423
12120
|
this.app.use(express.static(webDistPath));
|
|
11424
12121
|
this.app.get("*", (_req, res) => {
|
|
11425
|
-
res.sendFile(
|
|
12122
|
+
res.sendFile(path20.join(webDistPath, "index.html"));
|
|
11426
12123
|
});
|
|
11427
12124
|
} else {
|
|
11428
12125
|
this.app.get("/", (_req, res) => {
|