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/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var Database = require('better-sqlite3');
4
- var path25 = require('path');
5
- var fs24 = require('fs');
4
+ var path26 = require('path');
5
+ var fs25 = require('fs');
6
6
  var os6 = require('os');
7
7
  var crypto4 = require('crypto');
8
8
  var EventEmitter = require('events');
@@ -61,8 +61,8 @@ function _interopNamespace(e) {
61
61
  }
62
62
 
63
63
  var Database__default = /*#__PURE__*/_interopDefault(Database);
64
- var path25__default = /*#__PURE__*/_interopDefault(path25);
65
- var fs24__default = /*#__PURE__*/_interopDefault(fs24);
64
+ var path26__default = /*#__PURE__*/_interopDefault(path26);
65
+ var fs25__default = /*#__PURE__*/_interopDefault(fs25);
66
66
  var os6__default = /*#__PURE__*/_interopDefault(os6);
67
67
  var crypto4__default = /*#__PURE__*/_interopDefault(crypto4);
68
68
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
@@ -107,12 +107,12 @@ var init_audit_logger = __esm({
107
107
  constructor(workspacePath, debugMode = false) {
108
108
  this.workspacePath = workspacePath;
109
109
  this.debugMode = debugMode;
110
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
111
- if (!fs24__default.default.existsSync(cascadeDir)) {
112
- fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
110
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
111
+ if (!fs25__default.default.existsSync(cascadeDir)) {
112
+ fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
113
113
  }
114
- this.keyPath = path25__default.default.join(cascadeDir, "audit_log.key");
115
- this.dbPath = path25__default.default.join(cascadeDir, "audit_log.db");
114
+ this.keyPath = path26__default.default.join(cascadeDir, "audit_log.key");
115
+ this.dbPath = path26__default.default.join(cascadeDir, "audit_log.db");
116
116
  this.initEncryptionKey();
117
117
  this.db = new Database__default.default(this.dbPath);
118
118
  this.db.pragma("journal_mode = WAL");
@@ -142,11 +142,11 @@ var init_audit_logger = __esm({
142
142
  return crypto4__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
143
143
  }
144
144
  initEncryptionKey() {
145
- if (fs24__default.default.existsSync(this.keyPath)) {
146
- this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
145
+ if (fs25__default.default.existsSync(this.keyPath)) {
146
+ this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
147
147
  } else {
148
148
  this.encryptionKey = crypto4__default.default.randomBytes(32);
149
- fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
149
+ fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
150
150
  }
151
151
  }
152
152
  encrypt(text) {
@@ -224,9 +224,9 @@ var init_audit_logger = __esm({
224
224
  dumpDebugIfNeeded() {
225
225
  if (!this.debugMode) return;
226
226
  try {
227
- const dumpPath = path25__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
227
+ const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
228
228
  const entries = this.getAllLogs();
229
- fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
229
+ fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
230
230
  } catch (err) {
231
231
  console.error("Failed to dump debug audit logs", err);
232
232
  }
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
1908
1908
  }
1909
1909
 
1910
1910
  // src/constants.ts
1911
- var CASCADE_VERSION = "0.67.0";
1911
+ var CASCADE_VERSION = "0.69.0";
1912
1912
  var CASCADE_CONFIG_DIR = ".cascade";
1913
1913
  var CASCADE_MD_FILE = "CASCADE.md";
1914
1914
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4494,13 +4494,13 @@ var DeadModelStore = class {
4494
4494
  function fileDeadModelPersistence(filePath) {
4495
4495
  return {
4496
4496
  load() {
4497
- if (!fs24.existsSync(filePath)) return [];
4498
- const raw = JSON.parse(fs24.readFileSync(filePath, "utf-8"));
4497
+ if (!fs25.existsSync(filePath)) return [];
4498
+ const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
4499
4499
  return Array.isArray(raw) ? raw : [];
4500
4500
  },
4501
4501
  save(records) {
4502
- fs24.mkdirSync(path25.dirname(filePath), { recursive: true });
4503
- fs24.writeFileSync(filePath, JSON.stringify(records, null, 2));
4502
+ fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
4503
+ fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
4504
4504
  }
4505
4505
  };
4506
4506
  }
@@ -4663,7 +4663,7 @@ function computeDelegationSavings(stats, t1Model) {
4663
4663
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
4664
4664
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
4665
4665
  var FETCH_TIMEOUT_MS = 8e3;
4666
- var DEFAULT_CACHE_FILE = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4666
+ var DEFAULT_CACHE_FILE = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4667
4667
  function normalizeModelId2(id) {
4668
4668
  let s = id.toLowerCase();
4669
4669
  const slash = s.lastIndexOf("/");
@@ -4810,7 +4810,7 @@ var LiveDataProvider = class {
4810
4810
  }
4811
4811
  async saveCache() {
4812
4812
  try {
4813
- await fs5__default.default.mkdir(path25__default.default.dirname(this.opts.cacheFile), { recursive: true });
4813
+ await fs5__default.default.mkdir(path26__default.default.dirname(this.opts.cacheFile), { recursive: true });
4814
4814
  const cache = {
4815
4815
  fetchedAt: this.fetchedAt,
4816
4816
  snapshot: this.snapshot ?? void 0,
@@ -6070,6 +6070,21 @@ var BaseTier = class extends EventEmitter__default.default {
6070
6070
  * which node (Cockpit node panel / Why panel).
6071
6071
  */
6072
6072
  servingModel;
6073
+ /**
6074
+ * This tier's identity IN THE TASK GRAPH — the section or subtask id the
6075
+ * planner assigned — plus where it sat in the dependency order.
6076
+ *
6077
+ * `id` and `graphNodeId` are deliberately different id spaces, and both are
6078
+ * needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
6079
+ * construction); `graphNodeId` identifies the WORK ("s1"), which is what
6080
+ * dependency edges point at. Without the distinction a graph view cannot be
6081
+ * drawn at all: `dependsOn` lists section ids, while every running tier
6082
+ * reports a generated tier id, so every edge would name a node that never
6083
+ * appears in the stream.
6084
+ */
6085
+ graphNodeId;
6086
+ graphDependsOn;
6087
+ graphWave;
6073
6088
  constructor(role, id, parentId) {
6074
6089
  super();
6075
6090
  this.role = role;
@@ -6081,6 +6096,33 @@ var BaseTier = class extends EventEmitter__default.default {
6081
6096
  setPresenter(on = true) {
6082
6097
  this.isPresenter = on;
6083
6098
  }
6099
+ /**
6100
+ * Record where this tier sits in the compiled task graph.
6101
+ *
6102
+ * Called by whichever tier scheduled this one: the id and edges are known at
6103
+ * construction, the wave only once the scheduler reaches it, so the fields
6104
+ * are set independently rather than all at once.
6105
+ */
6106
+ setGraphPosition(position) {
6107
+ if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
6108
+ if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
6109
+ if (position.wave !== void 0) this.graphWave = position.wave;
6110
+ }
6111
+ /**
6112
+ * The graph fields, for spreading into a status payload.
6113
+ *
6114
+ * One helper rather than two literals because `tier:status` is emitted from
6115
+ * two different call sites with two different payload shapes; adding a field
6116
+ * to only one of them makes it arrive intermittently, which is worse for a
6117
+ * consumer than never arriving at all.
6118
+ */
6119
+ graphFields() {
6120
+ return {
6121
+ nodeId: this.graphNodeId,
6122
+ dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
6123
+ waveId: this.graphWave
6124
+ };
6125
+ }
6084
6126
  getStatus() {
6085
6127
  return this.status;
6086
6128
  }
@@ -6095,7 +6137,8 @@ var BaseTier = class extends EventEmitter__default.default {
6095
6137
  status,
6096
6138
  timestamp,
6097
6139
  output,
6098
- model: this.servingModel
6140
+ model: this.servingModel,
6141
+ ...this.graphFields()
6099
6142
  };
6100
6143
  this.emit("status", event);
6101
6144
  this.emit("tier:status", event);
@@ -6135,7 +6178,8 @@ var BaseTier = class extends EventEmitter__default.default {
6135
6178
  progressPct: update.progressPct,
6136
6179
  timestamp,
6137
6180
  output: update.output,
6138
- model: this.servingModel
6181
+ model: this.servingModel,
6182
+ ...this.graphFields()
6139
6183
  });
6140
6184
  }
6141
6185
  buildMessage(type, to, payload) {
@@ -6602,6 +6646,79 @@ function describeProviderError(c, modelId) {
6602
6646
  }
6603
6647
  }
6604
6648
 
6649
+ // src/core/verification/acceptance.ts
6650
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
6651
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
6652
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
6653
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
6654
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
6655
+ function targetFile(criterion, ownedFiles) {
6656
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
6657
+ if (mentioned.length > 1) return null;
6658
+ if (mentioned.length === 1) {
6659
+ const candidate = mentioned[0];
6660
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
6661
+ }
6662
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
6663
+ }
6664
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
6665
+ const results = [];
6666
+ for (const criterion of criteria) {
6667
+ const text = criterion.trim();
6668
+ if (!text) {
6669
+ results.push({ criterion, verdict: "undecidable" });
6670
+ continue;
6671
+ }
6672
+ if (NEGATION.test(text)) {
6673
+ results.push({ criterion, verdict: "undecidable" });
6674
+ continue;
6675
+ }
6676
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
6677
+ const wantsExists = EXISTS.test(text);
6678
+ const wantsNonEmpty = NON_EMPTY.test(text);
6679
+ if (!needle && !wantsExists && !wantsNonEmpty) {
6680
+ results.push({ criterion, verdict: "undecidable" });
6681
+ continue;
6682
+ }
6683
+ const file = targetFile(text, ownedFiles);
6684
+ if (!file) {
6685
+ results.push({ criterion, verdict: "undecidable" });
6686
+ continue;
6687
+ }
6688
+ const stat = await probe.stat(file);
6689
+ if (!stat) {
6690
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
6691
+ continue;
6692
+ }
6693
+ if (wantsNonEmpty && stat.size === 0) {
6694
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
6695
+ continue;
6696
+ }
6697
+ if (needle) {
6698
+ const content = await probe.read(file);
6699
+ if (content === null) {
6700
+ results.push({ criterion, verdict: "undecidable" });
6701
+ continue;
6702
+ }
6703
+ const found = content.includes(needle);
6704
+ results.push({
6705
+ criterion,
6706
+ verdict: found ? "passed" : "failed",
6707
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
6708
+ });
6709
+ continue;
6710
+ }
6711
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
6712
+ }
6713
+ return results;
6714
+ }
6715
+ function undecided(results) {
6716
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
6717
+ }
6718
+ function failures(results) {
6719
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
6720
+ }
6721
+
6605
6722
  // src/core/tiers/t3-worker.ts
6606
6723
  var CriticalToolError = class extends Error {
6607
6724
  constructor(message, toolName) {
@@ -6918,7 +7035,7 @@ Now execute your subtask using this context where relevant.`
6918
7035
  }
6919
7036
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6920
7037
  if (!artifactCheck.ok) {
6921
- correctionAttempts = 1;
7038
+ correctionAttempts++;
6922
7039
  issues.push(...artifactCheck.issues);
6923
7040
  output = await this.correctOutput(output, artifactCheck.issues);
6924
7041
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -6936,22 +7053,45 @@ Now execute your subtask using this context where relevant.`
6936
7053
  this.tools.map((t) => t.name)
6937
7054
  );
6938
7055
  if (visualIssue) {
6939
- correctionAttempts = 1;
7056
+ correctionAttempts++;
6940
7057
  issues.push(visualIssue);
6941
7058
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
6942
7059
  output = await this.correctOutput(output, [visualIssue]);
6943
7060
  }
7061
+ const acceptanceResults = await this.checkAcceptance(assignment);
7062
+ const acceptanceFailures = failures(acceptanceResults);
7063
+ if (acceptanceFailures.length > 0) {
7064
+ correctionAttempts++;
7065
+ issues.push(...acceptanceFailures);
7066
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
7067
+ output = await this.correctOutput(output, acceptanceFailures);
7068
+ const recheck = failures(await this.checkAcceptance(assignment));
7069
+ if (recheck.length > 0) {
7070
+ issues.push(...recheck);
7071
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
7072
+ failed.push(...recheck);
7073
+ this.setStatus("FAILED");
7074
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
7075
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
7076
+ }
7077
+ }
7078
+ for (const result2 of acceptanceResults) {
7079
+ if (result2.verdict === "undecidable") continue;
7080
+ checksRun.push(result2.criterion);
7081
+ if (result2.verdict === "passed") passed.push(result2.criterion);
7082
+ }
6944
7083
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
6945
- const testResult = await this.selfTest(assignment, output);
7084
+ const pendingAcceptance = undecided(acceptanceResults);
7085
+ const testResult = await this.selfTest(assignment, output, pendingAcceptance);
6946
7086
  checksRun.push(...testResult.checksRun);
6947
7087
  passed.push(...testResult.passed);
6948
7088
  failed.push(...testResult.failed);
6949
7089
  if (testResult.failed.length > 0) {
6950
- correctionAttempts = 1;
7090
+ correctionAttempts++;
6951
7091
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
6952
7092
  const corrected = await this.correctOutput(output, testResult.failed);
6953
7093
  output = corrected;
6954
- const retest = await this.selfTest(assignment, output);
7094
+ const retest = await this.selfTest(assignment, output, pendingAcceptance);
6955
7095
  passed.push(...retest.passed);
6956
7096
  if (retest.failed.length > 0) {
6957
7097
  failed.push(...retest.failed);
@@ -7255,8 +7395,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
7255
7395
  // Media generation can run for a minute; without this a cancelled run
7256
7396
  // still pays for an image nobody will see.
7257
7397
  ...this.signal ? { signal: this.signal } : {},
7258
- saveSnapshot: async (path27, content) => {
7259
- this.store?.addFileSnapshot(this.taskId, path27, content);
7398
+ saveSnapshot: async (path28, content) => {
7399
+ this.store?.addFileSnapshot(this.taskId, path28, content);
7260
7400
  },
7261
7401
  sendPeerSync: (to, syncType, content) => {
7262
7402
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -7443,7 +7583,7 @@ ${assignment.expectedOutput}`;
7443
7583
  const { promisify: promisify4 } = await import('util');
7444
7584
  const execAsync2 = promisify4(exec2);
7445
7585
  for (const artifactPath of artifactPaths) {
7446
- const absolutePath = path25__default.default.resolve(process.cwd(), artifactPath);
7586
+ const absolutePath = path26__default.default.resolve(process.cwd(), artifactPath);
7447
7587
  try {
7448
7588
  const stat = await fs5__default.default.stat(absolutePath);
7449
7589
  if (!stat.isFile()) {
@@ -7478,7 +7618,7 @@ ${assignment.expectedOutput}`;
7478
7618
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
7479
7619
  continue;
7480
7620
  }
7481
- const ext = path25__default.default.extname(absolutePath).toLowerCase();
7621
+ const ext = path26__default.default.extname(absolutePath).toLowerCase();
7482
7622
  try {
7483
7623
  if (ext === ".ts" || ext === ".tsx") {
7484
7624
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -7564,14 +7704,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7564
7704
  }
7565
7705
  return current;
7566
7706
  }
7567
- async selfTest(assignment, output) {
7707
+ /**
7708
+ * Run the acceptance criteria that can be settled by looking at the workspace.
7709
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
7710
+ */
7711
+ async checkAcceptance(assignment) {
7712
+ const criteria = assignment.acceptance ?? [];
7713
+ if (!criteria.length) return [];
7714
+ const resolve2 = (target) => path26__default.default.resolve(process.cwd(), target);
7715
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
7716
+ stat: async (target) => {
7717
+ try {
7718
+ const stat = await fs5__default.default.stat(resolve2(target));
7719
+ return stat.isFile() ? { size: stat.size } : null;
7720
+ } catch {
7721
+ return null;
7722
+ }
7723
+ },
7724
+ read: async (target) => {
7725
+ try {
7726
+ const content = await fs5__default.default.readFile(resolve2(target), "utf-8");
7727
+ return content.includes("\uFFFD") ? null : content;
7728
+ } catch {
7729
+ return null;
7730
+ }
7731
+ }
7732
+ });
7733
+ }
7734
+ async selfTest(assignment, output, pendingAcceptance) {
7735
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
7568
7736
  const prompt = `Self-test this output against the assignment requirements.
7569
7737
 
7570
7738
  Assignment: ${assignment.description}
7571
7739
  Expected output: ${assignment.expectedOutput}
7572
7740
  Constraints: ${assignment.constraints.join("; ")}
7573
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7574
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
7741
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7742
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
7575
7743
  ` : ""}
7576
7744
  Output to test:
7577
7745
  ${output}
@@ -7638,8 +7806,8 @@ ${output.slice(0, 4e3)}`;
7638
7806
  } catch {
7639
7807
  }
7640
7808
  }
7641
- async correctOutput(originalOutput, failures) {
7642
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
7809
+ async correctOutput(originalOutput, failures2) {
7810
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
7643
7811
 
7644
7812
  Original output:
7645
7813
  ${originalOutput}
@@ -8300,12 +8468,220 @@ function describeGenerationForPlanner(has) {
8300
8468
  return lines.join("\n");
8301
8469
  }
8302
8470
 
8471
+ // src/core/orchestration/task-graph.ts
8472
+ var TaskGraphValidationError = class extends Error {
8473
+ constructor(issues) {
8474
+ super(issues.map((issue) => issue.message).join("; "));
8475
+ this.issues = issues;
8476
+ this.name = "TaskGraphValidationError";
8477
+ }
8478
+ issues;
8479
+ };
8480
+ function normalizeDependencies(input) {
8481
+ if (!input?.length) return [];
8482
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
8483
+ }
8484
+ function cycleNodes(nodes) {
8485
+ const adjacency = new Map(
8486
+ nodes.map((node) => [node.id, node.dependsOn])
8487
+ );
8488
+ const index = /* @__PURE__ */ new Map();
8489
+ const lowlink = /* @__PURE__ */ new Map();
8490
+ const onStack = /* @__PURE__ */ new Set();
8491
+ const stack = [];
8492
+ const inCycle = /* @__PURE__ */ new Set();
8493
+ let counter = 0;
8494
+ const open = (id) => {
8495
+ index.set(id, counter);
8496
+ lowlink.set(id, counter);
8497
+ counter++;
8498
+ stack.push(id);
8499
+ onStack.add(id);
8500
+ };
8501
+ for (const root of nodes) {
8502
+ if (index.has(root.id)) continue;
8503
+ open(root.id);
8504
+ const frames = [{ id: root.id, edge: 0 }];
8505
+ while (frames.length > 0) {
8506
+ const frame = frames[frames.length - 1];
8507
+ const neighbours = adjacency.get(frame.id) ?? [];
8508
+ if (frame.edge < neighbours.length) {
8509
+ const next = neighbours[frame.edge];
8510
+ frame.edge++;
8511
+ if (!adjacency.has(next)) continue;
8512
+ if (!index.has(next)) {
8513
+ open(next);
8514
+ frames.push({ id: next, edge: 0 });
8515
+ } else if (onStack.has(next)) {
8516
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
8517
+ }
8518
+ continue;
8519
+ }
8520
+ frames.pop();
8521
+ const parent = frames[frames.length - 1];
8522
+ if (parent) {
8523
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
8524
+ }
8525
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
8526
+ const component = [];
8527
+ for (; ; ) {
8528
+ const member = stack.pop();
8529
+ onStack.delete(member);
8530
+ component.push(member);
8531
+ if (member === frame.id) break;
8532
+ }
8533
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
8534
+ if (cyclic) for (const member of component) inCycle.add(member);
8535
+ }
8536
+ }
8537
+ }
8538
+ return inCycle;
8539
+ }
8540
+ function throwIfStrict(mode, issues) {
8541
+ if (mode === "strict" && issues.length > 0) {
8542
+ throw new TaskGraphValidationError(issues);
8543
+ }
8544
+ }
8545
+ function compileTaskGraph(inputNodes, options = {}) {
8546
+ const mode = options.mode ?? "strict";
8547
+ const issues = [];
8548
+ if (inputNodes.length === 0) {
8549
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
8550
+ throwIfStrict(mode, issues);
8551
+ }
8552
+ const seen = /* @__PURE__ */ new Set();
8553
+ const nodes = [];
8554
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
8555
+ const input = inputNodes[ordinal];
8556
+ const id = input.id?.trim();
8557
+ const title = input.title?.trim();
8558
+ if (!id || !title) {
8559
+ issues.push({
8560
+ code: "INVALID_NODE",
8561
+ nodeId: id || void 0,
8562
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
8563
+ });
8564
+ continue;
8565
+ }
8566
+ if (seen.has(id)) {
8567
+ issues.push({
8568
+ code: "DUPLICATE_NODE_ID",
8569
+ nodeId: id,
8570
+ message: `Duplicate task graph node id: ${id}`
8571
+ });
8572
+ continue;
8573
+ }
8574
+ seen.add(id);
8575
+ nodes.push({
8576
+ id,
8577
+ title,
8578
+ kind: input.kind,
8579
+ dependsOn: normalizeDependencies(input.dependsOn),
8580
+ payload: input.payload,
8581
+ ordinal
8582
+ });
8583
+ }
8584
+ throwIfStrict(mode, issues);
8585
+ const ids = new Set(nodes.map((node) => node.id));
8586
+ const sanitized = nodes.map((node) => {
8587
+ const dependsOn = [];
8588
+ for (const dependencyId of node.dependsOn) {
8589
+ if (dependencyId === node.id) {
8590
+ issues.push({
8591
+ code: "SELF_DEPENDENCY",
8592
+ nodeId: node.id,
8593
+ dependencyId,
8594
+ message: `Task graph node ${node.id} cannot depend on itself`
8595
+ });
8596
+ continue;
8597
+ }
8598
+ if (!ids.has(dependencyId)) {
8599
+ issues.push({
8600
+ code: "UNKNOWN_DEPENDENCY",
8601
+ nodeId: node.id,
8602
+ dependencyId,
8603
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
8604
+ });
8605
+ continue;
8606
+ }
8607
+ dependsOn.push(dependencyId);
8608
+ }
8609
+ return { ...node, dependsOn };
8610
+ });
8611
+ throwIfStrict(mode, issues);
8612
+ const cyclic = cycleNodes(sanitized);
8613
+ let repaired = sanitized;
8614
+ if (cyclic.size > 0) {
8615
+ const cycleIssues = [];
8616
+ repaired = sanitized.map((node) => {
8617
+ if (!cyclic.has(node.id)) return node;
8618
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
8619
+ if (!cyclic.has(dependencyId)) return true;
8620
+ cycleIssues.push({
8621
+ code: "CYCLE_EDGE_REMOVED",
8622
+ nodeId: node.id,
8623
+ dependencyId,
8624
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
8625
+ });
8626
+ return false;
8627
+ });
8628
+ return { ...node, dependsOn };
8629
+ });
8630
+ issues.push(...cycleIssues);
8631
+ throwIfStrict(mode, cycleIssues);
8632
+ }
8633
+ const remainingCycle = cycleNodes(repaired);
8634
+ if (remainingCycle.size > 0) {
8635
+ throw new TaskGraphValidationError([
8636
+ {
8637
+ code: "INVALID_NODE",
8638
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
8639
+ }
8640
+ ]);
8641
+ }
8642
+ return {
8643
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
8644
+ issues
8645
+ };
8646
+ }
8647
+
8648
+ // src/core/orchestration/adapters.ts
8649
+ function compileSectionGraph(sections) {
8650
+ return compileTaskGraph(
8651
+ sections.map((section) => ({
8652
+ id: section.sectionId,
8653
+ title: section.sectionTitle,
8654
+ kind: "section",
8655
+ dependsOn: section.dependsOn,
8656
+ payload: section
8657
+ })),
8658
+ { mode: "repair" }
8659
+ );
8660
+ }
8661
+ function compileSubtaskGraph(subtasks) {
8662
+ return compileTaskGraph(
8663
+ subtasks.map((subtask) => ({
8664
+ id: subtask.subtaskId,
8665
+ title: subtask.subtaskTitle,
8666
+ kind: "subtask",
8667
+ dependsOn: subtask.dependsOn,
8668
+ payload: subtask
8669
+ })),
8670
+ { mode: "repair" }
8671
+ );
8672
+ }
8673
+
8303
8674
  // src/core/tiers/t2-manager.ts
8304
8675
  function buildT2SystemPrompt(has) {
8305
8676
  const generation = describeGenerationForPlanner(has);
8306
8677
  return [
8307
8678
  "You are a T2 Manager agent in the Cascade AI system.",
8308
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
8679
+ // Must agree with the decomposition prompt in decomposeSection(), which is
8680
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
8681
+ // fully cover it"; this said 2-5. The model received both, so its floor was
8682
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
8683
+ // real worker call on every small section.
8684
+ "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.",
8309
8685
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
8310
8686
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
8311
8687
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -8639,6 +9015,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8639
9015
  for (const a of assignments) {
8640
9016
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
8641
9017
  if (this.store) worker.setStore(this.store, taskId);
9018
+ worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
8642
9019
  worker.setPeerBus(this.t3PeerBus);
8643
9020
  if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
8644
9021
  if (this.toolCreator) worker.setToolCreator(this.toolCreator);
@@ -8701,21 +9078,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8701
9078
  * then reduce in-degrees of their dependents and repeat.
8702
9079
  */
8703
9080
  async runWithDependencies(assignments, workerMap, taskId) {
9081
+ const compiled = compileSubtaskGraph(assignments);
9082
+ for (const issue of compiled.issues) {
9083
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
9084
+ }
9085
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
9086
+ const assignment = node.payload;
9087
+ const original = assignment.dependsOn ?? [];
9088
+ const repaired = node.dependsOn;
9089
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
9090
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
9091
+ });
8704
9092
  const adj = /* @__PURE__ */ new Map();
8705
9093
  const inDegree = /* @__PURE__ */ new Map();
8706
9094
  const resultMap = /* @__PURE__ */ new Map();
8707
- for (const a of assignments) {
9095
+ for (const a of sanitizedAssignments) {
8708
9096
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
8709
9097
  inDegree.set(a.subtaskId, 0);
8710
9098
  }
8711
- for (const a of assignments) {
8712
- const deps = a.dependsOn ?? [];
8713
- for (const dep of deps) {
9099
+ for (const a of sanitizedAssignments) {
9100
+ for (const dep of a.dependsOn ?? []) {
8714
9101
  adj.get(dep).add(a.subtaskId);
8715
9102
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
8716
9103
  }
8717
9104
  }
8718
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
8719
9105
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
8720
9106
  let wave = 0;
8721
9107
  let respawnBudget = 1;
@@ -8750,6 +9136,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8750
9136
  runnableIds.push(fallbackId);
8751
9137
  }
8752
9138
  wave++;
9139
+ for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
8753
9140
  this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
8754
9141
  this.sendStatusUpdate({
8755
9142
  progressPct: 20 + Math.min(wave * 10, 60),
@@ -8922,49 +9309,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8922
9309
  }
8923
9310
  return [...resultMap.values()];
8924
9311
  }
8925
- /**
8926
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
8927
- * by removing back-edges. Returns a sanitized copy of assignments.
8928
- *
8929
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
8930
- * meaning t3 will start without waiting for t1, preventing deadlock.
8931
- */
8932
- breakCycles(assignments, adj, inDegree) {
8933
- const degree = new Map(inDegree);
8934
- const queue = [];
8935
- const visited = /* @__PURE__ */ new Set();
8936
- for (const [id, d] of degree) {
8937
- if (d === 0) queue.push(id);
8938
- }
8939
- while (queue.length > 0) {
8940
- const id = queue.shift();
8941
- visited.add(id);
8942
- for (const dep of adj.get(id) ?? []) {
8943
- const newDeg = (degree.get(dep) ?? 1) - 1;
8944
- degree.set(dep, newDeg);
8945
- if (newDeg === 0) queue.push(dep);
8946
- }
8947
- }
8948
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
8949
- if (cycleNodes.length === 0) return assignments;
8950
- this.log(
8951
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
8952
- );
8953
- return assignments.map((a) => {
8954
- if (!cycleNodes.includes(a.subtaskId)) return a;
8955
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
8956
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
8957
- this.log(
8958
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
8959
- );
8960
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
8961
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
8962
- adj.get(removed)?.delete(a.subtaskId);
8963
- }
8964
- }
8965
- return { ...a, dependsOn: safeDeps };
8966
- });
8967
- }
8968
9312
  async retryT3(assignment, taskId) {
8969
9313
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
8970
9314
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -9100,6 +9444,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9100
9444
  }
9101
9445
  };
9102
9446
 
9447
+ // src/core/orchestration/scheduler.ts
9448
+ var DependencyScheduler = class {
9449
+ constructor(graph, hooks, options = {}) {
9450
+ this.graph = graph;
9451
+ this.hooks = hooks;
9452
+ this.options = options;
9453
+ }
9454
+ graph;
9455
+ hooks;
9456
+ options;
9457
+ async run() {
9458
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
9459
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
9460
+ const dependents = /* @__PURE__ */ new Map();
9461
+ for (const node of nodes) dependents.set(node.id, []);
9462
+ for (const node of nodes) {
9463
+ for (const dependencyId of node.dependsOn) {
9464
+ dependents.get(dependencyId)?.push(node.id);
9465
+ }
9466
+ }
9467
+ const policy = this.options.onUpstreamFailure ?? "continue";
9468
+ const settled = /* @__PURE__ */ new Set();
9469
+ const results = /* @__PURE__ */ new Map();
9470
+ const blocked = /* @__PURE__ */ new Map();
9471
+ const waves = [];
9472
+ let wave = 0;
9473
+ const blockDescendants = async (failedIds) => {
9474
+ const rootsFor = /* @__PURE__ */ new Map();
9475
+ const queue = [];
9476
+ for (const rootId of failedIds) {
9477
+ for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
9478
+ }
9479
+ while (queue.length > 0) {
9480
+ const { id, root } = queue.shift();
9481
+ if (settled.has(id)) continue;
9482
+ const known = rootsFor.get(id);
9483
+ if (known) {
9484
+ if (!known.has(root)) known.add(root);
9485
+ continue;
9486
+ }
9487
+ rootsFor.set(id, /* @__PURE__ */ new Set([root]));
9488
+ for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
9489
+ }
9490
+ for (const node of nodes) {
9491
+ const roots = rootsFor.get(node.id);
9492
+ if (!roots || settled.has(node.id)) continue;
9493
+ const causes = nodes.filter((n) => roots.has(n.id));
9494
+ const blockedBy = causes.map((n) => n.id);
9495
+ const blockedByTitles = causes.map((n) => n.title || n.id);
9496
+ const record = {
9497
+ blockedBy,
9498
+ blockedByTitles,
9499
+ reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
9500
+ };
9501
+ blocked.set(node.id, record);
9502
+ settled.add(node.id);
9503
+ await this.hooks.onNodeBlocked?.(node, record);
9504
+ }
9505
+ };
9506
+ while (settled.size < nodes.length) {
9507
+ const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
9508
+ if (ready.length === 0) {
9509
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
9510
+ }
9511
+ waves.push(ready.map((node) => node.id));
9512
+ await this.hooks.onWaveStart?.(ready, wave);
9513
+ const outcomes = /* @__PURE__ */ new Map();
9514
+ const runOne = async (node) => {
9515
+ const result = await this.hooks.execute(node);
9516
+ results.set(node.id, result);
9517
+ settled.add(node.id);
9518
+ const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
9519
+ outcomes.set(node.id, outcome);
9520
+ if (outcome === "failed" && policy === "block") ; else {
9521
+ for (const dependentId of dependents.get(node.id) ?? []) {
9522
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9523
+ }
9524
+ }
9525
+ await this.hooks.onNodeComplete?.(node, result);
9526
+ };
9527
+ if ((this.options.mode ?? "parallel") === "sequential") {
9528
+ for (const node of ready) await runOne(node);
9529
+ } else {
9530
+ await Promise.all(ready.map(runOne));
9531
+ }
9532
+ const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
9533
+ if (policy === "block" && failedThisWave.length > 0) {
9534
+ await blockDescendants(failedThisWave);
9535
+ }
9536
+ wave++;
9537
+ }
9538
+ return { results, waves, blocked };
9539
+ }
9540
+ };
9541
+
9103
9542
  // src/utils/json-extract.ts
9104
9543
  function extractFirstJsonObject(input) {
9105
9544
  if (!input) return null;
@@ -9171,7 +9610,7 @@ function buildT1SystemPrompt(has) {
9171
9610
  Your responsibilities:
9172
9611
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
9173
9612
  2. Decompose the task into logical sections (one per T2 Manager)
9174
- 3. For each section, define 2-5 subtasks for T3 Workers
9613
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
9175
9614
  4. Return a structured plan as JSON
9176
9615
 
9177
9616
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -9197,6 +9636,9 @@ QUALITY RULES:
9197
9636
 
9198
9637
  ${generation}` : ""}`;
9199
9638
  }
9639
+ function producedOutput(result) {
9640
+ return result.status === "COMPLETED" || result.status === "PARTIAL";
9641
+ }
9200
9642
  var T1Administrator = class extends BaseTier {
9201
9643
  router;
9202
9644
  toolRegistry;
@@ -9331,7 +9773,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
9331
9773
  let allT2Results = await this.dispatchT2Managers(plan.sections);
9332
9774
  let pass = 1;
9333
9775
  const maxReplanPasses = this.config.maxReplanPasses ?? 2;
9334
- const okCount = (rs) => rs.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL").length;
9776
+ const okCount = (rs) => rs.filter(producedOutput).length;
9335
9777
  while (pass <= maxReplanPasses) {
9336
9778
  const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
9337
9779
  if (reviewResult.approved) {
@@ -9462,7 +9904,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
9462
9904
  /** Structured, grounded summary of what's already done — used to keep
9463
9905
  * corrective replan passes from re-emitting completed sections. */
9464
9906
  summarizeCompletedSections(results) {
9465
- const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
9907
+ const done = results.filter(producedOutput);
9466
9908
  if (done.length === 0) return "";
9467
9909
  const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
9468
9910
  return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
@@ -9586,7 +10028,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9586
10028
  }
9587
10029
  }
9588
10030
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
9589
- if (plan.sections.length < min) ;
10031
+ if (plan.sections.length < min || plan.sections.length > max) {
10032
+ this.log(
10033
+ `\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.`
10034
+ );
10035
+ }
9590
10036
  }
9591
10037
  async dispatchT2Managers(sections) {
9592
10038
  for (const section of sections) {
@@ -9603,6 +10049,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9603
10049
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
9604
10050
  if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
9605
10051
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
10052
+ manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
9606
10053
  if (this.store) {
9607
10054
  manager.setStore(this.store);
9608
10055
  }
@@ -9698,118 +10145,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9698
10145
  return t2Results;
9699
10146
  }
9700
10147
  /**
9701
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
10148
+ * Runs T2 managers respecting dependsOn declarations.
10149
+ *
10150
+ * Graph construction, validation and cycle repair now live in
10151
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
10152
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
10153
+ * ordered by one implementation rather than two that drifted.
10154
+ *
10155
+ * The hand-rolled Kahn pass this replaces treated every node it could not
10156
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
10157
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
10158
+ * so breaking a cycle here also cut the dependencies of innocent later
10159
+ * sections and started them immediately — before the sections they consume.
10160
+ * compileTaskGraph uses strongly connected components, so only real cycle
10161
+ * members are touched.
9702
10162
  */
9703
10163
  async runT2sWithDependencies(sections, managers, taskId) {
9704
- const adj = /* @__PURE__ */ new Map();
9705
- const inDegree = /* @__PURE__ */ new Map();
9706
- const resultMap = /* @__PURE__ */ new Map();
9707
- const allKeys = new Set(sections.map((s) => s.sectionId));
9708
- for (const s of sections) {
9709
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
9710
- inDegree.set(s.sectionId, 0);
9711
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
10164
+ const compiled = compileSectionGraph(sections);
10165
+ for (const issue of compiled.issues) {
10166
+ this.log(`\u26A0 Section graph: ${issue.message}`);
9712
10167
  }
9713
- for (const s of sections) {
9714
- for (const dep of s.dependsOn ?? []) {
9715
- adj.get(dep).add(s.sectionId);
9716
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
9717
- }
9718
- }
9719
- const queue = [];
9720
- const degree = new Map(inDegree);
9721
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
9722
- const visited = /* @__PURE__ */ new Set();
9723
- while (queue.length > 0) {
9724
- const u = queue.shift();
9725
- visited.add(u);
9726
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
9727
- const newDeg = (degree.get(v) ?? 1) - 1;
9728
- degree.set(v, newDeg);
9729
- if (newDeg === 0) queue.push(v);
9730
- }
9731
- }
9732
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
9733
- if (cycleNodes.length > 0) {
9734
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
9735
- for (const s of sections) {
9736
- if (cycleNodes.includes(s.sectionId)) {
9737
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
9738
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
9739
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
9740
- adj.get(removed)?.delete(s.sectionId);
9741
- }
9742
- s.dependsOn = safeDeps;
9743
- }
9744
- }
10168
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
10169
+ for (const section of sections) {
10170
+ const node = byId.get(section.sectionId);
10171
+ if (node) section.dependsOn = [...node.dependsOn];
9745
10172
  }
9746
- const totalSections = sections.length;
10173
+ const resultMap = /* @__PURE__ */ new Map();
10174
+ const totalSections = compiled.graph.nodes.length;
9747
10175
  let completedSections = 0;
9748
- const executeWave = async () => {
9749
- const readyIds = [];
9750
- for (const [id, deg] of inDegree.entries()) {
9751
- if (deg === 0 && !resultMap.has(id)) {
9752
- readyIds.push(id);
9753
- }
9754
- }
9755
- if (readyIds.length === 0) return;
9756
- const runOne = async (id) => {
9757
- resultMap.set(id, null);
9758
- const index = sections.findIndex((s) => s.sectionId === id);
9759
- const section = sections[index];
9760
- const manager = managers[index];
9761
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
9762
- this.sendStatusUpdate({
9763
- progressPct,
9764
- currentAction: `T2 working on: ${section.sectionTitle}`,
9765
- status: "IN_PROGRESS"
9766
- });
9767
- this.throwIfCancelled();
9768
- let result;
9769
- try {
9770
- result = await manager.execute(section, taskId, this.signal);
9771
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
9772
- if (result.status === "ESCALATED") {
9773
- this.escalations.push({
9774
- raisedBy: `T2_${section.sectionId}`,
10176
+ const scheduler = new DependencyScheduler(
10177
+ compiled.graph,
10178
+ {
10179
+ // The wave is the scheduler's own counter, and it is the only place the
10180
+ // parallelism is visible: nodes sharing a wave ran at the same time.
10181
+ // Stamped before the wave executes so the first status event a node
10182
+ // emits already carries it.
10183
+ onWaveStart: (nodes, wave) => {
10184
+ for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
10185
+ },
10186
+ execute: async (node) => {
10187
+ const section = node.payload;
10188
+ const manager = managers[node.ordinal];
10189
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
10190
+ this.sendStatusUpdate({
10191
+ progressPct,
10192
+ currentAction: `T2 working on: ${section.sectionTitle}`,
10193
+ status: "IN_PROGRESS"
10194
+ });
10195
+ this.throwIfCancelled();
10196
+ try {
10197
+ const result = await manager.execute(section, taskId, this.signal);
10198
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
10199
+ if (result.status === "ESCALATED") {
10200
+ this.escalations.push({
10201
+ raisedBy: `T2_${section.sectionId}`,
10202
+ sectionId: section.sectionId,
10203
+ attempted: result.issues,
10204
+ blocker: result.issues.join("; "),
10205
+ needs: "Human review required"
10206
+ });
10207
+ }
10208
+ return result;
10209
+ } catch (err) {
10210
+ return {
9775
10211
  sectionId: section.sectionId,
9776
- attempted: result.issues,
9777
- blocker: result.issues.join("; "),
9778
- needs: "Human review required"
9779
- });
10212
+ sectionTitle: section.sectionTitle,
10213
+ status: "FAILED",
10214
+ t3Results: [],
10215
+ sectionSummary: "",
10216
+ issues: [err instanceof Error ? err.message : String(err)]
10217
+ };
9780
10218
  }
9781
- } catch (err) {
9782
- result = {
10219
+ },
10220
+ // PARTIAL is deliberately not a failure: a degraded-but-real section
10221
+ // can still feed the one after it, and cancelling that work would cost
10222
+ // the user more than letting it try.
10223
+ // BLOCKED cannot reach here today — the scheduler settles blocked nodes
10224
+ // through onNodeBlocked and never executes them, so classify only ever
10225
+ // sees results that ran. It is listed anyway because the failure mode if
10226
+ // that ever changes is silent and expensive: a blocked section would
10227
+ // classify as 'succeeded' and release its own dependents, so work that
10228
+ // should have been skipped would run and be billed.
10229
+ classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
10230
+ onNodeBlocked: (node, blockedInfo) => {
10231
+ const section = node.payload;
10232
+ this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
10233
+ resultMap.set(node.id, {
9783
10234
  sectionId: section.sectionId,
9784
10235
  sectionTitle: section.sectionTitle,
9785
- status: "FAILED",
10236
+ // BLOCKED, not FAILED. This section was never attempted — saying it
10237
+ // failed reports a breakage that did not happen, and hides the one
10238
+ // that did (the upstream section named below).
10239
+ status: "BLOCKED",
9786
10240
  t3Results: [],
9787
10241
  sectionSummary: "",
9788
- issues: [err instanceof Error ? err.message : String(err)]
9789
- };
9790
- }
9791
- resultMap.set(id, result);
9792
- completedSections++;
9793
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
9794
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9795
- }
9796
- };
9797
- if (this.router.getT3ExecutionMode?.() === "sequential") {
9798
- for (const id of readyIds) {
9799
- await runOne(id);
10242
+ issues: [
10243
+ `${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
10244
+ ],
10245
+ blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
10246
+ });
10247
+ completedSections++;
10248
+ this.sendStatusUpdate({
10249
+ progressPct: 10 + Math.floor(completedSections / totalSections * 85),
10250
+ currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
10251
+ status: "IN_PROGRESS"
10252
+ });
10253
+ this.emit("tier:status", {
10254
+ tierId: section.sectionId,
10255
+ // A blocked section has no tier instance — it was never
10256
+ // constructed — so its runtime id and its graph id are the same
10257
+ // thing here. Emitting nodeId anyway keeps every node in the
10258
+ // stream addressable in the graph's id space, which is what
10259
+ // dependsOn edges point at.
10260
+ nodeId: section.sectionId,
10261
+ dependsOn: [...section.dependsOn ?? []],
10262
+ role: "T2",
10263
+ label: section.sectionTitle,
10264
+ status: "BLOCKED",
10265
+ blockedBy: [...blockedInfo.blockedByTitles],
10266
+ output: blockedInfo.reason
10267
+ });
10268
+ this.emit("section:complete", {
10269
+ id: node.id,
10270
+ title: section.sectionTitle,
10271
+ status: "BLOCKED",
10272
+ blockedBy: [...blockedInfo.blockedByTitles],
10273
+ output: ""
10274
+ });
10275
+ },
10276
+ onNodeComplete: (node, result) => {
10277
+ resultMap.set(node.id, result);
10278
+ completedSections++;
10279
+ this.emit("section:complete", {
10280
+ id: node.id,
10281
+ title: result.sectionTitle,
10282
+ status: result.status,
10283
+ output: result.sectionSummary
10284
+ });
9800
10285
  }
9801
- } else {
9802
- await Promise.all(readyIds.map(runOne));
9803
- }
9804
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
9805
- await executeWave();
10286
+ },
10287
+ {
10288
+ // Cross-section concurrency respects the same t3Execution mode as the
10289
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
10290
+ // serialized T3 workers within a single section while every section's
10291
+ // T2Manager (and its T3 workers) still ran in parallel here.
10292
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
10293
+ // A section that declared `dependsOn` said it needs that section's
10294
+ // output. Running it after the dependency failed produces a second
10295
+ // failure, billed, whose cause is the first one — so don't.
10296
+ onUpstreamFailure: "block"
9806
10297
  }
9807
- };
9808
- await executeWave();
10298
+ );
10299
+ await scheduler.run();
9809
10300
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
9810
10301
  }
9811
10302
  async compileFinalOutput(originalPrompt, plan, t2Results) {
9812
- const completedSections = t2Results.filter((r) => r.status !== "FAILED");
10303
+ const completedSections = t2Results.filter(producedOutput);
9813
10304
  if (!completedSections.length) {
9814
10305
  const allIssues = [
9815
10306
  ...t2Results.flatMap((r) => r.issues ?? []),
@@ -9829,7 +10320,6 @@ ${r.sectionSummary}
9829
10320
  Outputs:
9830
10321
  ${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");
9831
10322
  const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
9832
- t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
9833
10323
  const compilePrompt = `Compile a final, coherent response to the user's original request.
9834
10324
 
9835
10325
  Original request: ${originalPrompt}
@@ -10321,6 +10811,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
10321
10811
  };
10322
10812
  var DEFAULT_VIDEO_SECONDS = 5;
10323
10813
  var MAX_VIDEO_SECONDS = 8;
10814
+ var MIN_VIDEO_SECONDS = 4;
10324
10815
  var GenerateVideoTool = class extends BaseTool {
10325
10816
  constructor(deps) {
10326
10817
  super();
@@ -10337,7 +10828,7 @@ var GenerateVideoTool = class extends BaseTool {
10337
10828
  type: "object",
10338
10829
  properties: {
10339
10830
  prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
10340
- seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
10831
+ seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
10341
10832
  aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
10342
10833
  model: { type: "string", description: "Optional: force a specific video model." }
10343
10834
  },
@@ -10349,7 +10840,7 @@ var GenerateVideoTool = class extends BaseTool {
10349
10840
  const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
10350
10841
  if (!r.ok) return r.error;
10351
10842
  const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
10352
- const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
10843
+ const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
10353
10844
  const asset = await generateVideo(
10354
10845
  r.selected.capability,
10355
10846
  r.cfg,
@@ -10366,7 +10857,7 @@ var GenerateVideoTool = class extends BaseTool {
10366
10857
  const location = await this.deps.sink(asset);
10367
10858
  const unit = r.selected.cost;
10368
10859
  const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
10369
- const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
10860
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
10370
10861
  return `Video generated and saved to ${location}.
10371
10862
  Model: ${asset.modelId}.${estimate}${clamped}`;
10372
10863
  }
@@ -10435,16 +10926,16 @@ function resolveInWorkspace(workspaceRoot, input) {
10435
10926
  if (typeof input !== "string" || input.length === 0) {
10436
10927
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
10437
10928
  }
10438
- const root = path25__default.default.resolve(workspaceRoot);
10439
- const abs = path25__default.default.isAbsolute(input) ? path25__default.default.resolve(input) : path25__default.default.resolve(root, input);
10440
- const rel = path25__default.default.relative(root, abs);
10441
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25__default.default.isAbsolute(rel)) {
10929
+ const root = path26__default.default.resolve(workspaceRoot);
10930
+ const abs = path26__default.default.isAbsolute(input) ? path26__default.default.resolve(input) : path26__default.default.resolve(root, input);
10931
+ const rel = path26__default.default.relative(root, abs);
10932
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26__default.default.isAbsolute(rel)) {
10442
10933
  throw new WorkspaceSandboxError(input, root);
10443
10934
  }
10444
10935
  try {
10445
- const real = fs24__default.default.realpathSync(abs);
10446
- const realRel = path25__default.default.relative(root, real);
10447
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25__default.default.isAbsolute(realRel))) {
10936
+ const real = fs25__default.default.realpathSync(abs);
10937
+ const realRel = path26__default.default.relative(root, real);
10938
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26__default.default.isAbsolute(realRel))) {
10448
10939
  throw new WorkspaceSandboxError(input, root);
10449
10940
  }
10450
10941
  } catch (e) {
@@ -11102,7 +11593,7 @@ var GenerateDocumentTool = class extends BaseTool {
11102
11593
  }
11103
11594
  }
11104
11595
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
11105
- await fs5__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
11596
+ await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
11106
11597
  await fs5__default.default.writeFile(absPath, bytes);
11107
11598
  const charts = countCharts(content);
11108
11599
  return [
@@ -11497,7 +11988,7 @@ var FileWriteTool = class extends BaseTool {
11497
11988
  } catch {
11498
11989
  }
11499
11990
  }
11500
- await fs5__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
11991
+ await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
11501
11992
  await fs5__default.default.writeFile(absPath, content, "utf-8");
11502
11993
  return `Written ${content.length} characters to ${filePath}`;
11503
11994
  }
@@ -11995,7 +12486,7 @@ var ImageAnalyzeTool = class extends BaseTool {
11995
12486
  };
11996
12487
  async function fileToImageAttachment(filePath) {
11997
12488
  const data = await fs5__default.default.readFile(filePath);
11998
- const ext = path25__default.default.extname(filePath).toLowerCase();
12489
+ const ext = path26__default.default.extname(filePath).toLowerCase();
11999
12490
  const mimeMap = {
12000
12491
  ".jpg": "image/jpeg",
12001
12492
  ".jpeg": "image/jpeg",
@@ -12029,14 +12520,14 @@ var PDFCreateTool = class extends BaseTool {
12029
12520
  const filePath = input["path"];
12030
12521
  const content = input["content"];
12031
12522
  const title = input["title"];
12032
- const dir = path25__default.default.dirname(filePath);
12033
- if (!fs24__default.default.existsSync(dir)) {
12034
- fs24__default.default.mkdirSync(dir, { recursive: true });
12523
+ const dir = path26__default.default.dirname(filePath);
12524
+ if (!fs25__default.default.existsSync(dir)) {
12525
+ fs25__default.default.mkdirSync(dir, { recursive: true });
12035
12526
  }
12036
12527
  return new Promise((resolve2, reject) => {
12037
12528
  try {
12038
12529
  const doc = new PDFDocument__default.default({ margin: 50 });
12039
- const stream = fs24__default.default.createWriteStream(filePath);
12530
+ const stream = fs25__default.default.createWriteStream(filePath);
12040
12531
  doc.pipe(stream);
12041
12532
  if (title) {
12042
12533
  doc.info["Title"] = title;
@@ -12114,22 +12605,22 @@ var CodeInterpreterTool = class extends BaseTool {
12114
12605
  }
12115
12606
  cmdPrefix = NODE_CMD;
12116
12607
  }
12117
- const tmpDir = path25__default.default.join(this.workspaceRoot, ".cascade", "tmp");
12118
- if (!fs24__default.default.existsSync(tmpDir)) {
12119
- fs24__default.default.mkdirSync(tmpDir, { recursive: true });
12608
+ const tmpDir = path26__default.default.join(this.workspaceRoot, ".cascade", "tmp");
12609
+ if (!fs25__default.default.existsSync(tmpDir)) {
12610
+ fs25__default.default.mkdirSync(tmpDir, { recursive: true });
12120
12611
  }
12121
12612
  const extension = language === "python" ? "py" : "js";
12122
12613
  const fileName = `intp_${crypto4.randomUUID().slice(0, 8)}.${extension}`;
12123
- const filePath = path25__default.default.join(tmpDir, fileName);
12124
- fs24__default.default.writeFileSync(filePath, code, "utf-8");
12614
+ const filePath = path26__default.default.join(tmpDir, fileName);
12615
+ fs25__default.default.writeFileSync(filePath, code, "utf-8");
12125
12616
  const execArgs = [filePath, ...args];
12126
12617
  return new Promise((resolve2) => {
12127
12618
  const startMs = Date.now();
12128
12619
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
12129
12620
  const duration = Date.now() - startMs;
12130
12621
  try {
12131
- if (fs24__default.default.existsSync(filePath)) {
12132
- fs24__default.default.unlinkSync(filePath);
12622
+ if (fs25__default.default.existsSync(filePath)) {
12623
+ fs25__default.default.unlinkSync(filePath);
12133
12624
  }
12134
12625
  } catch (cleanupErr) {
12135
12626
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -12441,7 +12932,7 @@ var GlobTool = class extends BaseTool {
12441
12932
  };
12442
12933
  async execute(input, _options) {
12443
12934
  const pattern = input["pattern"];
12444
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12935
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12445
12936
  const matches = await glob.glob(pattern, {
12446
12937
  cwd: searchPath,
12447
12938
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -12454,7 +12945,7 @@ var GlobTool = class extends BaseTool {
12454
12945
  const withMtime = await Promise.all(
12455
12946
  matches.map(async (rel) => {
12456
12947
  try {
12457
- const stat = await fs5__default.default.stat(path25__default.default.join(searchPath, rel));
12948
+ const stat = await fs5__default.default.stat(path26__default.default.join(searchPath, rel));
12458
12949
  return { rel, mtime: stat.mtimeMs };
12459
12950
  } catch {
12460
12951
  return { rel, mtime: 0 };
@@ -12503,7 +12994,7 @@ var GrepTool = class extends BaseTool {
12503
12994
  };
12504
12995
  async execute(input, _options) {
12505
12996
  const pattern = input["pattern"];
12506
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12997
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12507
12998
  const globPattern = input["glob"];
12508
12999
  const outputMode = input["output_mode"] ?? "content";
12509
13000
  const context = input["context"] ?? 0;
@@ -12557,12 +13048,12 @@ var GrepTool = class extends BaseTool {
12557
13048
  nodir: true
12558
13049
  });
12559
13050
  } catch {
12560
- files = [path25__default.default.relative(searchPath, searchPath) || "."];
13051
+ files = [path26__default.default.relative(searchPath, searchPath) || "."];
12561
13052
  }
12562
13053
  const results = [];
12563
13054
  let totalCount = 0;
12564
13055
  for (const rel of files) {
12565
- const abs = path25__default.default.join(searchPath, rel);
13056
+ const abs = path26__default.default.join(searchPath, rel);
12566
13057
  let content;
12567
13058
  try {
12568
13059
  content = await fs5__default.default.readFile(abs, "utf-8");
@@ -12949,10 +13440,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
12949
13440
  }
12950
13441
  isIgnored(filePath) {
12951
13442
  if (!filePath) return false;
12952
- const abs = path25__default.default.resolve(this.workspaceRoot, filePath);
12953
- const rel = path25__default.default.relative(this.workspaceRoot, abs);
12954
- if (!rel || rel.startsWith("..") || path25__default.default.isAbsolute(rel)) return true;
12955
- const posixRel = rel.split(path25__default.default.sep).join("/");
13443
+ const abs = path26__default.default.resolve(this.workspaceRoot, filePath);
13444
+ const rel = path26__default.default.relative(this.workspaceRoot, abs);
13445
+ if (!rel || rel.startsWith("..") || path26__default.default.isAbsolute(rel)) return true;
13446
+ const posixRel = rel.split(path26__default.default.sep).join("/");
12956
13447
  return this.ignoreMatcher.ignores(posixRel);
12957
13448
  }
12958
13449
  };
@@ -13186,22 +13677,22 @@ var FileMcpOAuthStore = class {
13186
13677
  filePath;
13187
13678
  load() {
13188
13679
  try {
13189
- return JSON.parse(fs24__default.default.readFileSync(this.filePath, "utf-8"));
13680
+ return JSON.parse(fs25__default.default.readFileSync(this.filePath, "utf-8"));
13190
13681
  } catch {
13191
13682
  return void 0;
13192
13683
  }
13193
13684
  }
13194
13685
  save(state) {
13195
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
13196
- fs24__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13686
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
13687
+ fs25__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13197
13688
  try {
13198
- fs24__default.default.chmodSync(this.filePath, 384);
13689
+ fs25__default.default.chmodSync(this.filePath, 384);
13199
13690
  } catch {
13200
13691
  }
13201
13692
  }
13202
13693
  clear() {
13203
13694
  try {
13204
- fs24__default.default.rmSync(this.filePath);
13695
+ fs25__default.default.rmSync(this.filePath);
13205
13696
  } catch {
13206
13697
  }
13207
13698
  }
@@ -14098,6 +14589,14 @@ function heuristicAnalyze(prompt) {
14098
14589
  const estimatedTokens = wordCount * 5;
14099
14590
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
14100
14591
  }
14592
+ function hashPrompt(prompt) {
14593
+ let hash = 2166136261;
14594
+ for (let index = 0; index < prompt.length; index++) {
14595
+ hash ^= prompt.charCodeAt(index);
14596
+ hash = Math.imul(hash, 16777619) >>> 0;
14597
+ }
14598
+ return `${prompt.length}:${hash.toString(36)}`;
14599
+ }
14101
14600
  var analysisCache = /* @__PURE__ */ new Map();
14102
14601
  var TASK_TYPE_TAGS = {
14103
14602
  code: ["code", "instruction"],
@@ -14112,7 +14611,24 @@ var TaskAnalyzer = class {
14112
14611
  feedback;
14113
14612
  bias;
14114
14613
  lastProfile = null;
14115
- lastSelectedModels = /* @__PURE__ */ new Map();
14614
+ /** Models chosen by the run currently in flight. Cleared when it completes. */
14615
+ currentRunSelections = /* @__PURE__ */ new Map();
14616
+ /**
14617
+ * Immutable snapshot of the last COMPLETED run: its selections AND the task
14618
+ * type they were chosen for.
14619
+ *
14620
+ * The task type has to travel WITH the selections. `lastProfile` is overwritten
14621
+ * by the next analyze(), so if run B started before the user rated run A, A's
14622
+ * models were credited under B's task type — teaching the router that a coding
14623
+ * model is good at creative writing, from a rating that never said so.
14624
+ *
14625
+ * Explicit ratings arrive after the run finishes — rateLastRun() is by nature
14626
+ * a reaction to a finished answer — but the run finalizer calls
14627
+ * recordRunOutcome() first, and that used to clear the only map there was. So
14628
+ * every explicit rating iterated an empty map, recorded nothing, and returned
14629
+ * false: the 3x-weighted user signal never reached the tracker at all.
14630
+ */
14631
+ lastCompletedRun;
14116
14632
  constructor(tracker, bias = "balanced") {
14117
14633
  this.tracker = tracker;
14118
14634
  this.bias = bias;
@@ -14141,7 +14657,7 @@ var TaskAnalyzer = class {
14141
14657
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
14142
14658
  */
14143
14659
  async analyze(prompt) {
14144
- const cacheKey = prompt.slice(0, 200);
14660
+ const cacheKey = hashPrompt(prompt);
14145
14661
  const cached = analysisCache.get(cacheKey);
14146
14662
  if (cached) {
14147
14663
  this.lastProfile = cached;
@@ -14159,11 +14675,15 @@ var TaskAnalyzer = class {
14159
14675
  */
14160
14676
  async selectModel(prompt, tier, selector, opts) {
14161
14677
  const profile = await this.analyze(prompt);
14678
+ const recordSelection = (model) => {
14679
+ if (model) this.currentRunSelections.set(tier, model);
14680
+ return model;
14681
+ };
14162
14682
  if (profile.requiresVision) {
14163
- return selector.selectVisionModel();
14683
+ return recordSelection(selector.selectVisionModel());
14164
14684
  }
14165
14685
  let candidates = selector.getCandidatesForTier(tier);
14166
- if (candidates.length === 0) return selector.selectForTier(tier);
14686
+ if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
14167
14687
  if (opts?.requiresToolUse) {
14168
14688
  const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
14169
14689
  if (toolCapable.length > 0) candidates = toolCapable;
@@ -14173,9 +14693,7 @@ var TaskAnalyzer = class {
14173
14693
  score: this.scoreModel(m, profile)
14174
14694
  }));
14175
14695
  scored.sort((a, b) => b.score - a.score);
14176
- const best = scored[0]?.model ?? selector.selectForTier(tier);
14177
- if (best) this.lastSelectedModels.set(tier, best);
14178
- return best;
14696
+ return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
14179
14697
  }
14180
14698
  /**
14181
14699
  * Record the outcome of a completed run across all tiers that were selected
@@ -14184,25 +14702,38 @@ var TaskAnalyzer = class {
14184
14702
  recordRunOutcome(outcome, costByTier, contextTokens = 0) {
14185
14703
  if (!this.tracker || !this.lastProfile) return;
14186
14704
  const taskType = this.lastProfile.type;
14187
- for (const [tier, model] of this.lastSelectedModels) {
14705
+ for (const [tier, model] of this.currentRunSelections) {
14188
14706
  const cost = costByTier[tier] ?? 0;
14189
14707
  this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
14190
14708
  }
14191
- this.lastSelectedModels.clear();
14709
+ this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
14710
+ this.currentRunSelections.clear();
14192
14711
  void this.tracker.save();
14193
14712
  }
14194
14713
  /**
14195
- * Record an explicit user rating (good/bad) for the last run's selected models.
14714
+ * Record an explicit user rating (good/bad) for the last COMPLETED run's models.
14196
14715
  * Explicit ratings carry 3× the weight of auto-detected outcomes.
14197
- * Does NOT clear lastSelectedModels — the auto record already did that.
14716
+ *
14717
+ * Reads the completed-run snapshot, not the in-flight map: by the time a user
14718
+ * can rate an answer, the finalizer has already run and the in-flight map is
14719
+ * empty.
14720
+ *
14721
+ * The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
14722
+ * three samples, so a double submit would inject six — a stutter on the
14723
+ * button would count as two opinions. Consuming makes a second call a no-op
14724
+ * that reports false, which is what "rate this run" should mean.
14198
14725
  */
14199
14726
  recordExplicitRating(rating) {
14200
- if (!this.tracker || !this.lastProfile) return false;
14201
- const taskType = this.lastProfile.type;
14202
- for (const [, model] of this.lastSelectedModels) {
14203
- this.tracker.recordExplicit(model.id, taskType, rating, 0);
14204
- }
14205
- return this.lastSelectedModels.size > 0;
14727
+ const snapshot = this.lastCompletedRun;
14728
+ if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
14729
+ const ratedModels = /* @__PURE__ */ new Set();
14730
+ for (const [, model] of snapshot.selections) {
14731
+ if (ratedModels.has(model.id)) continue;
14732
+ ratedModels.add(model.id);
14733
+ this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
14734
+ }
14735
+ this.lastCompletedRun = void 0;
14736
+ return true;
14206
14737
  }
14207
14738
  scoreModel(model, profile) {
14208
14739
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
@@ -14240,7 +14771,7 @@ var TaskAnalyzer = class {
14240
14771
  analysisCache.clear();
14241
14772
  }
14242
14773
  };
14243
- var DEFAULT_STATS_FILE = path25__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
14774
+ var DEFAULT_STATS_FILE = path26__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
14244
14775
  var ModelPerformanceTracker = class {
14245
14776
  stats = /* @__PURE__ */ new Map();
14246
14777
  featureStats = /* @__PURE__ */ new Map();
@@ -14280,7 +14811,7 @@ var ModelPerformanceTracker = class {
14280
14811
  async save() {
14281
14812
  if (this.readOnly) return;
14282
14813
  try {
14283
- await fs5__default.default.mkdir(path25__default.default.dirname(this.statsFile), { recursive: true });
14814
+ await fs5__default.default.mkdir(path26__default.default.dirname(this.statsFile), { recursive: true });
14284
14815
  const modelsObj = {};
14285
14816
  const featuresObj = {};
14286
14817
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -14828,7 +15359,7 @@ Required capability: ${description.slice(0, 300)}`;
14828
15359
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
14829
15360
  async loadPersistedTools() {
14830
15361
  if (!this.workspacePath || !this.persistEnabled) return;
14831
- const file = path25__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
15362
+ const file = path26__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
14832
15363
  try {
14833
15364
  const raw = await fs5__default.default.readFile(file, "utf-8");
14834
15365
  const specs = JSON.parse(raw);
@@ -14852,8 +15383,8 @@ Required capability: ${description.slice(0, 300)}`;
14852
15383
  }
14853
15384
  async persist() {
14854
15385
  if (!this.workspacePath || !this.persistEnabled) return;
14855
- const dir = path25__default.default.join(this.workspacePath, ".cascade");
14856
- const file = path25__default.default.join(dir, DYNAMIC_TOOLS_FILE);
15386
+ const dir = path26__default.default.join(this.workspacePath, ".cascade");
15387
+ const file = path26__default.default.join(dir, DYNAMIC_TOOLS_FILE);
14857
15388
  try {
14858
15389
  await fs5__default.default.mkdir(dir, { recursive: true });
14859
15390
  await fs5__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -14873,12 +15404,12 @@ var WorldStateDB = class {
14873
15404
  constructor(workspacePath, debugMode = false) {
14874
15405
  this.workspacePath = workspacePath;
14875
15406
  this.debugMode = debugMode;
14876
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
14877
- if (!fs24__default.default.existsSync(cascadeDir)) {
14878
- fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
15407
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
15408
+ if (!fs25__default.default.existsSync(cascadeDir)) {
15409
+ fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
14879
15410
  }
14880
- this.keyPath = path25__default.default.join(cascadeDir, "world_state.key");
14881
- this.dbPath = path25__default.default.join(cascadeDir, "world_state.db");
15411
+ this.keyPath = path26__default.default.join(cascadeDir, "world_state.key");
15412
+ this.dbPath = path26__default.default.join(cascadeDir, "world_state.db");
14882
15413
  this.initEncryptionKey();
14883
15414
  this.db = new Database__default.default(this.dbPath);
14884
15415
  this.db.pragma("journal_mode = WAL");
@@ -14935,11 +15466,11 @@ var WorldStateDB = class {
14935
15466
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
14936
15467
  }
14937
15468
  initEncryptionKey() {
14938
- if (fs24__default.default.existsSync(this.keyPath)) {
14939
- this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
15469
+ if (fs25__default.default.existsSync(this.keyPath)) {
15470
+ this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
14940
15471
  } else {
14941
15472
  this.encryptionKey = crypto4__default.default.randomBytes(32);
14942
- fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
15473
+ fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
14943
15474
  }
14944
15475
  }
14945
15476
  encrypt(text) {
@@ -15225,9 +15756,9 @@ var WorldStateDB = class {
15225
15756
  dumpDebugIfNeeded() {
15226
15757
  if (!this.debugMode) return;
15227
15758
  try {
15228
- const dumpPath = path25__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
15759
+ const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
15229
15760
  const entries = this.getAllEntries();
15230
- fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15761
+ fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15231
15762
  } catch (err) {
15232
15763
  console.error("Failed to dump debug world state", err);
15233
15764
  }
@@ -15236,6 +15767,212 @@ var WorldStateDB = class {
15236
15767
  this.db.close();
15237
15768
  }
15238
15769
  };
15770
+ var RESUME_SCHEMA_VERSION = 1;
15771
+ var DEFAULT_MAX_CHECKPOINTS = 5;
15772
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
15773
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
15774
+ var CLAIM_SUFFIX = ".claimed";
15775
+ var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
15776
+ function safeId(taskId) {
15777
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
15778
+ return cleaned.slice(0, 100) || "run";
15779
+ }
15780
+ function isCheckpoint(value) {
15781
+ if (typeof value !== "object" || value === null) return false;
15782
+ const c = value;
15783
+ 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);
15784
+ }
15785
+ var ResumeStore = class {
15786
+ dir;
15787
+ maxCheckpoints;
15788
+ maxAgeMs;
15789
+ now;
15790
+ claimTimeoutMs;
15791
+ constructor(options) {
15792
+ this.dir = options.dir;
15793
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
15794
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
15795
+ this.now = options.now ?? Date.now;
15796
+ this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
15797
+ }
15798
+ /**
15799
+ * Persist a checkpoint and prune.
15800
+ *
15801
+ * Never throws — failing to save a resume file must not turn a recoverable
15802
+ * stop into a crash — but DOES report whether the atomic rename landed.
15803
+ * Callers decide whether an original claim is safe to discard based on that
15804
+ * answer, so "I swallowed the error" and "the checkpoint is on disk" must not
15805
+ * look alike to them.
15806
+ *
15807
+ * @returns true only when the checkpoint is durably on disk.
15808
+ */
15809
+ async save(checkpoint) {
15810
+ try {
15811
+ const timestamp = this.now();
15812
+ const record = {
15813
+ ...checkpoint,
15814
+ version: RESUME_SCHEMA_VERSION,
15815
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
15816
+ };
15817
+ await fs25.promises.mkdir(this.dir, { recursive: true });
15818
+ const finalPath = path26__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
15819
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
15820
+ await fs25.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
15821
+ await fs25.promises.rename(tempPath, finalPath);
15822
+ await this.prune();
15823
+ return true;
15824
+ } catch {
15825
+ return false;
15826
+ }
15827
+ }
15828
+ /** Valid, unexpired checkpoints, newest first. */
15829
+ async list() {
15830
+ let entries;
15831
+ try {
15832
+ entries = await fs25.promises.readdir(this.dir);
15833
+ } catch {
15834
+ return [];
15835
+ }
15836
+ const cutoff = this.now() - this.maxAgeMs;
15837
+ const candidates = 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);
15838
+ const checkpoints = [];
15839
+ for (const candidate of candidates) {
15840
+ try {
15841
+ const raw = await fs25.promises.readFile(path26__default.default.join(this.dir, candidate.name), "utf-8");
15842
+ const parsed = JSON.parse(raw);
15843
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
15844
+ } catch {
15845
+ continue;
15846
+ }
15847
+ }
15848
+ return checkpoints;
15849
+ }
15850
+ /** The most recent resumable run, or null. */
15851
+ async latest() {
15852
+ return (await this.list())[0] ?? null;
15853
+ }
15854
+ /**
15855
+ * Claim a checkpoint for a resume attempt.
15856
+ *
15857
+ * This used to DELETE the file and return it, which opened a window with no
15858
+ * recovery record at all: between preparing the continuation and the resumed
15859
+ * run writing its own checkpoint, a crash lost the only copy — and a crash is
15860
+ * precisely what this store exists to survive. Deleting on read optimised for
15861
+ * "never resume twice" at the cost of "never lose the work", which is the
15862
+ * wrong trade for a recovery mechanism.
15863
+ *
15864
+ * Instead the file is RENAMED to `.claimed`, which still hides it from
15865
+ * `list()` (so a concurrent resume cannot take the same work) but keeps it on
15866
+ * disk. The caller then either `release`s it — restoring it for another
15867
+ * attempt — or `settle`s it once the resumed run is safely under way. A claim
15868
+ * abandoned by a dead process is reclaimed after `claimTimeoutMs`.
15869
+ */
15870
+ async claim(taskId) {
15871
+ const all = await this.list();
15872
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
15873
+ if (!target) return null;
15874
+ try {
15875
+ const entries = await fs25.promises.readdir(this.dir);
15876
+ const wanted = safeId(target.taskId);
15877
+ const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
15878
+ if (!source) return null;
15879
+ const claimId = `${this.now()}-${safeId(target.taskId)}`;
15880
+ await fs25.promises.rename(path26__default.default.join(this.dir, source), path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
15881
+ return { checkpoint: target, claimId };
15882
+ } catch {
15883
+ return null;
15884
+ }
15885
+ }
15886
+ /** Give a claim back, so the work stays recoverable after a failed start. */
15887
+ async release(claimId) {
15888
+ try {
15889
+ await fs25.promises.rename(
15890
+ path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
15891
+ path26__default.default.join(this.dir, `${claimId}.json`)
15892
+ );
15893
+ } catch {
15894
+ }
15895
+ }
15896
+ /** Discard a claim for good — the resumed run has taken ownership. */
15897
+ async settle(claimId) {
15898
+ try {
15899
+ await fs25.promises.rm(path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
15900
+ } catch {
15901
+ }
15902
+ }
15903
+ /**
15904
+ * Claim-and-discard in one step. Kept for callers that genuinely want the old
15905
+ * semantics; prefer claim/settle so a crash mid-resume stays recoverable.
15906
+ */
15907
+ async consume(taskId) {
15908
+ const claimed = await this.claim(taskId);
15909
+ if (!claimed) return null;
15910
+ await this.settle(claimed.claimId);
15911
+ return claimed.checkpoint;
15912
+ }
15913
+ /**
15914
+ * Return claims abandoned by a process that died mid-resume. Without this a
15915
+ * crash between claim and settle would strand the checkpoint invisibly —
15916
+ * trading one loss window for another.
15917
+ */
15918
+ async reclaimStale() {
15919
+ let reclaimed = 0;
15920
+ try {
15921
+ const entries = await fs25.promises.readdir(this.dir);
15922
+ const cutoff = this.now() - this.claimTimeoutMs;
15923
+ for (const name of entries) {
15924
+ if (!name.endsWith(CLAIM_SUFFIX)) continue;
15925
+ const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
15926
+ if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
15927
+ await fs25.promises.rename(
15928
+ path26__default.default.join(this.dir, name),
15929
+ path26__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
15930
+ );
15931
+ reclaimed++;
15932
+ }
15933
+ } catch {
15934
+ }
15935
+ return reclaimed;
15936
+ }
15937
+ /** Remove every checkpoint for a task id. */
15938
+ async delete(taskId) {
15939
+ try {
15940
+ const entries = await fs25.promises.readdir(this.dir);
15941
+ const wanted = safeId(taskId);
15942
+ await Promise.all(entries.map(async (name) => {
15943
+ const match = FILE_PATTERN.exec(name);
15944
+ if (match?.[2] === wanted) {
15945
+ await fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true });
15946
+ }
15947
+ }));
15948
+ } catch {
15949
+ }
15950
+ }
15951
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
15952
+ async prune() {
15953
+ try {
15954
+ const entries = await fs25.promises.readdir(this.dir);
15955
+ const cutoff = this.now() - this.maxAgeMs;
15956
+ 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);
15957
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
15958
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
15959
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
15960
+ (name) => fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true })
15961
+ ));
15962
+ } catch {
15963
+ }
15964
+ }
15965
+ };
15966
+ function summarizeCompleted(completed, perNodeChars = 500) {
15967
+ if (!completed.length) return "";
15968
+ const lines = completed.map((node) => {
15969
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
15970
+ return `- [${node.status}] ${node.title}
15971
+ ${body.replace(/\n/g, "\n ")}`;
15972
+ });
15973
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
15974
+ ${lines.join("\n")}`;
15975
+ }
15239
15976
  var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
15240
15977
  var PrivacyPaths = class {
15241
15978
  localOnly;
@@ -15282,7 +16019,7 @@ var CascadeIgnore = class {
15282
16019
  ]);
15283
16020
  }
15284
16021
  async load(workspacePath) {
15285
- const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
16022
+ const filePath = path26__default.default.join(workspacePath, ".cascadeignore");
15286
16023
  try {
15287
16024
  const content = await fs5__default.default.readFile(filePath, "utf-8");
15288
16025
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -15293,7 +16030,7 @@ var CascadeIgnore = class {
15293
16030
  }
15294
16031
  isIgnored(filePath, workspacePath) {
15295
16032
  try {
15296
- const relative = workspacePath ? path25__default.default.relative(workspacePath, filePath) : filePath;
16033
+ const relative = workspacePath ? path26__default.default.relative(workspacePath, filePath) : filePath;
15297
16034
  return this.ig.ignores(relative);
15298
16035
  } catch {
15299
16036
  return false;
@@ -15636,7 +16373,7 @@ var WorkspaceIndex = class {
15636
16373
  isIgnored;
15637
16374
  chunker;
15638
16375
  constructor(opts) {
15639
- this.root = path25__default.default.resolve(opts.root);
16376
+ this.root = path26__default.default.resolve(opts.root);
15640
16377
  this.db = opts.db;
15641
16378
  this.namespace = opts.namespace ?? "code";
15642
16379
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -15668,14 +16405,14 @@ var WorkspaceIndex = class {
15668
16405
  return;
15669
16406
  }
15670
16407
  for (const entry of entries) {
15671
- const abs = path25__default.default.join(dir, entry.name);
16408
+ const abs = path26__default.default.join(dir, entry.name);
15672
16409
  if (entry.isDirectory()) {
15673
16410
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
15674
16411
  await walk(abs);
15675
16412
  } else if (entry.isFile()) {
15676
- const ext = path25__default.default.extname(entry.name).slice(1).toLowerCase();
16413
+ const ext = path26__default.default.extname(entry.name).slice(1).toLowerCase();
15677
16414
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
15678
- out.push(path25__default.default.relative(this.root, abs));
16415
+ out.push(path26__default.default.relative(this.root, abs));
15679
16416
  }
15680
16417
  }
15681
16418
  };
@@ -15705,7 +16442,7 @@ var WorkspaceIndex = class {
15705
16442
  const entries = [];
15706
16443
  for (const rel of rels) {
15707
16444
  try {
15708
- const abs = path25__default.default.join(this.root, rel);
16445
+ const abs = path26__default.default.join(this.root, rel);
15709
16446
  const stat = await fs5__default.default.stat(abs);
15710
16447
  if (stat.size > this.maxFileBytes) continue;
15711
16448
  const buf = await fs5__default.default.readFile(abs);
@@ -16243,8 +16980,35 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16243
16980
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
16244
16981
  decisionLog = [];
16245
16982
  initialized = false;
16246
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
16983
+ /**
16984
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
16985
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
16986
+ * so a crash or an exit no longer throws away everything already finished.
16987
+ */
16247
16988
  lastInterruptedRun;
16989
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
16990
+ resumeStore;
16991
+ /** Claim held by an in-flight resume, settled once the run owns the work. */
16992
+ claimedResumeId;
16993
+ /**
16994
+ * Facts inherited from the checkpoint this run is resuming, so its own
16995
+ * checkpoint is cumulative rather than only covering this attempt.
16996
+ */
16997
+ resumedFrom;
16998
+ /** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
16999
+ wroteCheckpointThisRun = false;
17000
+ /**
17001
+ * Whether the run reached the end of its normal path.
17002
+ *
17003
+ * NOT the same as `runError == null`: the cancellation and budget handlers
17004
+ * both null `runError` deliberately (an intentional stop is not a telemetry
17005
+ * error) and the breaker path never sets it at all. Treating that as success
17006
+ * meant an immediate cancel — no output, no replacement checkpoint — deleted
17007
+ * the original claim and the finished sections with it.
17008
+ */
17009
+ completedNormally = false;
17010
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
17011
+ currentRunCompleted = [];
16248
17012
  initPromise;
16249
17013
  store;
16250
17014
  audit;
@@ -16277,10 +17041,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16277
17041
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16278
17042
  try {
16279
17043
  this.router.setDeadModelStore(new DeadModelStore(
16280
- fileDeadModelPersistence(path25__default.default.join(workspacePath, ".cascade", "dead-models.json"))
17044
+ fileDeadModelPersistence(path26__default.default.join(workspacePath, ".cascade", "dead-models.json"))
16281
17045
  ));
16282
17046
  } catch {
16283
17047
  }
17048
+ try {
17049
+ this.resumeStore = new ResumeStore({
17050
+ dir: path26__default.default.join(workspacePath, ".cascade", "resume")
17051
+ });
17052
+ } catch {
17053
+ }
16284
17054
  this.multimodal = new MultimodalRegistry(
16285
17055
  (this.config.providers ?? []).map((p) => p.type)
16286
17056
  );
@@ -16365,8 +17135,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
16365
17135
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
16366
17136
  return;
16367
17137
  }
16368
- const dbPath = ci.dbPath || path25__default.default.join(this.workspacePath, ".cascade", "code-index.db");
16369
- fs24__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
17138
+ const dbPath = ci.dbPath || path26__default.default.join(this.workspacePath, ".cascade", "code-index.db");
17139
+ fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
16370
17140
  const db = new Database__default.default(dbPath);
16371
17141
  db.pragma("journal_mode = WAL");
16372
17142
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -16746,10 +17516,57 @@ ${input}` }],
16746
17516
  setFeedbackSource(source) {
16747
17517
  this.router.setFeedbackSource(source);
16748
17518
  }
16749
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
17519
+ /**
17520
+ * Write a durable checkpoint for an interrupted run.
17521
+ *
17522
+ * Best-effort throughout: a run is already ending badly on every path that
17523
+ * calls this, and failing to record a resume file must not make that ending
17524
+ * worse. Nothing here is allowed to throw.
17525
+ */
17526
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
17527
+ const inherited = this.resumedFrom?.completed ?? [];
17528
+ const merged = /* @__PURE__ */ new Map();
17529
+ for (const node of inherited) merged.set(node.id, node);
17530
+ for (const node of this.currentRunCompleted) {
17531
+ const existing = merged.get(node.id);
17532
+ if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
17533
+ merged.set(node.id, node);
17534
+ }
17535
+ const completed = [...merged.values()];
17536
+ const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
17537
+ const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
17538
+ if (!completed.length && !carriedPartial) return false;
17539
+ try {
17540
+ const saved = await this.resumeStore?.save({
17541
+ taskId,
17542
+ prompt: canonicalPrompt,
17543
+ reason,
17544
+ ...detail ? { detail } : {},
17545
+ partialOutput: carriedPartial,
17546
+ completed
17547
+ });
17548
+ this.wroteCheckpointThisRun = saved === true;
17549
+ return saved === true;
17550
+ } catch {
17551
+ return false;
17552
+ }
17553
+ }
17554
+ /** True when a task can be resumed via /continue (this process or a previous one). */
16750
17555
  hasResumableRun() {
16751
17556
  return this.lastInterruptedRun != null;
16752
17557
  }
17558
+ /**
17559
+ * True when a checkpoint from ANY process is resumable. Async because the
17560
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
17561
+ * answer for callers already on a hot path.
17562
+ */
17563
+ async hasDurableResume() {
17564
+ return await this.resumeStore?.latest() != null;
17565
+ }
17566
+ /** Interrupted runs available to resume, newest first. */
17567
+ async listResumableRuns() {
17568
+ return await this.resumeStore?.list() ?? [];
17569
+ }
16753
17570
  /**
16754
17571
  * Raise the per-run token budget for a resume and return the continuation
16755
17572
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -16760,24 +17577,105 @@ ${input}` }],
16760
17577
  const last = this.lastInterruptedRun;
16761
17578
  if (!last) return null;
16762
17579
  this.lastInterruptedRun = void 0;
16763
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
17580
+ this.raiseBudgetForResume(opts.maxTokens);
17581
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
17582
+ }
17583
+ /**
17584
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
17585
+ * process, including one that crashed before it could return. Consumes the
17586
+ * checkpoint, so the same finished work is never restored into two runs.
17587
+ */
17588
+ async prepareDurableResume(opts = {}) {
17589
+ await this.resumeStore?.reclaimStale();
17590
+ const claimed = await this.resumeStore?.claim(opts.taskId);
17591
+ if (!claimed) return null;
17592
+ try {
17593
+ this.lastInterruptedRun = void 0;
17594
+ this.raiseBudgetForResume(opts.maxTokens);
17595
+ const prompt = this.buildResumePrompt(
17596
+ claimed.checkpoint.prompt,
17597
+ claimed.checkpoint.partialOutput,
17598
+ claimed.checkpoint.completed
17599
+ );
17600
+ this.resumedFrom = {
17601
+ prompt: claimed.checkpoint.prompt,
17602
+ completed: claimed.checkpoint.completed,
17603
+ partialOutput: claimed.checkpoint.partialOutput
17604
+ };
17605
+ this.claimedResumeId = claimed.claimId;
17606
+ return prompt;
17607
+ } catch (err) {
17608
+ await this.resumeStore?.release(claimed.claimId);
17609
+ throw err;
17610
+ }
17611
+ }
17612
+ /**
17613
+ * Discard the claimed checkpoint once the resumed run has taken ownership of
17614
+ * the work (it has produced its own checkpoint, or finished). Called by the
17615
+ * run loop; safe to call when nothing is claimed.
17616
+ */
17617
+ async settleClaimedResume(succeeded) {
17618
+ const claimId = this.claimedResumeId;
17619
+ if (!claimId) return;
17620
+ this.claimedResumeId = void 0;
17621
+ if (succeeded || this.wroteCheckpointThisRun) {
17622
+ await this.resumeStore?.settle(claimId);
17623
+ } else {
17624
+ await this.resumeStore?.release(claimId);
17625
+ }
17626
+ }
17627
+ /**
17628
+ * Hand back a claim taken by prepareDurableResume() when the caller never
17629
+ * gets as far as run(). The REPL prepares the continuation and submits it
17630
+ * separately, so a failure in between would otherwise strand the checkpoint
17631
+ * for the whole lease timeout before reclaimStale() could recover it.
17632
+ */
17633
+ async releaseClaimedResume() {
17634
+ const claimId = this.claimedResumeId;
17635
+ if (!claimId) return;
17636
+ this.claimedResumeId = void 0;
17637
+ await this.resumeStore?.release(claimId);
17638
+ }
17639
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
17640
+ raiseBudgetForResume(maxTokens) {
17641
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
16764
17642
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
16765
17643
  this.router.setMaxTokensPerRun(raised);
16766
- 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.
16767
-
16768
- Original task: ${last.prompt}` + (last.partialOutput ? `
16769
-
17644
+ }
17645
+ /**
17646
+ * The resume prompt. Completed sections are named explicitly rather than
17647
+ * gestured at: "do not recreate the files" is an instruction a planner can
17648
+ * only guess at, whereas a list of what is already done, and what it produced,
17649
+ * is something it can act on. That difference is the entire reason node
17650
+ * results are kept — a re-plan that cannot see the finished work will plan the
17651
+ * finished work again, and the user pays for it a second time.
17652
+ */
17653
+ buildResumePrompt(prompt, partialOutput, completed) {
17654
+ const done = summarizeCompleted(completed);
17655
+ return [
17656
+ "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.",
17657
+ "",
17658
+ `Original task: ${prompt}`,
17659
+ done ? `
17660
+ ${done}` : "",
17661
+ partialOutput ? `
16770
17662
  Partial result so far:
16771
- ${last.partialOutput}` : "");
17663
+ ${partialOutput}` : ""
17664
+ ].filter(Boolean).join("\n");
16772
17665
  }
16773
17666
  /**
16774
17667
  * Resume the last budget-capped task with a raised budget (SDK/headless).
16775
17668
  * Returns null when there is nothing to resume.
16776
17669
  */
16777
17670
  async resumeRun(opts = {}) {
16778
- const prompt = this.prepareResume(opts);
17671
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
16779
17672
  if (!prompt) return null;
16780
- return this.run({ prompt });
17673
+ try {
17674
+ return await this.run({ prompt });
17675
+ } catch (err) {
17676
+ await this.releaseClaimedResume();
17677
+ throw err;
17678
+ }
16781
17679
  }
16782
17680
  getWorkspacePath() {
16783
17681
  return this.workspacePath;
@@ -17146,6 +18044,10 @@ ${prompt}` : prompt;
17146
18044
  const startMs = Date.now();
17147
18045
  const taskId = crypto4.randomUUID();
17148
18046
  this.decisionLog = [];
18047
+ this.currentRunCompleted = [];
18048
+ this.wroteCheckpointThisRun = false;
18049
+ this.completedNormally = false;
18050
+ if (!this.claimedResumeId) this.resumedFrom = void 0;
17149
18051
  if (options.fastAnswer) {
17150
18052
  return this.runFastAnswer(options, startMs, taskId);
17151
18053
  }
@@ -17260,6 +18162,11 @@ ${prompt}` : prompt;
17260
18162
  });
17261
18163
  tier.on("log", (e) => this.emit("log", e));
17262
18164
  tier.on("tier:status", (e) => this.emit("tier:status", e));
18165
+ tier.on("section:complete", (e) => {
18166
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
18167
+ this.currentRunCompleted.push(e);
18168
+ this.emit("section:complete", e);
18169
+ });
17263
18170
  tier.on("tool:call", (e) => this.emit("tool:call", e));
17264
18171
  tier.on("tool:result", (e) => this.emit("tool:result", e));
17265
18172
  tier.on("tool:approval-request", async (request) => {
@@ -17406,6 +18313,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17406
18313
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
17407
18314
  const trip = runBreaker.reason();
17408
18315
  if (trip) {
18316
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
17409
18317
  finalOutput = [
17410
18318
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
17411
18319
  "",
@@ -17418,6 +18326,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17418
18326
  ${finalOutput}` : ""
17419
18327
  ].join("\n").trimEnd();
17420
18328
  }
18329
+ this.completedNormally = trip == null;
17421
18330
  } catch (err) {
17422
18331
  if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
17423
18332
  this.emit("run:cancelled", {
@@ -17425,6 +18334,13 @@ ${finalOutput}` : ""
17425
18334
  reason: err instanceof Error ? err.message : "Task cancelled",
17426
18335
  partialOutput: finalOutput || ""
17427
18336
  });
18337
+ await this.checkpointRun(
18338
+ taskId,
18339
+ options.prompt,
18340
+ finalOutput || "",
18341
+ "cancelled",
18342
+ err instanceof Error ? err.message : "Task cancelled"
18343
+ );
17428
18344
  runError = null;
17429
18345
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
17430
18346
  this.emit("run:budget-exceeded", {
@@ -17433,9 +18349,17 @@ ${finalOutput}` : ""
17433
18349
  partialOutput: finalOutput || ""
17434
18350
  });
17435
18351
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
18352
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
17436
18353
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
17437
18354
  runError = null;
17438
18355
  } else {
18356
+ await this.checkpointRun(
18357
+ taskId,
18358
+ options.prompt,
18359
+ finalOutput || "",
18360
+ "error",
18361
+ err instanceof Error ? err.message : String(err)
18362
+ );
17439
18363
  runError = err;
17440
18364
  throw err;
17441
18365
  }
@@ -17444,6 +18368,10 @@ ${finalOutput}` : ""
17444
18368
  escalator.cancelAllPending();
17445
18369
  } catch {
17446
18370
  }
18371
+ try {
18372
+ await this.settleClaimedResume(this.completedNormally);
18373
+ } catch {
18374
+ }
17447
18375
  try {
17448
18376
  this.releasePendingEscalations();
17449
18377
  } catch {
@@ -17683,7 +18611,7 @@ var Keystore = class {
17683
18611
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
17684
18612
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
17685
18613
  this.backend = "keytar";
17686
- if (password && fs24__default.default.existsSync(this.storePath)) {
18614
+ if (password && fs25__default.default.existsSync(this.storePath)) {
17687
18615
  try {
17688
18616
  const fileEntries = this.decryptFile(password);
17689
18617
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -17702,7 +18630,7 @@ var Keystore = class {
17702
18630
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
17703
18631
  );
17704
18632
  }
17705
- if (!fs24__default.default.existsSync(this.storePath)) {
18633
+ if (!fs25__default.default.existsSync(this.storePath)) {
17706
18634
  const salt = crypto4__default.default.randomBytes(SALT_LEN);
17707
18635
  this.masterKey = this.deriveKey(password, salt);
17708
18636
  this.writeWithSalt({}, salt);
@@ -17716,7 +18644,7 @@ var Keystore = class {
17716
18644
  }
17717
18645
  /** Synchronous legacy unlock kept for AES-only environments. */
17718
18646
  unlockSync(password) {
17719
- if (!fs24__default.default.existsSync(this.storePath)) {
18647
+ if (!fs25__default.default.existsSync(this.storePath)) {
17720
18648
  const salt = crypto4__default.default.randomBytes(SALT_LEN);
17721
18649
  this.masterKey = this.deriveKey(password, salt);
17722
18650
  this.writeWithSalt({}, salt);
@@ -17774,7 +18702,7 @@ var Keystore = class {
17774
18702
  }
17775
18703
  }
17776
18704
  decryptFile(password, knownSalt) {
17777
- if (!fs24__default.default.existsSync(this.storePath)) return {};
18705
+ if (!fs25__default.default.existsSync(this.storePath)) return {};
17778
18706
  try {
17779
18707
  const { salt, ciphertext, iv, tag } = this.readRaw();
17780
18708
  const useSalt = knownSalt ?? salt;
@@ -17796,8 +18724,8 @@ var Keystore = class {
17796
18724
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17797
18725
  const tag = cipher.getAuthTag();
17798
18726
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
17799
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
17800
- fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
18727
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
18728
+ fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
17801
18729
  }
17802
18730
  writeWithSalt(data, salt) {
17803
18731
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -17807,11 +18735,11 @@ var Keystore = class {
17807
18735
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17808
18736
  const tag = cipher.getAuthTag();
17809
18737
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
17810
- fs24__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
17811
- fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
18738
+ fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
18739
+ fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
17812
18740
  }
17813
18741
  readRaw() {
17814
- const buf = fs24__default.default.readFileSync(this.storePath);
18742
+ const buf = fs25__default.default.readFileSync(this.storePath);
17815
18743
  let offset = 0;
17816
18744
  const salt = buf.subarray(offset, offset + SALT_LEN);
17817
18745
  offset += SALT_LEN;
@@ -17827,7 +18755,7 @@ var Keystore = class {
17827
18755
  }
17828
18756
  };
17829
18757
  async function loadCascadeMd(workspacePath) {
17830
- const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
18758
+ const filePath = path26__default.default.join(workspacePath, "CASCADE.md");
17831
18759
  try {
17832
18760
  const raw = await fs5__default.default.readFile(filePath, "utf-8");
17833
18761
  return parseCascadeMd(raw);
@@ -17858,7 +18786,7 @@ ${raw.trim()}`;
17858
18786
  var MemoryStore = class _MemoryStore {
17859
18787
  db;
17860
18788
  constructor(dbPath) {
17861
- fs24__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
18789
+ fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
17862
18790
  try {
17863
18791
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
17864
18792
  this.db.pragma("journal_mode = WAL");
@@ -18694,7 +19622,7 @@ Original error: ${err.message}`
18694
19622
  }
18695
19623
  };
18696
19624
  function credentialsPath(globalDir2) {
18697
- return path25__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
19625
+ return path26__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
18698
19626
  }
18699
19627
  function providerKey(p) {
18700
19628
  if (p.type === "azure") {
@@ -18707,7 +19635,7 @@ function isPersistable(p) {
18707
19635
  }
18708
19636
  function loadGlobalCredentials(globalDir2) {
18709
19637
  try {
18710
- const raw = fs24__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
19638
+ const raw = fs25__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
18711
19639
  const parsed = JSON.parse(raw);
18712
19640
  if (!Array.isArray(parsed.providers)) return [];
18713
19641
  return parsed.providers.filter(
@@ -18719,13 +19647,13 @@ function loadGlobalCredentials(globalDir2) {
18719
19647
  }
18720
19648
  function saveGlobalCredentials(globalDir2, providers) {
18721
19649
  const filePath = credentialsPath(globalDir2);
18722
- fs24__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
19650
+ fs25__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
18723
19651
  const body = { version: 1, providers: providers.filter(isPersistable) };
18724
19652
  const tmp = `${filePath}.tmp`;
18725
- fs24__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
18726
- fs24__default.default.renameSync(tmp, filePath);
19653
+ fs25__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
19654
+ fs25__default.default.renameSync(tmp, filePath);
18727
19655
  try {
18728
- fs24__default.default.chmodSync(filePath, 384);
19656
+ fs25__default.default.chmodSync(filePath, 384);
18729
19657
  } catch {
18730
19658
  }
18731
19659
  }
@@ -18760,7 +19688,7 @@ var ConfigManager = class {
18760
19688
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
18761
19689
  constructor(workspacePath = process.cwd(), globalDirOverride) {
18762
19690
  this.workspacePath = workspacePath;
18763
- this.globalDir = globalDirOverride ?? path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
19691
+ this.globalDir = globalDirOverride ?? path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
18764
19692
  }
18765
19693
  async load() {
18766
19694
  this.config = await this.loadConfig();
@@ -18774,8 +19702,8 @@ var ConfigManager = class {
18774
19702
  this.ignore = new CascadeIgnore();
18775
19703
  await this.ignore.load(this.workspacePath);
18776
19704
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
18777
- this.keystore = new Keystore(path25__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
18778
- this.store = new MemoryStore(path25__default.default.join(this.workspacePath, CASCADE_DB_FILE));
19705
+ this.keystore = new Keystore(path26__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19706
+ this.store = new MemoryStore(path26__default.default.join(this.workspacePath, CASCADE_DB_FILE));
18779
19707
  await this.injectEnvKeys();
18780
19708
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
18781
19709
  await this.ensureDefaultIdentity();
@@ -18800,8 +19728,8 @@ var ConfigManager = class {
18800
19728
  return this.workspacePath;
18801
19729
  }
18802
19730
  async save() {
18803
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
18804
- await fs5__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
19731
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19732
+ await fs5__default.default.mkdir(path26__default.default.dirname(configPath), { recursive: true });
18805
19733
  await fs5__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
18806
19734
  try {
18807
19735
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -18837,7 +19765,7 @@ var ConfigManager = class {
18837
19765
  return configProvider?.apiKey;
18838
19766
  }
18839
19767
  async loadConfig() {
18840
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19768
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
18841
19769
  try {
18842
19770
  const raw = await fs5__default.default.readFile(configPath, "utf-8");
18843
19771
  return validateConfig(JSON.parse(raw));
@@ -19072,8 +20000,14 @@ var DashboardSocket = class {
19072
20000
  emitCascadeEvent(ev) {
19073
20001
  this.io.emit("cascade:event", ev);
19074
20002
  }
19075
- emitTierStatus(tierId, role, status, sessionId, action) {
19076
- const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId };
20003
+ /**
20004
+ * `extra` exists because this signature is positional: every field the tier
20005
+ * emits that is not named here is silently dropped on this path, while the
20006
+ * other two forwarders spread the whole event through. That asymmetry is how
20007
+ * a field can reach the web dashboard from one code path and not another.
20008
+ */
20009
+ emitTierStatus(tierId, role, status, sessionId, action, extra) {
20010
+ const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
19077
20011
  this.io.emit("tier:status", payload);
19078
20012
  this.io.to(`session:${sessionId}`).emit("tier:status", payload);
19079
20013
  }
@@ -19369,7 +20303,7 @@ function aggregateCostStats(sessions, opts = {}) {
19369
20303
  }
19370
20304
 
19371
20305
  // src/dashboard/server.ts
19372
- var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
20306
+ var __dirname$1 = path26__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
19373
20307
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
19374
20308
  var DashboardServer = class {
19375
20309
  app;
@@ -19649,14 +20583,14 @@ var DashboardServer = class {
19649
20583
  */
19650
20584
  persistConfig() {
19651
20585
  try {
19652
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
19653
- fs24__default.default.mkdirSync(path25__default.default.dirname(configPath), { recursive: true });
19654
- fs24__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
20586
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
20587
+ fs25__default.default.mkdirSync(path26__default.default.dirname(configPath), { recursive: true });
20588
+ fs25__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
19655
20589
  } catch (err) {
19656
20590
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
19657
20591
  }
19658
20592
  try {
19659
- saveGlobalCredentials(path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
20593
+ saveGlobalCredentials(path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
19660
20594
  } catch (err) {
19661
20595
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
19662
20596
  }
@@ -19671,15 +20605,15 @@ var DashboardServer = class {
19671
20605
  resolveDashboardSecret() {
19672
20606
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
19673
20607
  if (fromConfig) return fromConfig;
19674
- const secretPath = path25__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
20608
+ const secretPath = path26__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
19675
20609
  try {
19676
- if (fs24__default.default.existsSync(secretPath)) {
19677
- const existing = fs24__default.default.readFileSync(secretPath, "utf-8").trim();
20610
+ if (fs25__default.default.existsSync(secretPath)) {
20611
+ const existing = fs25__default.default.readFileSync(secretPath, "utf-8").trim();
19678
20612
  if (existing.length >= 16) return existing;
19679
20613
  }
19680
20614
  const generated = crypto4.randomUUID();
19681
- fs24__default.default.mkdirSync(path25__default.default.dirname(secretPath), { recursive: true });
19682
- fs24__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
20615
+ fs25__default.default.mkdirSync(path26__default.default.dirname(secretPath), { recursive: true });
20616
+ fs25__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
19683
20617
  if (this.config.dashboard.auth) {
19684
20618
  console.warn(
19685
20619
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -19706,7 +20640,7 @@ var DashboardServer = class {
19706
20640
  // ── Setup ─────────────────────────────────────
19707
20641
  getGlobalStore() {
19708
20642
  if (!this.globalStore) {
19709
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20643
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
19710
20644
  this.globalStore = new MemoryStore(globalDbPath);
19711
20645
  }
19712
20646
  return this.globalStore;
@@ -20086,12 +21020,12 @@ ${prompt}`;
20086
21020
  }
20087
21021
  }
20088
21022
  watchRuntimeChanges() {
20089
- const workspaceDbPath = path25__default.default.join(this.workspacePath, CASCADE_DB_FILE);
20090
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21023
+ const workspaceDbPath = path26__default.default.join(this.workspacePath, CASCADE_DB_FILE);
21024
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20091
21025
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
20092
21026
  for (const watchPath of watchPaths) {
20093
- if (!fs24__default.default.existsSync(watchPath)) continue;
20094
- fs24__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
21027
+ if (!fs25__default.default.existsSync(watchPath)) continue;
21028
+ fs25__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
20095
21029
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
20096
21030
  });
20097
21031
  }
@@ -20213,7 +21147,7 @@ ${prompt}`;
20213
21147
  const sessionId = req.params.id;
20214
21148
  this.store.deleteSession(sessionId);
20215
21149
  this.store.deleteRuntimeSession(sessionId);
20216
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21150
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20217
21151
  const globalStore = new MemoryStore(globalDbPath);
20218
21152
  try {
20219
21153
  globalStore.deleteRuntimeSession(sessionId);
@@ -20306,7 +21240,7 @@ ${prompt}`;
20306
21240
  });
20307
21241
  this.app.delete("/api/sessions", auth2, (req, res) => {
20308
21242
  const body = req.body;
20309
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21243
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20310
21244
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
20311
21245
  const globalStore = new MemoryStore(globalDbPath);
20312
21246
  try {
@@ -20329,7 +21263,7 @@ ${prompt}`;
20329
21263
  });
20330
21264
  this.app.delete("/api/runtime", auth2, (_req, res) => {
20331
21265
  this.store.deleteAllRuntimeNodes();
20332
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21266
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20333
21267
  const globalStore = new MemoryStore(globalDbPath);
20334
21268
  try {
20335
21269
  globalStore.deleteAllRuntimeNodes();
@@ -20415,12 +21349,12 @@ ${prompt}`;
20415
21349
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
20416
21350
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
20417
21351
  try {
20418
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
20419
- const existing = fs24__default.default.existsSync(configPath) ? JSON.parse(fs24__default.default.readFileSync(configPath, "utf-8")) : {};
21352
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
21353
+ const existing = fs25__default.default.existsSync(configPath) ? JSON.parse(fs25__default.default.readFileSync(configPath, "utf-8")) : {};
20420
21354
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
20421
21355
  const tmp = configPath + ".tmp";
20422
- fs24__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20423
- fs24__default.default.renameSync(tmp, configPath);
21356
+ fs25__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
21357
+ fs25__default.default.renameSync(tmp, configPath);
20424
21358
  res.json({ ok: true });
20425
21359
  } catch (err) {
20426
21360
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -20448,7 +21382,7 @@ ${prompt}`;
20448
21382
  this.app.get("/api/runtime", auth2, (req, res) => {
20449
21383
  const scope = req.query["scope"] ?? "workspace";
20450
21384
  if (scope === "global") {
20451
- const globalDbPath = path25__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21385
+ const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20452
21386
  const globalStore = new MemoryStore(globalDbPath);
20453
21387
  try {
20454
21388
  res.json({
@@ -20789,13 +21723,13 @@ ${prompt}`;
20789
21723
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
20790
21724
  }
20791
21725
  });
20792
- const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
20793
- const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
20794
- const webDistPath = fs24__default.default.existsSync(prodPath) ? prodPath : devPath;
20795
- if (fs24__default.default.existsSync(webDistPath)) {
21726
+ const prodPath = path26__default.default.resolve(__dirname$1, "../web/dist");
21727
+ const devPath = path26__default.default.resolve(__dirname$1, "../../web/dist");
21728
+ const webDistPath = fs25__default.default.existsSync(prodPath) ? prodPath : devPath;
21729
+ if (fs25__default.default.existsSync(webDistPath)) {
20796
21730
  this.app.use(express__default.default.static(webDistPath));
20797
21731
  this.app.get("*", (_req, res) => {
20798
- res.sendFile(path25__default.default.join(webDistPath, "index.html"));
21732
+ res.sendFile(path26__default.default.join(webDistPath, "index.html"));
20799
21733
  });
20800
21734
  } else {
20801
21735
  this.app.get("/", (_req, res) => {
@@ -20982,14 +21916,14 @@ function planRetrieval(ctx) {
20982
21916
  }
20983
21917
  var CLOUD_SESSION_FILE = "cloud-session.json";
20984
21918
  function globalDir(homeDir = os6__default.default.homedir()) {
20985
- return path25__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
21919
+ return path26__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20986
21920
  }
20987
21921
  function cloudSessionPath(dir = globalDir()) {
20988
- return path25__default.default.join(dir, CLOUD_SESSION_FILE);
21922
+ return path26__default.default.join(dir, CLOUD_SESSION_FILE);
20989
21923
  }
20990
21924
  function loadCloudSession(dir = globalDir()) {
20991
21925
  try {
20992
- const raw = fs24__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
21926
+ const raw = fs25__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20993
21927
  const parsed = JSON.parse(raw);
20994
21928
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20995
21929
  return parsed;
@@ -20998,17 +21932,17 @@ function loadCloudSession(dir = globalDir()) {
20998
21932
  }
20999
21933
  }
21000
21934
  function saveCloudSession(session, dir = globalDir()) {
21001
- fs24__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21935
+ fs25__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21002
21936
  const file = cloudSessionPath(dir);
21003
- fs24__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21937
+ fs25__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21004
21938
  try {
21005
- fs24__default.default.chmodSync(file, 384);
21939
+ fs25__default.default.chmodSync(file, 384);
21006
21940
  } catch {
21007
21941
  }
21008
21942
  }
21009
21943
  function clearCloudSession(dir = globalDir()) {
21010
21944
  try {
21011
- fs24__default.default.rmSync(cloudSessionPath(dir));
21945
+ fs25__default.default.rmSync(cloudSessionPath(dir));
21012
21946
  } catch {
21013
21947
  }
21014
21948
  }
@@ -21180,16 +22114,16 @@ var CloudClient = class _CloudClient {
21180
22114
  return (await this.refresh()).accessToken;
21181
22115
  }
21182
22116
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
21183
- async authed(path27, init = {}) {
21184
- const call = (token) => fetch(this.url(path27), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
22117
+ async authed(path28, init = {}) {
22118
+ const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21185
22119
  let res = await call(await this.accessToken());
21186
22120
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21187
22121
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21188
22122
  const text = await res.text();
21189
22123
  return text ? JSON.parse(text) : {};
21190
22124
  }
21191
- authedGet(path27) {
21192
- return this.authed(path27);
22125
+ authedGet(path28) {
22126
+ return this.authed(path28);
21193
22127
  }
21194
22128
  async fetchMe(token) {
21195
22129
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21560,6 +22494,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
21560
22494
  exports.DEFAULT_THEME = DEFAULT_THEME;
21561
22495
  exports.DOCUMENT_MIME = DOCUMENT_MIME;
21562
22496
  exports.DashboardServer = DashboardServer;
22497
+ exports.DependencyScheduler = DependencyScheduler;
21563
22498
  exports.FileMcpOAuthStore = FileMcpOAuthStore;
21564
22499
  exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
21565
22500
  exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
@@ -21585,6 +22520,8 @@ exports.MemoryStore = MemoryStore;
21585
22520
  exports.OLLAMA_BASE_URL = OLLAMA_BASE_URL;
21586
22521
  exports.OpenAICompatibleEmbedder = OpenAICompatibleEmbedder;
21587
22522
  exports.PROVIDER_DISPLAY_NAMES = PROVIDER_DISPLAY_NAMES;
22523
+ exports.RESUME_SCHEMA_VERSION = RESUME_SCHEMA_VERSION;
22524
+ exports.ResumeStore = ResumeStore;
21588
22525
  exports.Retriever = Retriever;
21589
22526
  exports.SqliteVectorStore = SqliteVectorStore;
21590
22527
  exports.T1Administrator = T1Administrator;
@@ -21595,6 +22532,7 @@ exports.T3Worker = T3Worker;
21595
22532
  exports.T3_MODEL_PRIORITY = T3_MODEL_PRIORITY;
21596
22533
  exports.THEME_NAMES = THEME_NAMES;
21597
22534
  exports.TOOL_NAMES = TOOL_NAMES;
22535
+ exports.TaskGraphValidationError = TaskGraphValidationError;
21598
22536
  exports.TaskScheduler = TaskScheduler;
21599
22537
  exports.Telemetry = Telemetry;
21600
22538
  exports.ToolRegistry = ToolRegistry;
@@ -21613,6 +22551,9 @@ exports.chartToTableRows = chartToTableRows;
21613
22551
  exports.chatCompleterFromProviders = chatCompleterFromProviders;
21614
22552
  exports.chunkCode = chunkCode;
21615
22553
  exports.chunkText = chunkText2;
22554
+ exports.compileSectionGraph = compileSectionGraph;
22555
+ exports.compileSubtaskGraph = compileSubtaskGraph;
22556
+ exports.compileTaskGraph = compileTaskGraph;
21616
22557
  exports.completeMcpOAuth = completeMcpOAuth;
21617
22558
  exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
21618
22559
  exports.createCascade = createCascade;
@@ -21624,7 +22565,9 @@ exports.discoverMcpTools = discoverMcpTools;
21624
22565
  exports.distillSessionFacts = distillSessionFacts;
21625
22566
  exports.embedderFromProviders = embedderFromProviders;
21626
22567
  exports.encryptSyncBlob = encryptJSON;
22568
+ exports.evaluateAcceptance = evaluateAcceptance;
21627
22569
  exports.extractCharts = extractCharts;
22570
+ exports.failures = failures;
21628
22571
  exports.fileExt = fileExt;
21629
22572
  exports.fileOAuthProvider = fileOAuthProvider;
21630
22573
  exports.gatherSyncBundle = gatherSyncBundle;
@@ -21658,6 +22601,8 @@ exports.sniffImage = sniffImage;
21658
22601
  exports.splitSlides = splitSlides;
21659
22602
  exports.streamCascade = streamCascade;
21660
22603
  exports.stripInline = stripInline;
22604
+ exports.summarizeCompleted = summarizeCompleted;
22605
+ exports.undecided = undecided;
21661
22606
  exports.uniqueMcpServerName = uniqueMcpServerName;
21662
22607
  //# sourceMappingURL=index.cjs.map
21663
22608
  //# sourceMappingURL=index.cjs.map