cascade-ai 0.67.0 → 0.69.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.js CHANGED
@@ -7,8 +7,8 @@ import https from 'https';
7
7
  import zlib from 'zlib';
8
8
  import { Readable } from 'stream';
9
9
  import Database2 from 'better-sqlite3';
10
- import path33, { dirname } from 'path';
11
- import fs30, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
10
+ import path34, { dirname } from 'path';
11
+ import fs31, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
12
12
  import os10 from 'os';
13
13
  import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
14
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
@@ -1757,7 +1757,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1757
1757
  var init_constants = __esm({
1758
1758
  "src/constants.ts"() {
1759
1759
  init_pricing();
1760
- CASCADE_VERSION = "0.67.0";
1760
+ CASCADE_VERSION = "0.69.0";
1761
1761
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1762
1762
  CASCADE_DB_FILE = ".cascade/memory.db";
1763
1763
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -3879,12 +3879,12 @@ var init_audit_logger = __esm({
3879
3879
  constructor(workspacePath, debugMode = false) {
3880
3880
  this.workspacePath = workspacePath;
3881
3881
  this.debugMode = debugMode;
3882
- const cascadeDir = path33.join(workspacePath, ".cascade");
3883
- if (!fs30.existsSync(cascadeDir)) {
3884
- fs30.mkdirSync(cascadeDir, { recursive: true });
3882
+ const cascadeDir = path34.join(workspacePath, ".cascade");
3883
+ if (!fs31.existsSync(cascadeDir)) {
3884
+ fs31.mkdirSync(cascadeDir, { recursive: true });
3885
3885
  }
3886
- this.keyPath = path33.join(cascadeDir, "audit_log.key");
3887
- this.dbPath = path33.join(cascadeDir, "audit_log.db");
3886
+ this.keyPath = path34.join(cascadeDir, "audit_log.key");
3887
+ this.dbPath = path34.join(cascadeDir, "audit_log.db");
3888
3888
  this.initEncryptionKey();
3889
3889
  this.db = new Database2(this.dbPath);
3890
3890
  this.db.pragma("journal_mode = WAL");
@@ -3914,11 +3914,11 @@ var init_audit_logger = __esm({
3914
3914
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3915
3915
  }
3916
3916
  initEncryptionKey() {
3917
- if (fs30.existsSync(this.keyPath)) {
3918
- this.encryptionKey = fs30.readFileSync(this.keyPath);
3917
+ if (fs31.existsSync(this.keyPath)) {
3918
+ this.encryptionKey = fs31.readFileSync(this.keyPath);
3919
3919
  } else {
3920
3920
  this.encryptionKey = crypto.randomBytes(32);
3921
- fs30.writeFileSync(this.keyPath, this.encryptionKey);
3921
+ fs31.writeFileSync(this.keyPath, this.encryptionKey);
3922
3922
  }
3923
3923
  }
3924
3924
  encrypt(text) {
@@ -3996,9 +3996,9 @@ var init_audit_logger = __esm({
3996
3996
  dumpDebugIfNeeded() {
3997
3997
  if (!this.debugMode) return;
3998
3998
  try {
3999
- const dumpPath = path33.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
3999
+ const dumpPath = path34.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
4000
4000
  const entries = this.getAllLogs();
4001
- fs30.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4001
+ fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4002
4002
  } catch (err) {
4003
4003
  console.error("Failed to dump debug audit logs", err);
4004
4004
  }
@@ -4053,7 +4053,7 @@ var Keystore = class {
4053
4053
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
4054
4054
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
4055
4055
  this.backend = "keytar";
4056
- if (password && fs30.existsSync(this.storePath)) {
4056
+ if (password && fs31.existsSync(this.storePath)) {
4057
4057
  try {
4058
4058
  const fileEntries = this.decryptFile(password);
4059
4059
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -4072,7 +4072,7 @@ var Keystore = class {
4072
4072
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
4073
4073
  );
4074
4074
  }
4075
- if (!fs30.existsSync(this.storePath)) {
4075
+ if (!fs31.existsSync(this.storePath)) {
4076
4076
  const salt = crypto.randomBytes(SALT_LEN);
4077
4077
  this.masterKey = this.deriveKey(password, salt);
4078
4078
  this.writeWithSalt({}, salt);
@@ -4086,7 +4086,7 @@ var Keystore = class {
4086
4086
  }
4087
4087
  /** Synchronous legacy unlock kept for AES-only environments. */
4088
4088
  unlockSync(password) {
4089
- if (!fs30.existsSync(this.storePath)) {
4089
+ if (!fs31.existsSync(this.storePath)) {
4090
4090
  const salt = crypto.randomBytes(SALT_LEN);
4091
4091
  this.masterKey = this.deriveKey(password, salt);
4092
4092
  this.writeWithSalt({}, salt);
@@ -4144,7 +4144,7 @@ var Keystore = class {
4144
4144
  }
4145
4145
  }
4146
4146
  decryptFile(password, knownSalt) {
4147
- if (!fs30.existsSync(this.storePath)) return {};
4147
+ if (!fs31.existsSync(this.storePath)) return {};
4148
4148
  try {
4149
4149
  const { salt, ciphertext, iv, tag } = this.readRaw();
4150
4150
  const useSalt = knownSalt ?? salt;
@@ -4166,8 +4166,8 @@ var Keystore = class {
4166
4166
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4167
4167
  const tag = cipher.getAuthTag();
4168
4168
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4169
- fs30.mkdirSync(path33.dirname(this.storePath), { recursive: true });
4170
- fs30.writeFileSync(this.storePath, out, { mode: 384 });
4169
+ fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4170
+ fs31.writeFileSync(this.storePath, out, { mode: 384 });
4171
4171
  }
4172
4172
  writeWithSalt(data, salt) {
4173
4173
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4177,11 +4177,11 @@ var Keystore = class {
4177
4177
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4178
4178
  const tag = cipher.getAuthTag();
4179
4179
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4180
- fs30.mkdirSync(path33.dirname(this.storePath), { recursive: true });
4181
- fs30.writeFileSync(this.storePath, out, { mode: 384 });
4180
+ fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4181
+ fs31.writeFileSync(this.storePath, out, { mode: 384 });
4182
4182
  }
4183
4183
  readRaw() {
4184
- const buf = fs30.readFileSync(this.storePath);
4184
+ const buf = fs31.readFileSync(this.storePath);
4185
4185
  let offset = 0;
4186
4186
  const salt = buf.subarray(offset, offset + SALT_LEN);
4187
4187
  offset += SALT_LEN;
@@ -4214,7 +4214,7 @@ var CascadeIgnore = class {
4214
4214
  ]);
4215
4215
  }
4216
4216
  async load(workspacePath) {
4217
- const filePath = path33.join(workspacePath, ".cascadeignore");
4217
+ const filePath = path34.join(workspacePath, ".cascadeignore");
4218
4218
  try {
4219
4219
  const content = await fs11.readFile(filePath, "utf-8");
4220
4220
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4225,7 +4225,7 @@ var CascadeIgnore = class {
4225
4225
  }
4226
4226
  isIgnored(filePath, workspacePath) {
4227
4227
  try {
4228
- const relative = workspacePath ? path33.relative(workspacePath, filePath) : filePath;
4228
+ const relative = workspacePath ? path34.relative(workspacePath, filePath) : filePath;
4229
4229
  return this.ig.ignores(relative);
4230
4230
  } catch {
4231
4231
  return false;
@@ -4236,7 +4236,7 @@ var CascadeIgnore = class {
4236
4236
  }
4237
4237
  };
4238
4238
  async function createDefaultIgnoreFile(workspacePath) {
4239
- const filePath = path33.join(workspacePath, ".cascadeignore");
4239
+ const filePath = path34.join(workspacePath, ".cascadeignore");
4240
4240
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4241
4241
  # Syntax identical to .gitignore
4242
4242
 
@@ -4266,7 +4266,7 @@ Thumbs.db
4266
4266
  await fs11.writeFile(filePath, content, "utf-8");
4267
4267
  }
4268
4268
  async function loadCascadeMd(workspacePath) {
4269
- const filePath = path33.join(workspacePath, "CASCADE.md");
4269
+ const filePath = path34.join(workspacePath, "CASCADE.md");
4270
4270
  try {
4271
4271
  const raw = await fs11.readFile(filePath, "utf-8");
4272
4272
  return parseCascadeMd(raw);
@@ -4295,7 +4295,7 @@ ${raw.trim()}`;
4295
4295
  return { raw, sections, systemPrompt };
4296
4296
  }
4297
4297
  async function createDefaultCascadeMd(workspacePath) {
4298
- const filePath = path33.join(workspacePath, "CASCADE.md");
4298
+ const filePath = path34.join(workspacePath, "CASCADE.md");
4299
4299
  const content = `# Cascade Project Instructions
4300
4300
 
4301
4301
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4330,7 +4330,7 @@ List any areas the agent should not touch.
4330
4330
  var MemoryStore = class _MemoryStore {
4331
4331
  db;
4332
4332
  constructor(dbPath) {
4333
- fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
4333
+ fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
4334
4334
  try {
4335
4335
  this.db = new Database2(dbPath, { timeout: 5e3 });
4336
4336
  this.db.pragma("journal_mode = WAL");
@@ -5582,7 +5582,7 @@ function validateConfig(raw) {
5582
5582
  // src/config/global-credentials.ts
5583
5583
  init_constants();
5584
5584
  function credentialsPath(globalDir2) {
5585
- return path33.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5585
+ return path34.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5586
5586
  }
5587
5587
  function providerKey(p) {
5588
5588
  if (p.type === "azure") {
@@ -5595,7 +5595,7 @@ function isPersistable(p) {
5595
5595
  }
5596
5596
  function loadGlobalCredentials(globalDir2) {
5597
5597
  try {
5598
- const raw = fs30.readFileSync(credentialsPath(globalDir2), "utf-8");
5598
+ const raw = fs31.readFileSync(credentialsPath(globalDir2), "utf-8");
5599
5599
  const parsed = JSON.parse(raw);
5600
5600
  if (!Array.isArray(parsed.providers)) return [];
5601
5601
  return parsed.providers.filter(
@@ -5607,13 +5607,13 @@ function loadGlobalCredentials(globalDir2) {
5607
5607
  }
5608
5608
  function saveGlobalCredentials(globalDir2, providers) {
5609
5609
  const filePath = credentialsPath(globalDir2);
5610
- fs30.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5610
+ fs31.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5611
5611
  const body = { version: 1, providers: providers.filter(isPersistable) };
5612
5612
  const tmp = `${filePath}.tmp`;
5613
- fs30.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5614
- fs30.renameSync(tmp, filePath);
5613
+ fs31.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5614
+ fs31.renameSync(tmp, filePath);
5615
5615
  try {
5616
- fs30.chmodSync(filePath, 384);
5616
+ fs31.chmodSync(filePath, 384);
5617
5617
  } catch {
5618
5618
  }
5619
5619
  }
@@ -5831,7 +5831,7 @@ var ConfigManager = class {
5831
5831
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5832
5832
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5833
5833
  this.workspacePath = workspacePath;
5834
- this.globalDir = globalDirOverride ?? path33.join(os10.homedir(), GLOBAL_CONFIG_DIR);
5834
+ this.globalDir = globalDirOverride ?? path34.join(os10.homedir(), GLOBAL_CONFIG_DIR);
5835
5835
  }
5836
5836
  async load() {
5837
5837
  this.config = await this.loadConfig();
@@ -5845,8 +5845,8 @@ var ConfigManager = class {
5845
5845
  this.ignore = new CascadeIgnore();
5846
5846
  await this.ignore.load(this.workspacePath);
5847
5847
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5848
- this.keystore = new Keystore(path33.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5849
- this.store = new MemoryStore(path33.join(this.workspacePath, CASCADE_DB_FILE));
5848
+ this.keystore = new Keystore(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5849
+ this.store = new MemoryStore(path34.join(this.workspacePath, CASCADE_DB_FILE));
5850
5850
  await this.injectEnvKeys();
5851
5851
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
5852
5852
  await this.ensureDefaultIdentity();
@@ -5871,8 +5871,8 @@ var ConfigManager = class {
5871
5871
  return this.workspacePath;
5872
5872
  }
5873
5873
  async save() {
5874
- const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
5875
- await fs11.mkdir(path33.dirname(configPath), { recursive: true });
5874
+ const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
5875
+ await fs11.mkdir(path34.dirname(configPath), { recursive: true });
5876
5876
  await fs11.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
5877
5877
  try {
5878
5878
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -5908,7 +5908,7 @@ var ConfigManager = class {
5908
5908
  return configProvider?.apiKey;
5909
5909
  }
5910
5910
  async loadConfig() {
5911
- const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
5911
+ const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
5912
5912
  try {
5913
5913
  const raw = await fs11.readFile(configPath, "utf-8");
5914
5914
  return validateConfig(JSON.parse(raw));
@@ -7075,7 +7075,7 @@ init_pricing();
7075
7075
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7076
7076
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7077
7077
  var FETCH_TIMEOUT_MS = 8e3;
7078
- var DEFAULT_CACHE_FILE = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7078
+ var DEFAULT_CACHE_FILE = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7079
7079
  function normalizeModelId2(id) {
7080
7080
  let s = id.toLowerCase();
7081
7081
  const slash = s.lastIndexOf("/");
@@ -7222,7 +7222,7 @@ var LiveDataProvider = class {
7222
7222
  }
7223
7223
  async saveCache() {
7224
7224
  try {
7225
- await fs11.mkdir(path33.dirname(this.opts.cacheFile), { recursive: true });
7225
+ await fs11.mkdir(path34.dirname(this.opts.cacheFile), { recursive: true });
7226
7226
  const cache = {
7227
7227
  fetchedAt: this.fetchedAt,
7228
7228
  snapshot: this.snapshot ?? void 0,
@@ -8482,6 +8482,21 @@ var BaseTier = class extends EventEmitter {
8482
8482
  * which node (Cockpit node panel / Why panel).
8483
8483
  */
8484
8484
  servingModel;
8485
+ /**
8486
+ * This tier's identity IN THE TASK GRAPH — the section or subtask id the
8487
+ * planner assigned — plus where it sat in the dependency order.
8488
+ *
8489
+ * `id` and `graphNodeId` are deliberately different id spaces, and both are
8490
+ * needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
8491
+ * construction); `graphNodeId` identifies the WORK ("s1"), which is what
8492
+ * dependency edges point at. Without the distinction a graph view cannot be
8493
+ * drawn at all: `dependsOn` lists section ids, while every running tier
8494
+ * reports a generated tier id, so every edge would name a node that never
8495
+ * appears in the stream.
8496
+ */
8497
+ graphNodeId;
8498
+ graphDependsOn;
8499
+ graphWave;
8485
8500
  constructor(role, id, parentId) {
8486
8501
  super();
8487
8502
  this.role = role;
@@ -8493,6 +8508,33 @@ var BaseTier = class extends EventEmitter {
8493
8508
  setPresenter(on = true) {
8494
8509
  this.isPresenter = on;
8495
8510
  }
8511
+ /**
8512
+ * Record where this tier sits in the compiled task graph.
8513
+ *
8514
+ * Called by whichever tier scheduled this one: the id and edges are known at
8515
+ * construction, the wave only once the scheduler reaches it, so the fields
8516
+ * are set independently rather than all at once.
8517
+ */
8518
+ setGraphPosition(position) {
8519
+ if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
8520
+ if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
8521
+ if (position.wave !== void 0) this.graphWave = position.wave;
8522
+ }
8523
+ /**
8524
+ * The graph fields, for spreading into a status payload.
8525
+ *
8526
+ * One helper rather than two literals because `tier:status` is emitted from
8527
+ * two different call sites with two different payload shapes; adding a field
8528
+ * to only one of them makes it arrive intermittently, which is worse for a
8529
+ * consumer than never arriving at all.
8530
+ */
8531
+ graphFields() {
8532
+ return {
8533
+ nodeId: this.graphNodeId,
8534
+ dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
8535
+ waveId: this.graphWave
8536
+ };
8537
+ }
8496
8538
  getStatus() {
8497
8539
  return this.status;
8498
8540
  }
@@ -8507,7 +8549,8 @@ var BaseTier = class extends EventEmitter {
8507
8549
  status,
8508
8550
  timestamp,
8509
8551
  output,
8510
- model: this.servingModel
8552
+ model: this.servingModel,
8553
+ ...this.graphFields()
8511
8554
  };
8512
8555
  this.emit("status", event);
8513
8556
  this.emit("tier:status", event);
@@ -8547,7 +8590,8 @@ var BaseTier = class extends EventEmitter {
8547
8590
  progressPct: update.progressPct,
8548
8591
  timestamp,
8549
8592
  output: update.output,
8550
- model: this.servingModel
8593
+ model: this.servingModel,
8594
+ ...this.graphFields()
8551
8595
  });
8552
8596
  }
8553
8597
  buildMessage(type, to, payload) {
@@ -9015,6 +9059,79 @@ function describeProviderError(c, modelId) {
9015
9059
  }
9016
9060
  }
9017
9061
 
9062
+ // src/core/verification/acceptance.ts
9063
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
9064
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
9065
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
9066
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
9067
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
9068
+ function targetFile(criterion, ownedFiles) {
9069
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
9070
+ if (mentioned.length > 1) return null;
9071
+ if (mentioned.length === 1) {
9072
+ const candidate = mentioned[0];
9073
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
9074
+ }
9075
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
9076
+ }
9077
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
9078
+ const results = [];
9079
+ for (const criterion of criteria) {
9080
+ const text = criterion.trim();
9081
+ if (!text) {
9082
+ results.push({ criterion, verdict: "undecidable" });
9083
+ continue;
9084
+ }
9085
+ if (NEGATION.test(text)) {
9086
+ results.push({ criterion, verdict: "undecidable" });
9087
+ continue;
9088
+ }
9089
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
9090
+ const wantsExists = EXISTS.test(text);
9091
+ const wantsNonEmpty = NON_EMPTY.test(text);
9092
+ if (!needle && !wantsExists && !wantsNonEmpty) {
9093
+ results.push({ criterion, verdict: "undecidable" });
9094
+ continue;
9095
+ }
9096
+ const file = targetFile(text, ownedFiles);
9097
+ if (!file) {
9098
+ results.push({ criterion, verdict: "undecidable" });
9099
+ continue;
9100
+ }
9101
+ const stat = await probe.stat(file);
9102
+ if (!stat) {
9103
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
9104
+ continue;
9105
+ }
9106
+ if (wantsNonEmpty && stat.size === 0) {
9107
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
9108
+ continue;
9109
+ }
9110
+ if (needle) {
9111
+ const content = await probe.read(file);
9112
+ if (content === null) {
9113
+ results.push({ criterion, verdict: "undecidable" });
9114
+ continue;
9115
+ }
9116
+ const found = content.includes(needle);
9117
+ results.push({
9118
+ criterion,
9119
+ verdict: found ? "passed" : "failed",
9120
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
9121
+ });
9122
+ continue;
9123
+ }
9124
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
9125
+ }
9126
+ return results;
9127
+ }
9128
+ function undecided(results) {
9129
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
9130
+ }
9131
+ function failures(results) {
9132
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
9133
+ }
9134
+
9018
9135
  // src/core/tiers/t3-worker.ts
9019
9136
  var CriticalToolError = class extends Error {
9020
9137
  constructor(message, toolName) {
@@ -9331,7 +9448,7 @@ Now execute your subtask using this context where relevant.`
9331
9448
  }
9332
9449
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
9333
9450
  if (!artifactCheck.ok) {
9334
- correctionAttempts = 1;
9451
+ correctionAttempts++;
9335
9452
  issues.push(...artifactCheck.issues);
9336
9453
  output = await this.correctOutput(output, artifactCheck.issues);
9337
9454
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -9349,22 +9466,45 @@ Now execute your subtask using this context where relevant.`
9349
9466
  this.tools.map((t) => t.name)
9350
9467
  );
9351
9468
  if (visualIssue) {
9352
- correctionAttempts = 1;
9469
+ correctionAttempts++;
9353
9470
  issues.push(visualIssue);
9354
9471
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
9355
9472
  output = await this.correctOutput(output, [visualIssue]);
9356
9473
  }
9474
+ const acceptanceResults = await this.checkAcceptance(assignment);
9475
+ const acceptanceFailures = failures(acceptanceResults);
9476
+ if (acceptanceFailures.length > 0) {
9477
+ correctionAttempts++;
9478
+ issues.push(...acceptanceFailures);
9479
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
9480
+ output = await this.correctOutput(output, acceptanceFailures);
9481
+ const recheck = failures(await this.checkAcceptance(assignment));
9482
+ if (recheck.length > 0) {
9483
+ issues.push(...recheck);
9484
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
9485
+ failed.push(...recheck);
9486
+ this.setStatus("FAILED");
9487
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
9488
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
9489
+ }
9490
+ }
9491
+ for (const result2 of acceptanceResults) {
9492
+ if (result2.verdict === "undecidable") continue;
9493
+ checksRun.push(result2.criterion);
9494
+ if (result2.verdict === "passed") passed.push(result2.criterion);
9495
+ }
9357
9496
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
9358
- const testResult = await this.selfTest(assignment, output);
9497
+ const pendingAcceptance = undecided(acceptanceResults);
9498
+ const testResult = await this.selfTest(assignment, output, pendingAcceptance);
9359
9499
  checksRun.push(...testResult.checksRun);
9360
9500
  passed.push(...testResult.passed);
9361
9501
  failed.push(...testResult.failed);
9362
9502
  if (testResult.failed.length > 0) {
9363
- correctionAttempts = 1;
9503
+ correctionAttempts++;
9364
9504
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
9365
9505
  const corrected = await this.correctOutput(output, testResult.failed);
9366
9506
  output = corrected;
9367
- const retest = await this.selfTest(assignment, output);
9507
+ const retest = await this.selfTest(assignment, output, pendingAcceptance);
9368
9508
  passed.push(...retest.passed);
9369
9509
  if (retest.failed.length > 0) {
9370
9510
  failed.push(...retest.failed);
@@ -9668,8 +9808,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
9668
9808
  // Media generation can run for a minute; without this a cancelled run
9669
9809
  // still pays for an image nobody will see.
9670
9810
  ...this.signal ? { signal: this.signal } : {},
9671
- saveSnapshot: async (path38, content) => {
9672
- this.store?.addFileSnapshot(this.taskId, path38, content);
9811
+ saveSnapshot: async (path39, content) => {
9812
+ this.store?.addFileSnapshot(this.taskId, path39, content);
9673
9813
  },
9674
9814
  sendPeerSync: (to, syncType, content) => {
9675
9815
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -9856,7 +9996,7 @@ ${assignment.expectedOutput}`;
9856
9996
  const { promisify: promisify4 } = await import('util');
9857
9997
  const execAsync3 = promisify4(exec3);
9858
9998
  for (const artifactPath of artifactPaths) {
9859
- const absolutePath = path33.resolve(process.cwd(), artifactPath);
9999
+ const absolutePath = path34.resolve(process.cwd(), artifactPath);
9860
10000
  try {
9861
10001
  const stat = await fs11.stat(absolutePath);
9862
10002
  if (!stat.isFile()) {
@@ -9891,7 +10031,7 @@ ${assignment.expectedOutput}`;
9891
10031
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
9892
10032
  continue;
9893
10033
  }
9894
- const ext = path33.extname(absolutePath).toLowerCase();
10034
+ const ext = path34.extname(absolutePath).toLowerCase();
9895
10035
  try {
9896
10036
  if (ext === ".ts" || ext === ".tsx") {
9897
10037
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -9977,14 +10117,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9977
10117
  }
9978
10118
  return current;
9979
10119
  }
9980
- async selfTest(assignment, output) {
10120
+ /**
10121
+ * Run the acceptance criteria that can be settled by looking at the workspace.
10122
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
10123
+ */
10124
+ async checkAcceptance(assignment) {
10125
+ const criteria = assignment.acceptance ?? [];
10126
+ if (!criteria.length) return [];
10127
+ const resolve2 = (target) => path34.resolve(process.cwd(), target);
10128
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
10129
+ stat: async (target) => {
10130
+ try {
10131
+ const stat = await fs11.stat(resolve2(target));
10132
+ return stat.isFile() ? { size: stat.size } : null;
10133
+ } catch {
10134
+ return null;
10135
+ }
10136
+ },
10137
+ read: async (target) => {
10138
+ try {
10139
+ const content = await fs11.readFile(resolve2(target), "utf-8");
10140
+ return content.includes("\uFFFD") ? null : content;
10141
+ } catch {
10142
+ return null;
10143
+ }
10144
+ }
10145
+ });
10146
+ }
10147
+ async selfTest(assignment, output, pendingAcceptance) {
10148
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
9981
10149
  const prompt = `Self-test this output against the assignment requirements.
9982
10150
 
9983
10151
  Assignment: ${assignment.description}
9984
10152
  Expected output: ${assignment.expectedOutput}
9985
10153
  Constraints: ${assignment.constraints.join("; ")}
9986
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
9987
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
10154
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
10155
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
9988
10156
  ` : ""}
9989
10157
  Output to test:
9990
10158
  ${output}
@@ -10051,8 +10219,8 @@ ${output.slice(0, 4e3)}`;
10051
10219
  } catch {
10052
10220
  }
10053
10221
  }
10054
- async correctOutput(originalOutput, failures) {
10055
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
10222
+ async correctOutput(originalOutput, failures2) {
10223
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
10056
10224
 
10057
10225
  Original output:
10058
10226
  ${originalOutput}
@@ -10714,12 +10882,220 @@ function describeGenerationForPlanner(has) {
10714
10882
  return lines.join("\n");
10715
10883
  }
10716
10884
 
10885
+ // src/core/orchestration/task-graph.ts
10886
+ var TaskGraphValidationError = class extends Error {
10887
+ constructor(issues) {
10888
+ super(issues.map((issue) => issue.message).join("; "));
10889
+ this.issues = issues;
10890
+ this.name = "TaskGraphValidationError";
10891
+ }
10892
+ issues;
10893
+ };
10894
+ function normalizeDependencies(input) {
10895
+ if (!input?.length) return [];
10896
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
10897
+ }
10898
+ function cycleNodes(nodes) {
10899
+ const adjacency = new Map(
10900
+ nodes.map((node) => [node.id, node.dependsOn])
10901
+ );
10902
+ const index = /* @__PURE__ */ new Map();
10903
+ const lowlink = /* @__PURE__ */ new Map();
10904
+ const onStack = /* @__PURE__ */ new Set();
10905
+ const stack = [];
10906
+ const inCycle = /* @__PURE__ */ new Set();
10907
+ let counter = 0;
10908
+ const open = (id) => {
10909
+ index.set(id, counter);
10910
+ lowlink.set(id, counter);
10911
+ counter++;
10912
+ stack.push(id);
10913
+ onStack.add(id);
10914
+ };
10915
+ for (const root of nodes) {
10916
+ if (index.has(root.id)) continue;
10917
+ open(root.id);
10918
+ const frames = [{ id: root.id, edge: 0 }];
10919
+ while (frames.length > 0) {
10920
+ const frame = frames[frames.length - 1];
10921
+ const neighbours = adjacency.get(frame.id) ?? [];
10922
+ if (frame.edge < neighbours.length) {
10923
+ const next = neighbours[frame.edge];
10924
+ frame.edge++;
10925
+ if (!adjacency.has(next)) continue;
10926
+ if (!index.has(next)) {
10927
+ open(next);
10928
+ frames.push({ id: next, edge: 0 });
10929
+ } else if (onStack.has(next)) {
10930
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
10931
+ }
10932
+ continue;
10933
+ }
10934
+ frames.pop();
10935
+ const parent = frames[frames.length - 1];
10936
+ if (parent) {
10937
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
10938
+ }
10939
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
10940
+ const component = [];
10941
+ for (; ; ) {
10942
+ const member = stack.pop();
10943
+ onStack.delete(member);
10944
+ component.push(member);
10945
+ if (member === frame.id) break;
10946
+ }
10947
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
10948
+ if (cyclic) for (const member of component) inCycle.add(member);
10949
+ }
10950
+ }
10951
+ }
10952
+ return inCycle;
10953
+ }
10954
+ function throwIfStrict(mode, issues) {
10955
+ if (mode === "strict" && issues.length > 0) {
10956
+ throw new TaskGraphValidationError(issues);
10957
+ }
10958
+ }
10959
+ function compileTaskGraph(inputNodes, options = {}) {
10960
+ const mode = options.mode ?? "strict";
10961
+ const issues = [];
10962
+ if (inputNodes.length === 0) {
10963
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
10964
+ throwIfStrict(mode, issues);
10965
+ }
10966
+ const seen = /* @__PURE__ */ new Set();
10967
+ const nodes = [];
10968
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
10969
+ const input = inputNodes[ordinal];
10970
+ const id = input.id?.trim();
10971
+ const title = input.title?.trim();
10972
+ if (!id || !title) {
10973
+ issues.push({
10974
+ code: "INVALID_NODE",
10975
+ nodeId: id || void 0,
10976
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
10977
+ });
10978
+ continue;
10979
+ }
10980
+ if (seen.has(id)) {
10981
+ issues.push({
10982
+ code: "DUPLICATE_NODE_ID",
10983
+ nodeId: id,
10984
+ message: `Duplicate task graph node id: ${id}`
10985
+ });
10986
+ continue;
10987
+ }
10988
+ seen.add(id);
10989
+ nodes.push({
10990
+ id,
10991
+ title,
10992
+ kind: input.kind,
10993
+ dependsOn: normalizeDependencies(input.dependsOn),
10994
+ payload: input.payload,
10995
+ ordinal
10996
+ });
10997
+ }
10998
+ throwIfStrict(mode, issues);
10999
+ const ids = new Set(nodes.map((node) => node.id));
11000
+ const sanitized = nodes.map((node) => {
11001
+ const dependsOn = [];
11002
+ for (const dependencyId of node.dependsOn) {
11003
+ if (dependencyId === node.id) {
11004
+ issues.push({
11005
+ code: "SELF_DEPENDENCY",
11006
+ nodeId: node.id,
11007
+ dependencyId,
11008
+ message: `Task graph node ${node.id} cannot depend on itself`
11009
+ });
11010
+ continue;
11011
+ }
11012
+ if (!ids.has(dependencyId)) {
11013
+ issues.push({
11014
+ code: "UNKNOWN_DEPENDENCY",
11015
+ nodeId: node.id,
11016
+ dependencyId,
11017
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
11018
+ });
11019
+ continue;
11020
+ }
11021
+ dependsOn.push(dependencyId);
11022
+ }
11023
+ return { ...node, dependsOn };
11024
+ });
11025
+ throwIfStrict(mode, issues);
11026
+ const cyclic = cycleNodes(sanitized);
11027
+ let repaired = sanitized;
11028
+ if (cyclic.size > 0) {
11029
+ const cycleIssues = [];
11030
+ repaired = sanitized.map((node) => {
11031
+ if (!cyclic.has(node.id)) return node;
11032
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
11033
+ if (!cyclic.has(dependencyId)) return true;
11034
+ cycleIssues.push({
11035
+ code: "CYCLE_EDGE_REMOVED",
11036
+ nodeId: node.id,
11037
+ dependencyId,
11038
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
11039
+ });
11040
+ return false;
11041
+ });
11042
+ return { ...node, dependsOn };
11043
+ });
11044
+ issues.push(...cycleIssues);
11045
+ throwIfStrict(mode, cycleIssues);
11046
+ }
11047
+ const remainingCycle = cycleNodes(repaired);
11048
+ if (remainingCycle.size > 0) {
11049
+ throw new TaskGraphValidationError([
11050
+ {
11051
+ code: "INVALID_NODE",
11052
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
11053
+ }
11054
+ ]);
11055
+ }
11056
+ return {
11057
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
11058
+ issues
11059
+ };
11060
+ }
11061
+
11062
+ // src/core/orchestration/adapters.ts
11063
+ function compileSectionGraph(sections) {
11064
+ return compileTaskGraph(
11065
+ sections.map((section) => ({
11066
+ id: section.sectionId,
11067
+ title: section.sectionTitle,
11068
+ kind: "section",
11069
+ dependsOn: section.dependsOn,
11070
+ payload: section
11071
+ })),
11072
+ { mode: "repair" }
11073
+ );
11074
+ }
11075
+ function compileSubtaskGraph(subtasks) {
11076
+ return compileTaskGraph(
11077
+ subtasks.map((subtask) => ({
11078
+ id: subtask.subtaskId,
11079
+ title: subtask.subtaskTitle,
11080
+ kind: "subtask",
11081
+ dependsOn: subtask.dependsOn,
11082
+ payload: subtask
11083
+ })),
11084
+ { mode: "repair" }
11085
+ );
11086
+ }
11087
+
10717
11088
  // src/core/tiers/t2-manager.ts
10718
11089
  function buildT2SystemPrompt(has) {
10719
11090
  const generation = describeGenerationForPlanner(has);
10720
11091
  return [
10721
11092
  "You are a T2 Manager agent in the Cascade AI system.",
10722
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
11093
+ // Must agree with the decomposition prompt in decomposeSection(), which is
11094
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
11095
+ // fully cover it"; this said 2-5. The model received both, so its floor was
11096
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
11097
+ // real worker call on every small section.
11098
+ "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.",
10723
11099
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
10724
11100
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
10725
11101
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -11053,6 +11429,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11053
11429
  for (const a of assignments) {
11054
11430
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
11055
11431
  if (this.store) worker.setStore(this.store, taskId);
11432
+ worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
11056
11433
  worker.setPeerBus(this.t3PeerBus);
11057
11434
  if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
11058
11435
  if (this.toolCreator) worker.setToolCreator(this.toolCreator);
@@ -11115,21 +11492,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11115
11492
  * then reduce in-degrees of their dependents and repeat.
11116
11493
  */
11117
11494
  async runWithDependencies(assignments, workerMap, taskId) {
11495
+ const compiled = compileSubtaskGraph(assignments);
11496
+ for (const issue of compiled.issues) {
11497
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
11498
+ }
11499
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
11500
+ const assignment = node.payload;
11501
+ const original = assignment.dependsOn ?? [];
11502
+ const repaired = node.dependsOn;
11503
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
11504
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
11505
+ });
11118
11506
  const adj = /* @__PURE__ */ new Map();
11119
11507
  const inDegree = /* @__PURE__ */ new Map();
11120
11508
  const resultMap = /* @__PURE__ */ new Map();
11121
- for (const a of assignments) {
11509
+ for (const a of sanitizedAssignments) {
11122
11510
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
11123
11511
  inDegree.set(a.subtaskId, 0);
11124
11512
  }
11125
- for (const a of assignments) {
11126
- const deps = a.dependsOn ?? [];
11127
- for (const dep of deps) {
11513
+ for (const a of sanitizedAssignments) {
11514
+ for (const dep of a.dependsOn ?? []) {
11128
11515
  adj.get(dep).add(a.subtaskId);
11129
11516
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
11130
11517
  }
11131
11518
  }
11132
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
11133
11519
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
11134
11520
  let wave = 0;
11135
11521
  let respawnBudget = 1;
@@ -11164,6 +11550,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11164
11550
  runnableIds.push(fallbackId);
11165
11551
  }
11166
11552
  wave++;
11553
+ for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
11167
11554
  this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
11168
11555
  this.sendStatusUpdate({
11169
11556
  progressPct: 20 + Math.min(wave * 10, 60),
@@ -11336,49 +11723,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11336
11723
  }
11337
11724
  return [...resultMap.values()];
11338
11725
  }
11339
- /**
11340
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
11341
- * by removing back-edges. Returns a sanitized copy of assignments.
11342
- *
11343
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
11344
- * meaning t3 will start without waiting for t1, preventing deadlock.
11345
- */
11346
- breakCycles(assignments, adj, inDegree) {
11347
- const degree = new Map(inDegree);
11348
- const queue = [];
11349
- const visited = /* @__PURE__ */ new Set();
11350
- for (const [id, d] of degree) {
11351
- if (d === 0) queue.push(id);
11352
- }
11353
- while (queue.length > 0) {
11354
- const id = queue.shift();
11355
- visited.add(id);
11356
- for (const dep of adj.get(id) ?? []) {
11357
- const newDeg = (degree.get(dep) ?? 1) - 1;
11358
- degree.set(dep, newDeg);
11359
- if (newDeg === 0) queue.push(dep);
11360
- }
11361
- }
11362
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
11363
- if (cycleNodes.length === 0) return assignments;
11364
- this.log(
11365
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
11366
- );
11367
- return assignments.map((a) => {
11368
- if (!cycleNodes.includes(a.subtaskId)) return a;
11369
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
11370
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
11371
- this.log(
11372
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
11373
- );
11374
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
11375
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
11376
- adj.get(removed)?.delete(a.subtaskId);
11377
- }
11378
- }
11379
- return { ...a, dependsOn: safeDeps };
11380
- });
11381
- }
11382
11726
  async retryT3(assignment, taskId) {
11383
11727
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
11384
11728
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -11517,6 +11861,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
11517
11861
  // src/core/tiers/t1-administrator.ts
11518
11862
  init_constants();
11519
11863
 
11864
+ // src/core/orchestration/scheduler.ts
11865
+ var DependencyScheduler = class {
11866
+ constructor(graph, hooks, options = {}) {
11867
+ this.graph = graph;
11868
+ this.hooks = hooks;
11869
+ this.options = options;
11870
+ }
11871
+ graph;
11872
+ hooks;
11873
+ options;
11874
+ async run() {
11875
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
11876
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
11877
+ const dependents = /* @__PURE__ */ new Map();
11878
+ for (const node of nodes) dependents.set(node.id, []);
11879
+ for (const node of nodes) {
11880
+ for (const dependencyId of node.dependsOn) {
11881
+ dependents.get(dependencyId)?.push(node.id);
11882
+ }
11883
+ }
11884
+ const policy = this.options.onUpstreamFailure ?? "continue";
11885
+ const settled = /* @__PURE__ */ new Set();
11886
+ const results = /* @__PURE__ */ new Map();
11887
+ const blocked = /* @__PURE__ */ new Map();
11888
+ const waves = [];
11889
+ let wave = 0;
11890
+ const blockDescendants = async (failedIds) => {
11891
+ const rootsFor = /* @__PURE__ */ new Map();
11892
+ const queue = [];
11893
+ for (const rootId of failedIds) {
11894
+ for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
11895
+ }
11896
+ while (queue.length > 0) {
11897
+ const { id, root } = queue.shift();
11898
+ if (settled.has(id)) continue;
11899
+ const known = rootsFor.get(id);
11900
+ if (known) {
11901
+ if (!known.has(root)) known.add(root);
11902
+ continue;
11903
+ }
11904
+ rootsFor.set(id, /* @__PURE__ */ new Set([root]));
11905
+ for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
11906
+ }
11907
+ for (const node of nodes) {
11908
+ const roots = rootsFor.get(node.id);
11909
+ if (!roots || settled.has(node.id)) continue;
11910
+ const causes = nodes.filter((n) => roots.has(n.id));
11911
+ const blockedBy = causes.map((n) => n.id);
11912
+ const blockedByTitles = causes.map((n) => n.title || n.id);
11913
+ const record = {
11914
+ blockedBy,
11915
+ blockedByTitles,
11916
+ reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
11917
+ };
11918
+ blocked.set(node.id, record);
11919
+ settled.add(node.id);
11920
+ await this.hooks.onNodeBlocked?.(node, record);
11921
+ }
11922
+ };
11923
+ while (settled.size < nodes.length) {
11924
+ const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
11925
+ if (ready.length === 0) {
11926
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
11927
+ }
11928
+ waves.push(ready.map((node) => node.id));
11929
+ await this.hooks.onWaveStart?.(ready, wave);
11930
+ const outcomes = /* @__PURE__ */ new Map();
11931
+ const runOne = async (node) => {
11932
+ const result = await this.hooks.execute(node);
11933
+ results.set(node.id, result);
11934
+ settled.add(node.id);
11935
+ const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
11936
+ outcomes.set(node.id, outcome);
11937
+ if (outcome === "failed" && policy === "block") ; else {
11938
+ for (const dependentId of dependents.get(node.id) ?? []) {
11939
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
11940
+ }
11941
+ }
11942
+ await this.hooks.onNodeComplete?.(node, result);
11943
+ };
11944
+ if ((this.options.mode ?? "parallel") === "sequential") {
11945
+ for (const node of ready) await runOne(node);
11946
+ } else {
11947
+ await Promise.all(ready.map(runOne));
11948
+ }
11949
+ const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
11950
+ if (policy === "block" && failedThisWave.length > 0) {
11951
+ await blockDescendants(failedThisWave);
11952
+ }
11953
+ wave++;
11954
+ }
11955
+ return { results, waves, blocked };
11956
+ }
11957
+ };
11958
+
11520
11959
  // src/utils/json-extract.ts
11521
11960
  function extractFirstJsonObject(input) {
11522
11961
  if (!input) return null;
@@ -11588,7 +12027,7 @@ function buildT1SystemPrompt(has) {
11588
12027
  Your responsibilities:
11589
12028
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
11590
12029
  2. Decompose the task into logical sections (one per T2 Manager)
11591
- 3. For each section, define 2-5 subtasks for T3 Workers
12030
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
11592
12031
  4. Return a structured plan as JSON
11593
12032
 
11594
12033
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -11614,6 +12053,9 @@ QUALITY RULES:
11614
12053
 
11615
12054
  ${generation}` : ""}`;
11616
12055
  }
12056
+ function producedOutput(result) {
12057
+ return result.status === "COMPLETED" || result.status === "PARTIAL";
12058
+ }
11617
12059
  var T1Administrator = class extends BaseTier {
11618
12060
  router;
11619
12061
  toolRegistry;
@@ -11748,7 +12190,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
11748
12190
  let allT2Results = await this.dispatchT2Managers(plan.sections);
11749
12191
  let pass = 1;
11750
12192
  const maxReplanPasses = this.config.maxReplanPasses ?? 2;
11751
- const okCount = (rs) => rs.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL").length;
12193
+ const okCount = (rs) => rs.filter(producedOutput).length;
11752
12194
  while (pass <= maxReplanPasses) {
11753
12195
  const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
11754
12196
  if (reviewResult.approved) {
@@ -11879,7 +12321,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
11879
12321
  /** Structured, grounded summary of what's already done — used to keep
11880
12322
  * corrective replan passes from re-emitting completed sections. */
11881
12323
  summarizeCompletedSections(results) {
11882
- const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
12324
+ const done = results.filter(producedOutput);
11883
12325
  if (done.length === 0) return "";
11884
12326
  const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
11885
12327
  return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
@@ -12003,7 +12445,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12003
12445
  }
12004
12446
  }
12005
12447
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
12006
- if (plan.sections.length < min) ;
12448
+ if (plan.sections.length < min || plan.sections.length > max) {
12449
+ this.log(
12450
+ `\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.`
12451
+ );
12452
+ }
12007
12453
  }
12008
12454
  async dispatchT2Managers(sections) {
12009
12455
  for (const section of sections) {
@@ -12020,6 +12466,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12020
12466
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
12021
12467
  if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
12022
12468
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
12469
+ manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
12023
12470
  if (this.store) {
12024
12471
  manager.setStore(this.store);
12025
12472
  }
@@ -12115,118 +12562,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
12115
12562
  return t2Results;
12116
12563
  }
12117
12564
  /**
12118
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
12565
+ * Runs T2 managers respecting dependsOn declarations.
12566
+ *
12567
+ * Graph construction, validation and cycle repair now live in
12568
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
12569
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
12570
+ * ordered by one implementation rather than two that drifted.
12571
+ *
12572
+ * The hand-rolled Kahn pass this replaces treated every node it could not
12573
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
12574
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
12575
+ * so breaking a cycle here also cut the dependencies of innocent later
12576
+ * sections and started them immediately — before the sections they consume.
12577
+ * compileTaskGraph uses strongly connected components, so only real cycle
12578
+ * members are touched.
12119
12579
  */
12120
12580
  async runT2sWithDependencies(sections, managers, taskId) {
12121
- const adj = /* @__PURE__ */ new Map();
12122
- const inDegree = /* @__PURE__ */ new Map();
12123
- const resultMap = /* @__PURE__ */ new Map();
12124
- const allKeys = new Set(sections.map((s) => s.sectionId));
12125
- for (const s of sections) {
12126
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
12127
- inDegree.set(s.sectionId, 0);
12128
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
12129
- }
12130
- for (const s of sections) {
12131
- for (const dep of s.dependsOn ?? []) {
12132
- adj.get(dep).add(s.sectionId);
12133
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
12134
- }
12135
- }
12136
- const queue = [];
12137
- const degree = new Map(inDegree);
12138
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
12139
- const visited = /* @__PURE__ */ new Set();
12140
- while (queue.length > 0) {
12141
- const u = queue.shift();
12142
- visited.add(u);
12143
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
12144
- const newDeg = (degree.get(v) ?? 1) - 1;
12145
- degree.set(v, newDeg);
12146
- if (newDeg === 0) queue.push(v);
12147
- }
12148
- }
12149
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
12150
- if (cycleNodes.length > 0) {
12151
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
12152
- for (const s of sections) {
12153
- if (cycleNodes.includes(s.sectionId)) {
12154
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
12155
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
12156
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
12157
- adj.get(removed)?.delete(s.sectionId);
12158
- }
12159
- s.dependsOn = safeDeps;
12160
- }
12161
- }
12581
+ const compiled = compileSectionGraph(sections);
12582
+ for (const issue of compiled.issues) {
12583
+ this.log(`\u26A0 Section graph: ${issue.message}`);
12162
12584
  }
12163
- const totalSections = sections.length;
12585
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
12586
+ for (const section of sections) {
12587
+ const node = byId.get(section.sectionId);
12588
+ if (node) section.dependsOn = [...node.dependsOn];
12589
+ }
12590
+ const resultMap = /* @__PURE__ */ new Map();
12591
+ const totalSections = compiled.graph.nodes.length;
12164
12592
  let completedSections = 0;
12165
- const executeWave = async () => {
12166
- const readyIds = [];
12167
- for (const [id, deg] of inDegree.entries()) {
12168
- if (deg === 0 && !resultMap.has(id)) {
12169
- readyIds.push(id);
12170
- }
12171
- }
12172
- if (readyIds.length === 0) return;
12173
- const runOne = async (id) => {
12174
- resultMap.set(id, null);
12175
- const index = sections.findIndex((s) => s.sectionId === id);
12176
- const section = sections[index];
12177
- const manager = managers[index];
12178
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12179
- this.sendStatusUpdate({
12180
- progressPct,
12181
- currentAction: `T2 working on: ${section.sectionTitle}`,
12182
- status: "IN_PROGRESS"
12183
- });
12184
- this.throwIfCancelled();
12185
- let result;
12186
- try {
12187
- result = await manager.execute(section, taskId, this.signal);
12188
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12189
- if (result.status === "ESCALATED") {
12190
- this.escalations.push({
12191
- raisedBy: `T2_${section.sectionId}`,
12593
+ const scheduler = new DependencyScheduler(
12594
+ compiled.graph,
12595
+ {
12596
+ // The wave is the scheduler's own counter, and it is the only place the
12597
+ // parallelism is visible: nodes sharing a wave ran at the same time.
12598
+ // Stamped before the wave executes so the first status event a node
12599
+ // emits already carries it.
12600
+ onWaveStart: (nodes, wave) => {
12601
+ for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
12602
+ },
12603
+ execute: async (node) => {
12604
+ const section = node.payload;
12605
+ const manager = managers[node.ordinal];
12606
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
12607
+ this.sendStatusUpdate({
12608
+ progressPct,
12609
+ currentAction: `T2 working on: ${section.sectionTitle}`,
12610
+ status: "IN_PROGRESS"
12611
+ });
12612
+ this.throwIfCancelled();
12613
+ try {
12614
+ const result = await manager.execute(section, taskId, this.signal);
12615
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
12616
+ if (result.status === "ESCALATED") {
12617
+ this.escalations.push({
12618
+ raisedBy: `T2_${section.sectionId}`,
12619
+ sectionId: section.sectionId,
12620
+ attempted: result.issues,
12621
+ blocker: result.issues.join("; "),
12622
+ needs: "Human review required"
12623
+ });
12624
+ }
12625
+ return result;
12626
+ } catch (err) {
12627
+ return {
12192
12628
  sectionId: section.sectionId,
12193
- attempted: result.issues,
12194
- blocker: result.issues.join("; "),
12195
- needs: "Human review required"
12196
- });
12629
+ sectionTitle: section.sectionTitle,
12630
+ status: "FAILED",
12631
+ t3Results: [],
12632
+ sectionSummary: "",
12633
+ issues: [err instanceof Error ? err.message : String(err)]
12634
+ };
12197
12635
  }
12198
- } catch (err) {
12199
- result = {
12636
+ },
12637
+ // PARTIAL is deliberately not a failure: a degraded-but-real section
12638
+ // can still feed the one after it, and cancelling that work would cost
12639
+ // the user more than letting it try.
12640
+ // BLOCKED cannot reach here today — the scheduler settles blocked nodes
12641
+ // through onNodeBlocked and never executes them, so classify only ever
12642
+ // sees results that ran. It is listed anyway because the failure mode if
12643
+ // that ever changes is silent and expensive: a blocked section would
12644
+ // classify as 'succeeded' and release its own dependents, so work that
12645
+ // should have been skipped would run and be billed.
12646
+ classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
12647
+ onNodeBlocked: (node, blockedInfo) => {
12648
+ const section = node.payload;
12649
+ this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
12650
+ resultMap.set(node.id, {
12200
12651
  sectionId: section.sectionId,
12201
12652
  sectionTitle: section.sectionTitle,
12202
- status: "FAILED",
12653
+ // BLOCKED, not FAILED. This section was never attempted — saying it
12654
+ // failed reports a breakage that did not happen, and hides the one
12655
+ // that did (the upstream section named below).
12656
+ status: "BLOCKED",
12203
12657
  t3Results: [],
12204
12658
  sectionSummary: "",
12205
- issues: [err instanceof Error ? err.message : String(err)]
12206
- };
12207
- }
12208
- resultMap.set(id, result);
12209
- completedSections++;
12210
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
12211
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
12212
- }
12213
- };
12214
- if (this.router.getT3ExecutionMode?.() === "sequential") {
12215
- for (const id of readyIds) {
12216
- await runOne(id);
12659
+ issues: [
12660
+ `${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
12661
+ ],
12662
+ blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
12663
+ });
12664
+ completedSections++;
12665
+ this.sendStatusUpdate({
12666
+ progressPct: 10 + Math.floor(completedSections / totalSections * 85),
12667
+ currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
12668
+ status: "IN_PROGRESS"
12669
+ });
12670
+ this.emit("tier:status", {
12671
+ tierId: section.sectionId,
12672
+ // A blocked section has no tier instance — it was never
12673
+ // constructed — so its runtime id and its graph id are the same
12674
+ // thing here. Emitting nodeId anyway keeps every node in the
12675
+ // stream addressable in the graph's id space, which is what
12676
+ // dependsOn edges point at.
12677
+ nodeId: section.sectionId,
12678
+ dependsOn: [...section.dependsOn ?? []],
12679
+ role: "T2",
12680
+ label: section.sectionTitle,
12681
+ status: "BLOCKED",
12682
+ blockedBy: [...blockedInfo.blockedByTitles],
12683
+ output: blockedInfo.reason
12684
+ });
12685
+ this.emit("section:complete", {
12686
+ id: node.id,
12687
+ title: section.sectionTitle,
12688
+ status: "BLOCKED",
12689
+ blockedBy: [...blockedInfo.blockedByTitles],
12690
+ output: ""
12691
+ });
12692
+ },
12693
+ onNodeComplete: (node, result) => {
12694
+ resultMap.set(node.id, result);
12695
+ completedSections++;
12696
+ this.emit("section:complete", {
12697
+ id: node.id,
12698
+ title: result.sectionTitle,
12699
+ status: result.status,
12700
+ output: result.sectionSummary
12701
+ });
12217
12702
  }
12218
- } else {
12219
- await Promise.all(readyIds.map(runOne));
12220
- }
12221
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
12222
- await executeWave();
12703
+ },
12704
+ {
12705
+ // Cross-section concurrency respects the same t3Execution mode as the
12706
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
12707
+ // serialized T3 workers within a single section while every section's
12708
+ // T2Manager (and its T3 workers) still ran in parallel here.
12709
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
12710
+ // A section that declared `dependsOn` said it needs that section's
12711
+ // output. Running it after the dependency failed produces a second
12712
+ // failure, billed, whose cause is the first one — so don't.
12713
+ onUpstreamFailure: "block"
12223
12714
  }
12224
- };
12225
- await executeWave();
12715
+ );
12716
+ await scheduler.run();
12226
12717
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
12227
12718
  }
12228
12719
  async compileFinalOutput(originalPrompt, plan, t2Results) {
12229
- const completedSections = t2Results.filter((r) => r.status !== "FAILED");
12720
+ const completedSections = t2Results.filter(producedOutput);
12230
12721
  if (!completedSections.length) {
12231
12722
  const allIssues = [
12232
12723
  ...t2Results.flatMap((r) => r.issues ?? []),
@@ -12246,7 +12737,6 @@ ${r.sectionSummary}
12246
12737
  Outputs:
12247
12738
  ${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
12248
12739
  const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
12249
- t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
12250
12740
  const compilePrompt = `Compile a final, coherent response to the user's original request.
12251
12741
 
12252
12742
  Original request: ${originalPrompt}
@@ -12741,6 +13231,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
12741
13231
  };
12742
13232
  var DEFAULT_VIDEO_SECONDS = 5;
12743
13233
  var MAX_VIDEO_SECONDS = 8;
13234
+ var MIN_VIDEO_SECONDS = 4;
12744
13235
  var GenerateVideoTool = class extends BaseTool {
12745
13236
  constructor(deps) {
12746
13237
  super();
@@ -12757,7 +13248,7 @@ var GenerateVideoTool = class extends BaseTool {
12757
13248
  type: "object",
12758
13249
  properties: {
12759
13250
  prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
12760
- seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
13251
+ seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
12761
13252
  aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
12762
13253
  model: { type: "string", description: "Optional: force a specific video model." }
12763
13254
  },
@@ -12769,7 +13260,7 @@ var GenerateVideoTool = class extends BaseTool {
12769
13260
  const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
12770
13261
  if (!r.ok) return r.error;
12771
13262
  const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
12772
- const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
13263
+ const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
12773
13264
  const asset = await generateVideo(
12774
13265
  r.selected.capability,
12775
13266
  r.cfg,
@@ -12786,7 +13277,7 @@ var GenerateVideoTool = class extends BaseTool {
12786
13277
  const location = await this.deps.sink(asset);
12787
13278
  const unit = r.selected.cost;
12788
13279
  const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
12789
- const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
13280
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
12790
13281
  return `Video generated and saved to ${location}.
12791
13282
  Model: ${asset.modelId}.${estimate}${clamped}`;
12792
13283
  }
@@ -12855,16 +13346,16 @@ function resolveInWorkspace(workspaceRoot, input) {
12855
13346
  if (typeof input !== "string" || input.length === 0) {
12856
13347
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
12857
13348
  }
12858
- const root = path33.resolve(workspaceRoot);
12859
- const abs = path33.isAbsolute(input) ? path33.resolve(input) : path33.resolve(root, input);
12860
- const rel = path33.relative(root, abs);
12861
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path33.isAbsolute(rel)) {
13349
+ const root = path34.resolve(workspaceRoot);
13350
+ const abs = path34.isAbsolute(input) ? path34.resolve(input) : path34.resolve(root, input);
13351
+ const rel = path34.relative(root, abs);
13352
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34.isAbsolute(rel)) {
12862
13353
  throw new WorkspaceSandboxError(input, root);
12863
13354
  }
12864
13355
  try {
12865
- const real = fs30.realpathSync(abs);
12866
- const realRel = path33.relative(root, real);
12867
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path33.isAbsolute(realRel))) {
13356
+ const real = fs31.realpathSync(abs);
13357
+ const realRel = path34.relative(root, real);
13358
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34.isAbsolute(realRel))) {
12868
13359
  throw new WorkspaceSandboxError(input, root);
12869
13360
  }
12870
13361
  } catch (e) {
@@ -13517,7 +14008,7 @@ var GenerateDocumentTool = class extends BaseTool {
13517
14008
  }
13518
14009
  }
13519
14010
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
13520
- await fs11.mkdir(path33.dirname(absPath), { recursive: true });
14011
+ await fs11.mkdir(path34.dirname(absPath), { recursive: true });
13521
14012
  await fs11.writeFile(absPath, bytes);
13522
14013
  const charts = countCharts(content);
13523
14014
  return [
@@ -13738,7 +14229,7 @@ var FileWriteTool = class extends BaseTool {
13738
14229
  } catch {
13739
14230
  }
13740
14231
  }
13741
- await fs11.mkdir(path33.dirname(absPath), { recursive: true });
14232
+ await fs11.mkdir(path34.dirname(absPath), { recursive: true });
13742
14233
  await fs11.writeFile(absPath, content, "utf-8");
13743
14234
  return `Written ${content.length} characters to ${filePath}`;
13744
14235
  }
@@ -14236,7 +14727,7 @@ var ImageAnalyzeTool = class extends BaseTool {
14236
14727
  };
14237
14728
  async function fileToImageAttachment(filePath) {
14238
14729
  const data = await fs11.readFile(filePath);
14239
- const ext = path33.extname(filePath).toLowerCase();
14730
+ const ext = path34.extname(filePath).toLowerCase();
14240
14731
  const mimeMap = {
14241
14732
  ".jpg": "image/jpeg",
14242
14733
  ".jpeg": "image/jpeg",
@@ -14270,14 +14761,14 @@ var PDFCreateTool = class extends BaseTool {
14270
14761
  const filePath = input["path"];
14271
14762
  const content = input["content"];
14272
14763
  const title = input["title"];
14273
- const dir = path33.dirname(filePath);
14274
- if (!fs30.existsSync(dir)) {
14275
- fs30.mkdirSync(dir, { recursive: true });
14764
+ const dir = path34.dirname(filePath);
14765
+ if (!fs31.existsSync(dir)) {
14766
+ fs31.mkdirSync(dir, { recursive: true });
14276
14767
  }
14277
14768
  return new Promise((resolve2, reject) => {
14278
14769
  try {
14279
14770
  const doc = new PDFDocument({ margin: 50 });
14280
- const stream = fs30.createWriteStream(filePath);
14771
+ const stream = fs31.createWriteStream(filePath);
14281
14772
  doc.pipe(stream);
14282
14773
  if (title) {
14283
14774
  doc.info["Title"] = title;
@@ -14355,22 +14846,22 @@ var CodeInterpreterTool = class extends BaseTool {
14355
14846
  }
14356
14847
  cmdPrefix = NODE_CMD;
14357
14848
  }
14358
- const tmpDir = path33.join(this.workspaceRoot, ".cascade", "tmp");
14359
- if (!fs30.existsSync(tmpDir)) {
14360
- fs30.mkdirSync(tmpDir, { recursive: true });
14849
+ const tmpDir = path34.join(this.workspaceRoot, ".cascade", "tmp");
14850
+ if (!fs31.existsSync(tmpDir)) {
14851
+ fs31.mkdirSync(tmpDir, { recursive: true });
14361
14852
  }
14362
14853
  const extension = language === "python" ? "py" : "js";
14363
14854
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
14364
- const filePath = path33.join(tmpDir, fileName);
14365
- fs30.writeFileSync(filePath, code, "utf-8");
14855
+ const filePath = path34.join(tmpDir, fileName);
14856
+ fs31.writeFileSync(filePath, code, "utf-8");
14366
14857
  const execArgs = [filePath, ...args];
14367
14858
  return new Promise((resolve2) => {
14368
14859
  const startMs = Date.now();
14369
14860
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
14370
14861
  const duration = Date.now() - startMs;
14371
14862
  try {
14372
- if (fs30.existsSync(filePath)) {
14373
- fs30.unlinkSync(filePath);
14863
+ if (fs31.existsSync(filePath)) {
14864
+ fs31.unlinkSync(filePath);
14374
14865
  }
14375
14866
  } catch (cleanupErr) {
14376
14867
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -14682,7 +15173,7 @@ var GlobTool = class extends BaseTool {
14682
15173
  };
14683
15174
  async execute(input, _options) {
14684
15175
  const pattern = input["pattern"];
14685
- const searchPath = input["path"] ? path33.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15176
+ const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14686
15177
  const matches = await glob(pattern, {
14687
15178
  cwd: searchPath,
14688
15179
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -14695,7 +15186,7 @@ var GlobTool = class extends BaseTool {
14695
15186
  const withMtime = await Promise.all(
14696
15187
  matches.map(async (rel) => {
14697
15188
  try {
14698
- const stat = await fs11.stat(path33.join(searchPath, rel));
15189
+ const stat = await fs11.stat(path34.join(searchPath, rel));
14699
15190
  return { rel, mtime: stat.mtimeMs };
14700
15191
  } catch {
14701
15192
  return { rel, mtime: 0 };
@@ -14744,7 +15235,7 @@ var GrepTool = class extends BaseTool {
14744
15235
  };
14745
15236
  async execute(input, _options) {
14746
15237
  const pattern = input["pattern"];
14747
- const searchPath = input["path"] ? path33.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15238
+ const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
14748
15239
  const globPattern = input["glob"];
14749
15240
  const outputMode = input["output_mode"] ?? "content";
14750
15241
  const context = input["context"] ?? 0;
@@ -14798,12 +15289,12 @@ var GrepTool = class extends BaseTool {
14798
15289
  nodir: true
14799
15290
  });
14800
15291
  } catch {
14801
- files = [path33.relative(searchPath, searchPath) || "."];
15292
+ files = [path34.relative(searchPath, searchPath) || "."];
14802
15293
  }
14803
15294
  const results = [];
14804
15295
  let totalCount = 0;
14805
15296
  for (const rel of files) {
14806
- const abs = path33.join(searchPath, rel);
15297
+ const abs = path34.join(searchPath, rel);
14807
15298
  let content;
14808
15299
  try {
14809
15300
  content = await fs11.readFile(abs, "utf-8");
@@ -15190,10 +15681,10 @@ var ToolRegistry = class extends EventEmitter {
15190
15681
  }
15191
15682
  isIgnored(filePath) {
15192
15683
  if (!filePath) return false;
15193
- const abs = path33.resolve(this.workspaceRoot, filePath);
15194
- const rel = path33.relative(this.workspaceRoot, abs);
15195
- if (!rel || rel.startsWith("..") || path33.isAbsolute(rel)) return true;
15196
- const posixRel = rel.split(path33.sep).join("/");
15684
+ const abs = path34.resolve(this.workspaceRoot, filePath);
15685
+ const rel = path34.relative(this.workspaceRoot, abs);
15686
+ if (!rel || rel.startsWith("..") || path34.isAbsolute(rel)) return true;
15687
+ const posixRel = rel.split(path34.sep).join("/");
15197
15688
  return this.ignoreMatcher.ignores(posixRel);
15198
15689
  }
15199
15690
  };
@@ -15427,22 +15918,22 @@ var FileMcpOAuthStore = class {
15427
15918
  filePath;
15428
15919
  load() {
15429
15920
  try {
15430
- return JSON.parse(fs30.readFileSync(this.filePath, "utf-8"));
15921
+ return JSON.parse(fs31.readFileSync(this.filePath, "utf-8"));
15431
15922
  } catch {
15432
15923
  return void 0;
15433
15924
  }
15434
15925
  }
15435
15926
  save(state) {
15436
- fs30.mkdirSync(path33.dirname(this.filePath), { recursive: true, mode: 448 });
15437
- fs30.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15927
+ fs31.mkdirSync(path34.dirname(this.filePath), { recursive: true, mode: 448 });
15928
+ fs31.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
15438
15929
  try {
15439
- fs30.chmodSync(this.filePath, 384);
15930
+ fs31.chmodSync(this.filePath, 384);
15440
15931
  } catch {
15441
15932
  }
15442
15933
  }
15443
15934
  clear() {
15444
15935
  try {
15445
- fs30.rmSync(this.filePath);
15936
+ fs31.rmSync(this.filePath);
15446
15937
  } catch {
15447
15938
  }
15448
15939
  }
@@ -15905,6 +16396,14 @@ function heuristicAnalyze(prompt) {
15905
16396
  const estimatedTokens = wordCount * 5;
15906
16397
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
15907
16398
  }
16399
+ function hashPrompt(prompt) {
16400
+ let hash = 2166136261;
16401
+ for (let index = 0; index < prompt.length; index++) {
16402
+ hash ^= prompt.charCodeAt(index);
16403
+ hash = Math.imul(hash, 16777619) >>> 0;
16404
+ }
16405
+ return `${prompt.length}:${hash.toString(36)}`;
16406
+ }
15908
16407
  var analysisCache = /* @__PURE__ */ new Map();
15909
16408
  var TASK_TYPE_TAGS = {
15910
16409
  code: ["code", "instruction"],
@@ -15919,7 +16418,24 @@ var TaskAnalyzer = class {
15919
16418
  feedback;
15920
16419
  bias;
15921
16420
  lastProfile = null;
15922
- lastSelectedModels = /* @__PURE__ */ new Map();
16421
+ /** Models chosen by the run currently in flight. Cleared when it completes. */
16422
+ currentRunSelections = /* @__PURE__ */ new Map();
16423
+ /**
16424
+ * Immutable snapshot of the last COMPLETED run: its selections AND the task
16425
+ * type they were chosen for.
16426
+ *
16427
+ * The task type has to travel WITH the selections. `lastProfile` is overwritten
16428
+ * by the next analyze(), so if run B started before the user rated run A, A's
16429
+ * models were credited under B's task type — teaching the router that a coding
16430
+ * model is good at creative writing, from a rating that never said so.
16431
+ *
16432
+ * Explicit ratings arrive after the run finishes — rateLastRun() is by nature
16433
+ * a reaction to a finished answer — but the run finalizer calls
16434
+ * recordRunOutcome() first, and that used to clear the only map there was. So
16435
+ * every explicit rating iterated an empty map, recorded nothing, and returned
16436
+ * false: the 3x-weighted user signal never reached the tracker at all.
16437
+ */
16438
+ lastCompletedRun;
15923
16439
  constructor(tracker, bias = "balanced") {
15924
16440
  this.tracker = tracker;
15925
16441
  this.bias = bias;
@@ -15948,7 +16464,7 @@ var TaskAnalyzer = class {
15948
16464
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
15949
16465
  */
15950
16466
  async analyze(prompt) {
15951
- const cacheKey = prompt.slice(0, 200);
16467
+ const cacheKey = hashPrompt(prompt);
15952
16468
  const cached = analysisCache.get(cacheKey);
15953
16469
  if (cached) {
15954
16470
  this.lastProfile = cached;
@@ -15966,11 +16482,15 @@ var TaskAnalyzer = class {
15966
16482
  */
15967
16483
  async selectModel(prompt, tier, selector, opts) {
15968
16484
  const profile = await this.analyze(prompt);
16485
+ const recordSelection = (model) => {
16486
+ if (model) this.currentRunSelections.set(tier, model);
16487
+ return model;
16488
+ };
15969
16489
  if (profile.requiresVision) {
15970
- return selector.selectVisionModel();
16490
+ return recordSelection(selector.selectVisionModel());
15971
16491
  }
15972
16492
  let candidates2 = selector.getCandidatesForTier(tier);
15973
- if (candidates2.length === 0) return selector.selectForTier(tier);
16493
+ if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
15974
16494
  if (opts?.requiresToolUse) {
15975
16495
  const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
15976
16496
  if (toolCapable.length > 0) candidates2 = toolCapable;
@@ -15980,9 +16500,7 @@ var TaskAnalyzer = class {
15980
16500
  score: this.scoreModel(m, profile)
15981
16501
  }));
15982
16502
  scored.sort((a, b) => b.score - a.score);
15983
- const best = scored[0]?.model ?? selector.selectForTier(tier);
15984
- if (best) this.lastSelectedModels.set(tier, best);
15985
- return best;
16503
+ return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
15986
16504
  }
15987
16505
  /**
15988
16506
  * Record the outcome of a completed run across all tiers that were selected
@@ -15991,25 +16509,38 @@ var TaskAnalyzer = class {
15991
16509
  recordRunOutcome(outcome, costByTier, contextTokens = 0) {
15992
16510
  if (!this.tracker || !this.lastProfile) return;
15993
16511
  const taskType = this.lastProfile.type;
15994
- for (const [tier, model] of this.lastSelectedModels) {
16512
+ for (const [tier, model] of this.currentRunSelections) {
15995
16513
  const cost = costByTier[tier] ?? 0;
15996
16514
  this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
15997
16515
  }
15998
- this.lastSelectedModels.clear();
16516
+ this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
16517
+ this.currentRunSelections.clear();
15999
16518
  void this.tracker.save();
16000
16519
  }
16001
16520
  /**
16002
- * Record an explicit user rating (good/bad) for the last run's selected models.
16521
+ * Record an explicit user rating (good/bad) for the last COMPLETED run's models.
16003
16522
  * Explicit ratings carry 3× the weight of auto-detected outcomes.
16004
- * Does NOT clear lastSelectedModels — the auto record already did that.
16523
+ *
16524
+ * Reads the completed-run snapshot, not the in-flight map: by the time a user
16525
+ * can rate an answer, the finalizer has already run and the in-flight map is
16526
+ * empty.
16527
+ *
16528
+ * The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
16529
+ * three samples, so a double submit would inject six — a stutter on the
16530
+ * button would count as two opinions. Consuming makes a second call a no-op
16531
+ * that reports false, which is what "rate this run" should mean.
16005
16532
  */
16006
16533
  recordExplicitRating(rating) {
16007
- if (!this.tracker || !this.lastProfile) return false;
16008
- const taskType = this.lastProfile.type;
16009
- for (const [, model] of this.lastSelectedModels) {
16010
- this.tracker.recordExplicit(model.id, taskType, rating, 0);
16011
- }
16012
- return this.lastSelectedModels.size > 0;
16534
+ const snapshot = this.lastCompletedRun;
16535
+ if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
16536
+ const ratedModels = /* @__PURE__ */ new Set();
16537
+ for (const [, model] of snapshot.selections) {
16538
+ if (ratedModels.has(model.id)) continue;
16539
+ ratedModels.add(model.id);
16540
+ this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
16541
+ }
16542
+ this.lastCompletedRun = void 0;
16543
+ return true;
16013
16544
  }
16014
16545
  scoreModel(model, profile) {
16015
16546
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
@@ -16050,7 +16581,7 @@ var TaskAnalyzer = class {
16050
16581
 
16051
16582
  // src/core/router/model-performance-tracker.ts
16052
16583
  init_pricing();
16053
- var DEFAULT_STATS_FILE = path33.join(os10.homedir(), ".cascade", "model-perf.json");
16584
+ var DEFAULT_STATS_FILE = path34.join(os10.homedir(), ".cascade", "model-perf.json");
16054
16585
  var ModelPerformanceTracker = class {
16055
16586
  stats = /* @__PURE__ */ new Map();
16056
16587
  featureStats = /* @__PURE__ */ new Map();
@@ -16090,7 +16621,7 @@ var ModelPerformanceTracker = class {
16090
16621
  async save() {
16091
16622
  if (this.readOnly) return;
16092
16623
  try {
16093
- await fs11.mkdir(path33.dirname(this.statsFile), { recursive: true });
16624
+ await fs11.mkdir(path34.dirname(this.statsFile), { recursive: true });
16094
16625
  const modelsObj = {};
16095
16626
  const featuresObj = {};
16096
16627
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -16638,7 +17169,7 @@ Required capability: ${description.slice(0, 300)}`;
16638
17169
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
16639
17170
  async loadPersistedTools() {
16640
17171
  if (!this.workspacePath || !this.persistEnabled) return;
16641
- const file = path33.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17172
+ const file = path34.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
16642
17173
  try {
16643
17174
  const raw = await fs11.readFile(file, "utf-8");
16644
17175
  const specs = JSON.parse(raw);
@@ -16662,8 +17193,8 @@ Required capability: ${description.slice(0, 300)}`;
16662
17193
  }
16663
17194
  async persist() {
16664
17195
  if (!this.workspacePath || !this.persistEnabled) return;
16665
- const dir = path33.join(this.workspacePath, ".cascade");
16666
- const file = path33.join(dir, DYNAMIC_TOOLS_FILE);
17196
+ const dir = path34.join(this.workspacePath, ".cascade");
17197
+ const file = path34.join(dir, DYNAMIC_TOOLS_FILE);
16667
17198
  try {
16668
17199
  await fs11.mkdir(dir, { recursive: true });
16669
17200
  await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -16683,12 +17214,12 @@ var WorldStateDB = class {
16683
17214
  constructor(workspacePath, debugMode = false) {
16684
17215
  this.workspacePath = workspacePath;
16685
17216
  this.debugMode = debugMode;
16686
- const cascadeDir = path33.join(workspacePath, ".cascade");
16687
- if (!fs30.existsSync(cascadeDir)) {
16688
- fs30.mkdirSync(cascadeDir, { recursive: true });
17217
+ const cascadeDir = path34.join(workspacePath, ".cascade");
17218
+ if (!fs31.existsSync(cascadeDir)) {
17219
+ fs31.mkdirSync(cascadeDir, { recursive: true });
16689
17220
  }
16690
- this.keyPath = path33.join(cascadeDir, "world_state.key");
16691
- this.dbPath = path33.join(cascadeDir, "world_state.db");
17221
+ this.keyPath = path34.join(cascadeDir, "world_state.key");
17222
+ this.dbPath = path34.join(cascadeDir, "world_state.db");
16692
17223
  this.initEncryptionKey();
16693
17224
  this.db = new Database2(this.dbPath);
16694
17225
  this.db.pragma("journal_mode = WAL");
@@ -16745,11 +17276,11 @@ var WorldStateDB = class {
16745
17276
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
16746
17277
  }
16747
17278
  initEncryptionKey() {
16748
- if (fs30.existsSync(this.keyPath)) {
16749
- this.encryptionKey = fs30.readFileSync(this.keyPath);
17279
+ if (fs31.existsSync(this.keyPath)) {
17280
+ this.encryptionKey = fs31.readFileSync(this.keyPath);
16750
17281
  } else {
16751
17282
  this.encryptionKey = crypto.randomBytes(32);
16752
- fs30.writeFileSync(this.keyPath, this.encryptionKey);
17283
+ fs31.writeFileSync(this.keyPath, this.encryptionKey);
16753
17284
  }
16754
17285
  }
16755
17286
  encrypt(text) {
@@ -17035,9 +17566,9 @@ var WorldStateDB = class {
17035
17566
  dumpDebugIfNeeded() {
17036
17567
  if (!this.debugMode) return;
17037
17568
  try {
17038
- const dumpPath = path33.join(os10.tmpdir(), "cascade_world_state_debug.json");
17569
+ const dumpPath = path34.join(os10.tmpdir(), "cascade_world_state_debug.json");
17039
17570
  const entries = this.getAllEntries();
17040
- fs30.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17571
+ fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
17041
17572
  } catch (err) {
17042
17573
  console.error("Failed to dump debug world state", err);
17043
17574
  }
@@ -17046,6 +17577,212 @@ var WorldStateDB = class {
17046
17577
  this.db.close();
17047
17578
  }
17048
17579
  };
17580
+ var RESUME_SCHEMA_VERSION = 1;
17581
+ var DEFAULT_MAX_CHECKPOINTS = 5;
17582
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
17583
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
17584
+ var CLAIM_SUFFIX = ".claimed";
17585
+ var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
17586
+ function safeId(taskId) {
17587
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
17588
+ return cleaned.slice(0, 100) || "run";
17589
+ }
17590
+ function isCheckpoint(value) {
17591
+ if (typeof value !== "object" || value === null) return false;
17592
+ const c = value;
17593
+ 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);
17594
+ }
17595
+ var ResumeStore = class {
17596
+ dir;
17597
+ maxCheckpoints;
17598
+ maxAgeMs;
17599
+ now;
17600
+ claimTimeoutMs;
17601
+ constructor(options) {
17602
+ this.dir = options.dir;
17603
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
17604
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
17605
+ this.now = options.now ?? Date.now;
17606
+ this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
17607
+ }
17608
+ /**
17609
+ * Persist a checkpoint and prune.
17610
+ *
17611
+ * Never throws — failing to save a resume file must not turn a recoverable
17612
+ * stop into a crash — but DOES report whether the atomic rename landed.
17613
+ * Callers decide whether an original claim is safe to discard based on that
17614
+ * answer, so "I swallowed the error" and "the checkpoint is on disk" must not
17615
+ * look alike to them.
17616
+ *
17617
+ * @returns true only when the checkpoint is durably on disk.
17618
+ */
17619
+ async save(checkpoint) {
17620
+ try {
17621
+ const timestamp = this.now();
17622
+ const record = {
17623
+ ...checkpoint,
17624
+ version: RESUME_SCHEMA_VERSION,
17625
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
17626
+ };
17627
+ await promises.mkdir(this.dir, { recursive: true });
17628
+ const finalPath = path34.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
17629
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
17630
+ await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
17631
+ await promises.rename(tempPath, finalPath);
17632
+ await this.prune();
17633
+ return true;
17634
+ } catch {
17635
+ return false;
17636
+ }
17637
+ }
17638
+ /** Valid, unexpired checkpoints, newest first. */
17639
+ async list() {
17640
+ let entries;
17641
+ try {
17642
+ entries = await promises.readdir(this.dir);
17643
+ } catch {
17644
+ return [];
17645
+ }
17646
+ const cutoff = this.now() - this.maxAgeMs;
17647
+ 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);
17648
+ const checkpoints = [];
17649
+ for (const candidate of candidates2) {
17650
+ try {
17651
+ const raw = await promises.readFile(path34.join(this.dir, candidate.name), "utf-8");
17652
+ const parsed = JSON.parse(raw);
17653
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
17654
+ } catch {
17655
+ continue;
17656
+ }
17657
+ }
17658
+ return checkpoints;
17659
+ }
17660
+ /** The most recent resumable run, or null. */
17661
+ async latest() {
17662
+ return (await this.list())[0] ?? null;
17663
+ }
17664
+ /**
17665
+ * Claim a checkpoint for a resume attempt.
17666
+ *
17667
+ * This used to DELETE the file and return it, which opened a window with no
17668
+ * recovery record at all: between preparing the continuation and the resumed
17669
+ * run writing its own checkpoint, a crash lost the only copy — and a crash is
17670
+ * precisely what this store exists to survive. Deleting on read optimised for
17671
+ * "never resume twice" at the cost of "never lose the work", which is the
17672
+ * wrong trade for a recovery mechanism.
17673
+ *
17674
+ * Instead the file is RENAMED to `.claimed`, which still hides it from
17675
+ * `list()` (so a concurrent resume cannot take the same work) but keeps it on
17676
+ * disk. The caller then either `release`s it — restoring it for another
17677
+ * attempt — or `settle`s it once the resumed run is safely under way. A claim
17678
+ * abandoned by a dead process is reclaimed after `claimTimeoutMs`.
17679
+ */
17680
+ async claim(taskId) {
17681
+ const all = await this.list();
17682
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
17683
+ if (!target) return null;
17684
+ try {
17685
+ const entries = await promises.readdir(this.dir);
17686
+ const wanted = safeId(target.taskId);
17687
+ const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
17688
+ if (!source) return null;
17689
+ const claimId = `${this.now()}-${safeId(target.taskId)}`;
17690
+ await promises.rename(path34.join(this.dir, source), path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
17691
+ return { checkpoint: target, claimId };
17692
+ } catch {
17693
+ return null;
17694
+ }
17695
+ }
17696
+ /** Give a claim back, so the work stays recoverable after a failed start. */
17697
+ async release(claimId) {
17698
+ try {
17699
+ await promises.rename(
17700
+ path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
17701
+ path34.join(this.dir, `${claimId}.json`)
17702
+ );
17703
+ } catch {
17704
+ }
17705
+ }
17706
+ /** Discard a claim for good — the resumed run has taken ownership. */
17707
+ async settle(claimId) {
17708
+ try {
17709
+ await promises.rm(path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
17710
+ } catch {
17711
+ }
17712
+ }
17713
+ /**
17714
+ * Claim-and-discard in one step. Kept for callers that genuinely want the old
17715
+ * semantics; prefer claim/settle so a crash mid-resume stays recoverable.
17716
+ */
17717
+ async consume(taskId) {
17718
+ const claimed = await this.claim(taskId);
17719
+ if (!claimed) return null;
17720
+ await this.settle(claimed.claimId);
17721
+ return claimed.checkpoint;
17722
+ }
17723
+ /**
17724
+ * Return claims abandoned by a process that died mid-resume. Without this a
17725
+ * crash between claim and settle would strand the checkpoint invisibly —
17726
+ * trading one loss window for another.
17727
+ */
17728
+ async reclaimStale() {
17729
+ let reclaimed = 0;
17730
+ try {
17731
+ const entries = await promises.readdir(this.dir);
17732
+ const cutoff = this.now() - this.claimTimeoutMs;
17733
+ for (const name of entries) {
17734
+ if (!name.endsWith(CLAIM_SUFFIX)) continue;
17735
+ const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
17736
+ if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
17737
+ await promises.rename(
17738
+ path34.join(this.dir, name),
17739
+ path34.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
17740
+ );
17741
+ reclaimed++;
17742
+ }
17743
+ } catch {
17744
+ }
17745
+ return reclaimed;
17746
+ }
17747
+ /** Remove every checkpoint for a task id. */
17748
+ async delete(taskId) {
17749
+ try {
17750
+ const entries = await promises.readdir(this.dir);
17751
+ const wanted = safeId(taskId);
17752
+ await Promise.all(entries.map(async (name) => {
17753
+ const match = FILE_PATTERN.exec(name);
17754
+ if (match?.[2] === wanted) {
17755
+ await promises.rm(path34.join(this.dir, name), { force: true });
17756
+ }
17757
+ }));
17758
+ } catch {
17759
+ }
17760
+ }
17761
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
17762
+ async prune() {
17763
+ try {
17764
+ const entries = await promises.readdir(this.dir);
17765
+ const cutoff = this.now() - this.maxAgeMs;
17766
+ 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);
17767
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
17768
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
17769
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
17770
+ (name) => promises.rm(path34.join(this.dir, name), { force: true })
17771
+ ));
17772
+ } catch {
17773
+ }
17774
+ }
17775
+ };
17776
+ function summarizeCompleted(completed, perNodeChars = 500) {
17777
+ if (!completed.length) return "";
17778
+ const lines = completed.map((node) => {
17779
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
17780
+ return `- [${node.status}] ${node.title}
17781
+ ${body.replace(/\n/g, "\n ")}`;
17782
+ });
17783
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
17784
+ ${lines.join("\n")}`;
17785
+ }
17049
17786
  var ignore3 = _ignoreModule.default ?? _ignoreModule;
17050
17787
  var PrivacyPaths = class {
17051
17788
  localOnly;
@@ -17407,7 +18144,7 @@ var WorkspaceIndex = class {
17407
18144
  isIgnored;
17408
18145
  chunker;
17409
18146
  constructor(opts) {
17410
- this.root = path33.resolve(opts.root);
18147
+ this.root = path34.resolve(opts.root);
17411
18148
  this.db = opts.db;
17412
18149
  this.namespace = opts.namespace ?? "code";
17413
18150
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -17439,14 +18176,14 @@ var WorkspaceIndex = class {
17439
18176
  return;
17440
18177
  }
17441
18178
  for (const entry of entries) {
17442
- const abs = path33.join(dir, entry.name);
18179
+ const abs = path34.join(dir, entry.name);
17443
18180
  if (entry.isDirectory()) {
17444
18181
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
17445
18182
  await walk(abs);
17446
18183
  } else if (entry.isFile()) {
17447
- const ext = path33.extname(entry.name).slice(1).toLowerCase();
18184
+ const ext = path34.extname(entry.name).slice(1).toLowerCase();
17448
18185
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
17449
- out.push(path33.relative(this.root, abs));
18186
+ out.push(path34.relative(this.root, abs));
17450
18187
  }
17451
18188
  }
17452
18189
  };
@@ -17476,7 +18213,7 @@ var WorkspaceIndex = class {
17476
18213
  const entries = [];
17477
18214
  for (const rel of rels) {
17478
18215
  try {
17479
- const abs = path33.join(this.root, rel);
18216
+ const abs = path34.join(this.root, rel);
17480
18217
  const stat = await fs11.stat(abs);
17481
18218
  if (stat.size > this.maxFileBytes) continue;
17482
18219
  const buf = await fs11.readFile(abs);
@@ -18014,8 +18751,35 @@ var Cascade = class _Cascade extends EventEmitter {
18014
18751
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
18015
18752
  decisionLog = [];
18016
18753
  initialized = false;
18017
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
18754
+ /**
18755
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
18756
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
18757
+ * so a crash or an exit no longer throws away everything already finished.
18758
+ */
18018
18759
  lastInterruptedRun;
18760
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
18761
+ resumeStore;
18762
+ /** Claim held by an in-flight resume, settled once the run owns the work. */
18763
+ claimedResumeId;
18764
+ /**
18765
+ * Facts inherited from the checkpoint this run is resuming, so its own
18766
+ * checkpoint is cumulative rather than only covering this attempt.
18767
+ */
18768
+ resumedFrom;
18769
+ /** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
18770
+ wroteCheckpointThisRun = false;
18771
+ /**
18772
+ * Whether the run reached the end of its normal path.
18773
+ *
18774
+ * NOT the same as `runError == null`: the cancellation and budget handlers
18775
+ * both null `runError` deliberately (an intentional stop is not a telemetry
18776
+ * error) and the breaker path never sets it at all. Treating that as success
18777
+ * meant an immediate cancel — no output, no replacement checkpoint — deleted
18778
+ * the original claim and the finished sections with it.
18779
+ */
18780
+ completedNormally = false;
18781
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
18782
+ currentRunCompleted = [];
18019
18783
  initPromise;
18020
18784
  store;
18021
18785
  audit;
@@ -18048,10 +18812,16 @@ var Cascade = class _Cascade extends EventEmitter {
18048
18812
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
18049
18813
  try {
18050
18814
  this.router.setDeadModelStore(new DeadModelStore(
18051
- fileDeadModelPersistence(path33.join(workspacePath, ".cascade", "dead-models.json"))
18815
+ fileDeadModelPersistence(path34.join(workspacePath, ".cascade", "dead-models.json"))
18052
18816
  ));
18053
18817
  } catch {
18054
18818
  }
18819
+ try {
18820
+ this.resumeStore = new ResumeStore({
18821
+ dir: path34.join(workspacePath, ".cascade", "resume")
18822
+ });
18823
+ } catch {
18824
+ }
18055
18825
  this.multimodal = new MultimodalRegistry(
18056
18826
  (this.config.providers ?? []).map((p) => p.type)
18057
18827
  );
@@ -18136,8 +18906,8 @@ var Cascade = class _Cascade extends EventEmitter {
18136
18906
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
18137
18907
  return;
18138
18908
  }
18139
- const dbPath = ci.dbPath || path33.join(this.workspacePath, ".cascade", "code-index.db");
18140
- fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
18909
+ const dbPath = ci.dbPath || path34.join(this.workspacePath, ".cascade", "code-index.db");
18910
+ fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
18141
18911
  const db = new Database2(dbPath);
18142
18912
  db.pragma("journal_mode = WAL");
18143
18913
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -18517,10 +19287,57 @@ ${input}` }],
18517
19287
  setFeedbackSource(source) {
18518
19288
  this.router.setFeedbackSource(source);
18519
19289
  }
18520
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
19290
+ /**
19291
+ * Write a durable checkpoint for an interrupted run.
19292
+ *
19293
+ * Best-effort throughout: a run is already ending badly on every path that
19294
+ * calls this, and failing to record a resume file must not make that ending
19295
+ * worse. Nothing here is allowed to throw.
19296
+ */
19297
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
19298
+ const inherited = this.resumedFrom?.completed ?? [];
19299
+ const merged = /* @__PURE__ */ new Map();
19300
+ for (const node of inherited) merged.set(node.id, node);
19301
+ for (const node of this.currentRunCompleted) {
19302
+ const existing = merged.get(node.id);
19303
+ if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
19304
+ merged.set(node.id, node);
19305
+ }
19306
+ const completed = [...merged.values()];
19307
+ const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
19308
+ const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
19309
+ if (!completed.length && !carriedPartial) return false;
19310
+ try {
19311
+ const saved = await this.resumeStore?.save({
19312
+ taskId,
19313
+ prompt: canonicalPrompt,
19314
+ reason,
19315
+ ...detail ? { detail } : {},
19316
+ partialOutput: carriedPartial,
19317
+ completed
19318
+ });
19319
+ this.wroteCheckpointThisRun = saved === true;
19320
+ return saved === true;
19321
+ } catch {
19322
+ return false;
19323
+ }
19324
+ }
19325
+ /** True when a task can be resumed via /continue (this process or a previous one). */
18521
19326
  hasResumableRun() {
18522
19327
  return this.lastInterruptedRun != null;
18523
19328
  }
19329
+ /**
19330
+ * True when a checkpoint from ANY process is resumable. Async because the
19331
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
19332
+ * answer for callers already on a hot path.
19333
+ */
19334
+ async hasDurableResume() {
19335
+ return await this.resumeStore?.latest() != null;
19336
+ }
19337
+ /** Interrupted runs available to resume, newest first. */
19338
+ async listResumableRuns() {
19339
+ return await this.resumeStore?.list() ?? [];
19340
+ }
18524
19341
  /**
18525
19342
  * Raise the per-run token budget for a resume and return the continuation
18526
19343
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -18531,24 +19348,105 @@ ${input}` }],
18531
19348
  const last = this.lastInterruptedRun;
18532
19349
  if (!last) return null;
18533
19350
  this.lastInterruptedRun = void 0;
18534
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
19351
+ this.raiseBudgetForResume(opts.maxTokens);
19352
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
19353
+ }
19354
+ /**
19355
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
19356
+ * process, including one that crashed before it could return. Consumes the
19357
+ * checkpoint, so the same finished work is never restored into two runs.
19358
+ */
19359
+ async prepareDurableResume(opts = {}) {
19360
+ await this.resumeStore?.reclaimStale();
19361
+ const claimed = await this.resumeStore?.claim(opts.taskId);
19362
+ if (!claimed) return null;
19363
+ try {
19364
+ this.lastInterruptedRun = void 0;
19365
+ this.raiseBudgetForResume(opts.maxTokens);
19366
+ const prompt = this.buildResumePrompt(
19367
+ claimed.checkpoint.prompt,
19368
+ claimed.checkpoint.partialOutput,
19369
+ claimed.checkpoint.completed
19370
+ );
19371
+ this.resumedFrom = {
19372
+ prompt: claimed.checkpoint.prompt,
19373
+ completed: claimed.checkpoint.completed,
19374
+ partialOutput: claimed.checkpoint.partialOutput
19375
+ };
19376
+ this.claimedResumeId = claimed.claimId;
19377
+ return prompt;
19378
+ } catch (err) {
19379
+ await this.resumeStore?.release(claimed.claimId);
19380
+ throw err;
19381
+ }
19382
+ }
19383
+ /**
19384
+ * Discard the claimed checkpoint once the resumed run has taken ownership of
19385
+ * the work (it has produced its own checkpoint, or finished). Called by the
19386
+ * run loop; safe to call when nothing is claimed.
19387
+ */
19388
+ async settleClaimedResume(succeeded) {
19389
+ const claimId = this.claimedResumeId;
19390
+ if (!claimId) return;
19391
+ this.claimedResumeId = void 0;
19392
+ if (succeeded || this.wroteCheckpointThisRun) {
19393
+ await this.resumeStore?.settle(claimId);
19394
+ } else {
19395
+ await this.resumeStore?.release(claimId);
19396
+ }
19397
+ }
19398
+ /**
19399
+ * Hand back a claim taken by prepareDurableResume() when the caller never
19400
+ * gets as far as run(). The REPL prepares the continuation and submits it
19401
+ * separately, so a failure in between would otherwise strand the checkpoint
19402
+ * for the whole lease timeout before reclaimStale() could recover it.
19403
+ */
19404
+ async releaseClaimedResume() {
19405
+ const claimId = this.claimedResumeId;
19406
+ if (!claimId) return;
19407
+ this.claimedResumeId = void 0;
19408
+ await this.resumeStore?.release(claimId);
19409
+ }
19410
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
19411
+ raiseBudgetForResume(maxTokens) {
19412
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
18535
19413
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
18536
19414
  this.router.setMaxTokensPerRun(raised);
18537
- 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.
18538
-
18539
- Original task: ${last.prompt}` + (last.partialOutput ? `
18540
-
19415
+ }
19416
+ /**
19417
+ * The resume prompt. Completed sections are named explicitly rather than
19418
+ * gestured at: "do not recreate the files" is an instruction a planner can
19419
+ * only guess at, whereas a list of what is already done, and what it produced,
19420
+ * is something it can act on. That difference is the entire reason node
19421
+ * results are kept — a re-plan that cannot see the finished work will plan the
19422
+ * finished work again, and the user pays for it a second time.
19423
+ */
19424
+ buildResumePrompt(prompt, partialOutput, completed) {
19425
+ const done = summarizeCompleted(completed);
19426
+ return [
19427
+ "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.",
19428
+ "",
19429
+ `Original task: ${prompt}`,
19430
+ done ? `
19431
+ ${done}` : "",
19432
+ partialOutput ? `
18541
19433
  Partial result so far:
18542
- ${last.partialOutput}` : "");
19434
+ ${partialOutput}` : ""
19435
+ ].filter(Boolean).join("\n");
18543
19436
  }
18544
19437
  /**
18545
19438
  * Resume the last budget-capped task with a raised budget (SDK/headless).
18546
19439
  * Returns null when there is nothing to resume.
18547
19440
  */
18548
19441
  async resumeRun(opts = {}) {
18549
- const prompt = this.prepareResume(opts);
19442
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
18550
19443
  if (!prompt) return null;
18551
- return this.run({ prompt });
19444
+ try {
19445
+ return await this.run({ prompt });
19446
+ } catch (err) {
19447
+ await this.releaseClaimedResume();
19448
+ throw err;
19449
+ }
18552
19450
  }
18553
19451
  getWorkspacePath() {
18554
19452
  return this.workspacePath;
@@ -18917,6 +19815,10 @@ ${prompt}` : prompt;
18917
19815
  const startMs = Date.now();
18918
19816
  const taskId = randomUUID();
18919
19817
  this.decisionLog = [];
19818
+ this.currentRunCompleted = [];
19819
+ this.wroteCheckpointThisRun = false;
19820
+ this.completedNormally = false;
19821
+ if (!this.claimedResumeId) this.resumedFrom = void 0;
18920
19822
  if (options.fastAnswer) {
18921
19823
  return this.runFastAnswer(options, startMs, taskId);
18922
19824
  }
@@ -19031,6 +19933,11 @@ ${prompt}` : prompt;
19031
19933
  });
19032
19934
  tier.on("log", (e) => this.emit("log", e));
19033
19935
  tier.on("tier:status", (e) => this.emit("tier:status", e));
19936
+ tier.on("section:complete", (e) => {
19937
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
19938
+ this.currentRunCompleted.push(e);
19939
+ this.emit("section:complete", e);
19940
+ });
19034
19941
  tier.on("tool:call", (e) => this.emit("tool:call", e));
19035
19942
  tier.on("tool:result", (e) => this.emit("tool:result", e));
19036
19943
  tier.on("tool:approval-request", async (request) => {
@@ -19177,6 +20084,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
19177
20084
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
19178
20085
  const trip = runBreaker.reason();
19179
20086
  if (trip) {
20087
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
19180
20088
  finalOutput = [
19181
20089
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
19182
20090
  "",
@@ -19189,6 +20097,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
19189
20097
  ${finalOutput}` : ""
19190
20098
  ].join("\n").trimEnd();
19191
20099
  }
20100
+ this.completedNormally = trip == null;
19192
20101
  } catch (err) {
19193
20102
  if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
19194
20103
  this.emit("run:cancelled", {
@@ -19196,6 +20105,13 @@ ${finalOutput}` : ""
19196
20105
  reason: err instanceof Error ? err.message : "Task cancelled",
19197
20106
  partialOutput: finalOutput || ""
19198
20107
  });
20108
+ await this.checkpointRun(
20109
+ taskId,
20110
+ options.prompt,
20111
+ finalOutput || "",
20112
+ "cancelled",
20113
+ err instanceof Error ? err.message : "Task cancelled"
20114
+ );
19199
20115
  runError = null;
19200
20116
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
19201
20117
  this.emit("run:budget-exceeded", {
@@ -19204,9 +20120,17 @@ ${finalOutput}` : ""
19204
20120
  partialOutput: finalOutput || ""
19205
20121
  });
19206
20122
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
20123
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
19207
20124
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
19208
20125
  runError = null;
19209
20126
  } else {
20127
+ await this.checkpointRun(
20128
+ taskId,
20129
+ options.prompt,
20130
+ finalOutput || "",
20131
+ "error",
20132
+ err instanceof Error ? err.message : String(err)
20133
+ );
19210
20134
  runError = err;
19211
20135
  throw err;
19212
20136
  }
@@ -19215,6 +20139,10 @@ ${finalOutput}` : ""
19215
20139
  escalator.cancelAllPending();
19216
20140
  } catch {
19217
20141
  }
20142
+ try {
20143
+ await this.settleClaimedResume(this.completedNormally);
20144
+ } catch {
20145
+ }
19218
20146
  try {
19219
20147
  this.releasePendingEscalations();
19220
20148
  } catch {
@@ -19418,14 +20346,14 @@ ${finalOutput}` : ""
19418
20346
  init_constants();
19419
20347
  var CLOUD_SESSION_FILE = "cloud-session.json";
19420
20348
  function globalDir(homeDir = os10.homedir()) {
19421
- return path33.join(homeDir, GLOBAL_CONFIG_DIR);
20349
+ return path34.join(homeDir, GLOBAL_CONFIG_DIR);
19422
20350
  }
19423
20351
  function cloudSessionPath(dir = globalDir()) {
19424
- return path33.join(dir, CLOUD_SESSION_FILE);
20352
+ return path34.join(dir, CLOUD_SESSION_FILE);
19425
20353
  }
19426
20354
  function loadCloudSession(dir = globalDir()) {
19427
20355
  try {
19428
- const raw = fs30.readFileSync(cloudSessionPath(dir), "utf-8");
20356
+ const raw = fs31.readFileSync(cloudSessionPath(dir), "utf-8");
19429
20357
  const parsed = JSON.parse(raw);
19430
20358
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
19431
20359
  return parsed;
@@ -19434,17 +20362,17 @@ function loadCloudSession(dir = globalDir()) {
19434
20362
  }
19435
20363
  }
19436
20364
  function saveCloudSession(session, dir = globalDir()) {
19437
- fs30.mkdirSync(dir, { recursive: true, mode: 448 });
20365
+ fs31.mkdirSync(dir, { recursive: true, mode: 448 });
19438
20366
  const file = cloudSessionPath(dir);
19439
- fs30.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20367
+ fs31.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
19440
20368
  try {
19441
- fs30.chmodSync(file, 384);
20369
+ fs31.chmodSync(file, 384);
19442
20370
  } catch {
19443
20371
  }
19444
20372
  }
19445
20373
  function clearCloudSession(dir = globalDir()) {
19446
20374
  try {
19447
- fs30.rmSync(cloudSessionPath(dir));
20375
+ fs31.rmSync(cloudSessionPath(dir));
19448
20376
  } catch {
19449
20377
  }
19450
20378
  }
@@ -19616,16 +20544,16 @@ var CloudClient = class _CloudClient {
19616
20544
  return (await this.refresh()).accessToken;
19617
20545
  }
19618
20546
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
19619
- async authed(path38, init = {}) {
19620
- const call = (token) => fetch(this.url(path38), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
20547
+ async authed(path39, init = {}) {
20548
+ const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
19621
20549
  let res = await call(await this.accessToken());
19622
20550
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
19623
20551
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
19624
20552
  const text = await res.text();
19625
20553
  return text ? JSON.parse(text) : {};
19626
20554
  }
19627
- authedGet(path38) {
19628
- return this.authed(path38);
20555
+ authedGet(path39) {
20556
+ return this.authed(path39);
19629
20557
  }
19630
20558
  async fetchMe(token) {
19631
20559
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -20405,7 +21333,8 @@ function AgentTreeInternal({
20405
21333
  ] }),
20406
21334
  node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
20407
21335
  node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
20408
- node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" })
21336
+ node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" }),
21337
+ node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
20409
21338
  ] }, `t2-${node.id}-${i}`);
20410
21339
  }
20411
21340
  if (row.type === "t3") {
@@ -20430,7 +21359,8 @@ function AgentTreeInternal({
20430
21359
  /* @__PURE__ */ jsx(Spinner, { type: "dots" })
20431
21360
  ] }),
20432
21361
  node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
20433
- node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" })
21362
+ node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
21363
+ node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
20434
21364
  ] }, `t3-${node.id}-${i}`);
20435
21365
  }
20436
21366
  return null;
@@ -21400,9 +22330,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21400
22330
  }
21401
22331
  } catch {
21402
22332
  }
21403
- const configPath = path33.join(workspacePath, ".cascade", "config.json");
22333
+ const configPath = path34.join(workspacePath, ".cascade", "config.json");
21404
22334
  try {
21405
- await fs11.mkdir(path33.dirname(configPath), { recursive: true });
22335
+ await fs11.mkdir(path34.dirname(configPath), { recursive: true });
21406
22336
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
21407
22337
  } catch (err) {
21408
22338
  const msg = err instanceof Error ? err.message : String(err);
@@ -21429,7 +22359,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21429
22359
  }, []);
21430
22360
  const recordNodeEvent = useCallback((event) => {
21431
22361
  const existing = treeNodesRef.current.get(event.tierId);
21432
- const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, children: [] };
22362
+ const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, nodeId: event.nodeId ?? existing?.nodeId, dependsOn: event.dependsOn ?? existing?.dependsOn, waveId: event.waveId ?? existing?.waveId, blockedBy: event.blockedBy ?? existing?.blockedBy, children: [] };
21433
22363
  treeNodesRef.current.set(event.tierId, node);
21434
22364
  const store = storeRef.current;
21435
22365
  if (store) {
@@ -21454,9 +22384,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21454
22384
  if (msg.includes("non-text parts")) return;
21455
22385
  originalLog(...args);
21456
22386
  };
21457
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
22387
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
21458
22388
  storeRef.current = store;
21459
- globalStoreRef.current = new MemoryStore(path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22389
+ globalStoreRef.current = new MemoryStore(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
21460
22390
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
21461
22391
  setIdentities(identityRows);
21462
22392
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -21524,7 +22454,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
21524
22454
  onThemeChange: (name) => setTheme(getTheme(name)),
21525
22455
  onExport: async (fmt) => {
21526
22456
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
21527
- const exportPath = path33.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22457
+ const exportPath = path34.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
21528
22458
  if (fmt === "json") {
21529
22459
  await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
21530
22460
  } else {
@@ -21766,11 +22696,21 @@ ${lastUser.content}`;
21766
22696
  onContinue: async (args) => {
21767
22697
  const cascade = cascadeRef.current;
21768
22698
  if (!cascade) return "Not ready yet.";
21769
- if (!cascade.hasResumableRun()) return "Nothing to continue \u2014 no task was stopped by the budget cap.";
22699
+ if (state.isExecuting) {
22700
+ return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
22701
+ }
21770
22702
  const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
21771
- const prompt = cascade.prepareResume(Number.isFinite(n) && n > 0 ? { maxTokens: n } : {});
21772
- if (!prompt) return "Nothing to continue.";
21773
- await handleSubmit(prompt);
22703
+ const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
22704
+ const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
22705
+ if (!prompt) {
22706
+ return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
22707
+ }
22708
+ try {
22709
+ await handleSubmit(prompt);
22710
+ } catch (err) {
22711
+ await cascade.releaseClaimedResume();
22712
+ throw err;
22713
+ }
21774
22714
  return "";
21775
22715
  }
21776
22716
  });
@@ -22422,7 +23362,7 @@ function stringifySlashOutput(val) {
22422
23362
  }
22423
23363
  async function searchSessionsAndMessages(query, workspacePath) {
22424
23364
  if (!query) return "Usage: /search <query>";
22425
- const dbPath = path33.join(workspacePath, CASCADE_DB_FILE);
23365
+ const dbPath = path34.join(workspacePath, CASCADE_DB_FILE);
22426
23366
  try {
22427
23367
  await fs11.access(dbPath);
22428
23368
  } catch {
@@ -22456,7 +23396,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
22456
23396
  async function diagnoseRuntime(config, workspacePath) {
22457
23397
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
22458
23398
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
22459
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
23399
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
22460
23400
  try {
22461
23401
  const sessions2 = store.listSessions(void 0, 3);
22462
23402
  return [
@@ -22475,7 +23415,7 @@ async function diagnoseRuntime(config, workspacePath) {
22475
23415
  }
22476
23416
  async function showRecentLogs(args, workspacePath) {
22477
23417
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
22478
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
23418
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
22479
23419
  try {
22480
23420
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
22481
23421
  if (!logs.length) return "No recent runtime logs.";
@@ -22487,7 +23427,7 @@ async function showRecentLogs(args, workspacePath) {
22487
23427
  async function loadSessionSnapshot(args, workspacePath) {
22488
23428
  const sessionId = args[0];
22489
23429
  if (!sessionId) return "Usage: /resume <sessionId>";
22490
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
23430
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
22491
23431
  try {
22492
23432
  const session = store.getSession(sessionId);
22493
23433
  if (!session) return `Session not found: ${sessionId}`;
@@ -22915,9 +23855,9 @@ function SetupWizard({ workspacePath, onComplete }) {
22915
23855
  ...anyAuto ? { cascadeAuto: true } : {}
22916
23856
  };
22917
23857
  const config = CascadeConfigSchema.parse(rawConfig);
22918
- const configDir = path33.join(workspacePath, ".cascade");
23858
+ const configDir = path34.join(workspacePath, ".cascade");
22919
23859
  await fs11.mkdir(configDir, { recursive: true });
22920
- const configPath = path33.join(workspacePath, CASCADE_CONFIG_FILE);
23860
+ const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
22921
23861
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22922
23862
  savedConfigRef.current = config;
22923
23863
  dispatchRef.current({ type: "GO_DONE" });
@@ -23259,14 +24199,14 @@ function printTelemetryBanner() {
23259
24199
  async function initCommand(workspacePath = process.cwd()) {
23260
24200
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
23261
24201
  try {
23262
- const configDir = path33.join(workspacePath, ".cascade");
24202
+ const configDir = path34.join(workspacePath, ".cascade");
23263
24203
  await fs11.mkdir(configDir, { recursive: true });
23264
- const mdPath = path33.join(workspacePath, "CASCADE.md");
24204
+ const mdPath = path34.join(workspacePath, "CASCADE.md");
23265
24205
  if (!await fileExists(mdPath)) {
23266
24206
  await createDefaultCascadeMd(workspacePath);
23267
24207
  spin.succeed(chalk7.green("Created CASCADE.md"));
23268
24208
  }
23269
- const ignorePath = path33.join(workspacePath, ".cascadeignore");
24209
+ const ignorePath = path34.join(workspacePath, ".cascadeignore");
23270
24210
  if (!await fileExists(ignorePath)) {
23271
24211
  await createDefaultIgnoreFile(workspacePath);
23272
24212
  spin.succeed(chalk7.green("Created .cascadeignore"));
@@ -23275,7 +24215,7 @@ async function initCommand(workspacePath = process.cwd()) {
23275
24215
  console.log();
23276
24216
  console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
23277
24217
  console.log();
23278
- const configPath = path33.join(workspacePath, CASCADE_CONFIG_FILE);
24218
+ const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
23279
24219
  if (await fileExists(configPath)) {
23280
24220
  console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
23281
24221
  console.log();
@@ -23333,7 +24273,7 @@ function fromEnv(env) {
23333
24273
  return out;
23334
24274
  }
23335
24275
  async function fromClaudeCode(home) {
23336
- const file = path33.join(home, ".claude", ".credentials.json");
24276
+ const file = path34.join(home, ".claude", ".credentials.json");
23337
24277
  const data = await readJson(file);
23338
24278
  if (!data) return [];
23339
24279
  const oauth = data["claudeAiOauth"];
@@ -23356,7 +24296,7 @@ async function fromClaudeCode(home) {
23356
24296
  return [];
23357
24297
  }
23358
24298
  async function fromCodex(home) {
23359
- const file = path33.join(home, ".codex", "auth.json");
24299
+ const file = path34.join(home, ".codex", "auth.json");
23360
24300
  const data = await readJson(file);
23361
24301
  if (!data) return [];
23362
24302
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -23379,7 +24319,7 @@ async function fromCodex(home) {
23379
24319
  return [];
23380
24320
  }
23381
24321
  async function fromGemini(home) {
23382
- const file = path33.join(home, ".gemini", "oauth_creds.json");
24322
+ const file = path34.join(home, ".gemini", "oauth_creds.json");
23383
24323
  const data = await readJson(file);
23384
24324
  const accessToken = str(data?.["access_token"]);
23385
24325
  if (!accessToken) return [];
@@ -23395,7 +24335,7 @@ async function fromGemini(home) {
23395
24335
  }
23396
24336
  async function fromCopilot(home) {
23397
24337
  for (const name of ["apps.json", "hosts.json"]) {
23398
- const file = path33.join(home, ".config", "github-copilot", name);
24338
+ const file = path34.join(home, ".config", "github-copilot", name);
23399
24339
  const data = await readJson(file);
23400
24340
  if (!data) continue;
23401
24341
  for (const value of Object.values(data)) {
@@ -23449,7 +24389,7 @@ async function doctorCommand() {
23449
24389
  checks.push({
23450
24390
  label: "Cascade config",
23451
24391
  ok: true,
23452
- detail: `Loaded ${path33.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24392
+ detail: `Loaded ${path34.join(process.cwd(), CASCADE_CONFIG_FILE)}`
23453
24393
  });
23454
24394
  const providers = [
23455
24395
  { type: "anthropic", name: "Anthropic" },
@@ -23532,17 +24472,17 @@ async function doctorCommand() {
23532
24472
  const detail = c.detail ? chalk7.gray(` \u2014 ${c.detail}`) : "";
23533
24473
  console.log(`${icon} ${label}${detail}`);
23534
24474
  }
23535
- const failures = checks.filter((c) => !c.ok);
24475
+ const failures2 = checks.filter((c) => !c.ok);
23536
24476
  console.log();
23537
- if (failures.length === 0) {
24477
+ if (failures2.length === 0) {
23538
24478
  console.log(chalk7.green(" All checks passed!\n"));
23539
24479
  } else {
23540
- const critical = failures.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
24480
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
23541
24481
  if (critical.length) {
23542
24482
  console.log(chalk7.yellow(` ${critical.length} issue(s) need attention.
23543
24483
  `));
23544
24484
  } else {
23545
- console.log(chalk7.gray(` ${failures.length} optional item(s) not configured.
24485
+ console.log(chalk7.gray(` ${failures2.length} optional item(s) not configured.
23546
24486
  `));
23547
24487
  }
23548
24488
  }
@@ -23560,7 +24500,7 @@ async function checkEndpoint(url) {
23560
24500
  }
23561
24501
  }
23562
24502
  async function indexCommand(dirPath) {
23563
- const workspace = path33.resolve(dirPath || process.cwd());
24503
+ const workspace = path34.resolve(dirPath || process.cwd());
23564
24504
  const cm = new ConfigManager(workspace);
23565
24505
  await cm.load();
23566
24506
  const config = cm.getConfig();
@@ -23571,8 +24511,8 @@ async function indexCommand(dirPath) {
23571
24511
  process.exitCode = 1;
23572
24512
  return;
23573
24513
  }
23574
- const dbPath = config.codeIndex?.dbPath || path33.join(workspace, ".cascade", "code-index.db");
23575
- fs30.mkdirSync(path33.dirname(dbPath), { recursive: true });
24514
+ const dbPath = config.codeIndex?.dbPath || path34.join(workspace, ".cascade", "code-index.db");
24515
+ fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
23576
24516
  const db = new Database2(dbPath);
23577
24517
  db.pragma("journal_mode = WAL");
23578
24518
  const complete = chatCompleterFromProviders(config.providers);
@@ -24061,7 +25001,7 @@ function openBrowser2(url) {
24061
25001
  }
24062
25002
  function storePathFor(name) {
24063
25003
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
24064
- return path33.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
25004
+ return path34.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
24065
25005
  }
24066
25006
  function defaultName(url) {
24067
25007
  try {
@@ -24263,8 +25203,14 @@ var DashboardSocket = class {
24263
25203
  emitCascadeEvent(ev) {
24264
25204
  this.io.emit("cascade:event", ev);
24265
25205
  }
24266
- emitTierStatus(tierId, role, status, sessionId, action) {
24267
- const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId };
25206
+ /**
25207
+ * `extra` exists because this signature is positional: every field the tier
25208
+ * emits that is not named here is silently dropped on this path, while the
25209
+ * other two forwarders spread the whole event through. That asymmetry is how
25210
+ * a field can reach the web dashboard from one code path and not another.
25211
+ */
25212
+ emitTierStatus(tierId, role, status, sessionId, action, extra) {
25213
+ const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
24268
25214
  this.io.emit("tier:status", payload);
24269
25215
  this.io.to(`session:${sessionId}`).emit("tier:status", payload);
24270
25216
  }
@@ -24563,7 +25509,7 @@ function aggregateCostStats(sessions2, opts = {}) {
24563
25509
  }
24564
25510
 
24565
25511
  // src/dashboard/server.ts
24566
- var __dirname$1 = path33.dirname(fileURLToPath(import.meta.url));
25512
+ var __dirname$1 = path34.dirname(fileURLToPath(import.meta.url));
24567
25513
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
24568
25514
  var DashboardServer = class {
24569
25515
  app;
@@ -24843,14 +25789,14 @@ var DashboardServer = class {
24843
25789
  */
24844
25790
  persistConfig() {
24845
25791
  try {
24846
- const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
24847
- fs30.mkdirSync(path33.dirname(configPath), { recursive: true });
24848
- fs30.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
25792
+ const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
25793
+ fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
25794
+ fs31.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
24849
25795
  } catch (err) {
24850
25796
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
24851
25797
  }
24852
25798
  try {
24853
- saveGlobalCredentials(path33.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
25799
+ saveGlobalCredentials(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
24854
25800
  } catch (err) {
24855
25801
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
24856
25802
  }
@@ -24865,15 +25811,15 @@ var DashboardServer = class {
24865
25811
  resolveDashboardSecret() {
24866
25812
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
24867
25813
  if (fromConfig) return fromConfig;
24868
- const secretPath = path33.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
25814
+ const secretPath = path34.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
24869
25815
  try {
24870
- if (fs30.existsSync(secretPath)) {
24871
- const existing = fs30.readFileSync(secretPath, "utf-8").trim();
25816
+ if (fs31.existsSync(secretPath)) {
25817
+ const existing = fs31.readFileSync(secretPath, "utf-8").trim();
24872
25818
  if (existing.length >= 16) return existing;
24873
25819
  }
24874
25820
  const generated = randomUUID();
24875
- fs30.mkdirSync(path33.dirname(secretPath), { recursive: true });
24876
- fs30.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
25821
+ fs31.mkdirSync(path34.dirname(secretPath), { recursive: true });
25822
+ fs31.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
24877
25823
  if (this.config.dashboard.auth) {
24878
25824
  console.warn(
24879
25825
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -24900,7 +25846,7 @@ var DashboardServer = class {
24900
25846
  // ── Setup ─────────────────────────────────────
24901
25847
  getGlobalStore() {
24902
25848
  if (!this.globalStore) {
24903
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25849
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
24904
25850
  this.globalStore = new MemoryStore(globalDbPath);
24905
25851
  }
24906
25852
  return this.globalStore;
@@ -25280,12 +26226,12 @@ ${prompt}`;
25280
26226
  }
25281
26227
  }
25282
26228
  watchRuntimeChanges() {
25283
- const workspaceDbPath = path33.join(this.workspacePath, CASCADE_DB_FILE);
25284
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26229
+ const workspaceDbPath = path34.join(this.workspacePath, CASCADE_DB_FILE);
26230
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25285
26231
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
25286
26232
  for (const watchPath of watchPaths) {
25287
- if (!fs30.existsSync(watchPath)) continue;
25288
- fs30.watchFile(watchPath, { interval: 3e3 }, () => {
26233
+ if (!fs31.existsSync(watchPath)) continue;
26234
+ fs31.watchFile(watchPath, { interval: 3e3 }, () => {
25289
26235
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
25290
26236
  });
25291
26237
  }
@@ -25407,7 +26353,7 @@ ${prompt}`;
25407
26353
  const sessionId = req.params.id;
25408
26354
  this.store.deleteSession(sessionId);
25409
26355
  this.store.deleteRuntimeSession(sessionId);
25410
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26356
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25411
26357
  const globalStore = new MemoryStore(globalDbPath);
25412
26358
  try {
25413
26359
  globalStore.deleteRuntimeSession(sessionId);
@@ -25500,7 +26446,7 @@ ${prompt}`;
25500
26446
  });
25501
26447
  this.app.delete("/api/sessions", auth2, (req, res) => {
25502
26448
  const body = req.body;
25503
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26449
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25504
26450
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
25505
26451
  const globalStore = new MemoryStore(globalDbPath);
25506
26452
  try {
@@ -25523,7 +26469,7 @@ ${prompt}`;
25523
26469
  });
25524
26470
  this.app.delete("/api/runtime", auth2, (_req, res) => {
25525
26471
  this.store.deleteAllRuntimeNodes();
25526
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26472
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25527
26473
  const globalStore = new MemoryStore(globalDbPath);
25528
26474
  try {
25529
26475
  globalStore.deleteAllRuntimeNodes();
@@ -25609,12 +26555,12 @@ ${prompt}`;
25609
26555
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
25610
26556
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
25611
26557
  try {
25612
- const configPath = path33.join(this.workspacePath, CASCADE_CONFIG_FILE);
25613
- const existing = fs30.existsSync(configPath) ? JSON.parse(fs30.readFileSync(configPath, "utf-8")) : {};
26558
+ const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
26559
+ const existing = fs31.existsSync(configPath) ? JSON.parse(fs31.readFileSync(configPath, "utf-8")) : {};
25614
26560
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
25615
26561
  const tmp = configPath + ".tmp";
25616
- fs30.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
25617
- fs30.renameSync(tmp, configPath);
26562
+ fs31.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
26563
+ fs31.renameSync(tmp, configPath);
25618
26564
  res.json({ ok: true });
25619
26565
  } catch (err) {
25620
26566
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -25642,7 +26588,7 @@ ${prompt}`;
25642
26588
  this.app.get("/api/runtime", auth2, (req, res) => {
25643
26589
  const scope = req.query["scope"] ?? "workspace";
25644
26590
  if (scope === "global") {
25645
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26591
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
25646
26592
  const globalStore = new MemoryStore(globalDbPath);
25647
26593
  try {
25648
26594
  res.json({
@@ -25983,13 +26929,13 @@ ${prompt}`;
25983
26929
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
25984
26930
  }
25985
26931
  });
25986
- const prodPath = path33.resolve(__dirname$1, "../web/dist");
25987
- const devPath = path33.resolve(__dirname$1, "../../web/dist");
25988
- const webDistPath = fs30.existsSync(prodPath) ? prodPath : devPath;
25989
- if (fs30.existsSync(webDistPath)) {
26932
+ const prodPath = path34.resolve(__dirname$1, "../web/dist");
26933
+ const devPath = path34.resolve(__dirname$1, "../../web/dist");
26934
+ const webDistPath = fs31.existsSync(prodPath) ? prodPath : devPath;
26935
+ if (fs31.existsSync(webDistPath)) {
25990
26936
  this.app.use(express.static(webDistPath));
25991
26937
  this.app.get("*", (_req, res) => {
25992
- res.sendFile(path33.join(webDistPath, "index.html"));
26938
+ res.sendFile(path34.join(webDistPath, "index.html"));
25993
26939
  });
25994
26940
  } else {
25995
26941
  this.app.get("/", (_req, res) => {
@@ -26010,7 +26956,7 @@ init_constants();
26010
26956
  async function dashboardCommand(config, workspacePath = process.cwd()) {
26011
26957
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
26012
26958
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
26013
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
26959
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
26014
26960
  const server = new DashboardServer(config, store, workspacePath);
26015
26961
  server.watchRuntimeChanges();
26016
26962
  const gThis = globalThis;
@@ -26042,7 +26988,7 @@ init_constants();
26042
26988
  function makeIdentityCommand(workspacePath = process.cwd()) {
26043
26989
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
26044
26990
  identity.command("list").description("List all available identities").action(() => {
26045
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
26991
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
26046
26992
  try {
26047
26993
  const identities = store.listIdentities();
26048
26994
  console.log(chalk7.bold("\n Identities:"));
@@ -26062,7 +27008,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26062
27008
  }
26063
27009
  });
26064
27010
  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) => {
26065
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
27011
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
26066
27012
  try {
26067
27013
  if (options.default) {
26068
27014
  const existingDefault = store.getDefaultIdentity();
@@ -26088,7 +27034,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
26088
27034
  }
26089
27035
  });
26090
27036
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
26091
- const store = new MemoryStore(path33.join(workspacePath, CASCADE_DB_FILE));
27037
+ const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
26092
27038
  try {
26093
27039
  const identities = store.listIdentities();
26094
27040
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -26237,8 +27183,8 @@ async function exportCommand(options = {}) {
26237
27183
  let store;
26238
27184
  try {
26239
27185
  const workspacePath = options.workspacePath ?? process.cwd();
26240
- const workspaceDbPath = path33.join(workspacePath, CASCADE_DB_FILE);
26241
- const globalDbPath = path33.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
27186
+ const workspaceDbPath = path34.join(workspacePath, CASCADE_DB_FILE);
27187
+ const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
26242
27188
  let dbPath = globalDbPath;
26243
27189
  try {
26244
27190
  await fs11.access(workspaceDbPath);
@@ -26280,7 +27226,7 @@ async function exportCommand(options = {}) {
26280
27226
  const ext = format === "json" ? ".json" : ".md";
26281
27227
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
26282
27228
  const defaultFile = `cascade-export-${safeName}${ext}`;
26283
- const outPath = options.output ? path33.resolve(options.output) : path33.join(process.cwd(), defaultFile);
27229
+ const outPath = options.output ? path34.resolve(options.output) : path34.join(process.cwd(), defaultFile);
26284
27230
  await fs11.writeFile(outPath, content, "utf-8");
26285
27231
  spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
26286
27232
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
@@ -26508,11 +27454,11 @@ async function statsCommand() {
26508
27454
  dotenv.config();
26509
27455
  function warnIfBuildIsStale() {
26510
27456
  try {
26511
- const here = path33.dirname(fileURLToPath(import.meta.url));
27457
+ const here = path34.dirname(fileURLToPath(import.meta.url));
26512
27458
  for (const rel of ["..", "../.."]) {
26513
- const pkgPath = path33.join(here, rel, "package.json");
26514
- if (!fs30.existsSync(pkgPath)) continue;
26515
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
27459
+ const pkgPath = path34.join(here, rel, "package.json");
27460
+ if (!fs31.existsSync(pkgPath)) continue;
27461
+ const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
26516
27462
  if (pkg.name !== "cascade-ai") continue;
26517
27463
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
26518
27464
  console.error(