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