cascade-ai 0.12.23 → 0.13.1

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/dist/index.cjs CHANGED
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var Database = require('better-sqlite3');
4
+ var path20 = require('path');
5
+ var fs19 = require('fs');
6
+ var os6 = require('os');
7
+ var crypto3 = require('crypto');
3
8
  var EventEmitter = require('events');
4
- var crypto = require('crypto');
5
9
  var glob = require('glob');
6
10
  var Anthropic = require('@anthropic-ai/sdk');
7
11
  var OpenAI = require('openai');
@@ -12,12 +16,9 @@ var https = require('https');
12
16
  var zlib = require('zlib');
13
17
  var stream = require('stream');
14
18
  var fs4 = require('fs/promises');
15
- var path18 = require('path');
16
- var os4 = require('os');
17
19
  var ignoreFactory = require('ignore');
18
20
  var child_process = require('child_process');
19
21
  var util = require('util');
20
- var fs17 = require('fs');
21
22
  var simpleGit = require('simple-git');
22
23
  var PDFDocument = require('pdfkit');
23
24
  var dns2 = require('dns/promises');
@@ -26,7 +27,6 @@ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
26
27
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
27
28
  var zod = require('zod');
28
29
  var worker_threads = require('worker_threads');
29
- var Database = require('better-sqlite3');
30
30
  var url = require('url');
31
31
  var express = require('express');
32
32
  var rateLimit = require('express-rate-limit');
@@ -57,8 +57,12 @@ function _interopNamespace(e) {
57
57
  return Object.freeze(n);
58
58
  }
59
59
 
60
+ var Database__default = /*#__PURE__*/_interopDefault(Database);
61
+ var path20__default = /*#__PURE__*/_interopDefault(path20);
62
+ var fs19__default = /*#__PURE__*/_interopDefault(fs19);
63
+ var os6__default = /*#__PURE__*/_interopDefault(os6);
64
+ var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
60
65
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
61
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
62
66
  var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
63
67
  var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
64
68
  var dns__default = /*#__PURE__*/_interopDefault(dns);
@@ -66,14 +70,10 @@ var http__default = /*#__PURE__*/_interopDefault(http);
66
70
  var https__default = /*#__PURE__*/_interopDefault(https);
67
71
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
68
72
  var fs4__default = /*#__PURE__*/_interopDefault(fs4);
69
- var path18__default = /*#__PURE__*/_interopDefault(path18);
70
- var os4__default = /*#__PURE__*/_interopDefault(os4);
71
73
  var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
72
- var fs17__default = /*#__PURE__*/_interopDefault(fs17);
73
74
  var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
74
75
  var dns2__default = /*#__PURE__*/_interopDefault(dns2);
75
76
  var net__default = /*#__PURE__*/_interopDefault(net);
76
- var Database__default = /*#__PURE__*/_interopDefault(Database);
77
77
  var express__default = /*#__PURE__*/_interopDefault(express);
78
78
  var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
79
79
  var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
@@ -82,10 +82,161 @@ var jwt__default = /*#__PURE__*/_interopDefault(jwt);
82
82
  var cron__default = /*#__PURE__*/_interopDefault(cron);
83
83
 
84
84
  // Cascade AI — Multi-tier AI Orchestration System
85
+ var __defProp = Object.defineProperty;
86
+ var __getOwnPropNames = Object.getOwnPropertyNames;
87
+ var __esm = (fn, res) => function __init() {
88
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
89
+ };
90
+ var __export = (target, all) => {
91
+ for (var name in all)
92
+ __defProp(target, name, { get: all[name], enumerable: true });
93
+ };
85
94
 
95
+ // src/core/audit/audit-logger.ts
96
+ var audit_logger_exports = {};
97
+ __export(audit_logger_exports, {
98
+ AuditLogger: () => AuditLogger2
99
+ });
100
+ var AuditLogger2;
101
+ var init_audit_logger = __esm({
102
+ "src/core/audit/audit-logger.ts"() {
103
+ AuditLogger2 = class {
104
+ constructor(workspacePath, debugMode = false) {
105
+ this.workspacePath = workspacePath;
106
+ this.debugMode = debugMode;
107
+ const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
108
+ if (!fs19__default.default.existsSync(cascadeDir)) {
109
+ fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
110
+ }
111
+ this.keyPath = path20__default.default.join(cascadeDir, "audit_log.key");
112
+ this.dbPath = path20__default.default.join(cascadeDir, "audit_log.db");
113
+ this.initEncryptionKey();
114
+ this.db = new Database__default.default(this.dbPath);
115
+ this.db.pragma("journal_mode = WAL");
116
+ this.db.exec(`
117
+ CREATE TABLE IF NOT EXISTS audit_logs (
118
+ id TEXT PRIMARY KEY,
119
+ timestamp TEXT NOT NULL,
120
+ event_type TEXT NOT NULL,
121
+ tier_id TEXT NOT NULL,
122
+ encrypted_payload TEXT NOT NULL,
123
+ prev_hash TEXT NOT NULL DEFAULT '',
124
+ hash TEXT NOT NULL DEFAULT ''
125
+ )
126
+ `);
127
+ const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
128
+ if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
129
+ if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
130
+ }
131
+ workspacePath;
132
+ debugMode;
133
+ db;
134
+ keyPath;
135
+ dbPath;
136
+ encryptionKey;
137
+ /** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
138
+ chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
139
+ return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
140
+ }
141
+ initEncryptionKey() {
142
+ if (fs19__default.default.existsSync(this.keyPath)) {
143
+ this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
144
+ } else {
145
+ this.encryptionKey = crypto3__default.default.randomBytes(32);
146
+ fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
147
+ }
148
+ }
149
+ encrypt(text) {
150
+ const iv = crypto3__default.default.randomBytes(16);
151
+ const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
152
+ let encrypted = cipher.update(text, "utf8", "hex");
153
+ encrypted += cipher.final("hex");
154
+ const authTag = cipher.getAuthTag().toString("hex");
155
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
156
+ }
157
+ decrypt(text) {
158
+ const parts = text.split(":");
159
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
160
+ const [ivHex, authTagHex, encryptedHex] = parts;
161
+ const iv = Buffer.from(ivHex, "hex");
162
+ const authTag = Buffer.from(authTagHex, "hex");
163
+ const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
164
+ decipher.setAuthTag(authTag);
165
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
166
+ decrypted += decipher.final("utf8");
167
+ return decrypted;
168
+ }
169
+ logEvent(eventType, tierId, payloadObj) {
170
+ const id = crypto3__default.default.randomUUID();
171
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
172
+ const payload = JSON.stringify(payloadObj);
173
+ const encryptedPayload = this.encrypt(payload);
174
+ const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
175
+ const prevHash = last?.hash ?? "";
176
+ const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
177
+ const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
178
+ stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
179
+ this.dumpDebugIfNeeded();
180
+ return id;
181
+ }
182
+ /**
183
+ * Recompute the whole hash chain in insertion (rowid) order. Any edited,
184
+ * removed, or reordered row makes every later hash mismatch. Rows written
185
+ * before the chain existed (empty hash) fail verification too — integrity
186
+ * can only be claimed for chained history.
187
+ */
188
+ verifyChain() {
189
+ const rows = this.db.prepare(
190
+ "SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
191
+ ).all();
192
+ let prevHash = "";
193
+ for (const row of rows) {
194
+ const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
195
+ if (row.prev_hash !== prevHash || row.hash !== expected) {
196
+ return { ok: false, entries: rows.length, firstBadRow: row.rowid };
197
+ }
198
+ prevHash = row.hash;
199
+ }
200
+ return { ok: true, entries: rows.length };
201
+ }
202
+ getAllLogs() {
203
+ const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
204
+ const rows = stmt.all();
205
+ return rows.map((row) => {
206
+ let payload = "";
207
+ try {
208
+ payload = this.decrypt(row.encrypted_payload);
209
+ } catch (err) {
210
+ payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
211
+ }
212
+ return {
213
+ id: row.id,
214
+ timestamp: row.timestamp,
215
+ eventType: row.event_type,
216
+ tierId: row.tier_id,
217
+ payload
218
+ };
219
+ });
220
+ }
221
+ dumpDebugIfNeeded() {
222
+ if (!this.debugMode) return;
223
+ try {
224
+ const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
225
+ const entries = this.getAllLogs();
226
+ fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
227
+ } catch (err) {
228
+ console.error("Failed to dump debug audit logs", err);
229
+ }
230
+ }
231
+ close() {
232
+ this.db.close();
233
+ }
234
+ };
235
+ }
236
+ });
86
237
 
87
238
  // src/constants.ts
88
- var CASCADE_VERSION = "0.12.23";
239
+ var CASCADE_VERSION = "0.13.1";
89
240
  var CASCADE_CONFIG_DIR = ".cascade";
90
241
  var CASCADE_MD_FILE = "CASCADE.md";
91
242
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2017,7 +2168,7 @@ function computeDelegationSavings(stats, t1Model) {
2017
2168
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
2018
2169
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2019
2170
  var FETCH_TIMEOUT_MS = 8e3;
2020
- var DEFAULT_CACHE_FILE = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2171
+ var DEFAULT_CACHE_FILE = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2021
2172
  function normalizeModelId(id) {
2022
2173
  let s = id.toLowerCase();
2023
2174
  const slash = s.lastIndexOf("/");
@@ -2132,7 +2283,7 @@ var LiveDataProvider = class {
2132
2283
  }
2133
2284
  async saveCache() {
2134
2285
  try {
2135
- await fs4__default.default.mkdir(path18__default.default.dirname(this.opts.cacheFile), { recursive: true });
2286
+ await fs4__default.default.mkdir(path20__default.default.dirname(this.opts.cacheFile), { recursive: true });
2136
2287
  const cache = {
2137
2288
  fetchedAt: this.fetchedAt,
2138
2289
  snapshot: this.snapshot ?? void 0,
@@ -2262,7 +2413,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2262
2413
  costByTier: {},
2263
2414
  tokensByTier: {},
2264
2415
  inputTokensByTier: {},
2265
- outputTokensByTier: {}
2416
+ outputTokensByTier: {},
2417
+ costByFeature: {}
2266
2418
  };
2267
2419
  tierModels = /* @__PURE__ */ new Map();
2268
2420
  config;
@@ -2284,6 +2436,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2284
2436
  tpmLimiter;
2285
2437
  localQueue;
2286
2438
  taskAnalyzer;
2439
+ worldStateDB;
2440
+ privacyPaths;
2441
+ guidanceQueue;
2287
2442
  liveData;
2288
2443
  /** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
2289
2444
  originalTierModels;
@@ -2441,7 +2596,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2441
2596
  if (options.model && !requireVision) {
2442
2597
  this.ensureProvider(options.model, this.config.providers);
2443
2598
  }
2444
- const model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
2599
+ let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
2600
+ if (options.forceLocal && model && !this.isPrivateModel(model)) {
2601
+ const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
2602
+ if (!localModel) {
2603
+ throw new Error(
2604
+ "privacy.paths: this subtask touches a local-only path but no LOCAL model is available. Configure Ollama or an OpenAI-compatible endpoint on a loopback/private host, or remove the privacy policy."
2605
+ );
2606
+ }
2607
+ this.ensureProvider(localModel, this.config.providers);
2608
+ model = localModel;
2609
+ }
2445
2610
  if (!model) throw new Error(`No model available for tier ${tier}`);
2446
2611
  const provider = this.getProvider(model);
2447
2612
  if (!provider) throw new Error(`No provider for model ${model.id}`);
@@ -2513,7 +2678,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2513
2678
  if (!result || typeof result.content !== "string" || !result.usage) {
2514
2679
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
2515
2680
  }
2516
- this.recordStats(tier, model, result.usage);
2681
+ this.recordStats(tier, model, result.usage, options.featureTag);
2517
2682
  this.failover.recordSuccess(model.provider);
2518
2683
  return result;
2519
2684
  } catch (err) {
@@ -2618,6 +2783,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2618
2783
  setTaskAnalyzer(analyzer) {
2619
2784
  this.taskAnalyzer = analyzer;
2620
2785
  }
2786
+ setWorldStateDB(db) {
2787
+ this.worldStateDB = db;
2788
+ }
2789
+ getWorldStateDB() {
2790
+ return this.worldStateDB;
2791
+ }
2792
+ setPrivacyPaths(paths) {
2793
+ this.privacyPaths = paths;
2794
+ }
2795
+ getPrivacyPaths() {
2796
+ return this.privacyPaths;
2797
+ }
2798
+ setGuidanceQueue(queue) {
2799
+ this.guidanceQueue = queue;
2800
+ }
2801
+ getGuidanceQueue() {
2802
+ return this.guidanceQueue;
2803
+ }
2804
+ /**
2805
+ * "Private" = inference never leaves the user's machine/network: Ollama
2806
+ * models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
2807
+ * LM Studio) whose configured host is loopback or a private range. A cloud
2808
+ * OpenAI-compatible endpoint (public host) does NOT qualify.
2809
+ */
2810
+ isPrivateModel(model) {
2811
+ if (model.isLocal) return true;
2812
+ if (model.provider !== "openai-compatible") return false;
2813
+ const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
2814
+ if (!baseUrl) return false;
2815
+ try {
2816
+ const host = new URL(baseUrl).hostname.toLowerCase();
2817
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith(".local");
2818
+ } catch {
2819
+ return false;
2820
+ }
2821
+ }
2621
2822
  /**
2622
2823
  * Cascade Auto per-subtask routing: pick the benchmark-best model for a
2623
2824
  * specific subtask's text, scoped to the tier's eligible candidates. Returns
@@ -2641,7 +2842,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2641
2842
  costByTier: { ...this.stats.costByTier },
2642
2843
  tokensByTier: { ...this.stats.tokensByTier },
2643
2844
  inputTokensByTier: { ...this.stats.inputTokensByTier },
2644
- outputTokensByTier: { ...this.stats.outputTokensByTier }
2845
+ outputTokensByTier: { ...this.stats.outputTokensByTier },
2846
+ costByFeature: { ...this.stats.costByFeature }
2645
2847
  };
2646
2848
  }
2647
2849
  /**
@@ -2691,7 +2893,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2691
2893
  costByTier: {},
2692
2894
  tokensByTier: {},
2693
2895
  inputTokensByTier: {},
2694
- outputTokensByTier: {}
2896
+ outputTokensByTier: {},
2897
+ costByFeature: {}
2695
2898
  };
2696
2899
  this.sessionCostUsd = 0;
2697
2900
  this.budgetState = "ok";
@@ -2857,7 +3060,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2857
3060
  }
2858
3061
  return void 0;
2859
3062
  }
2860
- recordStats(tier, model, usage) {
3063
+ recordStats(tier, model, usage, featureTag) {
2861
3064
  this.stats.totalTokens += usage.totalTokens;
2862
3065
  this.stats.totalCostUsd += usage.estimatedCostUsd;
2863
3066
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -2867,6 +3070,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2867
3070
  this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
2868
3071
  this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
2869
3072
  this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
3073
+ if (featureTag) {
3074
+ this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
3075
+ }
2870
3076
  this.runTokens += usage.totalTokens;
2871
3077
  this.runCostUsd += usage.estimatedCostUsd;
2872
3078
  this.updateBudgetState();
@@ -2966,7 +3172,7 @@ var BaseTier = class extends EventEmitter__default.default {
2966
3172
  constructor(role, id, parentId) {
2967
3173
  super();
2968
3174
  this.role = role;
2969
- this.id = id ?? `${role}_${crypto.randomUUID().slice(0, 8)}`;
3175
+ this.id = id ?? `${role}_${crypto3.randomUUID().slice(0, 8)}`;
2970
3176
  this.parentId = parentId;
2971
3177
  this.label = this.id;
2972
3178
  }
@@ -3217,7 +3423,7 @@ var AuditLogger = class {
3217
3423
  logAt(level, tierId, action, details) {
3218
3424
  if (!this.shouldLog(level)) return;
3219
3425
  const entry = {
3220
- id: crypto.randomUUID(),
3426
+ id: crypto3.randomUUID(),
3221
3427
  sessionId: this.sessionId,
3222
3428
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3223
3429
  tierId,
@@ -3403,6 +3609,8 @@ var T3Worker = class extends BaseTier {
3403
3609
  toolRegistry;
3404
3610
  context;
3405
3611
  assignment;
3612
+ /** True when this subtask matched a privacy.paths local-only pattern. */
3613
+ localOnlyMatch = false;
3406
3614
  peerSyncBuffer = [];
3407
3615
  store;
3408
3616
  audit;
@@ -3458,6 +3666,11 @@ var T3Worker = class extends BaseTier {
3458
3666
  this.taskId = taskId;
3459
3667
  this.setLabel(assignment.subtaskTitle);
3460
3668
  this.setStatus("ACTIVE");
3669
+ const privacy = this.router.getPrivacyPaths?.();
3670
+ this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
3671
+ if (this.localOnlyMatch) {
3672
+ this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
3673
+ }
3461
3674
  this.tools = this.toolRegistry.getToolDefinitions();
3462
3675
  if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
3463
3676
  this.tools = [...this.tools, {
@@ -3588,9 +3801,17 @@ Now execute your subtask using this context where relevant.`
3588
3801
  }
3589
3802
  const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
3590
3803
  if (reflectCfg.enabled) {
3591
- this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
3804
+ this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
3592
3805
  output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
3593
3806
  }
3807
+ const db = this.router.getWorldStateDB?.();
3808
+ if (db) {
3809
+ try {
3810
+ db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
3811
+ } catch (e) {
3812
+ this.log("Failed to write to World State DB");
3813
+ }
3814
+ }
3594
3815
  this.setStatus("COMPLETED", output);
3595
3816
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
3596
3817
  this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
@@ -3663,6 +3884,16 @@ Now execute your subtask using this context where relevant.`
3663
3884
  while (iterations < MAX_ITERATIONS) {
3664
3885
  iterations++;
3665
3886
  this.throwIfCancelled();
3887
+ const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
3888
+ for (const g of guidance) {
3889
+ this.log(`User intervention received: ${g.text}`);
3890
+ this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
3891
+ await this.context.addMessage({
3892
+ role: "user",
3893
+ content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
3894
+ ${g.text}`
3895
+ });
3896
+ }
3666
3897
  const options = {
3667
3898
  messages: this.context.getMessages(),
3668
3899
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -3671,7 +3902,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
3671
3902
  // Don't pass tools array when model can't use them natively
3672
3903
  tools: useTextTools ? void 0 : tools.length ? tools : void 0,
3673
3904
  maxTokens: 4096,
3674
- ...subtaskModel ? { model: subtaskModel } : {}
3905
+ ...subtaskModel ? { model: subtaskModel } : {},
3906
+ featureTag: this.assignment?.sectionTitle,
3907
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
3675
3908
  };
3676
3909
  const result = await this.router.generate(
3677
3910
  "T3",
@@ -3835,8 +4068,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
3835
4068
  tierId: this.id,
3836
4069
  sessionId: this.taskId,
3837
4070
  requireApproval: false,
3838
- saveSnapshot: async (path19, content) => {
3839
- this.store?.addFileSnapshot(this.taskId, path19, content);
4071
+ saveSnapshot: async (path21, content) => {
4072
+ this.store?.addFileSnapshot(this.taskId, path21, content);
3840
4073
  },
3841
4074
  sendPeerSync: (to, syncType, content) => {
3842
4075
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -4006,7 +4239,7 @@ ${assignment.expectedOutput}`;
4006
4239
  const { promisify: promisify4 } = await import('util');
4007
4240
  const execAsync2 = promisify4(exec2);
4008
4241
  for (const artifactPath of artifactPaths) {
4009
- const absolutePath = path18__default.default.resolve(process.cwd(), artifactPath);
4242
+ const absolutePath = path20__default.default.resolve(process.cwd(), artifactPath);
4010
4243
  try {
4011
4244
  const stat = await fs4__default.default.stat(absolutePath);
4012
4245
  if (!stat.isFile()) {
@@ -4027,7 +4260,7 @@ ${assignment.expectedOutput}`;
4027
4260
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4028
4261
  continue;
4029
4262
  }
4030
- const ext = path18__default.default.extname(absolutePath).toLowerCase();
4263
+ const ext = path20__default.default.extname(absolutePath).toLowerCase();
4031
4264
  try {
4032
4265
  if (ext === ".ts" || ext === ".tsx") {
4033
4266
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -4056,30 +4289,35 @@ ${stdout}`);
4056
4289
  * needed. Best-effort: any parse/error just keeps the current output.
4057
4290
  */
4058
4291
  async reflectAndImprove(assignment, output, maxRounds) {
4059
- const sys = this.systemPromptOverride + (this.hierarchyContext ? `
4060
-
4061
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
4062
4292
  let current = output;
4063
- for (let round = 0; round < Math.max(1, maxRounds); round++) {
4064
- try {
4065
- const verdict = await this.router.generate("T3", {
4293
+ try {
4294
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
4295
+ const verdictResult = await this.router.generate("T2", {
4066
4296
  messages: [{
4067
4297
  role: "user",
4068
- content: `Does this output FULLY achieve the goal \u2014 not just the literal task, but the intent behind it?
4298
+ content: `You are an independent critic reviewing another worker's output against its assignment.
4069
4299
 
4070
- Goal / expected: ${assignment.expectedOutput}
4300
+ Goal: ${assignment.expectedOutput}
4071
4301
  Subtask: ${assignment.description}
4072
-
4073
- Output:
4302
+ Current Output:
4074
4303
  ${current}
4075
4304
 
4076
- Reply with ONLY JSON: {"sufficient": true|false, "notes": "what is weak or missing if not sufficient"}`
4305
+ Is this output sufficient and correct? Respond with ONLY a JSON object:
4306
+ {"sufficient": true|false, "notes": "what is wrong or missing if false"}`
4077
4307
  }],
4078
- systemPrompt: sys,
4079
- maxTokens: 400
4308
+ systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
4309
+ maxTokens: 400,
4310
+ signal: this.signal,
4311
+ featureTag: assignment.sectionTitle,
4312
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
4080
4313
  });
4081
- const parsed = JSON.parse(/\{[\s\S]*\}/.exec(verdict.content)?.[0] ?? "{}");
4082
- if (parsed.sufficient !== false) break;
4314
+ const match = /\{[\s\S]*\}/.exec(verdictResult.content);
4315
+ const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
4316
+ if (parsed.sufficient !== false) {
4317
+ this.log("T2-Critic approved output.");
4318
+ break;
4319
+ }
4320
+ this.log(`T2-Critic rejected output: ${parsed.notes}`);
4083
4321
  const improved = await this.router.generate("T3", {
4084
4322
  messages: [{
4085
4323
  role: "user",
@@ -4091,16 +4329,20 @@ Goal / expected: ${assignment.expectedOutput}
4091
4329
  Current output:
4092
4330
  ${current}`
4093
4331
  }],
4094
- systemPrompt: sys,
4095
- maxTokens: 4096
4332
+ systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
4333
+
4334
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4335
+ maxTokens: 4096,
4336
+ featureTag: assignment.sectionTitle,
4337
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
4096
4338
  });
4097
4339
  const next = (improved.content ?? "").trim();
4098
4340
  if (!next) break;
4099
4341
  current = next;
4100
4342
  this.log("Reflection: revised output for better goal alignment.");
4101
- } catch {
4102
- break;
4103
4343
  }
4344
+ } catch (e) {
4345
+ this.log(`T2-Critic reflection failed: ${e}`);
4104
4346
  }
4105
4347
  return current;
4106
4348
  }
@@ -4121,7 +4363,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
4121
4363
  maxTokens: 500,
4122
4364
  systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
4123
4365
 
4124
- HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
4366
+ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4367
+ featureTag: assignment.sectionTitle,
4368
+ ...this.localOnlyMatch ? { forceLocal: true } : {}
4125
4369
  });
4126
4370
  try {
4127
4371
  const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
@@ -4216,6 +4460,7 @@ Begin execution now.`;
4216
4460
  issues,
4217
4461
  peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
4218
4462
  correctionAttempts,
4463
+ localOnly: this.localOnlyMatch || void 0,
4219
4464
  reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
4220
4465
  };
4221
4466
  }
@@ -4507,6 +4752,42 @@ var PeerBus = class extends EventEmitter__default.default {
4507
4752
  }
4508
4753
  };
4509
4754
 
4755
+ // src/core/audit/redaction.ts
4756
+ var RedactionLayer = class {
4757
+ // Regexes for common secrets/PII
4758
+ static RULES = [
4759
+ // IPv4 addresses (basic approximation)
4760
+ { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
4761
+ // Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
4762
+ { 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]" },
4763
+ // Email addresses
4764
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
4765
+ // Phone numbers (simplistic)
4766
+ { pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
4767
+ // AWS Access Key ID
4768
+ { pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
4769
+ ];
4770
+ /**
4771
+ * Applies all redaction rules to the input string.
4772
+ */
4773
+ static redact(text) {
4774
+ if (!text) return text;
4775
+ let redacted = text;
4776
+ for (const rule of this.RULES) {
4777
+ if (rule.pattern.test(redacted)) {
4778
+ rule.pattern.lastIndex = 0;
4779
+ redacted = redacted.replace(rule.pattern, (match, p1, offset, str) => {
4780
+ if (p1 && match.includes(p1) && p1 !== match) {
4781
+ return match.replace(p1, rule.replacement.replace("$1", ""));
4782
+ }
4783
+ return rule.replacement;
4784
+ });
4785
+ }
4786
+ }
4787
+ return redacted;
4788
+ }
4789
+ };
4790
+
4510
4791
  // src/core/tiers/t2-manager.ts
4511
4792
  var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
4512
4793
  Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
@@ -4735,16 +5016,19 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4735
5016
  try {
4736
5017
  const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
4737
5018
  if (!jsonMatch) throw new Error("No JSON array found");
4738
- return JSON.parse(jsonMatch[0]);
5019
+ const parsed = JSON.parse(jsonMatch[0]);
5020
+ return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
4739
5021
  } catch {
4740
5022
  return [{
4741
- subtaskId: crypto.randomUUID(),
5023
+ subtaskId: crypto3.randomUUID(),
4742
5024
  subtaskTitle: assignment.sectionTitle,
4743
5025
  description: assignment.description,
4744
5026
  expectedOutput: assignment.expectedOutput,
4745
5027
  constraints: assignment.constraints,
4746
5028
  peerT3Ids: [],
4747
5029
  parentT2: this.id,
5030
+ sectionTitle: assignment.sectionTitle,
5031
+ dependsOn: [],
4748
5032
  executionMode: "parallel"
4749
5033
  }];
4750
5034
  }
@@ -4864,6 +5148,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4864
5148
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4865
5149
  const worker = workerMap.get(id);
4866
5150
  const result = await worker.execute(assignment, taskId, waveSignal);
5151
+ if (result.localOnly) {
5152
+ 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}]`;
5153
+ } else {
5154
+ if (typeof result.output === "string" && result.output) {
5155
+ result.output = RedactionLayer.redact(result.output);
5156
+ }
5157
+ }
5158
+ if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
4867
5159
  resultMap.set(id, result);
4868
5160
  return result;
4869
5161
  };
@@ -5302,7 +5594,7 @@ var T1Administrator = class extends BaseTier {
5302
5594
  }
5303
5595
  async execute(userPrompt, images, systemContext, signal) {
5304
5596
  this.signal = signal;
5305
- this.taskId = crypto.randomUUID();
5597
+ this.taskId = crypto3.randomUUID();
5306
5598
  this.setLabel("Administrator");
5307
5599
  this.setStatus("ACTIVE");
5308
5600
  this.taskGoal = userPrompt;
@@ -5493,10 +5785,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
5493
5785
  }
5494
5786
  }
5495
5787
  async decomposeTask(prompt, systemContext) {
5788
+ const db = this.router.getWorldStateDB?.();
5789
+ const worldStateContext = db ? `
5790
+
5791
+ PROJECT WORLD STATE (Current architecture and recent changes):
5792
+ ${db.getFormattedState()}` : "";
5496
5793
  const contextSection = systemContext ? `
5497
5794
  Project context:
5498
5795
  ${systemContext}` : "";
5499
- const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
5796
+ const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
5500
5797
 
5501
5798
  Task: ${prompt}
5502
5799
 
@@ -5978,16 +6275,16 @@ function resolveInWorkspace(workspaceRoot, input) {
5978
6275
  if (typeof input !== "string" || input.length === 0) {
5979
6276
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
5980
6277
  }
5981
- const root = path18__default.default.resolve(workspaceRoot);
5982
- const abs = path18__default.default.isAbsolute(input) ? path18__default.default.resolve(input) : path18__default.default.resolve(root, input);
5983
- const rel = path18__default.default.relative(root, abs);
5984
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path18__default.default.isAbsolute(rel)) {
6278
+ const root = path20__default.default.resolve(workspaceRoot);
6279
+ const abs = path20__default.default.isAbsolute(input) ? path20__default.default.resolve(input) : path20__default.default.resolve(root, input);
6280
+ const rel = path20__default.default.relative(root, abs);
6281
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20__default.default.isAbsolute(rel)) {
5985
6282
  throw new WorkspaceSandboxError(input, root);
5986
6283
  }
5987
6284
  try {
5988
- const real = fs17__default.default.realpathSync(abs);
5989
- const realRel = path18__default.default.relative(root, real);
5990
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path18__default.default.isAbsolute(realRel))) {
6285
+ const real = fs19__default.default.realpathSync(abs);
6286
+ const realRel = path20__default.default.relative(root, real);
6287
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20__default.default.isAbsolute(realRel))) {
5991
6288
  throw new WorkspaceSandboxError(input, root);
5992
6289
  }
5993
6290
  } catch (e) {
@@ -6048,7 +6345,7 @@ var FileWriteTool = class extends BaseTool {
6048
6345
  } catch {
6049
6346
  }
6050
6347
  }
6051
- await fs4__default.default.mkdir(path18__default.default.dirname(absPath), { recursive: true });
6348
+ await fs4__default.default.mkdir(path20__default.default.dirname(absPath), { recursive: true });
6052
6349
  await fs4__default.default.writeFile(absPath, content, "utf-8");
6053
6350
  return `Written ${content.length} characters to ${filePath}`;
6054
6351
  }
@@ -6546,7 +6843,7 @@ var ImageAnalyzeTool = class extends BaseTool {
6546
6843
  };
6547
6844
  async function fileToImageAttachment(filePath) {
6548
6845
  const data = await fs4__default.default.readFile(filePath);
6549
- const ext = path18__default.default.extname(filePath).toLowerCase();
6846
+ const ext = path20__default.default.extname(filePath).toLowerCase();
6550
6847
  const mimeMap = {
6551
6848
  ".jpg": "image/jpeg",
6552
6849
  ".jpeg": "image/jpeg",
@@ -6580,14 +6877,14 @@ var PDFCreateTool = class extends BaseTool {
6580
6877
  const filePath = input["path"];
6581
6878
  const content = input["content"];
6582
6879
  const title = input["title"];
6583
- const dir = path18__default.default.dirname(filePath);
6584
- if (!fs17__default.default.existsSync(dir)) {
6585
- fs17__default.default.mkdirSync(dir, { recursive: true });
6880
+ const dir = path20__default.default.dirname(filePath);
6881
+ if (!fs19__default.default.existsSync(dir)) {
6882
+ fs19__default.default.mkdirSync(dir, { recursive: true });
6586
6883
  }
6587
6884
  return new Promise((resolve, reject) => {
6588
6885
  try {
6589
6886
  const doc = new PDFDocument__default.default({ margin: 50 });
6590
- const stream = fs17__default.default.createWriteStream(filePath);
6887
+ const stream = fs19__default.default.createWriteStream(filePath);
6591
6888
  doc.pipe(stream);
6592
6889
  if (title) {
6593
6890
  doc.info["Title"] = title;
@@ -6665,22 +6962,22 @@ var CodeInterpreterTool = class extends BaseTool {
6665
6962
  }
6666
6963
  cmdPrefix = NODE_CMD;
6667
6964
  }
6668
- const tmpDir = path18__default.default.join(this.workspaceRoot, ".cascade", "tmp");
6669
- if (!fs17__default.default.existsSync(tmpDir)) {
6670
- fs17__default.default.mkdirSync(tmpDir, { recursive: true });
6965
+ const tmpDir = path20__default.default.join(this.workspaceRoot, ".cascade", "tmp");
6966
+ if (!fs19__default.default.existsSync(tmpDir)) {
6967
+ fs19__default.default.mkdirSync(tmpDir, { recursive: true });
6671
6968
  }
6672
6969
  const extension = language === "python" ? "py" : "js";
6673
- const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
6674
- const filePath = path18__default.default.join(tmpDir, fileName);
6675
- fs17__default.default.writeFileSync(filePath, code, "utf-8");
6970
+ const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
6971
+ const filePath = path20__default.default.join(tmpDir, fileName);
6972
+ fs19__default.default.writeFileSync(filePath, code, "utf-8");
6676
6973
  const execArgs = [filePath, ...args];
6677
6974
  return new Promise((resolve) => {
6678
6975
  const startMs = Date.now();
6679
6976
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
6680
6977
  const duration = Date.now() - startMs;
6681
6978
  try {
6682
- if (fs17__default.default.existsSync(filePath)) {
6683
- fs17__default.default.unlinkSync(filePath);
6979
+ if (fs19__default.default.existsSync(filePath)) {
6980
+ fs19__default.default.unlinkSync(filePath);
6684
6981
  }
6685
6982
  } catch (cleanupErr) {
6686
6983
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -6959,7 +7256,7 @@ var GlobTool = class extends BaseTool {
6959
7256
  };
6960
7257
  async execute(input, _options) {
6961
7258
  const pattern = input["pattern"];
6962
- const searchPath = input["path"] ? path18__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7259
+ const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
6963
7260
  const matches = await glob.glob(pattern, {
6964
7261
  cwd: searchPath,
6965
7262
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -6972,7 +7269,7 @@ var GlobTool = class extends BaseTool {
6972
7269
  const withMtime = await Promise.all(
6973
7270
  matches.map(async (rel) => {
6974
7271
  try {
6975
- const stat = await fs4__default.default.stat(path18__default.default.join(searchPath, rel));
7272
+ const stat = await fs4__default.default.stat(path20__default.default.join(searchPath, rel));
6976
7273
  return { rel, mtime: stat.mtimeMs };
6977
7274
  } catch {
6978
7275
  return { rel, mtime: 0 };
@@ -7021,7 +7318,7 @@ var GrepTool = class extends BaseTool {
7021
7318
  };
7022
7319
  async execute(input, _options) {
7023
7320
  const pattern = input["pattern"];
7024
- const searchPath = input["path"] ? path18__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7321
+ const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7025
7322
  const globPattern = input["glob"];
7026
7323
  const outputMode = input["output_mode"] ?? "content";
7027
7324
  const context = input["context"] ?? 0;
@@ -7075,12 +7372,12 @@ var GrepTool = class extends BaseTool {
7075
7372
  nodir: true
7076
7373
  });
7077
7374
  } catch {
7078
- files = [path18__default.default.relative(searchPath, searchPath) || "."];
7375
+ files = [path20__default.default.relative(searchPath, searchPath) || "."];
7079
7376
  }
7080
7377
  const results = [];
7081
7378
  let totalCount = 0;
7082
7379
  for (const rel of files) {
7083
- const abs = path18__default.default.join(searchPath, rel);
7380
+ const abs = path20__default.default.join(searchPath, rel);
7084
7381
  let content;
7085
7382
  try {
7086
7383
  content = await fs4__default.default.readFile(abs, "utf-8");
@@ -7442,10 +7739,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
7442
7739
  }
7443
7740
  isIgnored(filePath) {
7444
7741
  if (!filePath) return false;
7445
- const abs = path18__default.default.resolve(this.workspaceRoot, filePath);
7446
- const rel = path18__default.default.relative(this.workspaceRoot, abs);
7447
- if (!rel || rel.startsWith("..") || path18__default.default.isAbsolute(rel)) return true;
7448
- const posixRel = rel.split(path18__default.default.sep).join("/");
7742
+ const abs = path20__default.default.resolve(this.workspaceRoot, filePath);
7743
+ const rel = path20__default.default.relative(this.workspaceRoot, abs);
7744
+ if (!rel || rel.startsWith("..") || path20__default.default.isAbsolute(rel)) return true;
7745
+ const posixRel = rel.split(path20__default.default.sep).join("/");
7449
7746
  return this.ignoreMatcher.ignores(posixRel);
7450
7747
  }
7451
7748
  };
@@ -7562,6 +7859,9 @@ var McpClient = class _McpClient {
7562
7859
  return this.clients.has(serverName);
7563
7860
  }
7564
7861
  };
7862
+
7863
+ // src/core/cascade.ts
7864
+ init_audit_logger();
7565
7865
  var SAFE_TOOLS = /* @__PURE__ */ new Set([
7566
7866
  "file_read",
7567
7867
  "file_list",
@@ -7814,7 +8114,8 @@ var WorkspaceConfigSchema = zod.z.object({
7814
8114
  cascadeMdPath: zod.z.string().default("CASCADE.md"),
7815
8115
  configPath: zod.z.string().default(".cascade/config.json"),
7816
8116
  keystorePath: zod.z.string().default(".cascade/keystore.enc"),
7817
- auditLogPath: zod.z.string().default(".cascade/audit.log")
8117
+ auditLogPath: zod.z.string().default(".cascade/audit.log"),
8118
+ debugWorldState: zod.z.boolean().default(false)
7818
8119
  });
7819
8120
  var CascadeConfigSchema = zod.z.object({
7820
8121
  version: zod.z.literal("1.0").default("1.0"),
@@ -7959,6 +8260,14 @@ var CascadeConfigSchema = zod.z.object({
7959
8260
  * 'parallel' / 'sequential' — force it.
7960
8261
  */
7961
8262
  t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
8263
+ /**
8264
+ * Per-path privacy tiers. A subtask touching a `local-only` path is forced
8265
+ * onto LOCAL models (never cloud) and its raw output is withheld from the
8266
+ * tiers above. Patterns use .gitignore syntax, like .cascadeignore.
8267
+ */
8268
+ privacy: zod.z.object({
8269
+ paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
8270
+ }).optional(),
7962
8271
  /**
7963
8272
  * T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
7964
8273
  * fan out can call the `request_workers` tool to have its T2 manager spawn
@@ -8227,9 +8536,10 @@ var TaskAnalyzer = class {
8227
8536
  analysisCache.clear();
8228
8537
  }
8229
8538
  };
8230
- var DEFAULT_STATS_FILE = path18__default.default.join(os4__default.default.homedir(), ".cascade", "model-perf.json");
8539
+ var DEFAULT_STATS_FILE = path20__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
8231
8540
  var ModelPerformanceTracker = class {
8232
8541
  stats = /* @__PURE__ */ new Map();
8542
+ featureStats = /* @__PURE__ */ new Map();
8233
8543
  statsFile;
8234
8544
  loaded = false;
8235
8545
  constructor(statsFile = DEFAULT_STATS_FILE) {
@@ -8241,18 +8551,29 @@ var ModelPerformanceTracker = class {
8241
8551
  try {
8242
8552
  const raw = await fs4__default.default.readFile(this.statsFile, "utf-8");
8243
8553
  const parsed = JSON.parse(raw);
8244
- for (const [key, stat] of Object.entries(parsed)) {
8245
- this.stats.set(key, stat);
8554
+ if (parsed.models) {
8555
+ for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
8556
+ } else {
8557
+ for (const [key, stat] of Object.entries(parsed)) {
8558
+ if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
8559
+ this.stats.set(key, stat);
8560
+ }
8561
+ }
8562
+ }
8563
+ if (parsed.features) {
8564
+ for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
8246
8565
  }
8247
8566
  } catch {
8248
8567
  }
8249
8568
  }
8250
8569
  async save() {
8251
8570
  try {
8252
- await fs4__default.default.mkdir(path18__default.default.dirname(this.statsFile), { recursive: true });
8253
- const obj = {};
8254
- for (const [key, stat] of this.stats) obj[key] = stat;
8255
- await fs4__default.default.writeFile(this.statsFile, JSON.stringify(obj, null, 2), "utf-8");
8571
+ await fs4__default.default.mkdir(path20__default.default.dirname(this.statsFile), { recursive: true });
8572
+ const modelsObj = {};
8573
+ const featuresObj = {};
8574
+ for (const [key, stat] of this.stats) modelsObj[key] = stat;
8575
+ for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
8576
+ await fs4__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
8256
8577
  } catch {
8257
8578
  }
8258
8579
  }
@@ -8273,6 +8594,13 @@ var ModelPerformanceTracker = class {
8273
8594
  sampleCount: s.sampleCount + 1
8274
8595
  });
8275
8596
  }
8597
+ recordFeatureCost(featureTag, costUsd) {
8598
+ const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
8599
+ this.featureStats.set(featureTag, {
8600
+ totalCostUsd: s.totalCostUsd + costUsd,
8601
+ runCount: s.runCount + 1
8602
+ });
8603
+ }
8276
8604
  /**
8277
8605
  * Record an explicit user rating (good/bad). Counts as 3 automatic samples
8278
8606
  * so user feedback carries significantly more weight than auto-detected outcomes.
@@ -8287,6 +8615,9 @@ var ModelPerformanceTracker = class {
8287
8615
  getAll() {
8288
8616
  return new Map(this.stats);
8289
8617
  }
8618
+ getAllFeatures() {
8619
+ return new Map(this.featureStats);
8620
+ }
8290
8621
  /**
8291
8622
  * Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
8292
8623
  * High retry counts penalise the score.
@@ -8651,7 +8982,7 @@ Required capability: ${description.slice(0, 300)}`;
8651
8982
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
8652
8983
  async loadPersistedTools() {
8653
8984
  if (!this.workspacePath || !this.persistEnabled) return;
8654
- const file = path18__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
8985
+ const file = path20__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
8655
8986
  try {
8656
8987
  const raw = await fs4__default.default.readFile(file, "utf-8");
8657
8988
  const specs = JSON.parse(raw);
@@ -8675,8 +9006,8 @@ Required capability: ${description.slice(0, 300)}`;
8675
9006
  }
8676
9007
  async persist() {
8677
9008
  if (!this.workspacePath || !this.persistEnabled) return;
8678
- const dir = path18__default.default.join(this.workspacePath, ".cascade");
8679
- const file = path18__default.default.join(dir, DYNAMIC_TOOLS_FILE);
9009
+ const dir = path20__default.default.join(this.workspacePath, ".cascade");
9010
+ const file = path20__default.default.join(dir, DYNAMIC_TOOLS_FILE);
8680
9011
  try {
8681
9012
  await fs4__default.default.mkdir(dir, { recursive: true });
8682
9013
  await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -8689,6 +9020,166 @@ Required capability: ${description.slice(0, 300)}`;
8689
9020
  return Array.from(this.specs.keys());
8690
9021
  }
8691
9022
  };
9023
+ var WorldStateDB = class {
9024
+ constructor(workspacePath, debugMode = false) {
9025
+ this.workspacePath = workspacePath;
9026
+ this.debugMode = debugMode;
9027
+ const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
9028
+ if (!fs19__default.default.existsSync(cascadeDir)) {
9029
+ fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
9030
+ }
9031
+ this.keyPath = path20__default.default.join(cascadeDir, "world_state.key");
9032
+ this.dbPath = path20__default.default.join(cascadeDir, "world_state.db");
9033
+ this.initEncryptionKey();
9034
+ this.db = new Database__default.default(this.dbPath);
9035
+ this.db.pragma("journal_mode = WAL");
9036
+ this.db.exec(`
9037
+ CREATE TABLE IF NOT EXISTS world_state (
9038
+ id TEXT PRIMARY KEY,
9039
+ timestamp TEXT NOT NULL,
9040
+ worker_id TEXT NOT NULL,
9041
+ encrypted_payload TEXT NOT NULL
9042
+ )
9043
+ `);
9044
+ }
9045
+ workspacePath;
9046
+ debugMode;
9047
+ db;
9048
+ keyPath;
9049
+ dbPath;
9050
+ encryptionKey;
9051
+ initEncryptionKey() {
9052
+ if (fs19__default.default.existsSync(this.keyPath)) {
9053
+ this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
9054
+ } else {
9055
+ this.encryptionKey = crypto3__default.default.randomBytes(32);
9056
+ fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9057
+ }
9058
+ }
9059
+ encrypt(text) {
9060
+ const iv = crypto3__default.default.randomBytes(16);
9061
+ const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
9062
+ let encrypted = cipher.update(text, "utf8", "hex");
9063
+ encrypted += cipher.final("hex");
9064
+ const authTag = cipher.getAuthTag().toString("hex");
9065
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
9066
+ }
9067
+ decrypt(text) {
9068
+ const parts = text.split(":");
9069
+ if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
9070
+ const [ivHex, authTagHex, encryptedHex] = parts;
9071
+ const iv = Buffer.from(ivHex, "hex");
9072
+ const authTag = Buffer.from(authTagHex, "hex");
9073
+ const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
9074
+ decipher.setAuthTag(authTag);
9075
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
9076
+ decrypted += decipher.final("utf8");
9077
+ return decrypted;
9078
+ }
9079
+ addEntry(workerId, summary) {
9080
+ const id = crypto3__default.default.randomUUID();
9081
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9082
+ const payload = JSON.stringify({ summary });
9083
+ const encryptedPayload = this.encrypt(payload);
9084
+ const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
9085
+ stmt.run(id, timestamp, workerId, encryptedPayload);
9086
+ this.dumpDebugIfNeeded();
9087
+ return id;
9088
+ }
9089
+ getAllEntries() {
9090
+ const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
9091
+ const rows = stmt.all();
9092
+ return rows.map((row) => {
9093
+ try {
9094
+ const decrypted = this.decrypt(row.encrypted_payload);
9095
+ const parsed = JSON.parse(decrypted);
9096
+ return {
9097
+ id: row.id,
9098
+ timestamp: row.timestamp,
9099
+ workerId: row.worker_id,
9100
+ summary: parsed.summary
9101
+ };
9102
+ } catch (err) {
9103
+ return {
9104
+ id: row.id,
9105
+ timestamp: row.timestamp,
9106
+ workerId: row.worker_id,
9107
+ summary: "[Decryption Failed - Payload Corrupted]"
9108
+ };
9109
+ }
9110
+ });
9111
+ }
9112
+ getFormattedState() {
9113
+ const entries = this.getAllEntries();
9114
+ if (entries.length === 0) return "World State is currently empty.";
9115
+ return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
9116
+ }
9117
+ dumpDebugIfNeeded() {
9118
+ if (!this.debugMode) return;
9119
+ try {
9120
+ const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
9121
+ const entries = this.getAllEntries();
9122
+ fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9123
+ } catch (err) {
9124
+ console.error("Failed to dump debug world state", err);
9125
+ }
9126
+ }
9127
+ close() {
9128
+ this.db.close();
9129
+ }
9130
+ };
9131
+ var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
9132
+ var PrivacyPaths = class {
9133
+ localOnly;
9134
+ hasRules;
9135
+ constructor(policies = []) {
9136
+ this.localOnly = ignore2();
9137
+ const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
9138
+ if (patterns.length) this.localOnly.add(patterns);
9139
+ this.hasRules = patterns.length > 0;
9140
+ }
9141
+ /** True when any privacy rules are configured at all (cheap short-circuit). */
9142
+ hasPolicies() {
9143
+ return this.hasRules;
9144
+ }
9145
+ /** True when the given workspace-relative path falls under a local-only policy. */
9146
+ isLocalOnly(relativePath) {
9147
+ if (!this.hasRules || !relativePath) return false;
9148
+ try {
9149
+ return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
9150
+ } catch {
9151
+ return false;
9152
+ }
9153
+ }
9154
+ /** True when ANY of the given paths falls under a local-only policy. */
9155
+ anyLocalOnly(relativePaths) {
9156
+ return relativePaths.some((p) => this.isLocalOnly(p));
9157
+ }
9158
+ };
9159
+
9160
+ // src/core/steering/guidance.ts
9161
+ var GuidanceQueue = class {
9162
+ entries = [];
9163
+ /** Per-consumer read cursor so a broadcast entry reaches each worker once. */
9164
+ cursors = /* @__PURE__ */ new Map();
9165
+ push(text, nodeId) {
9166
+ const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
9167
+ this.entries.push(entry);
9168
+ return entry;
9169
+ }
9170
+ /**
9171
+ * New entries for this consumer since its last drain, filtered to entries
9172
+ * that target it (or target everyone). Advances the consumer's cursor.
9173
+ */
9174
+ drain(consumerId) {
9175
+ const from = this.cursors.get(consumerId) ?? 0;
9176
+ this.cursors.set(consumerId, this.entries.length);
9177
+ return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
9178
+ }
9179
+ get size() {
9180
+ return this.entries.length;
9181
+ }
9182
+ };
8692
9183
 
8693
9184
  // src/core/cascade.ts
8694
9185
  var Cascade = class _Cascade extends EventEmitter__default.default {
@@ -8708,6 +9199,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
8708
9199
  taskAnalyzer;
8709
9200
  perfTracker;
8710
9201
  toolCreator;
9202
+ worldStateDB;
9203
+ encryptedAuditLogger;
9204
+ guidanceQueue;
8711
9205
  workspacePath;
8712
9206
  constructor(config, workspacePath, store) {
8713
9207
  super();
@@ -8729,6 +9223,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
8729
9223
  });
8730
9224
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
8731
9225
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
9226
+ this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
9227
+ this.router.setWorldStateDB(this.worldStateDB);
9228
+ this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
9229
+ const privacyPolicies = this.config.privacy?.paths ?? [];
9230
+ if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
9231
+ this.guidanceQueue = new GuidanceQueue();
9232
+ this.router.setGuidanceQueue(this.guidanceQueue);
9233
+ }
9234
+ /**
9235
+ * Live intervention: inject a user correction into the running hierarchy.
9236
+ * Every active T3 worker (or only the one matching `nodeId`) picks it up at
9237
+ * the top of its next agent-loop iteration as a USER INTERVENTION message.
9238
+ */
9239
+ injectGuidance(text, nodeId) {
9240
+ const entry = this.guidanceQueue.push(text, nodeId);
9241
+ this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
9242
+ this.emit("guidance:injected", entry);
8732
9243
  }
8733
9244
  initOptionalFeatures() {
8734
9245
  if (this.config.cascadeAuto === true) {
@@ -8891,6 +9402,12 @@ ${last.partialOutput}` : "");
8891
9402
  if (!prompt) return null;
8892
9403
  return this.run({ prompt });
8893
9404
  }
9405
+ getWorkspacePath() {
9406
+ return this.workspacePath;
9407
+ }
9408
+ getWorldStateDB() {
9409
+ return this.worldStateDB;
9410
+ }
8894
9411
  /**
8895
9412
  * Record an explicit user rating for the last completed run.
8896
9413
  * Explicit ratings carry 3× the weight of auto-detected outcomes so user
@@ -8977,6 +9494,11 @@ ${last.partialOutput}` : "");
8977
9494
  }
8978
9495
  this.initOptionalFeatures();
8979
9496
  if (this.toolCreator) await this.toolCreator.loadPersistedTools();
9497
+ if (this.encryptedAuditLogger) {
9498
+ this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
9499
+ this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
9500
+ this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
9501
+ }
8980
9502
  this.initialized = true;
8981
9503
  })();
8982
9504
  try {
@@ -9124,7 +9646,7 @@ ${prompt}` : prompt;
9124
9646
  this.router.beginRun();
9125
9647
  this.router.setRunSignal(options.signal);
9126
9648
  const startMs = Date.now();
9127
- const taskId = crypto.randomUUID();
9649
+ const taskId = crypto3.randomUUID();
9128
9650
  this.decisionLog = [];
9129
9651
  const escalator = new PermissionEscalator(this.config.approvalTimeoutMs ?? 6e5, this.config.autonomy === "auto");
9130
9652
  escalator.on("permission:user-required", async (req) => {
@@ -9402,6 +9924,7 @@ ${prompt}` : prompt;
9402
9924
  durationMs,
9403
9925
  costByTier: stats.costByTier,
9404
9926
  tokensByTier: stats.tokensByTier,
9927
+ costByFeature: stats.costByFeature,
9405
9928
  costPercentByTier: this.router.getTierCostPercentages()
9406
9929
  };
9407
9930
  }
@@ -9469,7 +9992,7 @@ var Keystore = class {
9469
9992
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
9470
9993
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
9471
9994
  this.backend = "keytar";
9472
- if (password && fs17__default.default.existsSync(this.storePath)) {
9995
+ if (password && fs19__default.default.existsSync(this.storePath)) {
9473
9996
  try {
9474
9997
  const fileEntries = this.decryptFile(password);
9475
9998
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -9488,8 +10011,8 @@ var Keystore = class {
9488
10011
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
9489
10012
  );
9490
10013
  }
9491
- if (!fs17__default.default.existsSync(this.storePath)) {
9492
- const salt = crypto__default.default.randomBytes(SALT_LEN);
10014
+ if (!fs19__default.default.existsSync(this.storePath)) {
10015
+ const salt = crypto3__default.default.randomBytes(SALT_LEN);
9493
10016
  this.masterKey = this.deriveKey(password, salt);
9494
10017
  this.writeWithSalt({}, salt);
9495
10018
  this.cache = {};
@@ -9502,8 +10025,8 @@ var Keystore = class {
9502
10025
  }
9503
10026
  /** Synchronous legacy unlock kept for AES-only environments. */
9504
10027
  unlockSync(password) {
9505
- if (!fs17__default.default.existsSync(this.storePath)) {
9506
- const salt = crypto__default.default.randomBytes(SALT_LEN);
10028
+ if (!fs19__default.default.existsSync(this.storePath)) {
10029
+ const salt = crypto3__default.default.randomBytes(SALT_LEN);
9507
10030
  this.masterKey = this.deriveKey(password, salt);
9508
10031
  this.writeWithSalt({}, salt);
9509
10032
  this.cache = {};
@@ -9560,12 +10083,12 @@ var Keystore = class {
9560
10083
  }
9561
10084
  }
9562
10085
  decryptFile(password, knownSalt) {
9563
- if (!fs17__default.default.existsSync(this.storePath)) return {};
10086
+ if (!fs19__default.default.existsSync(this.storePath)) return {};
9564
10087
  try {
9565
10088
  const { salt, ciphertext, iv, tag } = this.readRaw();
9566
10089
  const useSalt = knownSalt ?? salt;
9567
10090
  const key = this.masterKey ?? this.deriveKey(password, useSalt);
9568
- const decipher = crypto__default.default.createDecipheriv(ALGORITHM, key, iv);
10091
+ const decipher = crypto3__default.default.createDecipheriv(ALGORITHM, key, iv);
9569
10092
  decipher.setAuthTag(tag);
9570
10093
  const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
9571
10094
  return JSON.parse(decrypted.toString("utf-8"));
@@ -9576,28 +10099,28 @@ var Keystore = class {
9576
10099
  saveAll(data) {
9577
10100
  if (!this.masterKey) return;
9578
10101
  const raw = this.readRaw();
9579
- const iv = crypto__default.default.randomBytes(IV_LEN);
9580
- const cipher = crypto__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
10102
+ const iv = crypto3__default.default.randomBytes(IV_LEN);
10103
+ const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
9581
10104
  const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
9582
10105
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
9583
10106
  const tag = cipher.getAuthTag();
9584
10107
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
9585
- fs17__default.default.mkdirSync(path18__default.default.dirname(this.storePath), { recursive: true });
9586
- fs17__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10108
+ fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10109
+ fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
9587
10110
  }
9588
10111
  writeWithSalt(data, salt) {
9589
10112
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
9590
- const iv = crypto__default.default.randomBytes(IV_LEN);
9591
- const cipher = crypto__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
10113
+ const iv = crypto3__default.default.randomBytes(IV_LEN);
10114
+ const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
9592
10115
  const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
9593
10116
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
9594
10117
  const tag = cipher.getAuthTag();
9595
10118
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
9596
- fs17__default.default.mkdirSync(path18__default.default.dirname(this.storePath), { recursive: true });
9597
- fs17__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10119
+ fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10120
+ fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
9598
10121
  }
9599
10122
  readRaw() {
9600
- const buf = fs17__default.default.readFileSync(this.storePath);
10123
+ const buf = fs19__default.default.readFileSync(this.storePath);
9601
10124
  let offset = 0;
9602
10125
  const salt = buf.subarray(offset, offset + SALT_LEN);
9603
10126
  offset += SALT_LEN;
@@ -9609,15 +10132,15 @@ var Keystore = class {
9609
10132
  return { salt, iv, tag, ciphertext };
9610
10133
  }
9611
10134
  deriveKey(password, salt) {
9612
- return crypto__default.default.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
10135
+ return crypto3__default.default.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
9613
10136
  }
9614
10137
  };
9615
- var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
10138
+ var ignore3 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
9616
10139
  var CascadeIgnore = class {
9617
10140
  ig;
9618
10141
  loaded = false;
9619
10142
  constructor() {
9620
- this.ig = ignore2();
10143
+ this.ig = ignore3();
9621
10144
  this.ig.add([
9622
10145
  ".cascade/keystore.enc",
9623
10146
  ".cascade/memory.db",
@@ -9630,7 +10153,7 @@ var CascadeIgnore = class {
9630
10153
  ]);
9631
10154
  }
9632
10155
  async load(workspacePath) {
9633
- const filePath = path18__default.default.join(workspacePath, ".cascadeignore");
10156
+ const filePath = path20__default.default.join(workspacePath, ".cascadeignore");
9634
10157
  try {
9635
10158
  const content = await fs4__default.default.readFile(filePath, "utf-8");
9636
10159
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -9641,7 +10164,7 @@ var CascadeIgnore = class {
9641
10164
  }
9642
10165
  isIgnored(filePath, workspacePath) {
9643
10166
  try {
9644
- const relative = workspacePath ? path18__default.default.relative(workspacePath, filePath) : filePath;
10167
+ const relative = workspacePath ? path20__default.default.relative(workspacePath, filePath) : filePath;
9645
10168
  return this.ig.ignores(relative);
9646
10169
  } catch {
9647
10170
  return false;
@@ -9652,7 +10175,7 @@ var CascadeIgnore = class {
9652
10175
  }
9653
10176
  };
9654
10177
  async function loadCascadeMd(workspacePath) {
9655
- const filePath = path18__default.default.join(workspacePath, "CASCADE.md");
10178
+ const filePath = path20__default.default.join(workspacePath, "CASCADE.md");
9656
10179
  try {
9657
10180
  const raw = await fs4__default.default.readFile(filePath, "utf-8");
9658
10181
  return parseCascadeMd(raw);
@@ -9683,7 +10206,7 @@ ${raw.trim()}`;
9683
10206
  var MemoryStore = class _MemoryStore {
9684
10207
  db;
9685
10208
  constructor(dbPath) {
9686
- fs17__default.default.mkdirSync(path18__default.default.dirname(dbPath), { recursive: true });
10209
+ fs19__default.default.mkdirSync(path20__default.default.dirname(dbPath), { recursive: true });
9687
10210
  try {
9688
10211
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
9689
10212
  this.db.pragma("journal_mode = WAL");
@@ -9807,7 +10330,7 @@ Original error: ${err.message}`
9807
10330
  VALUES (?, ?, ?, ?, ?, ?, ?)
9808
10331
  `);
9809
10332
  for (const msg of messages) {
9810
- stmt.run(crypto.randomUUID(), newId, msg.role, msg.content, msg.timestamp, msg.tokens, msg.agent_messages);
10333
+ stmt.run(crypto3.randomUUID(), newId, msg.role, msg.content, msg.timestamp, msg.tokens, msg.agent_messages);
9811
10334
  }
9812
10335
  const snapshots = this.db.prepare("SELECT * FROM file_snapshots WHERE session_id = ?").all(originalId);
9813
10336
  const snapStmt = this.db.prepare(`
@@ -9815,7 +10338,7 @@ Original error: ${err.message}`
9815
10338
  VALUES (?, ?, ?, ?, ?)
9816
10339
  `);
9817
10340
  for (const snap of snapshots) {
9818
- snapStmt.run(crypto.randomUUID(), newId, snap.file_path, snap.content, snap.timestamp);
10341
+ snapStmt.run(crypto3.randomUUID(), newId, snap.file_path, snap.content, snap.timestamp);
9819
10342
  }
9820
10343
  }
9821
10344
  // ── Runtime Sessions / Nodes ─────────────────
@@ -10113,7 +10636,7 @@ Original error: ${err.message}`
10113
10636
  this.db.prepare(`
10114
10637
  INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
10115
10638
  VALUES (?, ?, ?, ?, ?)
10116
- `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
10639
+ `).run(crypto3.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
10117
10640
  });
10118
10641
  }
10119
10642
  getLatestFileSnapshots(sessionId) {
@@ -10441,15 +10964,15 @@ var ConfigManager = class {
10441
10964
  globalDir;
10442
10965
  constructor(workspacePath = process.cwd()) {
10443
10966
  this.workspacePath = workspacePath;
10444
- this.globalDir = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR);
10967
+ this.globalDir = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
10445
10968
  }
10446
10969
  async load() {
10447
10970
  this.config = await this.loadConfig();
10448
10971
  this.ignore = new CascadeIgnore();
10449
10972
  await this.ignore.load(this.workspacePath);
10450
10973
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
10451
- this.keystore = new Keystore(path18__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
10452
- this.store = new MemoryStore(path18__default.default.join(this.workspacePath, CASCADE_DB_FILE));
10974
+ this.keystore = new Keystore(path20__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
10975
+ this.store = new MemoryStore(path20__default.default.join(this.workspacePath, CASCADE_DB_FILE));
10453
10976
  await this.injectEnvKeys();
10454
10977
  await this.ensureDefaultIdentity();
10455
10978
  }
@@ -10472,8 +10995,8 @@ var ConfigManager = class {
10472
10995
  return this.workspacePath;
10473
10996
  }
10474
10997
  async save() {
10475
- const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
10476
- await fs4__default.default.mkdir(path18__default.default.dirname(configPath), { recursive: true });
10998
+ const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
10999
+ await fs4__default.default.mkdir(path20__default.default.dirname(configPath), { recursive: true });
10477
11000
  await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
10478
11001
  }
10479
11002
  async updateConfig(updates) {
@@ -10497,7 +11020,7 @@ var ConfigManager = class {
10497
11020
  return configProvider?.apiKey;
10498
11021
  }
10499
11022
  async loadConfig() {
10500
- const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11023
+ const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
10501
11024
  try {
10502
11025
  const raw = await fs4__default.default.readFile(configPath, "utf-8");
10503
11026
  return validateConfig(JSON.parse(raw));
@@ -10534,7 +11057,7 @@ var ConfigManager = class {
10534
11057
  const existing = this.store.getDefaultIdentity();
10535
11058
  if (existing) return;
10536
11059
  const identity = {
10537
- id: crypto.randomUUID(),
11060
+ id: crypto3.randomUUID(),
10538
11061
  name: "Default",
10539
11062
  description: "Default Cascade identity",
10540
11063
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10727,6 +11250,24 @@ var DashboardSocket = class {
10727
11250
  });
10728
11251
  });
10729
11252
  }
11253
+ onSessionHalt(callback) {
11254
+ this.io.on("connection", (socket) => {
11255
+ socket.on("session:halt", (payload) => {
11256
+ if (typeof payload?.sessionId === "string") {
11257
+ callback(payload.sessionId);
11258
+ }
11259
+ });
11260
+ });
11261
+ }
11262
+ onSessionSteer(callback) {
11263
+ this.io.on("connection", (socket) => {
11264
+ socket.on("session:steer", (payload) => {
11265
+ if (typeof payload?.message === "string" && payload.message.trim()) {
11266
+ callback(payload.message.trim(), payload.sessionId, payload.nodeId);
11267
+ }
11268
+ });
11269
+ });
11270
+ }
10730
11271
  onConfigUpdate(callback) {
10731
11272
  this.io.on("connection", (socket) => {
10732
11273
  socket.on("config:update", (payload) => {
@@ -10758,7 +11299,7 @@ var DashboardSocket = class {
10758
11299
  this.io.close();
10759
11300
  }
10760
11301
  };
10761
- var __dirname$1 = path18__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
11302
+ var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
10762
11303
  var DashboardServer = class {
10763
11304
  app;
10764
11305
  httpServer;
@@ -10769,6 +11310,13 @@ var DashboardServer = class {
10769
11310
  globalStore = null;
10770
11311
  broadcastTimer = null;
10771
11312
  activeSessions = /* @__PURE__ */ new Map();
11313
+ activeControllers = /* @__PURE__ */ new Map();
11314
+ /**
11315
+ * Run taskIds per chat session — file snapshots are keyed by the run's
11316
+ * taskId (see t3-worker saveSnapshot), so session rollback needs the list
11317
+ * of runs the session performed in this server's lifetime.
11318
+ */
11319
+ sessionTaskIds = /* @__PURE__ */ new Map();
10772
11320
  port;
10773
11321
  host;
10774
11322
  workspacePath;
@@ -10828,7 +11376,9 @@ var DashboardServer = class {
10828
11376
  this.persistConfig();
10829
11377
  });
10830
11378
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
10831
- const sessionId = requestedSessionId ?? crypto.randomUUID();
11379
+ const sessionId = requestedSessionId ?? crypto3.randomUUID();
11380
+ const abortController = new AbortController();
11381
+ this.activeControllers.set(sessionId, abortController);
10832
11382
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
10833
11383
  const title = this.persistRunStart(sessionId, prompt);
10834
11384
  const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
@@ -10847,7 +11397,8 @@ var DashboardServer = class {
10847
11397
  this.socket.emitPeerMessage(e);
10848
11398
  });
10849
11399
  try {
10850
- const result = await cascade.run({ prompt: runPrompt });
11400
+ const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
11401
+ this.recordSessionTask(sessionId, result.taskId);
10851
11402
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
10852
11403
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
10853
11404
  this.socket.broadcast("cost:update", {
@@ -10864,6 +11415,16 @@ var DashboardServer = class {
10864
11415
  });
10865
11416
  } finally {
10866
11417
  this.activeSessions.delete(sessionId);
11418
+ this.activeControllers.delete(sessionId);
11419
+ }
11420
+ });
11421
+ this.socket.onSessionHalt((sessionId) => {
11422
+ this.activeControllers.get(sessionId)?.abort();
11423
+ });
11424
+ this.socket.onSessionSteer((message, sessionId, nodeId) => {
11425
+ const steered = this.steerSessions(message, sessionId, nodeId);
11426
+ if (steered > 0) {
11427
+ this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
10867
11428
  }
10868
11429
  });
10869
11430
  }
@@ -10911,9 +11472,9 @@ var DashboardServer = class {
10911
11472
  */
10912
11473
  persistConfig() {
10913
11474
  try {
10914
- const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
10915
- fs17__default.default.mkdirSync(path18__default.default.dirname(configPath), { recursive: true });
10916
- fs17__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
11475
+ const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11476
+ fs19__default.default.mkdirSync(path20__default.default.dirname(configPath), { recursive: true });
11477
+ fs19__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
10917
11478
  } catch (err) {
10918
11479
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
10919
11480
  }
@@ -10928,15 +11489,15 @@ var DashboardServer = class {
10928
11489
  resolveDashboardSecret() {
10929
11490
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
10930
11491
  if (fromConfig) return fromConfig;
10931
- const secretPath = path18__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
11492
+ const secretPath = path20__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
10932
11493
  try {
10933
- if (fs17__default.default.existsSync(secretPath)) {
10934
- const existing = fs17__default.default.readFileSync(secretPath, "utf-8").trim();
11494
+ if (fs19__default.default.existsSync(secretPath)) {
11495
+ const existing = fs19__default.default.readFileSync(secretPath, "utf-8").trim();
10935
11496
  if (existing.length >= 16) return existing;
10936
11497
  }
10937
- const generated = crypto.randomUUID();
10938
- fs17__default.default.mkdirSync(path18__default.default.dirname(secretPath), { recursive: true });
10939
- fs17__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
11498
+ const generated = crypto3.randomUUID();
11499
+ fs19__default.default.mkdirSync(path20__default.default.dirname(secretPath), { recursive: true });
11500
+ fs19__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
10940
11501
  if (this.config.dashboard.auth) {
10941
11502
  console.warn(
10942
11503
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -10945,7 +11506,7 @@ var DashboardServer = class {
10945
11506
  return generated;
10946
11507
  } catch {
10947
11508
  console.warn("Unable to persist dashboard secret; falling back to a process-ephemeral secret.");
10948
- return crypto.randomUUID();
11509
+ return crypto3.randomUUID();
10949
11510
  }
10950
11511
  }
10951
11512
  /**
@@ -10963,7 +11524,7 @@ var DashboardServer = class {
10963
11524
  // ── Setup ─────────────────────────────────────
10964
11525
  getGlobalStore() {
10965
11526
  if (!this.globalStore) {
10966
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11527
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10967
11528
  this.globalStore = new MemoryStore(globalDbPath);
10968
11529
  }
10969
11530
  return this.globalStore;
@@ -10993,7 +11554,7 @@ var DashboardServer = class {
10993
11554
  metadata: { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 }
10994
11555
  });
10995
11556
  }
10996
- this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "user", content: prompt, timestamp: now });
11557
+ this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "user", content: prompt, timestamp: now });
10997
11558
  } catch (err) {
10998
11559
  console.warn("[dashboard] failed to persist run start:", err);
10999
11560
  }
@@ -11004,13 +11565,28 @@ var DashboardServer = class {
11004
11565
  persistRunEnd(sessionId, title, latestPrompt, reply, status) {
11005
11566
  try {
11006
11567
  if (reply && reply.trim()) {
11007
- this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11568
+ this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11008
11569
  }
11009
11570
  } catch (err) {
11010
11571
  console.warn("[dashboard] failed to persist run end:", err);
11011
11572
  }
11012
11573
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
11013
11574
  }
11575
+ /**
11576
+ * Route steering text into running Cascade instances. Targets the given
11577
+ * session, or every active session when none is specified (the desktop
11578
+ * usually has exactly one run in flight). Returns how many were reached.
11579
+ */
11580
+ steerSessions(message, sessionId, nodeId) {
11581
+ const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
11582
+ for (const cascade of targets) cascade.injectGuidance(message, nodeId);
11583
+ return targets.length;
11584
+ }
11585
+ recordSessionTask(sessionId, taskId) {
11586
+ const list = this.sessionTaskIds.get(sessionId) ?? [];
11587
+ list.push(taskId);
11588
+ this.sessionTaskIds.set(sessionId, list);
11589
+ }
11014
11590
  persistRuntimeRow(sessionId, title, status, latestPrompt) {
11015
11591
  const now = (/* @__PURE__ */ new Date()).toISOString();
11016
11592
  const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
@@ -11104,12 +11680,12 @@ ${prompt}`;
11104
11680
  }
11105
11681
  }
11106
11682
  watchRuntimeChanges() {
11107
- const workspaceDbPath = path18__default.default.join(this.workspacePath, CASCADE_DB_FILE);
11108
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11683
+ const workspaceDbPath = path20__default.default.join(this.workspacePath, CASCADE_DB_FILE);
11684
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11109
11685
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
11110
11686
  for (const watchPath of watchPaths) {
11111
- if (!fs17__default.default.existsSync(watchPath)) continue;
11112
- fs17__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
11687
+ if (!fs19__default.default.existsSync(watchPath)) continue;
11688
+ fs19__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
11113
11689
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
11114
11690
  });
11115
11691
  }
@@ -11179,7 +11755,7 @@ ${prompt}`;
11179
11755
  const truthy = Buffer.from("1");
11180
11756
  const falsy = Buffer.from("0");
11181
11757
  const probe = ok ? truthy : falsy;
11182
- const authorized = crypto.timingSafeEqual(probe, truthy);
11758
+ const authorized = crypto3.timingSafeEqual(probe, truthy);
11183
11759
  if (authorized) {
11184
11760
  const token = createToken(
11185
11761
  { id: username, username, role: "admin" },
@@ -11217,7 +11793,8 @@ ${prompt}`;
11217
11793
  res.status(400).json({ error: "message is required and must be a string" });
11218
11794
  return;
11219
11795
  }
11220
- const payload = { sessionId, nodeId, message, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
11796
+ const steered = this.steerSessions(message, sessionId, nodeId);
11797
+ const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
11221
11798
  this.socket.broadcast("session:message-injected", payload);
11222
11799
  if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
11223
11800
  res.json({ success: true, ...payload });
@@ -11239,7 +11816,7 @@ ${prompt}`;
11239
11816
  const sessionId = req.params.id;
11240
11817
  this.store.deleteSession(sessionId);
11241
11818
  this.store.deleteRuntimeSession(sessionId);
11242
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11819
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11243
11820
  const globalStore = new MemoryStore(globalDbPath);
11244
11821
  try {
11245
11822
  globalStore.deleteRuntimeSession(sessionId);
@@ -11251,9 +11828,34 @@ ${prompt}`;
11251
11828
  this.socket.broadcast("runtime:refresh", { scope: "global" });
11252
11829
  res.json({ ok: true });
11253
11830
  });
11831
+ this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
11832
+ const sessionId = req.params.id;
11833
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
11834
+ if (!taskIds.length) {
11835
+ res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
11836
+ return;
11837
+ }
11838
+ const toRestore = /* @__PURE__ */ new Map();
11839
+ for (const taskId of taskIds) {
11840
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
11841
+ if (!toRestore.has(filePath)) toRestore.set(filePath, content);
11842
+ }
11843
+ }
11844
+ const { writeFile } = await import('fs/promises');
11845
+ let restored = 0;
11846
+ for (const [filePath, content] of toRestore) {
11847
+ try {
11848
+ await writeFile(filePath, content, "utf-8");
11849
+ restored++;
11850
+ } catch (err) {
11851
+ console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
11852
+ }
11853
+ }
11854
+ res.json({ ok: true, restored });
11855
+ });
11254
11856
  this.app.delete("/api/sessions", auth, (req, res) => {
11255
11857
  const body = req.body;
11256
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11858
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11257
11859
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
11258
11860
  const globalStore = new MemoryStore(globalDbPath);
11259
11861
  try {
@@ -11276,7 +11878,7 @@ ${prompt}`;
11276
11878
  });
11277
11879
  this.app.delete("/api/runtime", auth, (_req, res) => {
11278
11880
  this.store.deleteAllRuntimeNodes();
11279
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11881
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11280
11882
  const globalStore = new MemoryStore(globalDbPath);
11281
11883
  try {
11282
11884
  globalStore.deleteAllRuntimeNodes();
@@ -11300,7 +11902,7 @@ ${prompt}`;
11300
11902
  const existing = this.store.getDefaultIdentity();
11301
11903
  if (existing) this.store.updateIdentity(existing.id, { isDefault: false });
11302
11904
  }
11303
- const id = crypto.randomUUID();
11905
+ const id = crypto3.randomUUID();
11304
11906
  const identity = {
11305
11907
  id,
11306
11908
  name: body.name,
@@ -11327,6 +11929,19 @@ ${prompt}`;
11327
11929
  this.store.deleteIdentity(req.params.id);
11328
11930
  res.json({ ok: true });
11329
11931
  });
11932
+ this.app.get("/api/audit/verify", auth, async (_req, res) => {
11933
+ try {
11934
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
11935
+ const logger = new AuditLogger3(this.workspacePath);
11936
+ try {
11937
+ res.json(logger.verifyChain());
11938
+ } finally {
11939
+ logger.close();
11940
+ }
11941
+ } catch (err) {
11942
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
11943
+ }
11944
+ });
11330
11945
  this.app.get("/api/audit/:sessionId", auth, (req, res) => {
11331
11946
  const log = this.store.getAuditLog(req.params.sessionId);
11332
11947
  res.json(log);
@@ -11349,12 +11964,12 @@ ${prompt}`;
11349
11964
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
11350
11965
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
11351
11966
  try {
11352
- const configPath = path18__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11353
- const existing = fs17__default.default.existsSync(configPath) ? JSON.parse(fs17__default.default.readFileSync(configPath, "utf-8")) : {};
11967
+ const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11968
+ const existing = fs19__default.default.existsSync(configPath) ? JSON.parse(fs19__default.default.readFileSync(configPath, "utf-8")) : {};
11354
11969
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
11355
11970
  const tmp = configPath + ".tmp";
11356
- fs17__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
11357
- fs17__default.default.renameSync(tmp, configPath);
11971
+ fs19__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
11972
+ fs19__default.default.renameSync(tmp, configPath);
11358
11973
  res.json({ ok: true });
11359
11974
  } catch (err) {
11360
11975
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -11382,7 +11997,7 @@ ${prompt}`;
11382
11997
  this.app.get("/api/runtime", auth, (req, res) => {
11383
11998
  const scope = req.query["scope"] ?? "workspace";
11384
11999
  if (scope === "global") {
11385
- const globalDbPath = path18__default.default.join(os4__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12000
+ const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11386
12001
  const globalStore = new MemoryStore(globalDbPath);
11387
12002
  try {
11388
12003
  res.json({
@@ -11410,7 +12025,7 @@ ${prompt}`;
11410
12025
  return;
11411
12026
  }
11412
12027
  const requestedSessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId.trim() : void 0;
11413
- const sessionId = requestedSessionId ?? crypto.randomUUID();
12028
+ const sessionId = requestedSessionId ?? crypto3.randomUUID();
11414
12029
  res.json({ sessionId, status: "ACTIVE" });
11415
12030
  const prompt = body.prompt;
11416
12031
  const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
@@ -11432,6 +12047,7 @@ ${prompt}`;
11432
12047
  });
11433
12048
  try {
11434
12049
  const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
12050
+ this.recordSessionTask(sessionId, result.taskId);
11435
12051
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
11436
12052
  this.socket.broadcast("cost:update", {
11437
12053
  sessionId,
@@ -11462,13 +12078,13 @@ ${prompt}`;
11462
12078
  }))
11463
12079
  });
11464
12080
  });
11465
- const prodPath = path18__default.default.resolve(__dirname$1, "../web/dist");
11466
- const devPath = path18__default.default.resolve(__dirname$1, "../../web/dist");
11467
- const webDistPath = fs17__default.default.existsSync(prodPath) ? prodPath : devPath;
11468
- if (fs17__default.default.existsSync(webDistPath)) {
12081
+ const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
12082
+ const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
12083
+ const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
12084
+ if (fs19__default.default.existsSync(webDistPath)) {
11469
12085
  this.app.use(express__default.default.static(webDistPath));
11470
12086
  this.app.get("*", (_req, res) => {
11471
- res.sendFile(path18__default.default.join(webDistPath, "index.html"));
12087
+ res.sendFile(path20__default.default.join(webDistPath, "index.html"));
11472
12088
  });
11473
12089
  } else {
11474
12090
  this.app.get("/", (_req, res) => {