cascade-ai 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -9,8 +9,8 @@ var https = require('https');
9
9
  var zlib = require('zlib');
10
10
  var stream = require('stream');
11
11
  var Database2 = require('better-sqlite3');
12
- var path33 = require('path');
13
- var fs30 = require('fs');
12
+ var path34 = require('path');
13
+ var fs31 = require('fs');
14
14
  var os10 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var ink = require('ink');
@@ -77,8 +77,8 @@ var http__default = /*#__PURE__*/_interopDefault(http);
77
77
  var https__default = /*#__PURE__*/_interopDefault(https);
78
78
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
79
79
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
80
- var path33__default = /*#__PURE__*/_interopDefault(path33);
81
- var fs30__default = /*#__PURE__*/_interopDefault(fs30);
80
+ var path34__default = /*#__PURE__*/_interopDefault(path34);
81
+ var fs31__default = /*#__PURE__*/_interopDefault(fs31);
82
82
  var os10__default = /*#__PURE__*/_interopDefault(os10);
83
83
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
84
84
  var React2__default = /*#__PURE__*/_interopDefault(React2);
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1810
1810
  var init_constants = __esm({
1811
1811
  "src/constants.ts"() {
1812
1812
  init_pricing();
1813
- CASCADE_VERSION = "0.67.0";
1813
+ CASCADE_VERSION = "0.68.0";
1814
1814
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1815
1815
  CASCADE_DB_FILE = ".cascade/memory.db";
1816
1816
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -3932,12 +3932,12 @@ var init_audit_logger = __esm({
3932
3932
  constructor(workspacePath, debugMode = false) {
3933
3933
  this.workspacePath = workspacePath;
3934
3934
  this.debugMode = debugMode;
3935
- const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
3936
- if (!fs30__default.default.existsSync(cascadeDir)) {
3937
- fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
3935
+ const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
3936
+ if (!fs31__default.default.existsSync(cascadeDir)) {
3937
+ fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
3938
3938
  }
3939
- this.keyPath = path33__default.default.join(cascadeDir, "audit_log.key");
3940
- this.dbPath = path33__default.default.join(cascadeDir, "audit_log.db");
3939
+ this.keyPath = path34__default.default.join(cascadeDir, "audit_log.key");
3940
+ this.dbPath = path34__default.default.join(cascadeDir, "audit_log.db");
3941
3941
  this.initEncryptionKey();
3942
3942
  this.db = new Database2__default.default(this.dbPath);
3943
3943
  this.db.pragma("journal_mode = WAL");
@@ -3967,11 +3967,11 @@ var init_audit_logger = __esm({
3967
3967
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3968
3968
  }
3969
3969
  initEncryptionKey() {
3970
- if (fs30__default.default.existsSync(this.keyPath)) {
3971
- this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
3970
+ if (fs31__default.default.existsSync(this.keyPath)) {
3971
+ this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
3972
3972
  } else {
3973
3973
  this.encryptionKey = crypto__default.default.randomBytes(32);
3974
- fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3974
+ fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3975
3975
  }
3976
3976
  }
3977
3977
  encrypt(text) {
@@ -4049,9 +4049,9 @@ var init_audit_logger = __esm({
4049
4049
  dumpDebugIfNeeded() {
4050
4050
  if (!this.debugMode) return;
4051
4051
  try {
4052
- const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
4052
+ const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
4053
4053
  const entries = this.getAllLogs();
4054
- fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4054
+ fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4055
4055
  } catch (err) {
4056
4056
  console.error("Failed to dump debug audit logs", err);
4057
4057
  }
@@ -4106,7 +4106,7 @@ var Keystore = class {
4106
4106
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
4107
4107
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
4108
4108
  this.backend = "keytar";
4109
- if (password && fs30__default.default.existsSync(this.storePath)) {
4109
+ if (password && fs31__default.default.existsSync(this.storePath)) {
4110
4110
  try {
4111
4111
  const fileEntries = this.decryptFile(password);
4112
4112
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -4125,7 +4125,7 @@ var Keystore = class {
4125
4125
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
4126
4126
  );
4127
4127
  }
4128
- if (!fs30__default.default.existsSync(this.storePath)) {
4128
+ if (!fs31__default.default.existsSync(this.storePath)) {
4129
4129
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4130
4130
  this.masterKey = this.deriveKey(password, salt);
4131
4131
  this.writeWithSalt({}, salt);
@@ -4139,7 +4139,7 @@ var Keystore = class {
4139
4139
  }
4140
4140
  /** Synchronous legacy unlock kept for AES-only environments. */
4141
4141
  unlockSync(password) {
4142
- if (!fs30__default.default.existsSync(this.storePath)) {
4142
+ if (!fs31__default.default.existsSync(this.storePath)) {
4143
4143
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4144
4144
  this.masterKey = this.deriveKey(password, salt);
4145
4145
  this.writeWithSalt({}, salt);
@@ -4197,7 +4197,7 @@ var Keystore = class {
4197
4197
  }
4198
4198
  }
4199
4199
  decryptFile(password, knownSalt) {
4200
- if (!fs30__default.default.existsSync(this.storePath)) return {};
4200
+ if (!fs31__default.default.existsSync(this.storePath)) return {};
4201
4201
  try {
4202
4202
  const { salt, ciphertext, iv, tag } = this.readRaw();
4203
4203
  const useSalt = knownSalt ?? salt;
@@ -4219,8 +4219,8 @@ var Keystore = class {
4219
4219
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4220
4220
  const tag = cipher.getAuthTag();
4221
4221
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4222
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
4223
- fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4222
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4223
+ fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4224
4224
  }
4225
4225
  writeWithSalt(data, salt) {
4226
4226
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4230,11 +4230,11 @@ var Keystore = class {
4230
4230
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4231
4231
  const tag = cipher.getAuthTag();
4232
4232
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4233
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.storePath), { recursive: true });
4234
- fs30__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4233
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4234
+ fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4235
4235
  }
4236
4236
  readRaw() {
4237
- const buf = fs30__default.default.readFileSync(this.storePath);
4237
+ const buf = fs31__default.default.readFileSync(this.storePath);
4238
4238
  let offset = 0;
4239
4239
  const salt = buf.subarray(offset, offset + SALT_LEN);
4240
4240
  offset += SALT_LEN;
@@ -4267,7 +4267,7 @@ var CascadeIgnore = class {
4267
4267
  ]);
4268
4268
  }
4269
4269
  async load(workspacePath) {
4270
- const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
4270
+ const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4271
4271
  try {
4272
4272
  const content = await fs11__default.default.readFile(filePath, "utf-8");
4273
4273
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4278,7 +4278,7 @@ var CascadeIgnore = class {
4278
4278
  }
4279
4279
  isIgnored(filePath, workspacePath) {
4280
4280
  try {
4281
- const relative = workspacePath ? path33__default.default.relative(workspacePath, filePath) : filePath;
4281
+ const relative = workspacePath ? path34__default.default.relative(workspacePath, filePath) : filePath;
4282
4282
  return this.ig.ignores(relative);
4283
4283
  } catch {
4284
4284
  return false;
@@ -4289,7 +4289,7 @@ var CascadeIgnore = class {
4289
4289
  }
4290
4290
  };
4291
4291
  async function createDefaultIgnoreFile(workspacePath) {
4292
- const filePath = path33__default.default.join(workspacePath, ".cascadeignore");
4292
+ const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4293
4293
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4294
4294
  # Syntax identical to .gitignore
4295
4295
 
@@ -4319,7 +4319,7 @@ Thumbs.db
4319
4319
  await fs11__default.default.writeFile(filePath, content, "utf-8");
4320
4320
  }
4321
4321
  async function loadCascadeMd(workspacePath) {
4322
- const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
4322
+ const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4323
4323
  try {
4324
4324
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
4325
4325
  return parseCascadeMd(raw);
@@ -4348,7 +4348,7 @@ ${raw.trim()}`;
4348
4348
  return { raw, sections, systemPrompt };
4349
4349
  }
4350
4350
  async function createDefaultCascadeMd(workspacePath) {
4351
- const filePath = path33__default.default.join(workspacePath, "CASCADE.md");
4351
+ const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4352
4352
  const content = `# Cascade Project Instructions
4353
4353
 
4354
4354
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4383,7 +4383,7 @@ List any areas the agent should not touch.
4383
4383
  var MemoryStore = class _MemoryStore {
4384
4384
  db;
4385
4385
  constructor(dbPath) {
4386
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
4386
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
4387
4387
  try {
4388
4388
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
4389
4389
  this.db.pragma("journal_mode = WAL");
@@ -5635,7 +5635,7 @@ function validateConfig(raw) {
5635
5635
  // src/config/global-credentials.ts
5636
5636
  init_constants();
5637
5637
  function credentialsPath(globalDir2) {
5638
- return path33__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5638
+ return path34__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5639
5639
  }
5640
5640
  function providerKey(p) {
5641
5641
  if (p.type === "azure") {
@@ -5648,7 +5648,7 @@ function isPersistable(p) {
5648
5648
  }
5649
5649
  function loadGlobalCredentials(globalDir2) {
5650
5650
  try {
5651
- const raw = fs30__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5651
+ const raw = fs31__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5652
5652
  const parsed = JSON.parse(raw);
5653
5653
  if (!Array.isArray(parsed.providers)) return [];
5654
5654
  return parsed.providers.filter(
@@ -5660,13 +5660,13 @@ function loadGlobalCredentials(globalDir2) {
5660
5660
  }
5661
5661
  function saveGlobalCredentials(globalDir2, providers) {
5662
5662
  const filePath = credentialsPath(globalDir2);
5663
- fs30__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5663
+ fs31__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5664
5664
  const body = { version: 1, providers: providers.filter(isPersistable) };
5665
5665
  const tmp = `${filePath}.tmp`;
5666
- fs30__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5667
- fs30__default.default.renameSync(tmp, filePath);
5666
+ fs31__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5667
+ fs31__default.default.renameSync(tmp, filePath);
5668
5668
  try {
5669
- fs30__default.default.chmodSync(filePath, 384);
5669
+ fs31__default.default.chmodSync(filePath, 384);
5670
5670
  } catch {
5671
5671
  }
5672
5672
  }
@@ -5884,7 +5884,7 @@ var ConfigManager = class {
5884
5884
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5885
5885
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5886
5886
  this.workspacePath = workspacePath;
5887
- this.globalDir = globalDirOverride ?? path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
5887
+ this.globalDir = globalDirOverride ?? path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
5888
5888
  }
5889
5889
  async load() {
5890
5890
  this.config = await this.loadConfig();
@@ -5898,8 +5898,8 @@ var ConfigManager = class {
5898
5898
  this.ignore = new CascadeIgnore();
5899
5899
  await this.ignore.load(this.workspacePath);
5900
5900
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5901
- this.keystore = new Keystore(path33__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5902
- this.store = new MemoryStore(path33__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5901
+ this.keystore = new Keystore(path34__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5902
+ this.store = new MemoryStore(path34__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5903
5903
  await this.injectEnvKeys();
5904
5904
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
5905
5905
  await this.ensureDefaultIdentity();
@@ -5924,8 +5924,8 @@ var ConfigManager = class {
5924
5924
  return this.workspacePath;
5925
5925
  }
5926
5926
  async save() {
5927
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5928
- await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
5927
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5928
+ await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
5929
5929
  await fs11__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
5930
5930
  try {
5931
5931
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -5961,7 +5961,7 @@ var ConfigManager = class {
5961
5961
  return configProvider?.apiKey;
5962
5962
  }
5963
5963
  async loadConfig() {
5964
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5964
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5965
5965
  try {
5966
5966
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
5967
5967
  return validateConfig(JSON.parse(raw));
@@ -6963,13 +6963,13 @@ var DeadModelStore = class {
6963
6963
  function fileDeadModelPersistence(filePath) {
6964
6964
  return {
6965
6965
  load() {
6966
- if (!fs30.existsSync(filePath)) return [];
6967
- const raw = JSON.parse(fs30.readFileSync(filePath, "utf-8"));
6966
+ if (!fs31.existsSync(filePath)) return [];
6967
+ const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
6968
6968
  return Array.isArray(raw) ? raw : [];
6969
6969
  },
6970
6970
  save(records) {
6971
- fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
6972
- fs30.writeFileSync(filePath, JSON.stringify(records, null, 2));
6971
+ fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
6972
+ fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
6973
6973
  }
6974
6974
  };
6975
6975
  }
@@ -7128,7 +7128,7 @@ init_pricing();
7128
7128
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7129
7129
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7130
7130
  var FETCH_TIMEOUT_MS = 8e3;
7131
- var DEFAULT_CACHE_FILE = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7131
+ var DEFAULT_CACHE_FILE = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7132
7132
  function normalizeModelId2(id) {
7133
7133
  let s = id.toLowerCase();
7134
7134
  const slash = s.lastIndexOf("/");
@@ -7275,7 +7275,7 @@ var LiveDataProvider = class {
7275
7275
  }
7276
7276
  async saveCache() {
7277
7277
  try {
7278
- await fs11__default.default.mkdir(path33__default.default.dirname(this.opts.cacheFile), { recursive: true });
7278
+ await fs11__default.default.mkdir(path34__default.default.dirname(this.opts.cacheFile), { recursive: true });
7279
7279
  const cache = {
7280
7280
  fetchedAt: this.fetchedAt,
7281
7281
  snapshot: this.snapshot ?? void 0,
@@ -9068,6 +9068,79 @@ function describeProviderError(c, modelId) {
9068
9068
  }
9069
9069
  }
9070
9070
 
9071
+ // src/core/verification/acceptance.ts
9072
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
9073
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
9074
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
9075
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
9076
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
9077
+ function targetFile(criterion, ownedFiles) {
9078
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
9079
+ if (mentioned.length > 1) return null;
9080
+ if (mentioned.length === 1) {
9081
+ const candidate = mentioned[0];
9082
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
9083
+ }
9084
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
9085
+ }
9086
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
9087
+ const results = [];
9088
+ for (const criterion of criteria) {
9089
+ const text = criterion.trim();
9090
+ if (!text) {
9091
+ results.push({ criterion, verdict: "undecidable" });
9092
+ continue;
9093
+ }
9094
+ if (NEGATION.test(text)) {
9095
+ results.push({ criterion, verdict: "undecidable" });
9096
+ continue;
9097
+ }
9098
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
9099
+ const wantsExists = EXISTS.test(text);
9100
+ const wantsNonEmpty = NON_EMPTY.test(text);
9101
+ if (!needle && !wantsExists && !wantsNonEmpty) {
9102
+ results.push({ criterion, verdict: "undecidable" });
9103
+ continue;
9104
+ }
9105
+ const file = targetFile(text, ownedFiles);
9106
+ if (!file) {
9107
+ results.push({ criterion, verdict: "undecidable" });
9108
+ continue;
9109
+ }
9110
+ const stat = await probe.stat(file);
9111
+ if (!stat) {
9112
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
9113
+ continue;
9114
+ }
9115
+ if (wantsNonEmpty && stat.size === 0) {
9116
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
9117
+ continue;
9118
+ }
9119
+ if (needle) {
9120
+ const content = await probe.read(file);
9121
+ if (content === null) {
9122
+ results.push({ criterion, verdict: "undecidable" });
9123
+ continue;
9124
+ }
9125
+ const found = content.includes(needle);
9126
+ results.push({
9127
+ criterion,
9128
+ verdict: found ? "passed" : "failed",
9129
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
9130
+ });
9131
+ continue;
9132
+ }
9133
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
9134
+ }
9135
+ return results;
9136
+ }
9137
+ function undecided(results) {
9138
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
9139
+ }
9140
+ function failures(results) {
9141
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
9142
+ }
9143
+
9071
9144
  // src/core/tiers/t3-worker.ts
9072
9145
  var CriticalToolError = class extends Error {
9073
9146
  constructor(message, toolName) {
@@ -9384,7 +9457,7 @@ Now execute your subtask using this context where relevant.`
9384
9457
  }
9385
9458
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
9386
9459
  if (!artifactCheck.ok) {
9387
- correctionAttempts = 1;
9460
+ correctionAttempts++;
9388
9461
  issues.push(...artifactCheck.issues);
9389
9462
  output = await this.correctOutput(output, artifactCheck.issues);
9390
9463
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -9402,18 +9475,40 @@ Now execute your subtask using this context where relevant.`
9402
9475
  this.tools.map((t) => t.name)
9403
9476
  );
9404
9477
  if (visualIssue) {
9405
- correctionAttempts = 1;
9478
+ correctionAttempts++;
9406
9479
  issues.push(visualIssue);
9407
9480
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
9408
9481
  output = await this.correctOutput(output, [visualIssue]);
9409
9482
  }
9483
+ const acceptanceResults = await this.checkAcceptance(assignment);
9484
+ const acceptanceFailures = failures(acceptanceResults);
9485
+ if (acceptanceFailures.length > 0) {
9486
+ correctionAttempts++;
9487
+ issues.push(...acceptanceFailures);
9488
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
9489
+ output = await this.correctOutput(output, acceptanceFailures);
9490
+ const recheck = failures(await this.checkAcceptance(assignment));
9491
+ if (recheck.length > 0) {
9492
+ issues.push(...recheck);
9493
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
9494
+ failed.push(...recheck);
9495
+ this.setStatus("FAILED");
9496
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
9497
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
9498
+ }
9499
+ }
9500
+ for (const result2 of acceptanceResults) {
9501
+ if (result2.verdict === "undecidable") continue;
9502
+ checksRun.push(result2.criterion);
9503
+ if (result2.verdict === "passed") passed.push(result2.criterion);
9504
+ }
9410
9505
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
9411
- const testResult = await this.selfTest(assignment, output);
9506
+ const testResult = await this.selfTest(assignment, output, undecided(acceptanceResults));
9412
9507
  checksRun.push(...testResult.checksRun);
9413
9508
  passed.push(...testResult.passed);
9414
9509
  failed.push(...testResult.failed);
9415
9510
  if (testResult.failed.length > 0) {
9416
- correctionAttempts = 1;
9511
+ correctionAttempts++;
9417
9512
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
9418
9513
  const corrected = await this.correctOutput(output, testResult.failed);
9419
9514
  output = corrected;
@@ -9721,8 +9816,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9721
9816
  // Media generation can run for a minute; without this a cancelled run
9722
9817
  // still pays for an image nobody will see.
9723
9818
  ...this.signal ? { signal: this.signal } : {},
9724
- saveSnapshot: async (path38, content) => {
9725
- this.store?.addFileSnapshot(this.taskId, path38, content);
9819
+ saveSnapshot: async (path39, content) => {
9820
+ this.store?.addFileSnapshot(this.taskId, path39, content);
9726
9821
  },
9727
9822
  sendPeerSync: (to, syncType, content) => {
9728
9823
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -9909,7 +10004,7 @@ ${assignment.expectedOutput}`;
9909
10004
  const { promisify: promisify4 } = await import('util');
9910
10005
  const execAsync3 = promisify4(exec3);
9911
10006
  for (const artifactPath of artifactPaths) {
9912
- const absolutePath = path33__default.default.resolve(process.cwd(), artifactPath);
10007
+ const absolutePath = path34__default.default.resolve(process.cwd(), artifactPath);
9913
10008
  try {
9914
10009
  const stat = await fs11__default.default.stat(absolutePath);
9915
10010
  if (!stat.isFile()) {
@@ -9944,7 +10039,7 @@ ${assignment.expectedOutput}`;
9944
10039
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
9945
10040
  continue;
9946
10041
  }
9947
- const ext = path33__default.default.extname(absolutePath).toLowerCase();
10042
+ const ext = path34__default.default.extname(absolutePath).toLowerCase();
9948
10043
  try {
9949
10044
  if (ext === ".ts" || ext === ".tsx") {
9950
10045
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -10030,14 +10125,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10030
10125
  }
10031
10126
  return current;
10032
10127
  }
10033
- async selfTest(assignment, output) {
10128
+ /**
10129
+ * Run the acceptance criteria that can be settled by looking at the workspace.
10130
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
10131
+ */
10132
+ async checkAcceptance(assignment) {
10133
+ const criteria = assignment.acceptance ?? [];
10134
+ if (!criteria.length) return [];
10135
+ const resolve2 = (target) => path34__default.default.resolve(process.cwd(), target);
10136
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
10137
+ stat: async (target) => {
10138
+ try {
10139
+ const stat = await fs11__default.default.stat(resolve2(target));
10140
+ return stat.isFile() ? { size: stat.size } : null;
10141
+ } catch {
10142
+ return null;
10143
+ }
10144
+ },
10145
+ read: async (target) => {
10146
+ try {
10147
+ const content = await fs11__default.default.readFile(resolve2(target), "utf-8");
10148
+ return content.includes("\uFFFD") ? null : content;
10149
+ } catch {
10150
+ return null;
10151
+ }
10152
+ }
10153
+ });
10154
+ }
10155
+ async selfTest(assignment, output, pendingAcceptance) {
10156
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
10034
10157
  const prompt = `Self-test this output against the assignment requirements.
10035
10158
 
10036
10159
  Assignment: ${assignment.description}
10037
10160
  Expected output: ${assignment.expectedOutput}
10038
10161
  Constraints: ${assignment.constraints.join("; ")}
10039
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
10040
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
10162
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
10163
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
10041
10164
  ` : ""}
10042
10165
  Output to test:
10043
10166
  ${output}
@@ -10104,8 +10227,8 @@ ${output.slice(0, 4e3)}`;
10104
10227
  } catch {
10105
10228
  }
10106
10229
  }
10107
- async correctOutput(originalOutput, failures) {
10108
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
10230
+ async correctOutput(originalOutput, failures2) {
10231
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
10109
10232
 
10110
10233
  Original output:
10111
10234
  ${originalOutput}
@@ -10767,12 +10890,220 @@ function describeGenerationForPlanner(has) {
10767
10890
  return lines.join("\n");
10768
10891
  }
10769
10892
 
10893
+ // src/core/orchestration/task-graph.ts
10894
+ var TaskGraphValidationError = class extends Error {
10895
+ constructor(issues) {
10896
+ super(issues.map((issue) => issue.message).join("; "));
10897
+ this.issues = issues;
10898
+ this.name = "TaskGraphValidationError";
10899
+ }
10900
+ issues;
10901
+ };
10902
+ function normalizeDependencies(input) {
10903
+ if (!input?.length) return [];
10904
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
10905
+ }
10906
+ function cycleNodes(nodes) {
10907
+ const adjacency = new Map(
10908
+ nodes.map((node) => [node.id, node.dependsOn])
10909
+ );
10910
+ const index = /* @__PURE__ */ new Map();
10911
+ const lowlink = /* @__PURE__ */ new Map();
10912
+ const onStack = /* @__PURE__ */ new Set();
10913
+ const stack = [];
10914
+ const inCycle = /* @__PURE__ */ new Set();
10915
+ let counter = 0;
10916
+ const open = (id) => {
10917
+ index.set(id, counter);
10918
+ lowlink.set(id, counter);
10919
+ counter++;
10920
+ stack.push(id);
10921
+ onStack.add(id);
10922
+ };
10923
+ for (const root of nodes) {
10924
+ if (index.has(root.id)) continue;
10925
+ open(root.id);
10926
+ const frames = [{ id: root.id, edge: 0 }];
10927
+ while (frames.length > 0) {
10928
+ const frame = frames[frames.length - 1];
10929
+ const neighbours = adjacency.get(frame.id) ?? [];
10930
+ if (frame.edge < neighbours.length) {
10931
+ const next = neighbours[frame.edge];
10932
+ frame.edge++;
10933
+ if (!adjacency.has(next)) continue;
10934
+ if (!index.has(next)) {
10935
+ open(next);
10936
+ frames.push({ id: next, edge: 0 });
10937
+ } else if (onStack.has(next)) {
10938
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
10939
+ }
10940
+ continue;
10941
+ }
10942
+ frames.pop();
10943
+ const parent = frames[frames.length - 1];
10944
+ if (parent) {
10945
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
10946
+ }
10947
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
10948
+ const component = [];
10949
+ for (; ; ) {
10950
+ const member = stack.pop();
10951
+ onStack.delete(member);
10952
+ component.push(member);
10953
+ if (member === frame.id) break;
10954
+ }
10955
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
10956
+ if (cyclic) for (const member of component) inCycle.add(member);
10957
+ }
10958
+ }
10959
+ }
10960
+ return inCycle;
10961
+ }
10962
+ function throwIfStrict(mode, issues) {
10963
+ if (mode === "strict" && issues.length > 0) {
10964
+ throw new TaskGraphValidationError(issues);
10965
+ }
10966
+ }
10967
+ function compileTaskGraph(inputNodes, options = {}) {
10968
+ const mode = options.mode ?? "strict";
10969
+ const issues = [];
10970
+ if (inputNodes.length === 0) {
10971
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
10972
+ throwIfStrict(mode, issues);
10973
+ }
10974
+ const seen = /* @__PURE__ */ new Set();
10975
+ const nodes = [];
10976
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
10977
+ const input = inputNodes[ordinal];
10978
+ const id = input.id?.trim();
10979
+ const title = input.title?.trim();
10980
+ if (!id || !title) {
10981
+ issues.push({
10982
+ code: "INVALID_NODE",
10983
+ nodeId: id || void 0,
10984
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
10985
+ });
10986
+ continue;
10987
+ }
10988
+ if (seen.has(id)) {
10989
+ issues.push({
10990
+ code: "DUPLICATE_NODE_ID",
10991
+ nodeId: id,
10992
+ message: `Duplicate task graph node id: ${id}`
10993
+ });
10994
+ continue;
10995
+ }
10996
+ seen.add(id);
10997
+ nodes.push({
10998
+ id,
10999
+ title,
11000
+ kind: input.kind,
11001
+ dependsOn: normalizeDependencies(input.dependsOn),
11002
+ payload: input.payload,
11003
+ ordinal
11004
+ });
11005
+ }
11006
+ throwIfStrict(mode, issues);
11007
+ const ids = new Set(nodes.map((node) => node.id));
11008
+ const sanitized = nodes.map((node) => {
11009
+ const dependsOn = [];
11010
+ for (const dependencyId of node.dependsOn) {
11011
+ if (dependencyId === node.id) {
11012
+ issues.push({
11013
+ code: "SELF_DEPENDENCY",
11014
+ nodeId: node.id,
11015
+ dependencyId,
11016
+ message: `Task graph node ${node.id} cannot depend on itself`
11017
+ });
11018
+ continue;
11019
+ }
11020
+ if (!ids.has(dependencyId)) {
11021
+ issues.push({
11022
+ code: "UNKNOWN_DEPENDENCY",
11023
+ nodeId: node.id,
11024
+ dependencyId,
11025
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
11026
+ });
11027
+ continue;
11028
+ }
11029
+ dependsOn.push(dependencyId);
11030
+ }
11031
+ return { ...node, dependsOn };
11032
+ });
11033
+ throwIfStrict(mode, issues);
11034
+ const cyclic = cycleNodes(sanitized);
11035
+ let repaired = sanitized;
11036
+ if (cyclic.size > 0) {
11037
+ const cycleIssues = [];
11038
+ repaired = sanitized.map((node) => {
11039
+ if (!cyclic.has(node.id)) return node;
11040
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
11041
+ if (!cyclic.has(dependencyId)) return true;
11042
+ cycleIssues.push({
11043
+ code: "CYCLE_EDGE_REMOVED",
11044
+ nodeId: node.id,
11045
+ dependencyId,
11046
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
11047
+ });
11048
+ return false;
11049
+ });
11050
+ return { ...node, dependsOn };
11051
+ });
11052
+ issues.push(...cycleIssues);
11053
+ throwIfStrict(mode, cycleIssues);
11054
+ }
11055
+ const remainingCycle = cycleNodes(repaired);
11056
+ if (remainingCycle.size > 0) {
11057
+ throw new TaskGraphValidationError([
11058
+ {
11059
+ code: "INVALID_NODE",
11060
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
11061
+ }
11062
+ ]);
11063
+ }
11064
+ return {
11065
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
11066
+ issues
11067
+ };
11068
+ }
11069
+
11070
+ // src/core/orchestration/adapters.ts
11071
+ function compileSectionGraph(sections) {
11072
+ return compileTaskGraph(
11073
+ sections.map((section) => ({
11074
+ id: section.sectionId,
11075
+ title: section.sectionTitle,
11076
+ kind: "section",
11077
+ dependsOn: section.dependsOn,
11078
+ payload: section
11079
+ })),
11080
+ { mode: "repair" }
11081
+ );
11082
+ }
11083
+ function compileSubtaskGraph(subtasks) {
11084
+ return compileTaskGraph(
11085
+ subtasks.map((subtask) => ({
11086
+ id: subtask.subtaskId,
11087
+ title: subtask.subtaskTitle,
11088
+ kind: "subtask",
11089
+ dependsOn: subtask.dependsOn,
11090
+ payload: subtask
11091
+ })),
11092
+ { mode: "repair" }
11093
+ );
11094
+ }
11095
+
10770
11096
  // src/core/tiers/t2-manager.ts
10771
11097
  function buildT2SystemPrompt(has) {
10772
11098
  const generation = describeGenerationForPlanner(has);
10773
11099
  return [
10774
11100
  "You are a T2 Manager agent in the Cascade AI system.",
10775
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
11101
+ // Must agree with the decomposition prompt in decomposeSection(), which is
11102
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
11103
+ // fully cover it"; this said 2-5. The model received both, so its floor was
11104
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
11105
+ // real worker call on every small section.
11106
+ "Your role is to analyze a section of a task and decompose it into 1-4 discrete subtasks for T3 Workers \u2014 the fewest that fully cover it.",
10776
11107
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
10777
11108
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
10778
11109
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -11168,21 +11499,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11168
11499
  * then reduce in-degrees of their dependents and repeat.
11169
11500
  */
11170
11501
  async runWithDependencies(assignments, workerMap, taskId) {
11502
+ const compiled = compileSubtaskGraph(assignments);
11503
+ for (const issue of compiled.issues) {
11504
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
11505
+ }
11506
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
11507
+ const assignment = node.payload;
11508
+ const original = assignment.dependsOn ?? [];
11509
+ const repaired = node.dependsOn;
11510
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
11511
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
11512
+ });
11171
11513
  const adj = /* @__PURE__ */ new Map();
11172
11514
  const inDegree = /* @__PURE__ */ new Map();
11173
11515
  const resultMap = /* @__PURE__ */ new Map();
11174
- for (const a of assignments) {
11516
+ for (const a of sanitizedAssignments) {
11175
11517
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
11176
11518
  inDegree.set(a.subtaskId, 0);
11177
11519
  }
11178
- for (const a of assignments) {
11179
- const deps = a.dependsOn ?? [];
11180
- for (const dep of deps) {
11520
+ for (const a of sanitizedAssignments) {
11521
+ for (const dep of a.dependsOn ?? []) {
11181
11522
  adj.get(dep).add(a.subtaskId);
11182
11523
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
11183
11524
  }
11184
11525
  }
11185
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
11186
11526
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
11187
11527
  let wave = 0;
11188
11528
  let respawnBudget = 1;
@@ -11389,49 +11729,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11389
11729
  }
11390
11730
  return [...resultMap.values()];
11391
11731
  }
11392
- /**
11393
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
11394
- * by removing back-edges. Returns a sanitized copy of assignments.
11395
- *
11396
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
11397
- * meaning t3 will start without waiting for t1, preventing deadlock.
11398
- */
11399
- breakCycles(assignments, adj, inDegree) {
11400
- const degree = new Map(inDegree);
11401
- const queue = [];
11402
- const visited = /* @__PURE__ */ new Set();
11403
- for (const [id, d] of degree) {
11404
- if (d === 0) queue.push(id);
11405
- }
11406
- while (queue.length > 0) {
11407
- const id = queue.shift();
11408
- visited.add(id);
11409
- for (const dep of adj.get(id) ?? []) {
11410
- const newDeg = (degree.get(dep) ?? 1) - 1;
11411
- degree.set(dep, newDeg);
11412
- if (newDeg === 0) queue.push(dep);
11413
- }
11414
- }
11415
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
11416
- if (cycleNodes.length === 0) return assignments;
11417
- this.log(
11418
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
11419
- );
11420
- return assignments.map((a) => {
11421
- if (!cycleNodes.includes(a.subtaskId)) return a;
11422
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
11423
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
11424
- this.log(
11425
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
11426
- );
11427
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
11428
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
11429
- adj.get(removed)?.delete(a.subtaskId);
11430
- }
11431
- }
11432
- return { ...a, dependsOn: safeDeps };
11433
- });
11434
- }
11435
11732
  async retryT3(assignment, taskId) {
11436
11733
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
11437
11734
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -11570,6 +11867,57 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11570
11867
  // src/core/tiers/t1-administrator.ts
11571
11868
  init_constants();
11572
11869
 
11870
+ // src/core/orchestration/scheduler.ts
11871
+ var DependencyScheduler = class {
11872
+ constructor(graph, hooks, options = {}) {
11873
+ this.graph = graph;
11874
+ this.hooks = hooks;
11875
+ this.options = options;
11876
+ }
11877
+ graph;
11878
+ hooks;
11879
+ options;
11880
+ async run() {
11881
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
11882
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
11883
+ const dependents = /* @__PURE__ */ new Map();
11884
+ for (const node of nodes) dependents.set(node.id, []);
11885
+ for (const node of nodes) {
11886
+ for (const dependencyId of node.dependsOn) {
11887
+ dependents.get(dependencyId)?.push(node.id);
11888
+ }
11889
+ }
11890
+ const completed = /* @__PURE__ */ new Set();
11891
+ const results = /* @__PURE__ */ new Map();
11892
+ const waves = [];
11893
+ let wave = 0;
11894
+ while (completed.size < nodes.length) {
11895
+ const ready = nodes.filter((node) => !completed.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
11896
+ if (ready.length === 0) {
11897
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
11898
+ }
11899
+ waves.push(ready.map((node) => node.id));
11900
+ await this.hooks.onWaveStart?.(ready, wave);
11901
+ const runOne = async (node) => {
11902
+ const result = await this.hooks.execute(node);
11903
+ results.set(node.id, result);
11904
+ completed.add(node.id);
11905
+ for (const dependentId of dependents.get(node.id) ?? []) {
11906
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
11907
+ }
11908
+ await this.hooks.onNodeComplete?.(node, result);
11909
+ };
11910
+ if ((this.options.mode ?? "parallel") === "sequential") {
11911
+ for (const node of ready) await runOne(node);
11912
+ } else {
11913
+ await Promise.all(ready.map(runOne));
11914
+ }
11915
+ wave++;
11916
+ }
11917
+ return { results, waves };
11918
+ }
11919
+ };
11920
+
11573
11921
  // src/utils/json-extract.ts
11574
11922
  function extractFirstJsonObject(input) {
11575
11923
  if (!input) return null;
@@ -11641,7 +11989,7 @@ function buildT1SystemPrompt(has) {
11641
11989
  Your responsibilities:
11642
11990
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
11643
11991
  2. Decompose the task into logical sections (one per T2 Manager)
11644
- 3. For each section, define 2-5 subtasks for T3 Workers
11992
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
11645
11993
  4. Return a structured plan as JSON
11646
11994
 
11647
11995
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -12056,7 +12404,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12056
12404
  }
12057
12405
  }
12058
12406
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
12059
- if (plan.sections.length < min) ;
12407
+ if (plan.sections.length < min || plan.sections.length > max) {
12408
+ this.log(
12409
+ `\u26A0 Plan has ${plan.sections.length} section(s) but complexity "${plan.complexity}" expects ${min}-${max}. Keeping the plan as written \u2014 a right-sized plan is better than one padded to hit a floor.`
12410
+ );
12411
+ }
12060
12412
  }
12061
12413
  async dispatchT2Managers(sections) {
12062
12414
  for (const section of sections) {
@@ -12168,114 +12520,91 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12168
12520
  return t2Results;
12169
12521
  }
12170
12522
  /**
12171
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
12523
+ * Runs T2 managers respecting dependsOn declarations.
12524
+ *
12525
+ * Graph construction, validation and cycle repair now live in
12526
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
12527
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
12528
+ * ordered by one implementation rather than two that drifted.
12529
+ *
12530
+ * The hand-rolled Kahn pass this replaces treated every node it could not
12531
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
12532
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
12533
+ * so breaking a cycle here also cut the dependencies of innocent later
12534
+ * sections and started them immediately — before the sections they consume.
12535
+ * compileTaskGraph uses strongly connected components, so only real cycle
12536
+ * members are touched.
12172
12537
  */
12173
12538
  async runT2sWithDependencies(sections, managers, taskId) {
12174
- const adj = /* @__PURE__ */ new Map();
12175
- const inDegree = /* @__PURE__ */ new Map();
12176
- const resultMap = /* @__PURE__ */ new Map();
12177
- const allKeys = new Set(sections.map((s) => s.sectionId));
12178
- for (const s of sections) {
12179
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
12180
- inDegree.set(s.sectionId, 0);
12181
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
12182
- }
12183
- for (const s of sections) {
12184
- for (const dep of s.dependsOn ?? []) {
12185
- adj.get(dep).add(s.sectionId);
12186
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
12187
- }
12188
- }
12189
- const queue = [];
12190
- const degree = new Map(inDegree);
12191
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
12192
- const visited = /* @__PURE__ */ new Set();
12193
- while (queue.length > 0) {
12194
- const u = queue.shift();
12195
- visited.add(u);
12196
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
12197
- const newDeg = (degree.get(v) ?? 1) - 1;
12198
- degree.set(v, newDeg);
12199
- if (newDeg === 0) queue.push(v);
12200
- }
12201
- }
12202
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
12203
- if (cycleNodes.length > 0) {
12204
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
12205
- for (const s of sections) {
12206
- if (cycleNodes.includes(s.sectionId)) {
12207
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
12208
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
12209
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
12210
- adj.get(removed)?.delete(s.sectionId);
12211
- }
12212
- s.dependsOn = safeDeps;
12213
- }
12214
- }
12539
+ const compiled = compileSectionGraph(sections);
12540
+ for (const issue of compiled.issues) {
12541
+ this.log(`\u26A0 Section graph: ${issue.message}`);
12542
+ }
12543
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
12544
+ for (const section of sections) {
12545
+ const node = byId.get(section.sectionId);
12546
+ if (node) section.dependsOn = [...node.dependsOn];
12215
12547
  }
12216
- const totalSections = sections.length;
12548
+ const resultMap = /* @__PURE__ */ new Map();
12549
+ const totalSections = compiled.graph.nodes.length;
12217
12550
  let completedSections = 0;
12218
- const executeWave = async () => {
12219
- const readyIds = [];
12220
- for (const [id, deg] of inDegree.entries()) {
12221
- if (deg === 0 && !resultMap.has(id)) {
12222
- readyIds.push(id);
12223
- }
12224
- }
12225
- if (readyIds.length === 0) return;
12226
- const runOne = async (id) => {
12227
- resultMap.set(id, null);
12228
- const index = sections.findIndex((s) => s.sectionId === id);
12229
- const section = sections[index];
12230
- const manager = managers[index];
12231
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12232
- this.sendStatusUpdate({
12233
- progressPct,
12234
- currentAction: `T2 working on: ${section.sectionTitle}`,
12235
- status: "IN_PROGRESS"
12236
- });
12237
- this.throwIfCancelled();
12238
- let result;
12239
- try {
12240
- result = await manager.execute(section, taskId, this.signal);
12241
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12242
- if (result.status === "ESCALATED") {
12243
- this.escalations.push({
12244
- raisedBy: `T2_${section.sectionId}`,
12551
+ const scheduler = new DependencyScheduler(
12552
+ compiled.graph,
12553
+ {
12554
+ execute: async (node) => {
12555
+ const section = node.payload;
12556
+ const manager = managers[node.ordinal];
12557
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12558
+ this.sendStatusUpdate({
12559
+ progressPct,
12560
+ currentAction: `T2 working on: ${section.sectionTitle}`,
12561
+ status: "IN_PROGRESS"
12562
+ });
12563
+ this.throwIfCancelled();
12564
+ try {
12565
+ const result = await manager.execute(section, taskId, this.signal);
12566
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12567
+ if (result.status === "ESCALATED") {
12568
+ this.escalations.push({
12569
+ raisedBy: `T2_${section.sectionId}`,
12570
+ sectionId: section.sectionId,
12571
+ attempted: result.issues,
12572
+ blocker: result.issues.join("; "),
12573
+ needs: "Human review required"
12574
+ });
12575
+ }
12576
+ return result;
12577
+ } catch (err) {
12578
+ return {
12245
12579
  sectionId: section.sectionId,
12246
- attempted: result.issues,
12247
- blocker: result.issues.join("; "),
12248
- needs: "Human review required"
12249
- });
12580
+ sectionTitle: section.sectionTitle,
12581
+ status: "FAILED",
12582
+ t3Results: [],
12583
+ sectionSummary: "",
12584
+ issues: [err instanceof Error ? err.message : String(err)]
12585
+ };
12250
12586
  }
12251
- } catch (err) {
12252
- result = {
12253
- sectionId: section.sectionId,
12254
- sectionTitle: section.sectionTitle,
12255
- status: "FAILED",
12256
- t3Results: [],
12257
- sectionSummary: "",
12258
- issues: [err instanceof Error ? err.message : String(err)]
12259
- };
12260
- }
12261
- resultMap.set(id, result);
12262
- completedSections++;
12263
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
12264
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
12265
- }
12266
- };
12267
- if (this.router.getT3ExecutionMode?.() === "sequential") {
12268
- for (const id of readyIds) {
12269
- await runOne(id);
12587
+ },
12588
+ onNodeComplete: (node, result) => {
12589
+ resultMap.set(node.id, result);
12590
+ completedSections++;
12591
+ this.emit("section:complete", {
12592
+ id: node.id,
12593
+ title: result.sectionTitle,
12594
+ status: result.status,
12595
+ output: result.sectionSummary
12596
+ });
12270
12597
  }
12271
- } else {
12272
- await Promise.all(readyIds.map(runOne));
12273
- }
12274
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
12275
- await executeWave();
12598
+ },
12599
+ {
12600
+ // Cross-section concurrency respects the same t3Execution mode as the
12601
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
12602
+ // serialized T3 workers within a single section while every section's
12603
+ // T2Manager (and its T3 workers) still ran in parallel here.
12604
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
12276
12605
  }
12277
- };
12278
- await executeWave();
12606
+ );
12607
+ await scheduler.run();
12279
12608
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
12280
12609
  }
12281
12610
  async compileFinalOutput(originalPrompt, plan, t2Results) {
@@ -12908,16 +13237,16 @@ function resolveInWorkspace(workspaceRoot, input) {
12908
13237
  if (typeof input !== "string" || input.length === 0) {
12909
13238
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
12910
13239
  }
12911
- const root = path33__default.default.resolve(workspaceRoot);
12912
- const abs = path33__default.default.isAbsolute(input) ? path33__default.default.resolve(input) : path33__default.default.resolve(root, input);
12913
- const rel = path33__default.default.relative(root, abs);
12914
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path33__default.default.isAbsolute(rel)) {
13240
+ const root = path34__default.default.resolve(workspaceRoot);
13241
+ const abs = path34__default.default.isAbsolute(input) ? path34__default.default.resolve(input) : path34__default.default.resolve(root, input);
13242
+ const rel = path34__default.default.relative(root, abs);
13243
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34__default.default.isAbsolute(rel)) {
12915
13244
  throw new WorkspaceSandboxError(input, root);
12916
13245
  }
12917
13246
  try {
12918
- const real = fs30__default.default.realpathSync(abs);
12919
- const realRel = path33__default.default.relative(root, real);
12920
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path33__default.default.isAbsolute(realRel))) {
13247
+ const real = fs31__default.default.realpathSync(abs);
13248
+ const realRel = path34__default.default.relative(root, real);
13249
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34__default.default.isAbsolute(realRel))) {
12921
13250
  throw new WorkspaceSandboxError(input, root);
12922
13251
  }
12923
13252
  } catch (e) {
@@ -13570,7 +13899,7 @@ var GenerateDocumentTool = class extends BaseTool {
13570
13899
  }
13571
13900
  }
13572
13901
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
13573
- await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
13902
+ await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
13574
13903
  await fs11__default.default.writeFile(absPath, bytes);
13575
13904
  const charts = countCharts(content);
13576
13905
  return [
@@ -13791,7 +14120,7 @@ var FileWriteTool = class extends BaseTool {
13791
14120
  } catch {
13792
14121
  }
13793
14122
  }
13794
- await fs11__default.default.mkdir(path33__default.default.dirname(absPath), { recursive: true });
14123
+ await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
13795
14124
  await fs11__default.default.writeFile(absPath, content, "utf-8");
13796
14125
  return `Written ${content.length} characters to ${filePath}`;
13797
14126
  }
@@ -14289,7 +14618,7 @@ var ImageAnalyzeTool = class extends BaseTool {
14289
14618
  };
14290
14619
  async function fileToImageAttachment(filePath) {
14291
14620
  const data = await fs11__default.default.readFile(filePath);
14292
- const ext = path33__default.default.extname(filePath).toLowerCase();
14621
+ const ext = path34__default.default.extname(filePath).toLowerCase();
14293
14622
  const mimeMap = {
14294
14623
  ".jpg": "image/jpeg",
14295
14624
  ".jpeg": "image/jpeg",
@@ -14323,14 +14652,14 @@ var PDFCreateTool = class extends BaseTool {
14323
14652
  const filePath = input["path"];
14324
14653
  const content = input["content"];
14325
14654
  const title = input["title"];
14326
- const dir = path33__default.default.dirname(filePath);
14327
- if (!fs30__default.default.existsSync(dir)) {
14328
- fs30__default.default.mkdirSync(dir, { recursive: true });
14655
+ const dir = path34__default.default.dirname(filePath);
14656
+ if (!fs31__default.default.existsSync(dir)) {
14657
+ fs31__default.default.mkdirSync(dir, { recursive: true });
14329
14658
  }
14330
14659
  return new Promise((resolve2, reject) => {
14331
14660
  try {
14332
14661
  const doc = new PDFDocument__default.default({ margin: 50 });
14333
- const stream = fs30__default.default.createWriteStream(filePath);
14662
+ const stream = fs31__default.default.createWriteStream(filePath);
14334
14663
  doc.pipe(stream);
14335
14664
  if (title) {
14336
14665
  doc.info["Title"] = title;
@@ -14408,22 +14737,22 @@ var CodeInterpreterTool = class extends BaseTool {
14408
14737
  }
14409
14738
  cmdPrefix = NODE_CMD;
14410
14739
  }
14411
- const tmpDir = path33__default.default.join(this.workspaceRoot, ".cascade", "tmp");
14412
- if (!fs30__default.default.existsSync(tmpDir)) {
14413
- fs30__default.default.mkdirSync(tmpDir, { recursive: true });
14740
+ const tmpDir = path34__default.default.join(this.workspaceRoot, ".cascade", "tmp");
14741
+ if (!fs31__default.default.existsSync(tmpDir)) {
14742
+ fs31__default.default.mkdirSync(tmpDir, { recursive: true });
14414
14743
  }
14415
14744
  const extension = language === "python" ? "py" : "js";
14416
14745
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
14417
- const filePath = path33__default.default.join(tmpDir, fileName);
14418
- fs30__default.default.writeFileSync(filePath, code, "utf-8");
14746
+ const filePath = path34__default.default.join(tmpDir, fileName);
14747
+ fs31__default.default.writeFileSync(filePath, code, "utf-8");
14419
14748
  const execArgs = [filePath, ...args];
14420
14749
  return new Promise((resolve2) => {
14421
14750
  const startMs = Date.now();
14422
14751
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
14423
14752
  const duration = Date.now() - startMs;
14424
14753
  try {
14425
- if (fs30__default.default.existsSync(filePath)) {
14426
- fs30__default.default.unlinkSync(filePath);
14754
+ if (fs31__default.default.existsSync(filePath)) {
14755
+ fs31__default.default.unlinkSync(filePath);
14427
14756
  }
14428
14757
  } catch (cleanupErr) {
14429
14758
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -14735,7 +15064,7 @@ var GlobTool = class extends BaseTool {
14735
15064
  };
14736
15065
  async execute(input, _options) {
14737
15066
  const pattern = input["pattern"];
14738
- const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15067
+ const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14739
15068
  const matches = await glob.glob(pattern, {
14740
15069
  cwd: searchPath,
14741
15070
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -14748,7 +15077,7 @@ var GlobTool = class extends BaseTool {
14748
15077
  const withMtime = await Promise.all(
14749
15078
  matches.map(async (rel) => {
14750
15079
  try {
14751
- const stat = await fs11__default.default.stat(path33__default.default.join(searchPath, rel));
15080
+ const stat = await fs11__default.default.stat(path34__default.default.join(searchPath, rel));
14752
15081
  return { rel, mtime: stat.mtimeMs };
14753
15082
  } catch {
14754
15083
  return { rel, mtime: 0 };
@@ -14797,7 +15126,7 @@ var GrepTool = class extends BaseTool {
14797
15126
  };
14798
15127
  async execute(input, _options) {
14799
15128
  const pattern = input["pattern"];
14800
- const searchPath = input["path"] ? path33__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15129
+ const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14801
15130
  const globPattern = input["glob"];
14802
15131
  const outputMode = input["output_mode"] ?? "content";
14803
15132
  const context = input["context"] ?? 0;
@@ -14851,12 +15180,12 @@ var GrepTool = class extends BaseTool {
14851
15180
  nodir: true
14852
15181
  });
14853
15182
  } catch {
14854
- files = [path33__default.default.relative(searchPath, searchPath) || "."];
15183
+ files = [path34__default.default.relative(searchPath, searchPath) || "."];
14855
15184
  }
14856
15185
  const results = [];
14857
15186
  let totalCount = 0;
14858
15187
  for (const rel of files) {
14859
- const abs = path33__default.default.join(searchPath, rel);
15188
+ const abs = path34__default.default.join(searchPath, rel);
14860
15189
  let content;
14861
15190
  try {
14862
15191
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -15243,10 +15572,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
15243
15572
  }
15244
15573
  isIgnored(filePath) {
15245
15574
  if (!filePath) return false;
15246
- const abs = path33__default.default.resolve(this.workspaceRoot, filePath);
15247
- const rel = path33__default.default.relative(this.workspaceRoot, abs);
15248
- if (!rel || rel.startsWith("..") || path33__default.default.isAbsolute(rel)) return true;
15249
- const posixRel = rel.split(path33__default.default.sep).join("/");
15575
+ const abs = path34__default.default.resolve(this.workspaceRoot, filePath);
15576
+ const rel = path34__default.default.relative(this.workspaceRoot, abs);
15577
+ if (!rel || rel.startsWith("..") || path34__default.default.isAbsolute(rel)) return true;
15578
+ const posixRel = rel.split(path34__default.default.sep).join("/");
15250
15579
  return this.ignoreMatcher.ignores(posixRel);
15251
15580
  }
15252
15581
  };
@@ -15480,22 +15809,22 @@ var FileMcpOAuthStore = class {
15480
15809
  filePath;
15481
15810
  load() {
15482
15811
  try {
15483
- return JSON.parse(fs30__default.default.readFileSync(this.filePath, "utf-8"));
15812
+ return JSON.parse(fs31__default.default.readFileSync(this.filePath, "utf-8"));
15484
15813
  } catch {
15485
15814
  return void 0;
15486
15815
  }
15487
15816
  }
15488
15817
  save(state) {
15489
- fs30__default.default.mkdirSync(path33__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
15490
- fs30__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15818
+ fs31__default.default.mkdirSync(path34__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
15819
+ fs31__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15491
15820
  try {
15492
- fs30__default.default.chmodSync(this.filePath, 384);
15821
+ fs31__default.default.chmodSync(this.filePath, 384);
15493
15822
  } catch {
15494
15823
  }
15495
15824
  }
15496
15825
  clear() {
15497
15826
  try {
15498
- fs30__default.default.rmSync(this.filePath);
15827
+ fs31__default.default.rmSync(this.filePath);
15499
15828
  } catch {
15500
15829
  }
15501
15830
  }
@@ -15958,6 +16287,14 @@ function heuristicAnalyze(prompt) {
15958
16287
  const estimatedTokens = wordCount * 5;
15959
16288
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
15960
16289
  }
16290
+ function hashPrompt(prompt) {
16291
+ let hash = 2166136261;
16292
+ for (let index = 0; index < prompt.length; index++) {
16293
+ hash ^= prompt.charCodeAt(index);
16294
+ hash = Math.imul(hash, 16777619) >>> 0;
16295
+ }
16296
+ return `${prompt.length}:${hash.toString(36)}`;
16297
+ }
15961
16298
  var analysisCache = /* @__PURE__ */ new Map();
15962
16299
  var TASK_TYPE_TAGS = {
15963
16300
  code: ["code", "instruction"],
@@ -16001,7 +16338,7 @@ var TaskAnalyzer = class {
16001
16338
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
16002
16339
  */
16003
16340
  async analyze(prompt) {
16004
- const cacheKey = prompt.slice(0, 200);
16341
+ const cacheKey = hashPrompt(prompt);
16005
16342
  const cached = analysisCache.get(cacheKey);
16006
16343
  if (cached) {
16007
16344
  this.lastProfile = cached;
@@ -16103,7 +16440,7 @@ var TaskAnalyzer = class {
16103
16440
 
16104
16441
  // src/core/router/model-performance-tracker.ts
16105
16442
  init_pricing();
16106
- var DEFAULT_STATS_FILE = path33__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
16443
+ var DEFAULT_STATS_FILE = path34__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
16107
16444
  var ModelPerformanceTracker = class {
16108
16445
  stats = /* @__PURE__ */ new Map();
16109
16446
  featureStats = /* @__PURE__ */ new Map();
@@ -16143,7 +16480,7 @@ var ModelPerformanceTracker = class {
16143
16480
  async save() {
16144
16481
  if (this.readOnly) return;
16145
16482
  try {
16146
- await fs11__default.default.mkdir(path33__default.default.dirname(this.statsFile), { recursive: true });
16483
+ await fs11__default.default.mkdir(path34__default.default.dirname(this.statsFile), { recursive: true });
16147
16484
  const modelsObj = {};
16148
16485
  const featuresObj = {};
16149
16486
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -16691,7 +17028,7 @@ Required capability: ${description.slice(0, 300)}`;
16691
17028
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
16692
17029
  async loadPersistedTools() {
16693
17030
  if (!this.workspacePath || !this.persistEnabled) return;
16694
- const file = path33__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17031
+ const file = path34__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
16695
17032
  try {
16696
17033
  const raw = await fs11__default.default.readFile(file, "utf-8");
16697
17034
  const specs = JSON.parse(raw);
@@ -16715,8 +17052,8 @@ Required capability: ${description.slice(0, 300)}`;
16715
17052
  }
16716
17053
  async persist() {
16717
17054
  if (!this.workspacePath || !this.persistEnabled) return;
16718
- const dir = path33__default.default.join(this.workspacePath, ".cascade");
16719
- const file = path33__default.default.join(dir, DYNAMIC_TOOLS_FILE);
17055
+ const dir = path34__default.default.join(this.workspacePath, ".cascade");
17056
+ const file = path34__default.default.join(dir, DYNAMIC_TOOLS_FILE);
16720
17057
  try {
16721
17058
  await fs11__default.default.mkdir(dir, { recursive: true });
16722
17059
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -16736,12 +17073,12 @@ var WorldStateDB = class {
16736
17073
  constructor(workspacePath, debugMode = false) {
16737
17074
  this.workspacePath = workspacePath;
16738
17075
  this.debugMode = debugMode;
16739
- const cascadeDir = path33__default.default.join(workspacePath, ".cascade");
16740
- if (!fs30__default.default.existsSync(cascadeDir)) {
16741
- fs30__default.default.mkdirSync(cascadeDir, { recursive: true });
17076
+ const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
17077
+ if (!fs31__default.default.existsSync(cascadeDir)) {
17078
+ fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
16742
17079
  }
16743
- this.keyPath = path33__default.default.join(cascadeDir, "world_state.key");
16744
- this.dbPath = path33__default.default.join(cascadeDir, "world_state.db");
17080
+ this.keyPath = path34__default.default.join(cascadeDir, "world_state.key");
17081
+ this.dbPath = path34__default.default.join(cascadeDir, "world_state.db");
16745
17082
  this.initEncryptionKey();
16746
17083
  this.db = new Database2__default.default(this.dbPath);
16747
17084
  this.db.pragma("journal_mode = WAL");
@@ -16798,11 +17135,11 @@ var WorldStateDB = class {
16798
17135
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
16799
17136
  }
16800
17137
  initEncryptionKey() {
16801
- if (fs30__default.default.existsSync(this.keyPath)) {
16802
- this.encryptionKey = fs30__default.default.readFileSync(this.keyPath);
17138
+ if (fs31__default.default.existsSync(this.keyPath)) {
17139
+ this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
16803
17140
  } else {
16804
17141
  this.encryptionKey = crypto__default.default.randomBytes(32);
16805
- fs30__default.default.writeFileSync(this.keyPath, this.encryptionKey);
17142
+ fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
16806
17143
  }
16807
17144
  }
16808
17145
  encrypt(text) {
@@ -17088,9 +17425,9 @@ var WorldStateDB = class {
17088
17425
  dumpDebugIfNeeded() {
17089
17426
  if (!this.debugMode) return;
17090
17427
  try {
17091
- const dumpPath = path33__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
17428
+ const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
17092
17429
  const entries = this.getAllEntries();
17093
- fs30__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17430
+ fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17094
17431
  } catch (err) {
17095
17432
  console.error("Failed to dump debug world state", err);
17096
17433
  }
@@ -17099,6 +17436,128 @@ var WorldStateDB = class {
17099
17436
  this.db.close();
17100
17437
  }
17101
17438
  };
17439
+ var RESUME_SCHEMA_VERSION = 1;
17440
+ var DEFAULT_MAX_CHECKPOINTS = 5;
17441
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
17442
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
17443
+ function safeId(taskId) {
17444
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
17445
+ return cleaned.slice(0, 100) || "run";
17446
+ }
17447
+ function isCheckpoint(value) {
17448
+ if (typeof value !== "object" || value === null) return false;
17449
+ const c = value;
17450
+ return c.version === RESUME_SCHEMA_VERSION && typeof c.taskId === "string" && typeof c.prompt === "string" && typeof c.createdAt === "string" && typeof c.partialOutput === "string" && Array.isArray(c.completed);
17451
+ }
17452
+ var ResumeStore = class {
17453
+ dir;
17454
+ maxCheckpoints;
17455
+ maxAgeMs;
17456
+ now;
17457
+ constructor(options) {
17458
+ this.dir = options.dir;
17459
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
17460
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
17461
+ this.now = options.now ?? Date.now;
17462
+ }
17463
+ /**
17464
+ * Persist a checkpoint and prune. Never throws: failing to save a resume file
17465
+ * must not turn a recoverable stop into a crash, which would be a strictly
17466
+ * worse outcome than the one this store exists to improve.
17467
+ */
17468
+ async save(checkpoint) {
17469
+ try {
17470
+ const timestamp = this.now();
17471
+ const record = {
17472
+ ...checkpoint,
17473
+ version: RESUME_SCHEMA_VERSION,
17474
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
17475
+ };
17476
+ await fs31.promises.mkdir(this.dir, { recursive: true });
17477
+ const finalPath = path34__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
17478
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
17479
+ await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
17480
+ await fs31.promises.rename(tempPath, finalPath);
17481
+ await this.prune();
17482
+ } catch {
17483
+ }
17484
+ }
17485
+ /** Valid, unexpired checkpoints, newest first. */
17486
+ async list() {
17487
+ let entries;
17488
+ try {
17489
+ entries = await fs31.promises.readdir(this.dir);
17490
+ } catch {
17491
+ return [];
17492
+ }
17493
+ const cutoff = this.now() - this.maxAgeMs;
17494
+ const candidates2 = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).filter((e) => e.timestamp >= cutoff).sort((a, b) => b.timestamp - a.timestamp);
17495
+ const checkpoints = [];
17496
+ for (const candidate of candidates2) {
17497
+ try {
17498
+ const raw = await fs31.promises.readFile(path34__default.default.join(this.dir, candidate.name), "utf-8");
17499
+ const parsed = JSON.parse(raw);
17500
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
17501
+ } catch {
17502
+ continue;
17503
+ }
17504
+ }
17505
+ return checkpoints;
17506
+ }
17507
+ /** The most recent resumable run, or null. */
17508
+ async latest() {
17509
+ return (await this.list())[0] ?? null;
17510
+ }
17511
+ /**
17512
+ * Load a checkpoint and delete it in the same step. Resuming twice from one
17513
+ * checkpoint would re-run restored work, so consumption is part of reading.
17514
+ */
17515
+ async consume(taskId) {
17516
+ const all = await this.list();
17517
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
17518
+ if (!target) return null;
17519
+ await this.delete(target.taskId);
17520
+ return target;
17521
+ }
17522
+ /** Remove every checkpoint for a task id. */
17523
+ async delete(taskId) {
17524
+ try {
17525
+ const entries = await fs31.promises.readdir(this.dir);
17526
+ const wanted = safeId(taskId);
17527
+ await Promise.all(entries.map(async (name) => {
17528
+ const match = FILE_PATTERN.exec(name);
17529
+ if (match?.[2] === wanted) {
17530
+ await fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true });
17531
+ }
17532
+ }));
17533
+ } catch {
17534
+ }
17535
+ }
17536
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
17537
+ async prune() {
17538
+ try {
17539
+ const entries = await fs31.promises.readdir(this.dir);
17540
+ const cutoff = this.now() - this.maxAgeMs;
17541
+ const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
17542
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
17543
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
17544
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
17545
+ (name) => fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true })
17546
+ ));
17547
+ } catch {
17548
+ }
17549
+ }
17550
+ };
17551
+ function summarizeCompleted(completed, perNodeChars = 500) {
17552
+ if (!completed.length) return "";
17553
+ const lines = completed.map((node) => {
17554
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
17555
+ return `- [${node.status}] ${node.title}
17556
+ ${body.replace(/\n/g, "\n ")}`;
17557
+ });
17558
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
17559
+ ${lines.join("\n")}`;
17560
+ }
17102
17561
  var ignore3 = _ignoreModule__namespace.default ?? _ignoreModule__namespace;
17103
17562
  var PrivacyPaths = class {
17104
17563
  localOnly;
@@ -17460,7 +17919,7 @@ var WorkspaceIndex = class {
17460
17919
  isIgnored;
17461
17920
  chunker;
17462
17921
  constructor(opts) {
17463
- this.root = path33__default.default.resolve(opts.root);
17922
+ this.root = path34__default.default.resolve(opts.root);
17464
17923
  this.db = opts.db;
17465
17924
  this.namespace = opts.namespace ?? "code";
17466
17925
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -17492,14 +17951,14 @@ var WorkspaceIndex = class {
17492
17951
  return;
17493
17952
  }
17494
17953
  for (const entry of entries) {
17495
- const abs = path33__default.default.join(dir, entry.name);
17954
+ const abs = path34__default.default.join(dir, entry.name);
17496
17955
  if (entry.isDirectory()) {
17497
17956
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
17498
17957
  await walk(abs);
17499
17958
  } else if (entry.isFile()) {
17500
- const ext = path33__default.default.extname(entry.name).slice(1).toLowerCase();
17959
+ const ext = path34__default.default.extname(entry.name).slice(1).toLowerCase();
17501
17960
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
17502
- out.push(path33__default.default.relative(this.root, abs));
17961
+ out.push(path34__default.default.relative(this.root, abs));
17503
17962
  }
17504
17963
  }
17505
17964
  };
@@ -17529,7 +17988,7 @@ var WorkspaceIndex = class {
17529
17988
  const entries = [];
17530
17989
  for (const rel of rels) {
17531
17990
  try {
17532
- const abs = path33__default.default.join(this.root, rel);
17991
+ const abs = path34__default.default.join(this.root, rel);
17533
17992
  const stat = await fs11__default.default.stat(abs);
17534
17993
  if (stat.size > this.maxFileBytes) continue;
17535
17994
  const buf = await fs11__default.default.readFile(abs);
@@ -18067,8 +18526,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18067
18526
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
18068
18527
  decisionLog = [];
18069
18528
  initialized = false;
18070
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
18529
+ /**
18530
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
18531
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
18532
+ * so a crash or an exit no longer throws away everything already finished.
18533
+ */
18071
18534
  lastInterruptedRun;
18535
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
18536
+ resumeStore;
18537
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
18538
+ currentRunCompleted = [];
18072
18539
  initPromise;
18073
18540
  store;
18074
18541
  audit;
@@ -18101,10 +18568,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18101
18568
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
18102
18569
  try {
18103
18570
  this.router.setDeadModelStore(new DeadModelStore(
18104
- fileDeadModelPersistence(path33__default.default.join(workspacePath, ".cascade", "dead-models.json"))
18571
+ fileDeadModelPersistence(path34__default.default.join(workspacePath, ".cascade", "dead-models.json"))
18105
18572
  ));
18106
18573
  } catch {
18107
18574
  }
18575
+ try {
18576
+ this.resumeStore = new ResumeStore({
18577
+ dir: path34__default.default.join(workspacePath, ".cascade", "resume")
18578
+ });
18579
+ } catch {
18580
+ }
18108
18581
  this.multimodal = new MultimodalRegistry(
18109
18582
  (this.config.providers ?? []).map((p) => p.type)
18110
18583
  );
@@ -18189,8 +18662,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
18189
18662
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
18190
18663
  return;
18191
18664
  }
18192
- const dbPath = ci.dbPath || path33__default.default.join(this.workspacePath, ".cascade", "code-index.db");
18193
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
18665
+ const dbPath = ci.dbPath || path34__default.default.join(this.workspacePath, ".cascade", "code-index.db");
18666
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
18194
18667
  const db = new Database2__default.default(dbPath);
18195
18668
  db.pragma("journal_mode = WAL");
18196
18669
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -18570,10 +19043,43 @@ ${input}` }],
18570
19043
  setFeedbackSource(source) {
18571
19044
  this.router.setFeedbackSource(source);
18572
19045
  }
18573
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
19046
+ /**
19047
+ * Write a durable checkpoint for an interrupted run.
19048
+ *
19049
+ * Best-effort throughout: a run is already ending badly on every path that
19050
+ * calls this, and failing to record a resume file must not make that ending
19051
+ * worse. Nothing here is allowed to throw.
19052
+ */
19053
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
19054
+ if (!this.currentRunCompleted.length && !partialOutput) return;
19055
+ try {
19056
+ await this.resumeStore?.save({
19057
+ taskId,
19058
+ prompt,
19059
+ reason,
19060
+ ...detail ? { detail } : {},
19061
+ partialOutput,
19062
+ completed: [...this.currentRunCompleted]
19063
+ });
19064
+ } catch {
19065
+ }
19066
+ }
19067
+ /** True when a task can be resumed via /continue (this process or a previous one). */
18574
19068
  hasResumableRun() {
18575
19069
  return this.lastInterruptedRun != null;
18576
19070
  }
19071
+ /**
19072
+ * True when a checkpoint from ANY process is resumable. Async because the
19073
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
19074
+ * answer for callers already on a hot path.
19075
+ */
19076
+ async hasDurableResume() {
19077
+ return await this.resumeStore?.latest() != null;
19078
+ }
19079
+ /** Interrupted runs available to resume, newest first. */
19080
+ async listResumableRuns() {
19081
+ return await this.resumeStore?.list() ?? [];
19082
+ }
18577
19083
  /**
18578
19084
  * Raise the per-run token budget for a resume and return the continuation
18579
19085
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -18584,22 +19090,54 @@ ${input}` }],
18584
19090
  const last = this.lastInterruptedRun;
18585
19091
  if (!last) return null;
18586
19092
  this.lastInterruptedRun = void 0;
18587
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
19093
+ this.raiseBudgetForResume(opts.maxTokens);
19094
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
19095
+ }
19096
+ /**
19097
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
19098
+ * process, including one that crashed before it could return. Consumes the
19099
+ * checkpoint, so the same finished work is never restored into two runs.
19100
+ */
19101
+ async prepareDurableResume(opts = {}) {
19102
+ const checkpoint = await this.resumeStore?.consume(opts.taskId);
19103
+ if (!checkpoint) return null;
19104
+ this.lastInterruptedRun = void 0;
19105
+ this.raiseBudgetForResume(opts.maxTokens);
19106
+ return this.buildResumePrompt(checkpoint.prompt, checkpoint.partialOutput, checkpoint.completed);
19107
+ }
19108
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
19109
+ raiseBudgetForResume(maxTokens) {
19110
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
18588
19111
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
18589
19112
  this.router.setMaxTokensPerRun(raised);
18590
- return `Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Complete only the remaining work.
18591
-
18592
- Original task: ${last.prompt}` + (last.partialOutput ? `
18593
-
19113
+ }
19114
+ /**
19115
+ * The resume prompt. Completed sections are named explicitly rather than
19116
+ * gestured at: "do not recreate the files" is an instruction a planner can
19117
+ * only guess at, whereas a list of what is already done, and what it produced,
19118
+ * is something it can act on. That difference is the entire reason node
19119
+ * results are kept — a re-plan that cannot see the finished work will plan the
19120
+ * finished work again, and the user pays for it a second time.
19121
+ */
19122
+ buildResumePrompt(prompt, partialOutput, completed) {
19123
+ const done = summarizeCompleted(completed);
19124
+ return [
19125
+ "Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Plan ONLY the remaining work.",
19126
+ "",
19127
+ `Original task: ${prompt}`,
19128
+ done ? `
19129
+ ${done}` : "",
19130
+ partialOutput ? `
18594
19131
  Partial result so far:
18595
- ${last.partialOutput}` : "");
19132
+ ${partialOutput}` : ""
19133
+ ].filter(Boolean).join("\n");
18596
19134
  }
18597
19135
  /**
18598
19136
  * Resume the last budget-capped task with a raised budget (SDK/headless).
18599
19137
  * Returns null when there is nothing to resume.
18600
19138
  */
18601
19139
  async resumeRun(opts = {}) {
18602
- const prompt = this.prepareResume(opts);
19140
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
18603
19141
  if (!prompt) return null;
18604
19142
  return this.run({ prompt });
18605
19143
  }
@@ -18970,6 +19508,7 @@ ${prompt}` : prompt;
18970
19508
  const startMs = Date.now();
18971
19509
  const taskId = crypto.randomUUID();
18972
19510
  this.decisionLog = [];
19511
+ this.currentRunCompleted = [];
18973
19512
  if (options.fastAnswer) {
18974
19513
  return this.runFastAnswer(options, startMs, taskId);
18975
19514
  }
@@ -19084,6 +19623,11 @@ ${prompt}` : prompt;
19084
19623
  });
19085
19624
  tier.on("log", (e) => this.emit("log", e));
19086
19625
  tier.on("tier:status", (e) => this.emit("tier:status", e));
19626
+ tier.on("section:complete", (e) => {
19627
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
19628
+ this.currentRunCompleted.push(e);
19629
+ this.emit("section:complete", e);
19630
+ });
19087
19631
  tier.on("tool:call", (e) => this.emit("tool:call", e));
19088
19632
  tier.on("tool:result", (e) => this.emit("tool:result", e));
19089
19633
  tier.on("tool:approval-request", async (request) => {
@@ -19230,6 +19774,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
19230
19774
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
19231
19775
  const trip = runBreaker.reason();
19232
19776
  if (trip) {
19777
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
19233
19778
  finalOutput = [
19234
19779
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
19235
19780
  "",
@@ -19249,6 +19794,13 @@ ${finalOutput}` : ""
19249
19794
  reason: err instanceof Error ? err.message : "Task cancelled",
19250
19795
  partialOutput: finalOutput || ""
19251
19796
  });
19797
+ await this.checkpointRun(
19798
+ taskId,
19799
+ options.prompt,
19800
+ finalOutput || "",
19801
+ "cancelled",
19802
+ err instanceof Error ? err.message : "Task cancelled"
19803
+ );
19252
19804
  runError = null;
19253
19805
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
19254
19806
  this.emit("run:budget-exceeded", {
@@ -19257,9 +19809,17 @@ ${finalOutput}` : ""
19257
19809
  partialOutput: finalOutput || ""
19258
19810
  });
19259
19811
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
19812
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
19260
19813
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
19261
19814
  runError = null;
19262
19815
  } else {
19816
+ await this.checkpointRun(
19817
+ taskId,
19818
+ options.prompt,
19819
+ finalOutput || "",
19820
+ "error",
19821
+ err instanceof Error ? err.message : String(err)
19822
+ );
19263
19823
  runError = err;
19264
19824
  throw err;
19265
19825
  }
@@ -19471,14 +20031,14 @@ ${finalOutput}` : ""
19471
20031
  init_constants();
19472
20032
  var CLOUD_SESSION_FILE = "cloud-session.json";
19473
20033
  function globalDir(homeDir = os10__default.default.homedir()) {
19474
- return path33__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20034
+ return path34__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
19475
20035
  }
19476
20036
  function cloudSessionPath(dir = globalDir()) {
19477
- return path33__default.default.join(dir, CLOUD_SESSION_FILE);
20037
+ return path34__default.default.join(dir, CLOUD_SESSION_FILE);
19478
20038
  }
19479
20039
  function loadCloudSession(dir = globalDir()) {
19480
20040
  try {
19481
- const raw = fs30__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20041
+ const raw = fs31__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
19482
20042
  const parsed = JSON.parse(raw);
19483
20043
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
19484
20044
  return parsed;
@@ -19487,17 +20047,17 @@ function loadCloudSession(dir = globalDir()) {
19487
20047
  }
19488
20048
  }
19489
20049
  function saveCloudSession(session, dir = globalDir()) {
19490
- fs30__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
20050
+ fs31__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
19491
20051
  const file = cloudSessionPath(dir);
19492
- fs30__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20052
+ fs31__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
19493
20053
  try {
19494
- fs30__default.default.chmodSync(file, 384);
20054
+ fs31__default.default.chmodSync(file, 384);
19495
20055
  } catch {
19496
20056
  }
19497
20057
  }
19498
20058
  function clearCloudSession(dir = globalDir()) {
19499
20059
  try {
19500
- fs30__default.default.rmSync(cloudSessionPath(dir));
20060
+ fs31__default.default.rmSync(cloudSessionPath(dir));
19501
20061
  } catch {
19502
20062
  }
19503
20063
  }
@@ -19669,16 +20229,16 @@ var CloudClient = class _CloudClient {
19669
20229
  return (await this.refresh()).accessToken;
19670
20230
  }
19671
20231
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
19672
- async authed(path38, init = {}) {
19673
- const call = (token) => fetch(this.url(path38), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
20232
+ async authed(path39, init = {}) {
20233
+ const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
19674
20234
  let res = await call(await this.accessToken());
19675
20235
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
19676
20236
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
19677
20237
  const text = await res.text();
19678
20238
  return text ? JSON.parse(text) : {};
19679
20239
  }
19680
- authedGet(path38) {
19681
- return this.authed(path38);
20240
+ authedGet(path39) {
20241
+ return this.authed(path39);
19682
20242
  }
19683
20243
  async fetchMe(token) {
19684
20244
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21453,9 +22013,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21453
22013
  }
21454
22014
  } catch {
21455
22015
  }
21456
- const configPath = path33__default.default.join(workspacePath, ".cascade", "config.json");
22016
+ const configPath = path34__default.default.join(workspacePath, ".cascade", "config.json");
21457
22017
  try {
21458
- await fs11__default.default.mkdir(path33__default.default.dirname(configPath), { recursive: true });
22018
+ await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
21459
22019
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
21460
22020
  } catch (err) {
21461
22021
  const msg = err instanceof Error ? err.message : String(err);
@@ -21507,9 +22067,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21507
22067
  if (msg.includes("non-text parts")) return;
21508
22068
  originalLog(...args);
21509
22069
  };
21510
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
22070
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
21511
22071
  storeRef.current = store;
21512
- globalStoreRef.current = new MemoryStore(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22072
+ globalStoreRef.current = new MemoryStore(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
21513
22073
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
21514
22074
  setIdentities(identityRows);
21515
22075
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -21577,7 +22137,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21577
22137
  onThemeChange: (name) => setTheme(getTheme(name)),
21578
22138
  onExport: async (fmt) => {
21579
22139
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
21580
- const exportPath = path33__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22140
+ const exportPath = path34__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
21581
22141
  if (fmt === "json") {
21582
22142
  await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
21583
22143
  } else {
@@ -21819,10 +22379,12 @@ ${lastUser.content}`;
21819
22379
  onContinue: async (args) => {
21820
22380
  const cascade = cascadeRef.current;
21821
22381
  if (!cascade) return "Not ready yet.";
21822
- if (!cascade.hasResumableRun()) return "Nothing to continue \u2014 no task was stopped by the budget cap.";
21823
22382
  const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
21824
- const prompt = cascade.prepareResume(Number.isFinite(n) && n > 0 ? { maxTokens: n } : {});
21825
- if (!prompt) return "Nothing to continue.";
22383
+ const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
22384
+ const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
22385
+ if (!prompt) {
22386
+ return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
22387
+ }
21826
22388
  await handleSubmit(prompt);
21827
22389
  return "";
21828
22390
  }
@@ -22475,7 +23037,7 @@ function stringifySlashOutput(val) {
22475
23037
  }
22476
23038
  async function searchSessionsAndMessages(query, workspacePath) {
22477
23039
  if (!query) return "Usage: /search <query>";
22478
- const dbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
23040
+ const dbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
22479
23041
  try {
22480
23042
  await fs11__default.default.access(dbPath);
22481
23043
  } catch {
@@ -22509,7 +23071,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
22509
23071
  async function diagnoseRuntime(config, workspacePath) {
22510
23072
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
22511
23073
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
22512
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23074
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22513
23075
  try {
22514
23076
  const sessions2 = store.listSessions(void 0, 3);
22515
23077
  return [
@@ -22528,7 +23090,7 @@ async function diagnoseRuntime(config, workspacePath) {
22528
23090
  }
22529
23091
  async function showRecentLogs(args, workspacePath) {
22530
23092
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
22531
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23093
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22532
23094
  try {
22533
23095
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
22534
23096
  if (!logs.length) return "No recent runtime logs.";
@@ -22540,7 +23102,7 @@ async function showRecentLogs(args, workspacePath) {
22540
23102
  async function loadSessionSnapshot(args, workspacePath) {
22541
23103
  const sessionId = args[0];
22542
23104
  if (!sessionId) return "Usage: /resume <sessionId>";
22543
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
23105
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
22544
23106
  try {
22545
23107
  const session = store.getSession(sessionId);
22546
23108
  if (!session) return `Session not found: ${sessionId}`;
@@ -22968,9 +23530,9 @@ function SetupWizard({ workspacePath, onComplete }) {
22968
23530
  ...anyAuto ? { cascadeAuto: true } : {}
22969
23531
  };
22970
23532
  const config = CascadeConfigSchema.parse(rawConfig);
22971
- const configDir = path33__default.default.join(workspacePath, ".cascade");
23533
+ const configDir = path34__default.default.join(workspacePath, ".cascade");
22972
23534
  await fs11__default.default.mkdir(configDir, { recursive: true });
22973
- const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
23535
+ const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
22974
23536
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22975
23537
  savedConfigRef.current = config;
22976
23538
  dispatchRef.current({ type: "GO_DONE" });
@@ -23312,14 +23874,14 @@ function printTelemetryBanner() {
23312
23874
  async function initCommand(workspacePath = process.cwd()) {
23313
23875
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
23314
23876
  try {
23315
- const configDir = path33__default.default.join(workspacePath, ".cascade");
23877
+ const configDir = path34__default.default.join(workspacePath, ".cascade");
23316
23878
  await fs11__default.default.mkdir(configDir, { recursive: true });
23317
- const mdPath = path33__default.default.join(workspacePath, "CASCADE.md");
23879
+ const mdPath = path34__default.default.join(workspacePath, "CASCADE.md");
23318
23880
  if (!await fileExists(mdPath)) {
23319
23881
  await createDefaultCascadeMd(workspacePath);
23320
23882
  spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
23321
23883
  }
23322
- const ignorePath = path33__default.default.join(workspacePath, ".cascadeignore");
23884
+ const ignorePath = path34__default.default.join(workspacePath, ".cascadeignore");
23323
23885
  if (!await fileExists(ignorePath)) {
23324
23886
  await createDefaultIgnoreFile(workspacePath);
23325
23887
  spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
@@ -23328,7 +23890,7 @@ async function initCommand(workspacePath = process.cwd()) {
23328
23890
  console.log();
23329
23891
  console.log(chalk7__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
23330
23892
  console.log();
23331
- const configPath = path33__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
23893
+ const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
23332
23894
  if (await fileExists(configPath)) {
23333
23895
  console.log(chalk7__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
23334
23896
  console.log();
@@ -23386,7 +23948,7 @@ function fromEnv(env) {
23386
23948
  return out;
23387
23949
  }
23388
23950
  async function fromClaudeCode(home) {
23389
- const file = path33__default.default.join(home, ".claude", ".credentials.json");
23951
+ const file = path34__default.default.join(home, ".claude", ".credentials.json");
23390
23952
  const data = await readJson(file);
23391
23953
  if (!data) return [];
23392
23954
  const oauth = data["claudeAiOauth"];
@@ -23409,7 +23971,7 @@ async function fromClaudeCode(home) {
23409
23971
  return [];
23410
23972
  }
23411
23973
  async function fromCodex(home) {
23412
- const file = path33__default.default.join(home, ".codex", "auth.json");
23974
+ const file = path34__default.default.join(home, ".codex", "auth.json");
23413
23975
  const data = await readJson(file);
23414
23976
  if (!data) return [];
23415
23977
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -23432,7 +23994,7 @@ async function fromCodex(home) {
23432
23994
  return [];
23433
23995
  }
23434
23996
  async function fromGemini(home) {
23435
- const file = path33__default.default.join(home, ".gemini", "oauth_creds.json");
23997
+ const file = path34__default.default.join(home, ".gemini", "oauth_creds.json");
23436
23998
  const data = await readJson(file);
23437
23999
  const accessToken = str(data?.["access_token"]);
23438
24000
  if (!accessToken) return [];
@@ -23448,7 +24010,7 @@ async function fromGemini(home) {
23448
24010
  }
23449
24011
  async function fromCopilot(home) {
23450
24012
  for (const name of ["apps.json", "hosts.json"]) {
23451
- const file = path33__default.default.join(home, ".config", "github-copilot", name);
24013
+ const file = path34__default.default.join(home, ".config", "github-copilot", name);
23452
24014
  const data = await readJson(file);
23453
24015
  if (!data) continue;
23454
24016
  for (const value of Object.values(data)) {
@@ -23502,7 +24064,7 @@ async function doctorCommand() {
23502
24064
  checks.push({
23503
24065
  label: "Cascade config",
23504
24066
  ok: true,
23505
- detail: `Loaded ${path33__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24067
+ detail: `Loaded ${path34__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
23506
24068
  });
23507
24069
  const providers = [
23508
24070
  { type: "anthropic", name: "Anthropic" },
@@ -23585,17 +24147,17 @@ async function doctorCommand() {
23585
24147
  const detail = c.detail ? chalk7__default.default.gray(` \u2014 ${c.detail}`) : "";
23586
24148
  console.log(`${icon} ${label}${detail}`);
23587
24149
  }
23588
- const failures = checks.filter((c) => !c.ok);
24150
+ const failures2 = checks.filter((c) => !c.ok);
23589
24151
  console.log();
23590
- if (failures.length === 0) {
24152
+ if (failures2.length === 0) {
23591
24153
  console.log(chalk7__default.default.green(" All checks passed!\n"));
23592
24154
  } else {
23593
- const critical = failures.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
24155
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
23594
24156
  if (critical.length) {
23595
24157
  console.log(chalk7__default.default.yellow(` ${critical.length} issue(s) need attention.
23596
24158
  `));
23597
24159
  } else {
23598
- console.log(chalk7__default.default.gray(` ${failures.length} optional item(s) not configured.
24160
+ console.log(chalk7__default.default.gray(` ${failures2.length} optional item(s) not configured.
23599
24161
  `));
23600
24162
  }
23601
24163
  }
@@ -23613,7 +24175,7 @@ async function checkEndpoint(url) {
23613
24175
  }
23614
24176
  }
23615
24177
  async function indexCommand(dirPath) {
23616
- const workspace = path33__default.default.resolve(dirPath || process.cwd());
24178
+ const workspace = path34__default.default.resolve(dirPath || process.cwd());
23617
24179
  const cm = new ConfigManager(workspace);
23618
24180
  await cm.load();
23619
24181
  const config = cm.getConfig();
@@ -23624,8 +24186,8 @@ async function indexCommand(dirPath) {
23624
24186
  process.exitCode = 1;
23625
24187
  return;
23626
24188
  }
23627
- const dbPath = config.codeIndex?.dbPath || path33__default.default.join(workspace, ".cascade", "code-index.db");
23628
- fs30__default.default.mkdirSync(path33__default.default.dirname(dbPath), { recursive: true });
24189
+ const dbPath = config.codeIndex?.dbPath || path34__default.default.join(workspace, ".cascade", "code-index.db");
24190
+ fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
23629
24191
  const db = new Database2__default.default(dbPath);
23630
24192
  db.pragma("journal_mode = WAL");
23631
24193
  const complete = chatCompleterFromProviders(config.providers);
@@ -24114,7 +24676,7 @@ function openBrowser2(url) {
24114
24676
  }
24115
24677
  function storePathFor(name) {
24116
24678
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
24117
- return path33__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
24679
+ return path34__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
24118
24680
  }
24119
24681
  function defaultName(url) {
24120
24682
  try {
@@ -24616,7 +25178,7 @@ function aggregateCostStats(sessions2, opts = {}) {
24616
25178
  }
24617
25179
 
24618
25180
  // src/dashboard/server.ts
24619
- var __dirname$1 = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
25181
+ var __dirname$1 = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
24620
25182
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
24621
25183
  var DashboardServer = class {
24622
25184
  app;
@@ -24896,14 +25458,14 @@ var DashboardServer = class {
24896
25458
  */
24897
25459
  persistConfig() {
24898
25460
  try {
24899
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
24900
- fs30__default.default.mkdirSync(path33__default.default.dirname(configPath), { recursive: true });
24901
- fs30__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
25461
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
25462
+ fs31__default.default.mkdirSync(path34__default.default.dirname(configPath), { recursive: true });
25463
+ fs31__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
24902
25464
  } catch (err) {
24903
25465
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
24904
25466
  }
24905
25467
  try {
24906
- saveGlobalCredentials(path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
25468
+ saveGlobalCredentials(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
24907
25469
  } catch (err) {
24908
25470
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
24909
25471
  }
@@ -24918,15 +25480,15 @@ var DashboardServer = class {
24918
25480
  resolveDashboardSecret() {
24919
25481
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
24920
25482
  if (fromConfig) return fromConfig;
24921
- const secretPath = path33__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
25483
+ const secretPath = path34__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
24922
25484
  try {
24923
- if (fs30__default.default.existsSync(secretPath)) {
24924
- const existing = fs30__default.default.readFileSync(secretPath, "utf-8").trim();
25485
+ if (fs31__default.default.existsSync(secretPath)) {
25486
+ const existing = fs31__default.default.readFileSync(secretPath, "utf-8").trim();
24925
25487
  if (existing.length >= 16) return existing;
24926
25488
  }
24927
25489
  const generated = crypto.randomUUID();
24928
- fs30__default.default.mkdirSync(path33__default.default.dirname(secretPath), { recursive: true });
24929
- fs30__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
25490
+ fs31__default.default.mkdirSync(path34__default.default.dirname(secretPath), { recursive: true });
25491
+ fs31__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
24930
25492
  if (this.config.dashboard.auth) {
24931
25493
  console.warn(
24932
25494
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -24953,7 +25515,7 @@ var DashboardServer = class {
24953
25515
  // ── Setup ─────────────────────────────────────
24954
25516
  getGlobalStore() {
24955
25517
  if (!this.globalStore) {
24956
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25518
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
24957
25519
  this.globalStore = new MemoryStore(globalDbPath);
24958
25520
  }
24959
25521
  return this.globalStore;
@@ -25333,12 +25895,12 @@ ${prompt}`;
25333
25895
  }
25334
25896
  }
25335
25897
  watchRuntimeChanges() {
25336
- const workspaceDbPath = path33__default.default.join(this.workspacePath, CASCADE_DB_FILE);
25337
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25898
+ const workspaceDbPath = path34__default.default.join(this.workspacePath, CASCADE_DB_FILE);
25899
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25338
25900
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
25339
25901
  for (const watchPath of watchPaths) {
25340
- if (!fs30__default.default.existsSync(watchPath)) continue;
25341
- fs30__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
25902
+ if (!fs31__default.default.existsSync(watchPath)) continue;
25903
+ fs31__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
25342
25904
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
25343
25905
  });
25344
25906
  }
@@ -25460,7 +26022,7 @@ ${prompt}`;
25460
26022
  const sessionId = req.params.id;
25461
26023
  this.store.deleteSession(sessionId);
25462
26024
  this.store.deleteRuntimeSession(sessionId);
25463
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26025
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25464
26026
  const globalStore = new MemoryStore(globalDbPath);
25465
26027
  try {
25466
26028
  globalStore.deleteRuntimeSession(sessionId);
@@ -25553,7 +26115,7 @@ ${prompt}`;
25553
26115
  });
25554
26116
  this.app.delete("/api/sessions", auth2, (req, res) => {
25555
26117
  const body = req.body;
25556
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26118
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25557
26119
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
25558
26120
  const globalStore = new MemoryStore(globalDbPath);
25559
26121
  try {
@@ -25576,7 +26138,7 @@ ${prompt}`;
25576
26138
  });
25577
26139
  this.app.delete("/api/runtime", auth2, (_req, res) => {
25578
26140
  this.store.deleteAllRuntimeNodes();
25579
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26141
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25580
26142
  const globalStore = new MemoryStore(globalDbPath);
25581
26143
  try {
25582
26144
  globalStore.deleteAllRuntimeNodes();
@@ -25662,12 +26224,12 @@ ${prompt}`;
25662
26224
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
25663
26225
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
25664
26226
  try {
25665
- const configPath = path33__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
25666
- const existing = fs30__default.default.existsSync(configPath) ? JSON.parse(fs30__default.default.readFileSync(configPath, "utf-8")) : {};
26227
+ const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
26228
+ const existing = fs31__default.default.existsSync(configPath) ? JSON.parse(fs31__default.default.readFileSync(configPath, "utf-8")) : {};
25667
26229
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
25668
26230
  const tmp = configPath + ".tmp";
25669
- fs30__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
25670
- fs30__default.default.renameSync(tmp, configPath);
26231
+ fs31__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
26232
+ fs31__default.default.renameSync(tmp, configPath);
25671
26233
  res.json({ ok: true });
25672
26234
  } catch (err) {
25673
26235
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -25695,7 +26257,7 @@ ${prompt}`;
25695
26257
  this.app.get("/api/runtime", auth2, (req, res) => {
25696
26258
  const scope = req.query["scope"] ?? "workspace";
25697
26259
  if (scope === "global") {
25698
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26260
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25699
26261
  const globalStore = new MemoryStore(globalDbPath);
25700
26262
  try {
25701
26263
  res.json({
@@ -26036,13 +26598,13 @@ ${prompt}`;
26036
26598
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
26037
26599
  }
26038
26600
  });
26039
- const prodPath = path33__default.default.resolve(__dirname$1, "../web/dist");
26040
- const devPath = path33__default.default.resolve(__dirname$1, "../../web/dist");
26041
- const webDistPath = fs30__default.default.existsSync(prodPath) ? prodPath : devPath;
26042
- if (fs30__default.default.existsSync(webDistPath)) {
26601
+ const prodPath = path34__default.default.resolve(__dirname$1, "../web/dist");
26602
+ const devPath = path34__default.default.resolve(__dirname$1, "../../web/dist");
26603
+ const webDistPath = fs31__default.default.existsSync(prodPath) ? prodPath : devPath;
26604
+ if (fs31__default.default.existsSync(webDistPath)) {
26043
26605
  this.app.use(express__default.default.static(webDistPath));
26044
26606
  this.app.get("*", (_req, res) => {
26045
- res.sendFile(path33__default.default.join(webDistPath, "index.html"));
26607
+ res.sendFile(path34__default.default.join(webDistPath, "index.html"));
26046
26608
  });
26047
26609
  } else {
26048
26610
  this.app.get("/", (_req, res) => {
@@ -26063,7 +26625,7 @@ init_constants();
26063
26625
  async function dashboardCommand(config, workspacePath = process.cwd()) {
26064
26626
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
26065
26627
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
26066
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
26628
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26067
26629
  const server = new DashboardServer(config, store, workspacePath);
26068
26630
  server.watchRuntimeChanges();
26069
26631
  const gThis = globalThis;
@@ -26095,7 +26657,7 @@ init_constants();
26095
26657
  function makeIdentityCommand(workspacePath = process.cwd()) {
26096
26658
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
26097
26659
  identity.command("list").description("List all available identities").action(() => {
26098
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
26660
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26099
26661
  try {
26100
26662
  const identities = store.listIdentities();
26101
26663
  console.log(chalk7__default.default.bold("\n Identities:"));
@@ -26115,7 +26677,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26115
26677
  }
26116
26678
  });
26117
26679
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
26118
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
26680
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26119
26681
  try {
26120
26682
  if (options.default) {
26121
26683
  const existingDefault = store.getDefaultIdentity();
@@ -26141,7 +26703,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26141
26703
  }
26142
26704
  });
26143
26705
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
26144
- const store = new MemoryStore(path33__default.default.join(workspacePath, CASCADE_DB_FILE));
26706
+ const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
26145
26707
  try {
26146
26708
  const identities = store.listIdentities();
26147
26709
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -26290,8 +26852,8 @@ async function exportCommand(options = {}) {
26290
26852
  let store;
26291
26853
  try {
26292
26854
  const workspacePath = options.workspacePath ?? process.cwd();
26293
- const workspaceDbPath = path33__default.default.join(workspacePath, CASCADE_DB_FILE);
26294
- const globalDbPath = path33__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
26855
+ const workspaceDbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
26856
+ const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
26295
26857
  let dbPath = globalDbPath;
26296
26858
  try {
26297
26859
  await fs11__default.default.access(workspaceDbPath);
@@ -26333,7 +26895,7 @@ async function exportCommand(options = {}) {
26333
26895
  const ext = format === "json" ? ".json" : ".md";
26334
26896
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
26335
26897
  const defaultFile = `cascade-export-${safeName}${ext}`;
26336
- const outPath = options.output ? path33__default.default.resolve(options.output) : path33__default.default.join(process.cwd(), defaultFile);
26898
+ const outPath = options.output ? path34__default.default.resolve(options.output) : path34__default.default.join(process.cwd(), defaultFile);
26337
26899
  await fs11__default.default.writeFile(outPath, content, "utf-8");
26338
26900
  spin.succeed(chalk7__default.default.green(`Exported to ${chalk7__default.default.white(outPath)}`));
26339
26901
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -26561,11 +27123,11 @@ async function statsCommand() {
26561
27123
  dotenv__default.default.config();
26562
27124
  function warnIfBuildIsStale() {
26563
27125
  try {
26564
- const here = path33__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
27126
+ const here = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
26565
27127
  for (const rel of ["..", "../.."]) {
26566
- const pkgPath = path33__default.default.join(here, rel, "package.json");
26567
- if (!fs30__default.default.existsSync(pkgPath)) continue;
26568
- const pkg = JSON.parse(fs30__default.default.readFileSync(pkgPath, "utf-8"));
27128
+ const pkgPath = path34__default.default.join(here, rel, "package.json");
27129
+ if (!fs31__default.default.existsSync(pkgPath)) continue;
27130
+ const pkg = JSON.parse(fs31__default.default.readFileSync(pkgPath, "utf-8"));
26569
27131
  if (pkg.name !== "cascade-ai") continue;
26570
27132
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
26571
27133
  console.error(