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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
- import path25, { dirname } from 'path';
3
- import fs24, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
2
+ import path26, { dirname } from 'path';
3
+ import fs25, { promises, mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
4
4
  import os6 from 'os';
5
5
  import crypto4, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
6
6
  import EventEmitter, { setMaxListeners } from 'events';
@@ -61,12 +61,12 @@ var init_audit_logger = __esm({
61
61
  constructor(workspacePath, debugMode = false) {
62
62
  this.workspacePath = workspacePath;
63
63
  this.debugMode = debugMode;
64
- const cascadeDir = path25.join(workspacePath, ".cascade");
65
- if (!fs24.existsSync(cascadeDir)) {
66
- fs24.mkdirSync(cascadeDir, { recursive: true });
64
+ const cascadeDir = path26.join(workspacePath, ".cascade");
65
+ if (!fs25.existsSync(cascadeDir)) {
66
+ fs25.mkdirSync(cascadeDir, { recursive: true });
67
67
  }
68
- this.keyPath = path25.join(cascadeDir, "audit_log.key");
69
- this.dbPath = path25.join(cascadeDir, "audit_log.db");
68
+ this.keyPath = path26.join(cascadeDir, "audit_log.key");
69
+ this.dbPath = path26.join(cascadeDir, "audit_log.db");
70
70
  this.initEncryptionKey();
71
71
  this.db = new Database(this.dbPath);
72
72
  this.db.pragma("journal_mode = WAL");
@@ -96,11 +96,11 @@ var init_audit_logger = __esm({
96
96
  return crypto4.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
97
97
  }
98
98
  initEncryptionKey() {
99
- if (fs24.existsSync(this.keyPath)) {
100
- this.encryptionKey = fs24.readFileSync(this.keyPath);
99
+ if (fs25.existsSync(this.keyPath)) {
100
+ this.encryptionKey = fs25.readFileSync(this.keyPath);
101
101
  } else {
102
102
  this.encryptionKey = crypto4.randomBytes(32);
103
- fs24.writeFileSync(this.keyPath, this.encryptionKey);
103
+ fs25.writeFileSync(this.keyPath, this.encryptionKey);
104
104
  }
105
105
  }
106
106
  encrypt(text) {
@@ -178,9 +178,9 @@ var init_audit_logger = __esm({
178
178
  dumpDebugIfNeeded() {
179
179
  if (!this.debugMode) return;
180
180
  try {
181
- const dumpPath = path25.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
181
+ const dumpPath = path26.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
182
182
  const entries = this.getAllLogs();
183
- fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
183
+ fs25.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
184
184
  } catch (err) {
185
185
  console.error("Failed to dump debug audit logs", err);
186
186
  }
@@ -1862,7 +1862,7 @@ function reconcilePrice(model, live) {
1862
1862
  }
1863
1863
 
1864
1864
  // src/constants.ts
1865
- var CASCADE_VERSION = "0.67.0";
1865
+ var CASCADE_VERSION = "0.69.0";
1866
1866
  var CASCADE_CONFIG_DIR = ".cascade";
1867
1867
  var CASCADE_MD_FILE = "CASCADE.md";
1868
1868
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4617,7 +4617,7 @@ function computeDelegationSavings(stats, t1Model) {
4617
4617
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
4618
4618
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
4619
4619
  var FETCH_TIMEOUT_MS = 8e3;
4620
- var DEFAULT_CACHE_FILE = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4620
+ var DEFAULT_CACHE_FILE = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4621
4621
  function normalizeModelId2(id) {
4622
4622
  let s = id.toLowerCase();
4623
4623
  const slash = s.lastIndexOf("/");
@@ -4764,7 +4764,7 @@ var LiveDataProvider = class {
4764
4764
  }
4765
4765
  async saveCache() {
4766
4766
  try {
4767
- await fs5.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
4767
+ await fs5.mkdir(path26.dirname(this.opts.cacheFile), { recursive: true });
4768
4768
  const cache = {
4769
4769
  fetchedAt: this.fetchedAt,
4770
4770
  snapshot: this.snapshot ?? void 0,
@@ -6024,6 +6024,21 @@ var BaseTier = class extends EventEmitter {
6024
6024
  * which node (Cockpit node panel / Why panel).
6025
6025
  */
6026
6026
  servingModel;
6027
+ /**
6028
+ * This tier's identity IN THE TASK GRAPH — the section or subtask id the
6029
+ * planner assigned — plus where it sat in the dependency order.
6030
+ *
6031
+ * `id` and `graphNodeId` are deliberately different id spaces, and both are
6032
+ * needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
6033
+ * construction); `graphNodeId` identifies the WORK ("s1"), which is what
6034
+ * dependency edges point at. Without the distinction a graph view cannot be
6035
+ * drawn at all: `dependsOn` lists section ids, while every running tier
6036
+ * reports a generated tier id, so every edge would name a node that never
6037
+ * appears in the stream.
6038
+ */
6039
+ graphNodeId;
6040
+ graphDependsOn;
6041
+ graphWave;
6027
6042
  constructor(role, id, parentId) {
6028
6043
  super();
6029
6044
  this.role = role;
@@ -6035,6 +6050,33 @@ var BaseTier = class extends EventEmitter {
6035
6050
  setPresenter(on = true) {
6036
6051
  this.isPresenter = on;
6037
6052
  }
6053
+ /**
6054
+ * Record where this tier sits in the compiled task graph.
6055
+ *
6056
+ * Called by whichever tier scheduled this one: the id and edges are known at
6057
+ * construction, the wave only once the scheduler reaches it, so the fields
6058
+ * are set independently rather than all at once.
6059
+ */
6060
+ setGraphPosition(position) {
6061
+ if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
6062
+ if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
6063
+ if (position.wave !== void 0) this.graphWave = position.wave;
6064
+ }
6065
+ /**
6066
+ * The graph fields, for spreading into a status payload.
6067
+ *
6068
+ * One helper rather than two literals because `tier:status` is emitted from
6069
+ * two different call sites with two different payload shapes; adding a field
6070
+ * to only one of them makes it arrive intermittently, which is worse for a
6071
+ * consumer than never arriving at all.
6072
+ */
6073
+ graphFields() {
6074
+ return {
6075
+ nodeId: this.graphNodeId,
6076
+ dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
6077
+ waveId: this.graphWave
6078
+ };
6079
+ }
6038
6080
  getStatus() {
6039
6081
  return this.status;
6040
6082
  }
@@ -6049,7 +6091,8 @@ var BaseTier = class extends EventEmitter {
6049
6091
  status,
6050
6092
  timestamp,
6051
6093
  output,
6052
- model: this.servingModel
6094
+ model: this.servingModel,
6095
+ ...this.graphFields()
6053
6096
  };
6054
6097
  this.emit("status", event);
6055
6098
  this.emit("tier:status", event);
@@ -6089,7 +6132,8 @@ var BaseTier = class extends EventEmitter {
6089
6132
  progressPct: update.progressPct,
6090
6133
  timestamp,
6091
6134
  output: update.output,
6092
- model: this.servingModel
6135
+ model: this.servingModel,
6136
+ ...this.graphFields()
6093
6137
  });
6094
6138
  }
6095
6139
  buildMessage(type, to, payload) {
@@ -6556,6 +6600,79 @@ function describeProviderError(c, modelId) {
6556
6600
  }
6557
6601
  }
6558
6602
 
6603
+ // src/core/verification/acceptance.ts
6604
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
6605
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
6606
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
6607
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
6608
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
6609
+ function targetFile(criterion, ownedFiles) {
6610
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
6611
+ if (mentioned.length > 1) return null;
6612
+ if (mentioned.length === 1) {
6613
+ const candidate = mentioned[0];
6614
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
6615
+ }
6616
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
6617
+ }
6618
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
6619
+ const results = [];
6620
+ for (const criterion of criteria) {
6621
+ const text = criterion.trim();
6622
+ if (!text) {
6623
+ results.push({ criterion, verdict: "undecidable" });
6624
+ continue;
6625
+ }
6626
+ if (NEGATION.test(text)) {
6627
+ results.push({ criterion, verdict: "undecidable" });
6628
+ continue;
6629
+ }
6630
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
6631
+ const wantsExists = EXISTS.test(text);
6632
+ const wantsNonEmpty = NON_EMPTY.test(text);
6633
+ if (!needle && !wantsExists && !wantsNonEmpty) {
6634
+ results.push({ criterion, verdict: "undecidable" });
6635
+ continue;
6636
+ }
6637
+ const file = targetFile(text, ownedFiles);
6638
+ if (!file) {
6639
+ results.push({ criterion, verdict: "undecidable" });
6640
+ continue;
6641
+ }
6642
+ const stat = await probe.stat(file);
6643
+ if (!stat) {
6644
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
6645
+ continue;
6646
+ }
6647
+ if (wantsNonEmpty && stat.size === 0) {
6648
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
6649
+ continue;
6650
+ }
6651
+ if (needle) {
6652
+ const content = await probe.read(file);
6653
+ if (content === null) {
6654
+ results.push({ criterion, verdict: "undecidable" });
6655
+ continue;
6656
+ }
6657
+ const found = content.includes(needle);
6658
+ results.push({
6659
+ criterion,
6660
+ verdict: found ? "passed" : "failed",
6661
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
6662
+ });
6663
+ continue;
6664
+ }
6665
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
6666
+ }
6667
+ return results;
6668
+ }
6669
+ function undecided(results) {
6670
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
6671
+ }
6672
+ function failures(results) {
6673
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
6674
+ }
6675
+
6559
6676
  // src/core/tiers/t3-worker.ts
6560
6677
  var CriticalToolError = class extends Error {
6561
6678
  constructor(message, toolName) {
@@ -6872,7 +6989,7 @@ Now execute your subtask using this context where relevant.`
6872
6989
  }
6873
6990
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6874
6991
  if (!artifactCheck.ok) {
6875
- correctionAttempts = 1;
6992
+ correctionAttempts++;
6876
6993
  issues.push(...artifactCheck.issues);
6877
6994
  output = await this.correctOutput(output, artifactCheck.issues);
6878
6995
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -6890,22 +7007,45 @@ Now execute your subtask using this context where relevant.`
6890
7007
  this.tools.map((t) => t.name)
6891
7008
  );
6892
7009
  if (visualIssue) {
6893
- correctionAttempts = 1;
7010
+ correctionAttempts++;
6894
7011
  issues.push(visualIssue);
6895
7012
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
6896
7013
  output = await this.correctOutput(output, [visualIssue]);
6897
7014
  }
7015
+ const acceptanceResults = await this.checkAcceptance(assignment);
7016
+ const acceptanceFailures = failures(acceptanceResults);
7017
+ if (acceptanceFailures.length > 0) {
7018
+ correctionAttempts++;
7019
+ issues.push(...acceptanceFailures);
7020
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
7021
+ output = await this.correctOutput(output, acceptanceFailures);
7022
+ const recheck = failures(await this.checkAcceptance(assignment));
7023
+ if (recheck.length > 0) {
7024
+ issues.push(...recheck);
7025
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
7026
+ failed.push(...recheck);
7027
+ this.setStatus("FAILED");
7028
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
7029
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
7030
+ }
7031
+ }
7032
+ for (const result2 of acceptanceResults) {
7033
+ if (result2.verdict === "undecidable") continue;
7034
+ checksRun.push(result2.criterion);
7035
+ if (result2.verdict === "passed") passed.push(result2.criterion);
7036
+ }
6898
7037
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
6899
- const testResult = await this.selfTest(assignment, output);
7038
+ const pendingAcceptance = undecided(acceptanceResults);
7039
+ const testResult = await this.selfTest(assignment, output, pendingAcceptance);
6900
7040
  checksRun.push(...testResult.checksRun);
6901
7041
  passed.push(...testResult.passed);
6902
7042
  failed.push(...testResult.failed);
6903
7043
  if (testResult.failed.length > 0) {
6904
- correctionAttempts = 1;
7044
+ correctionAttempts++;
6905
7045
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
6906
7046
  const corrected = await this.correctOutput(output, testResult.failed);
6907
7047
  output = corrected;
6908
- const retest = await this.selfTest(assignment, output);
7048
+ const retest = await this.selfTest(assignment, output, pendingAcceptance);
6909
7049
  passed.push(...retest.passed);
6910
7050
  if (retest.failed.length > 0) {
6911
7051
  failed.push(...retest.failed);
@@ -7209,8 +7349,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
7209
7349
  // Media generation can run for a minute; without this a cancelled run
7210
7350
  // still pays for an image nobody will see.
7211
7351
  ...this.signal ? { signal: this.signal } : {},
7212
- saveSnapshot: async (path27, content) => {
7213
- this.store?.addFileSnapshot(this.taskId, path27, content);
7352
+ saveSnapshot: async (path28, content) => {
7353
+ this.store?.addFileSnapshot(this.taskId, path28, content);
7214
7354
  },
7215
7355
  sendPeerSync: (to, syncType, content) => {
7216
7356
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -7397,7 +7537,7 @@ ${assignment.expectedOutput}`;
7397
7537
  const { promisify: promisify4 } = await import('util');
7398
7538
  const execAsync2 = promisify4(exec2);
7399
7539
  for (const artifactPath of artifactPaths) {
7400
- const absolutePath = path25.resolve(process.cwd(), artifactPath);
7540
+ const absolutePath = path26.resolve(process.cwd(), artifactPath);
7401
7541
  try {
7402
7542
  const stat = await fs5.stat(absolutePath);
7403
7543
  if (!stat.isFile()) {
@@ -7432,7 +7572,7 @@ ${assignment.expectedOutput}`;
7432
7572
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
7433
7573
  continue;
7434
7574
  }
7435
- const ext = path25.extname(absolutePath).toLowerCase();
7575
+ const ext = path26.extname(absolutePath).toLowerCase();
7436
7576
  try {
7437
7577
  if (ext === ".ts" || ext === ".tsx") {
7438
7578
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -7518,14 +7658,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7518
7658
  }
7519
7659
  return current;
7520
7660
  }
7521
- async selfTest(assignment, output) {
7661
+ /**
7662
+ * Run the acceptance criteria that can be settled by looking at the workspace.
7663
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
7664
+ */
7665
+ async checkAcceptance(assignment) {
7666
+ const criteria = assignment.acceptance ?? [];
7667
+ if (!criteria.length) return [];
7668
+ const resolve2 = (target) => path26.resolve(process.cwd(), target);
7669
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
7670
+ stat: async (target) => {
7671
+ try {
7672
+ const stat = await fs5.stat(resolve2(target));
7673
+ return stat.isFile() ? { size: stat.size } : null;
7674
+ } catch {
7675
+ return null;
7676
+ }
7677
+ },
7678
+ read: async (target) => {
7679
+ try {
7680
+ const content = await fs5.readFile(resolve2(target), "utf-8");
7681
+ return content.includes("\uFFFD") ? null : content;
7682
+ } catch {
7683
+ return null;
7684
+ }
7685
+ }
7686
+ });
7687
+ }
7688
+ async selfTest(assignment, output, pendingAcceptance) {
7689
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
7522
7690
  const prompt = `Self-test this output against the assignment requirements.
7523
7691
 
7524
7692
  Assignment: ${assignment.description}
7525
7693
  Expected output: ${assignment.expectedOutput}
7526
7694
  Constraints: ${assignment.constraints.join("; ")}
7527
- ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7528
- ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
7695
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7696
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
7529
7697
  ` : ""}
7530
7698
  Output to test:
7531
7699
  ${output}
@@ -7592,8 +7760,8 @@ ${output.slice(0, 4e3)}`;
7592
7760
  } catch {
7593
7761
  }
7594
7762
  }
7595
- async correctOutput(originalOutput, failures) {
7596
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
7763
+ async correctOutput(originalOutput, failures2) {
7764
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
7597
7765
 
7598
7766
  Original output:
7599
7767
  ${originalOutput}
@@ -8254,12 +8422,220 @@ function describeGenerationForPlanner(has) {
8254
8422
  return lines.join("\n");
8255
8423
  }
8256
8424
 
8425
+ // src/core/orchestration/task-graph.ts
8426
+ var TaskGraphValidationError = class extends Error {
8427
+ constructor(issues) {
8428
+ super(issues.map((issue) => issue.message).join("; "));
8429
+ this.issues = issues;
8430
+ this.name = "TaskGraphValidationError";
8431
+ }
8432
+ issues;
8433
+ };
8434
+ function normalizeDependencies(input) {
8435
+ if (!input?.length) return [];
8436
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
8437
+ }
8438
+ function cycleNodes(nodes) {
8439
+ const adjacency = new Map(
8440
+ nodes.map((node) => [node.id, node.dependsOn])
8441
+ );
8442
+ const index = /* @__PURE__ */ new Map();
8443
+ const lowlink = /* @__PURE__ */ new Map();
8444
+ const onStack = /* @__PURE__ */ new Set();
8445
+ const stack = [];
8446
+ const inCycle = /* @__PURE__ */ new Set();
8447
+ let counter = 0;
8448
+ const open = (id) => {
8449
+ index.set(id, counter);
8450
+ lowlink.set(id, counter);
8451
+ counter++;
8452
+ stack.push(id);
8453
+ onStack.add(id);
8454
+ };
8455
+ for (const root of nodes) {
8456
+ if (index.has(root.id)) continue;
8457
+ open(root.id);
8458
+ const frames = [{ id: root.id, edge: 0 }];
8459
+ while (frames.length > 0) {
8460
+ const frame = frames[frames.length - 1];
8461
+ const neighbours = adjacency.get(frame.id) ?? [];
8462
+ if (frame.edge < neighbours.length) {
8463
+ const next = neighbours[frame.edge];
8464
+ frame.edge++;
8465
+ if (!adjacency.has(next)) continue;
8466
+ if (!index.has(next)) {
8467
+ open(next);
8468
+ frames.push({ id: next, edge: 0 });
8469
+ } else if (onStack.has(next)) {
8470
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
8471
+ }
8472
+ continue;
8473
+ }
8474
+ frames.pop();
8475
+ const parent = frames[frames.length - 1];
8476
+ if (parent) {
8477
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
8478
+ }
8479
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
8480
+ const component = [];
8481
+ for (; ; ) {
8482
+ const member = stack.pop();
8483
+ onStack.delete(member);
8484
+ component.push(member);
8485
+ if (member === frame.id) break;
8486
+ }
8487
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
8488
+ if (cyclic) for (const member of component) inCycle.add(member);
8489
+ }
8490
+ }
8491
+ }
8492
+ return inCycle;
8493
+ }
8494
+ function throwIfStrict(mode, issues) {
8495
+ if (mode === "strict" && issues.length > 0) {
8496
+ throw new TaskGraphValidationError(issues);
8497
+ }
8498
+ }
8499
+ function compileTaskGraph(inputNodes, options = {}) {
8500
+ const mode = options.mode ?? "strict";
8501
+ const issues = [];
8502
+ if (inputNodes.length === 0) {
8503
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
8504
+ throwIfStrict(mode, issues);
8505
+ }
8506
+ const seen = /* @__PURE__ */ new Set();
8507
+ const nodes = [];
8508
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
8509
+ const input = inputNodes[ordinal];
8510
+ const id = input.id?.trim();
8511
+ const title = input.title?.trim();
8512
+ if (!id || !title) {
8513
+ issues.push({
8514
+ code: "INVALID_NODE",
8515
+ nodeId: id || void 0,
8516
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
8517
+ });
8518
+ continue;
8519
+ }
8520
+ if (seen.has(id)) {
8521
+ issues.push({
8522
+ code: "DUPLICATE_NODE_ID",
8523
+ nodeId: id,
8524
+ message: `Duplicate task graph node id: ${id}`
8525
+ });
8526
+ continue;
8527
+ }
8528
+ seen.add(id);
8529
+ nodes.push({
8530
+ id,
8531
+ title,
8532
+ kind: input.kind,
8533
+ dependsOn: normalizeDependencies(input.dependsOn),
8534
+ payload: input.payload,
8535
+ ordinal
8536
+ });
8537
+ }
8538
+ throwIfStrict(mode, issues);
8539
+ const ids = new Set(nodes.map((node) => node.id));
8540
+ const sanitized = nodes.map((node) => {
8541
+ const dependsOn = [];
8542
+ for (const dependencyId of node.dependsOn) {
8543
+ if (dependencyId === node.id) {
8544
+ issues.push({
8545
+ code: "SELF_DEPENDENCY",
8546
+ nodeId: node.id,
8547
+ dependencyId,
8548
+ message: `Task graph node ${node.id} cannot depend on itself`
8549
+ });
8550
+ continue;
8551
+ }
8552
+ if (!ids.has(dependencyId)) {
8553
+ issues.push({
8554
+ code: "UNKNOWN_DEPENDENCY",
8555
+ nodeId: node.id,
8556
+ dependencyId,
8557
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
8558
+ });
8559
+ continue;
8560
+ }
8561
+ dependsOn.push(dependencyId);
8562
+ }
8563
+ return { ...node, dependsOn };
8564
+ });
8565
+ throwIfStrict(mode, issues);
8566
+ const cyclic = cycleNodes(sanitized);
8567
+ let repaired = sanitized;
8568
+ if (cyclic.size > 0) {
8569
+ const cycleIssues = [];
8570
+ repaired = sanitized.map((node) => {
8571
+ if (!cyclic.has(node.id)) return node;
8572
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
8573
+ if (!cyclic.has(dependencyId)) return true;
8574
+ cycleIssues.push({
8575
+ code: "CYCLE_EDGE_REMOVED",
8576
+ nodeId: node.id,
8577
+ dependencyId,
8578
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
8579
+ });
8580
+ return false;
8581
+ });
8582
+ return { ...node, dependsOn };
8583
+ });
8584
+ issues.push(...cycleIssues);
8585
+ throwIfStrict(mode, cycleIssues);
8586
+ }
8587
+ const remainingCycle = cycleNodes(repaired);
8588
+ if (remainingCycle.size > 0) {
8589
+ throw new TaskGraphValidationError([
8590
+ {
8591
+ code: "INVALID_NODE",
8592
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
8593
+ }
8594
+ ]);
8595
+ }
8596
+ return {
8597
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
8598
+ issues
8599
+ };
8600
+ }
8601
+
8602
+ // src/core/orchestration/adapters.ts
8603
+ function compileSectionGraph(sections) {
8604
+ return compileTaskGraph(
8605
+ sections.map((section) => ({
8606
+ id: section.sectionId,
8607
+ title: section.sectionTitle,
8608
+ kind: "section",
8609
+ dependsOn: section.dependsOn,
8610
+ payload: section
8611
+ })),
8612
+ { mode: "repair" }
8613
+ );
8614
+ }
8615
+ function compileSubtaskGraph(subtasks) {
8616
+ return compileTaskGraph(
8617
+ subtasks.map((subtask) => ({
8618
+ id: subtask.subtaskId,
8619
+ title: subtask.subtaskTitle,
8620
+ kind: "subtask",
8621
+ dependsOn: subtask.dependsOn,
8622
+ payload: subtask
8623
+ })),
8624
+ { mode: "repair" }
8625
+ );
8626
+ }
8627
+
8257
8628
  // src/core/tiers/t2-manager.ts
8258
8629
  function buildT2SystemPrompt(has) {
8259
8630
  const generation = describeGenerationForPlanner(has);
8260
8631
  return [
8261
8632
  "You are a T2 Manager agent in the Cascade AI system.",
8262
- "Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
8633
+ // Must agree with the decomposition prompt in decomposeSection(), which is
8634
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
8635
+ // fully cover it"; this said 2-5. The model received both, so its floor was
8636
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
8637
+ // real worker call on every small section.
8638
+ "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.",
8263
8639
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
8264
8640
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
8265
8641
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -8593,6 +8969,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8593
8969
  for (const a of assignments) {
8594
8970
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
8595
8971
  if (this.store) worker.setStore(this.store, taskId);
8972
+ worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
8596
8973
  worker.setPeerBus(this.t3PeerBus);
8597
8974
  if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
8598
8975
  if (this.toolCreator) worker.setToolCreator(this.toolCreator);
@@ -8655,21 +9032,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8655
9032
  * then reduce in-degrees of their dependents and repeat.
8656
9033
  */
8657
9034
  async runWithDependencies(assignments, workerMap, taskId) {
9035
+ const compiled = compileSubtaskGraph(assignments);
9036
+ for (const issue of compiled.issues) {
9037
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
9038
+ }
9039
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
9040
+ const assignment = node.payload;
9041
+ const original = assignment.dependsOn ?? [];
9042
+ const repaired = node.dependsOn;
9043
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
9044
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
9045
+ });
8658
9046
  const adj = /* @__PURE__ */ new Map();
8659
9047
  const inDegree = /* @__PURE__ */ new Map();
8660
9048
  const resultMap = /* @__PURE__ */ new Map();
8661
- for (const a of assignments) {
9049
+ for (const a of sanitizedAssignments) {
8662
9050
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
8663
9051
  inDegree.set(a.subtaskId, 0);
8664
9052
  }
8665
- for (const a of assignments) {
8666
- const deps = a.dependsOn ?? [];
8667
- for (const dep of deps) {
9053
+ for (const a of sanitizedAssignments) {
9054
+ for (const dep of a.dependsOn ?? []) {
8668
9055
  adj.get(dep).add(a.subtaskId);
8669
9056
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
8670
9057
  }
8671
9058
  }
8672
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
8673
9059
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
8674
9060
  let wave = 0;
8675
9061
  let respawnBudget = 1;
@@ -8704,6 +9090,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8704
9090
  runnableIds.push(fallbackId);
8705
9091
  }
8706
9092
  wave++;
9093
+ for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
8707
9094
  this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
8708
9095
  this.sendStatusUpdate({
8709
9096
  progressPct: 20 + Math.min(wave * 10, 60),
@@ -8876,49 +9263,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8876
9263
  }
8877
9264
  return [...resultMap.values()];
8878
9265
  }
8879
- /**
8880
- * Detects cyclic dependencies using Kahn's algorithm and breaks them
8881
- * by removing back-edges. Returns a sanitized copy of assignments.
8882
- *
8883
- * A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
8884
- * meaning t3 will start without waiting for t1, preventing deadlock.
8885
- */
8886
- breakCycles(assignments, adj, inDegree) {
8887
- const degree = new Map(inDegree);
8888
- const queue = [];
8889
- const visited = /* @__PURE__ */ new Set();
8890
- for (const [id, d] of degree) {
8891
- if (d === 0) queue.push(id);
8892
- }
8893
- while (queue.length > 0) {
8894
- const id = queue.shift();
8895
- visited.add(id);
8896
- for (const dep of adj.get(id) ?? []) {
8897
- const newDeg = (degree.get(dep) ?? 1) - 1;
8898
- degree.set(dep, newDeg);
8899
- if (newDeg === 0) queue.push(dep);
8900
- }
8901
- }
8902
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
8903
- if (cycleNodes.length === 0) return assignments;
8904
- this.log(
8905
- `\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
8906
- );
8907
- return assignments.map((a) => {
8908
- if (!cycleNodes.includes(a.subtaskId)) return a;
8909
- const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
8910
- if (safeDeps.length !== (a.dependsOn ?? []).length) {
8911
- this.log(
8912
- ` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
8913
- );
8914
- for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
8915
- inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
8916
- adj.get(removed)?.delete(a.subtaskId);
8917
- }
8918
- }
8919
- return { ...a, dependsOn: safeDeps };
8920
- });
8921
- }
8922
9266
  async retryT3(assignment, taskId) {
8923
9267
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
8924
9268
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -9054,6 +9398,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9054
9398
  }
9055
9399
  };
9056
9400
 
9401
+ // src/core/orchestration/scheduler.ts
9402
+ var DependencyScheduler = class {
9403
+ constructor(graph, hooks, options = {}) {
9404
+ this.graph = graph;
9405
+ this.hooks = hooks;
9406
+ this.options = options;
9407
+ }
9408
+ graph;
9409
+ hooks;
9410
+ options;
9411
+ async run() {
9412
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
9413
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
9414
+ const dependents = /* @__PURE__ */ new Map();
9415
+ for (const node of nodes) dependents.set(node.id, []);
9416
+ for (const node of nodes) {
9417
+ for (const dependencyId of node.dependsOn) {
9418
+ dependents.get(dependencyId)?.push(node.id);
9419
+ }
9420
+ }
9421
+ const policy = this.options.onUpstreamFailure ?? "continue";
9422
+ const settled = /* @__PURE__ */ new Set();
9423
+ const results = /* @__PURE__ */ new Map();
9424
+ const blocked = /* @__PURE__ */ new Map();
9425
+ const waves = [];
9426
+ let wave = 0;
9427
+ const blockDescendants = async (failedIds) => {
9428
+ const rootsFor = /* @__PURE__ */ new Map();
9429
+ const queue = [];
9430
+ for (const rootId of failedIds) {
9431
+ for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
9432
+ }
9433
+ while (queue.length > 0) {
9434
+ const { id, root } = queue.shift();
9435
+ if (settled.has(id)) continue;
9436
+ const known = rootsFor.get(id);
9437
+ if (known) {
9438
+ if (!known.has(root)) known.add(root);
9439
+ continue;
9440
+ }
9441
+ rootsFor.set(id, /* @__PURE__ */ new Set([root]));
9442
+ for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
9443
+ }
9444
+ for (const node of nodes) {
9445
+ const roots = rootsFor.get(node.id);
9446
+ if (!roots || settled.has(node.id)) continue;
9447
+ const causes = nodes.filter((n) => roots.has(n.id));
9448
+ const blockedBy = causes.map((n) => n.id);
9449
+ const blockedByTitles = causes.map((n) => n.title || n.id);
9450
+ const record = {
9451
+ blockedBy,
9452
+ blockedByTitles,
9453
+ reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
9454
+ };
9455
+ blocked.set(node.id, record);
9456
+ settled.add(node.id);
9457
+ await this.hooks.onNodeBlocked?.(node, record);
9458
+ }
9459
+ };
9460
+ while (settled.size < nodes.length) {
9461
+ const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
9462
+ if (ready.length === 0) {
9463
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
9464
+ }
9465
+ waves.push(ready.map((node) => node.id));
9466
+ await this.hooks.onWaveStart?.(ready, wave);
9467
+ const outcomes = /* @__PURE__ */ new Map();
9468
+ const runOne = async (node) => {
9469
+ const result = await this.hooks.execute(node);
9470
+ results.set(node.id, result);
9471
+ settled.add(node.id);
9472
+ const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
9473
+ outcomes.set(node.id, outcome);
9474
+ if (outcome === "failed" && policy === "block") ; else {
9475
+ for (const dependentId of dependents.get(node.id) ?? []) {
9476
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9477
+ }
9478
+ }
9479
+ await this.hooks.onNodeComplete?.(node, result);
9480
+ };
9481
+ if ((this.options.mode ?? "parallel") === "sequential") {
9482
+ for (const node of ready) await runOne(node);
9483
+ } else {
9484
+ await Promise.all(ready.map(runOne));
9485
+ }
9486
+ const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
9487
+ if (policy === "block" && failedThisWave.length > 0) {
9488
+ await blockDescendants(failedThisWave);
9489
+ }
9490
+ wave++;
9491
+ }
9492
+ return { results, waves, blocked };
9493
+ }
9494
+ };
9495
+
9057
9496
  // src/utils/json-extract.ts
9058
9497
  function extractFirstJsonObject(input) {
9059
9498
  if (!input) return null;
@@ -9125,7 +9564,7 @@ function buildT1SystemPrompt(has) {
9125
9564
  Your responsibilities:
9126
9565
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
9127
9566
  2. Decompose the task into logical sections (one per T2 Manager)
9128
- 3. For each section, define 2-5 subtasks for T3 Workers
9567
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
9129
9568
  4. Return a structured plan as JSON
9130
9569
 
9131
9570
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -9151,6 +9590,9 @@ QUALITY RULES:
9151
9590
 
9152
9591
  ${generation}` : ""}`;
9153
9592
  }
9593
+ function producedOutput(result) {
9594
+ return result.status === "COMPLETED" || result.status === "PARTIAL";
9595
+ }
9154
9596
  var T1Administrator = class extends BaseTier {
9155
9597
  router;
9156
9598
  toolRegistry;
@@ -9285,7 +9727,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
9285
9727
  let allT2Results = await this.dispatchT2Managers(plan.sections);
9286
9728
  let pass = 1;
9287
9729
  const maxReplanPasses = this.config.maxReplanPasses ?? 2;
9288
- const okCount = (rs) => rs.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL").length;
9730
+ const okCount = (rs) => rs.filter(producedOutput).length;
9289
9731
  while (pass <= maxReplanPasses) {
9290
9732
  const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
9291
9733
  if (reviewResult.approved) {
@@ -9416,7 +9858,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
9416
9858
  /** Structured, grounded summary of what's already done — used to keep
9417
9859
  * corrective replan passes from re-emitting completed sections. */
9418
9860
  summarizeCompletedSections(results) {
9419
- const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
9861
+ const done = results.filter(producedOutput);
9420
9862
  if (done.length === 0) return "";
9421
9863
  const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
9422
9864
  return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
@@ -9540,7 +9982,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9540
9982
  }
9541
9983
  }
9542
9984
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
9543
- if (plan.sections.length < min) ;
9985
+ if (plan.sections.length < min || plan.sections.length > max) {
9986
+ this.log(
9987
+ `\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.`
9988
+ );
9989
+ }
9544
9990
  }
9545
9991
  async dispatchT2Managers(sections) {
9546
9992
  for (const section of sections) {
@@ -9557,6 +10003,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9557
10003
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
9558
10004
  if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
9559
10005
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
10006
+ manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
9560
10007
  if (this.store) {
9561
10008
  manager.setStore(this.store);
9562
10009
  }
@@ -9652,118 +10099,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9652
10099
  return t2Results;
9653
10100
  }
9654
10101
  /**
9655
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
10102
+ * Runs T2 managers respecting dependsOn declarations.
10103
+ *
10104
+ * Graph construction, validation and cycle repair now live in
10105
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
10106
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
10107
+ * ordered by one implementation rather than two that drifted.
10108
+ *
10109
+ * The hand-rolled Kahn pass this replaces treated every node it could not
10110
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
10111
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
10112
+ * so breaking a cycle here also cut the dependencies of innocent later
10113
+ * sections and started them immediately — before the sections they consume.
10114
+ * compileTaskGraph uses strongly connected components, so only real cycle
10115
+ * members are touched.
9656
10116
  */
9657
10117
  async runT2sWithDependencies(sections, managers, taskId) {
9658
- const adj = /* @__PURE__ */ new Map();
9659
- const inDegree = /* @__PURE__ */ new Map();
9660
- const resultMap = /* @__PURE__ */ new Map();
9661
- const allKeys = new Set(sections.map((s) => s.sectionId));
9662
- for (const s of sections) {
9663
- if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
9664
- inDegree.set(s.sectionId, 0);
9665
- s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
10118
+ const compiled = compileSectionGraph(sections);
10119
+ for (const issue of compiled.issues) {
10120
+ this.log(`\u26A0 Section graph: ${issue.message}`);
9666
10121
  }
9667
- for (const s of sections) {
9668
- for (const dep of s.dependsOn ?? []) {
9669
- adj.get(dep).add(s.sectionId);
9670
- inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
9671
- }
9672
- }
9673
- const queue = [];
9674
- const degree = new Map(inDegree);
9675
- for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
9676
- const visited = /* @__PURE__ */ new Set();
9677
- while (queue.length > 0) {
9678
- const u = queue.shift();
9679
- visited.add(u);
9680
- for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
9681
- const newDeg = (degree.get(v) ?? 1) - 1;
9682
- degree.set(v, newDeg);
9683
- if (newDeg === 0) queue.push(v);
9684
- }
9685
- }
9686
- const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
9687
- if (cycleNodes.length > 0) {
9688
- this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
9689
- for (const s of sections) {
9690
- if (cycleNodes.includes(s.sectionId)) {
9691
- const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
9692
- for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
9693
- inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
9694
- adj.get(removed)?.delete(s.sectionId);
9695
- }
9696
- s.dependsOn = safeDeps;
9697
- }
9698
- }
10122
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
10123
+ for (const section of sections) {
10124
+ const node = byId.get(section.sectionId);
10125
+ if (node) section.dependsOn = [...node.dependsOn];
9699
10126
  }
9700
- const totalSections = sections.length;
10127
+ const resultMap = /* @__PURE__ */ new Map();
10128
+ const totalSections = compiled.graph.nodes.length;
9701
10129
  let completedSections = 0;
9702
- const executeWave = async () => {
9703
- const readyIds = [];
9704
- for (const [id, deg] of inDegree.entries()) {
9705
- if (deg === 0 && !resultMap.has(id)) {
9706
- readyIds.push(id);
9707
- }
9708
- }
9709
- if (readyIds.length === 0) return;
9710
- const runOne = async (id) => {
9711
- resultMap.set(id, null);
9712
- const index = sections.findIndex((s) => s.sectionId === id);
9713
- const section = sections[index];
9714
- const manager = managers[index];
9715
- const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
9716
- this.sendStatusUpdate({
9717
- progressPct,
9718
- currentAction: `T2 working on: ${section.sectionTitle}`,
9719
- status: "IN_PROGRESS"
9720
- });
9721
- this.throwIfCancelled();
9722
- let result;
9723
- try {
9724
- result = await manager.execute(section, taskId, this.signal);
9725
- manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
9726
- if (result.status === "ESCALATED") {
9727
- this.escalations.push({
9728
- raisedBy: `T2_${section.sectionId}`,
10130
+ const scheduler = new DependencyScheduler(
10131
+ compiled.graph,
10132
+ {
10133
+ // The wave is the scheduler's own counter, and it is the only place the
10134
+ // parallelism is visible: nodes sharing a wave ran at the same time.
10135
+ // Stamped before the wave executes so the first status event a node
10136
+ // emits already carries it.
10137
+ onWaveStart: (nodes, wave) => {
10138
+ for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
10139
+ },
10140
+ execute: async (node) => {
10141
+ const section = node.payload;
10142
+ const manager = managers[node.ordinal];
10143
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
10144
+ this.sendStatusUpdate({
10145
+ progressPct,
10146
+ currentAction: `T2 working on: ${section.sectionTitle}`,
10147
+ status: "IN_PROGRESS"
10148
+ });
10149
+ this.throwIfCancelled();
10150
+ try {
10151
+ const result = await manager.execute(section, taskId, this.signal);
10152
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
10153
+ if (result.status === "ESCALATED") {
10154
+ this.escalations.push({
10155
+ raisedBy: `T2_${section.sectionId}`,
10156
+ sectionId: section.sectionId,
10157
+ attempted: result.issues,
10158
+ blocker: result.issues.join("; "),
10159
+ needs: "Human review required"
10160
+ });
10161
+ }
10162
+ return result;
10163
+ } catch (err) {
10164
+ return {
9729
10165
  sectionId: section.sectionId,
9730
- attempted: result.issues,
9731
- blocker: result.issues.join("; "),
9732
- needs: "Human review required"
9733
- });
10166
+ sectionTitle: section.sectionTitle,
10167
+ status: "FAILED",
10168
+ t3Results: [],
10169
+ sectionSummary: "",
10170
+ issues: [err instanceof Error ? err.message : String(err)]
10171
+ };
9734
10172
  }
9735
- } catch (err) {
9736
- result = {
10173
+ },
10174
+ // PARTIAL is deliberately not a failure: a degraded-but-real section
10175
+ // can still feed the one after it, and cancelling that work would cost
10176
+ // the user more than letting it try.
10177
+ // BLOCKED cannot reach here today — the scheduler settles blocked nodes
10178
+ // through onNodeBlocked and never executes them, so classify only ever
10179
+ // sees results that ran. It is listed anyway because the failure mode if
10180
+ // that ever changes is silent and expensive: a blocked section would
10181
+ // classify as 'succeeded' and release its own dependents, so work that
10182
+ // should have been skipped would run and be billed.
10183
+ classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
10184
+ onNodeBlocked: (node, blockedInfo) => {
10185
+ const section = node.payload;
10186
+ this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
10187
+ resultMap.set(node.id, {
9737
10188
  sectionId: section.sectionId,
9738
10189
  sectionTitle: section.sectionTitle,
9739
- status: "FAILED",
10190
+ // BLOCKED, not FAILED. This section was never attempted — saying it
10191
+ // failed reports a breakage that did not happen, and hides the one
10192
+ // that did (the upstream section named below).
10193
+ status: "BLOCKED",
9740
10194
  t3Results: [],
9741
10195
  sectionSummary: "",
9742
- issues: [err instanceof Error ? err.message : String(err)]
9743
- };
9744
- }
9745
- resultMap.set(id, result);
9746
- completedSections++;
9747
- for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
9748
- inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9749
- }
9750
- };
9751
- if (this.router.getT3ExecutionMode?.() === "sequential") {
9752
- for (const id of readyIds) {
9753
- await runOne(id);
10196
+ issues: [
10197
+ `${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
10198
+ ],
10199
+ blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
10200
+ });
10201
+ completedSections++;
10202
+ this.sendStatusUpdate({
10203
+ progressPct: 10 + Math.floor(completedSections / totalSections * 85),
10204
+ currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
10205
+ status: "IN_PROGRESS"
10206
+ });
10207
+ this.emit("tier:status", {
10208
+ tierId: section.sectionId,
10209
+ // A blocked section has no tier instance — it was never
10210
+ // constructed — so its runtime id and its graph id are the same
10211
+ // thing here. Emitting nodeId anyway keeps every node in the
10212
+ // stream addressable in the graph's id space, which is what
10213
+ // dependsOn edges point at.
10214
+ nodeId: section.sectionId,
10215
+ dependsOn: [...section.dependsOn ?? []],
10216
+ role: "T2",
10217
+ label: section.sectionTitle,
10218
+ status: "BLOCKED",
10219
+ blockedBy: [...blockedInfo.blockedByTitles],
10220
+ output: blockedInfo.reason
10221
+ });
10222
+ this.emit("section:complete", {
10223
+ id: node.id,
10224
+ title: section.sectionTitle,
10225
+ status: "BLOCKED",
10226
+ blockedBy: [...blockedInfo.blockedByTitles],
10227
+ output: ""
10228
+ });
10229
+ },
10230
+ onNodeComplete: (node, result) => {
10231
+ resultMap.set(node.id, result);
10232
+ completedSections++;
10233
+ this.emit("section:complete", {
10234
+ id: node.id,
10235
+ title: result.sectionTitle,
10236
+ status: result.status,
10237
+ output: result.sectionSummary
10238
+ });
9754
10239
  }
9755
- } else {
9756
- await Promise.all(readyIds.map(runOne));
9757
- }
9758
- if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
9759
- await executeWave();
10240
+ },
10241
+ {
10242
+ // Cross-section concurrency respects the same t3Execution mode as the
10243
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
10244
+ // serialized T3 workers within a single section while every section's
10245
+ // T2Manager (and its T3 workers) still ran in parallel here.
10246
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
10247
+ // A section that declared `dependsOn` said it needs that section's
10248
+ // output. Running it after the dependency failed produces a second
10249
+ // failure, billed, whose cause is the first one — so don't.
10250
+ onUpstreamFailure: "block"
9760
10251
  }
9761
- };
9762
- await executeWave();
10252
+ );
10253
+ await scheduler.run();
9763
10254
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
9764
10255
  }
9765
10256
  async compileFinalOutput(originalPrompt, plan, t2Results) {
9766
- const completedSections = t2Results.filter((r) => r.status !== "FAILED");
10257
+ const completedSections = t2Results.filter(producedOutput);
9767
10258
  if (!completedSections.length) {
9768
10259
  const allIssues = [
9769
10260
  ...t2Results.flatMap((r) => r.issues ?? []),
@@ -9783,7 +10274,6 @@ ${r.sectionSummary}
9783
10274
  Outputs:
9784
10275
  ${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");
9785
10276
  const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
9786
- t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
9787
10277
  const compilePrompt = `Compile a final, coherent response to the user's original request.
9788
10278
 
9789
10279
  Original request: ${originalPrompt}
@@ -10275,6 +10765,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
10275
10765
  };
10276
10766
  var DEFAULT_VIDEO_SECONDS = 5;
10277
10767
  var MAX_VIDEO_SECONDS = 8;
10768
+ var MIN_VIDEO_SECONDS = 4;
10278
10769
  var GenerateVideoTool = class extends BaseTool {
10279
10770
  constructor(deps) {
10280
10771
  super();
@@ -10291,7 +10782,7 @@ var GenerateVideoTool = class extends BaseTool {
10291
10782
  type: "object",
10292
10783
  properties: {
10293
10784
  prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
10294
- seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
10785
+ seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
10295
10786
  aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
10296
10787
  model: { type: "string", description: "Optional: force a specific video model." }
10297
10788
  },
@@ -10303,7 +10794,7 @@ var GenerateVideoTool = class extends BaseTool {
10303
10794
  const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
10304
10795
  if (!r.ok) return r.error;
10305
10796
  const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
10306
- const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
10797
+ const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
10307
10798
  const asset = await generateVideo(
10308
10799
  r.selected.capability,
10309
10800
  r.cfg,
@@ -10320,7 +10811,7 @@ var GenerateVideoTool = class extends BaseTool {
10320
10811
  const location = await this.deps.sink(asset);
10321
10812
  const unit = r.selected.cost;
10322
10813
  const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
10323
- const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
10814
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
10324
10815
  return `Video generated and saved to ${location}.
10325
10816
  Model: ${asset.modelId}.${estimate}${clamped}`;
10326
10817
  }
@@ -10389,16 +10880,16 @@ function resolveInWorkspace(workspaceRoot, input) {
10389
10880
  if (typeof input !== "string" || input.length === 0) {
10390
10881
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
10391
10882
  }
10392
- const root = path25.resolve(workspaceRoot);
10393
- const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
10394
- const rel = path25.relative(root, abs);
10395
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
10883
+ const root = path26.resolve(workspaceRoot);
10884
+ const abs = path26.isAbsolute(input) ? path26.resolve(input) : path26.resolve(root, input);
10885
+ const rel = path26.relative(root, abs);
10886
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26.isAbsolute(rel)) {
10396
10887
  throw new WorkspaceSandboxError(input, root);
10397
10888
  }
10398
10889
  try {
10399
- const real = fs24.realpathSync(abs);
10400
- const realRel = path25.relative(root, real);
10401
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
10890
+ const real = fs25.realpathSync(abs);
10891
+ const realRel = path26.relative(root, real);
10892
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26.isAbsolute(realRel))) {
10402
10893
  throw new WorkspaceSandboxError(input, root);
10403
10894
  }
10404
10895
  } catch (e) {
@@ -11056,7 +11547,7 @@ var GenerateDocumentTool = class extends BaseTool {
11056
11547
  }
11057
11548
  }
11058
11549
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
11059
- await fs5.mkdir(path25.dirname(absPath), { recursive: true });
11550
+ await fs5.mkdir(path26.dirname(absPath), { recursive: true });
11060
11551
  await fs5.writeFile(absPath, bytes);
11061
11552
  const charts = countCharts(content);
11062
11553
  return [
@@ -11451,7 +11942,7 @@ var FileWriteTool = class extends BaseTool {
11451
11942
  } catch {
11452
11943
  }
11453
11944
  }
11454
- await fs5.mkdir(path25.dirname(absPath), { recursive: true });
11945
+ await fs5.mkdir(path26.dirname(absPath), { recursive: true });
11455
11946
  await fs5.writeFile(absPath, content, "utf-8");
11456
11947
  return `Written ${content.length} characters to ${filePath}`;
11457
11948
  }
@@ -11949,7 +12440,7 @@ var ImageAnalyzeTool = class extends BaseTool {
11949
12440
  };
11950
12441
  async function fileToImageAttachment(filePath) {
11951
12442
  const data = await fs5.readFile(filePath);
11952
- const ext = path25.extname(filePath).toLowerCase();
12443
+ const ext = path26.extname(filePath).toLowerCase();
11953
12444
  const mimeMap = {
11954
12445
  ".jpg": "image/jpeg",
11955
12446
  ".jpeg": "image/jpeg",
@@ -11983,14 +12474,14 @@ var PDFCreateTool = class extends BaseTool {
11983
12474
  const filePath = input["path"];
11984
12475
  const content = input["content"];
11985
12476
  const title = input["title"];
11986
- const dir = path25.dirname(filePath);
11987
- if (!fs24.existsSync(dir)) {
11988
- fs24.mkdirSync(dir, { recursive: true });
12477
+ const dir = path26.dirname(filePath);
12478
+ if (!fs25.existsSync(dir)) {
12479
+ fs25.mkdirSync(dir, { recursive: true });
11989
12480
  }
11990
12481
  return new Promise((resolve2, reject) => {
11991
12482
  try {
11992
12483
  const doc = new PDFDocument({ margin: 50 });
11993
- const stream = fs24.createWriteStream(filePath);
12484
+ const stream = fs25.createWriteStream(filePath);
11994
12485
  doc.pipe(stream);
11995
12486
  if (title) {
11996
12487
  doc.info["Title"] = title;
@@ -12068,22 +12559,22 @@ var CodeInterpreterTool = class extends BaseTool {
12068
12559
  }
12069
12560
  cmdPrefix = NODE_CMD;
12070
12561
  }
12071
- const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
12072
- if (!fs24.existsSync(tmpDir)) {
12073
- fs24.mkdirSync(tmpDir, { recursive: true });
12562
+ const tmpDir = path26.join(this.workspaceRoot, ".cascade", "tmp");
12563
+ if (!fs25.existsSync(tmpDir)) {
12564
+ fs25.mkdirSync(tmpDir, { recursive: true });
12074
12565
  }
12075
12566
  const extension = language === "python" ? "py" : "js";
12076
12567
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
12077
- const filePath = path25.join(tmpDir, fileName);
12078
- fs24.writeFileSync(filePath, code, "utf-8");
12568
+ const filePath = path26.join(tmpDir, fileName);
12569
+ fs25.writeFileSync(filePath, code, "utf-8");
12079
12570
  const execArgs = [filePath, ...args];
12080
12571
  return new Promise((resolve2) => {
12081
12572
  const startMs = Date.now();
12082
12573
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
12083
12574
  const duration = Date.now() - startMs;
12084
12575
  try {
12085
- if (fs24.existsSync(filePath)) {
12086
- fs24.unlinkSync(filePath);
12576
+ if (fs25.existsSync(filePath)) {
12577
+ fs25.unlinkSync(filePath);
12087
12578
  }
12088
12579
  } catch (cleanupErr) {
12089
12580
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -12395,7 +12886,7 @@ var GlobTool = class extends BaseTool {
12395
12886
  };
12396
12887
  async execute(input, _options) {
12397
12888
  const pattern = input["pattern"];
12398
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12889
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12399
12890
  const matches = await glob(pattern, {
12400
12891
  cwd: searchPath,
12401
12892
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -12408,7 +12899,7 @@ var GlobTool = class extends BaseTool {
12408
12899
  const withMtime = await Promise.all(
12409
12900
  matches.map(async (rel) => {
12410
12901
  try {
12411
- const stat = await fs5.stat(path25.join(searchPath, rel));
12902
+ const stat = await fs5.stat(path26.join(searchPath, rel));
12412
12903
  return { rel, mtime: stat.mtimeMs };
12413
12904
  } catch {
12414
12905
  return { rel, mtime: 0 };
@@ -12457,7 +12948,7 @@ var GrepTool = class extends BaseTool {
12457
12948
  };
12458
12949
  async execute(input, _options) {
12459
12950
  const pattern = input["pattern"];
12460
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12951
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12461
12952
  const globPattern = input["glob"];
12462
12953
  const outputMode = input["output_mode"] ?? "content";
12463
12954
  const context = input["context"] ?? 0;
@@ -12511,12 +13002,12 @@ var GrepTool = class extends BaseTool {
12511
13002
  nodir: true
12512
13003
  });
12513
13004
  } catch {
12514
- files = [path25.relative(searchPath, searchPath) || "."];
13005
+ files = [path26.relative(searchPath, searchPath) || "."];
12515
13006
  }
12516
13007
  const results = [];
12517
13008
  let totalCount = 0;
12518
13009
  for (const rel of files) {
12519
- const abs = path25.join(searchPath, rel);
13010
+ const abs = path26.join(searchPath, rel);
12520
13011
  let content;
12521
13012
  try {
12522
13013
  content = await fs5.readFile(abs, "utf-8");
@@ -12903,10 +13394,10 @@ var ToolRegistry = class extends EventEmitter {
12903
13394
  }
12904
13395
  isIgnored(filePath) {
12905
13396
  if (!filePath) return false;
12906
- const abs = path25.resolve(this.workspaceRoot, filePath);
12907
- const rel = path25.relative(this.workspaceRoot, abs);
12908
- if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
12909
- const posixRel = rel.split(path25.sep).join("/");
13397
+ const abs = path26.resolve(this.workspaceRoot, filePath);
13398
+ const rel = path26.relative(this.workspaceRoot, abs);
13399
+ if (!rel || rel.startsWith("..") || path26.isAbsolute(rel)) return true;
13400
+ const posixRel = rel.split(path26.sep).join("/");
12910
13401
  return this.ignoreMatcher.ignores(posixRel);
12911
13402
  }
12912
13403
  };
@@ -13140,22 +13631,22 @@ var FileMcpOAuthStore = class {
13140
13631
  filePath;
13141
13632
  load() {
13142
13633
  try {
13143
- return JSON.parse(fs24.readFileSync(this.filePath, "utf-8"));
13634
+ return JSON.parse(fs25.readFileSync(this.filePath, "utf-8"));
13144
13635
  } catch {
13145
13636
  return void 0;
13146
13637
  }
13147
13638
  }
13148
13639
  save(state) {
13149
- fs24.mkdirSync(path25.dirname(this.filePath), { recursive: true, mode: 448 });
13150
- fs24.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13640
+ fs25.mkdirSync(path26.dirname(this.filePath), { recursive: true, mode: 448 });
13641
+ fs25.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13151
13642
  try {
13152
- fs24.chmodSync(this.filePath, 384);
13643
+ fs25.chmodSync(this.filePath, 384);
13153
13644
  } catch {
13154
13645
  }
13155
13646
  }
13156
13647
  clear() {
13157
13648
  try {
13158
- fs24.rmSync(this.filePath);
13649
+ fs25.rmSync(this.filePath);
13159
13650
  } catch {
13160
13651
  }
13161
13652
  }
@@ -14052,6 +14543,14 @@ function heuristicAnalyze(prompt) {
14052
14543
  const estimatedTokens = wordCount * 5;
14053
14544
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
14054
14545
  }
14546
+ function hashPrompt(prompt) {
14547
+ let hash = 2166136261;
14548
+ for (let index = 0; index < prompt.length; index++) {
14549
+ hash ^= prompt.charCodeAt(index);
14550
+ hash = Math.imul(hash, 16777619) >>> 0;
14551
+ }
14552
+ return `${prompt.length}:${hash.toString(36)}`;
14553
+ }
14055
14554
  var analysisCache = /* @__PURE__ */ new Map();
14056
14555
  var TASK_TYPE_TAGS = {
14057
14556
  code: ["code", "instruction"],
@@ -14066,7 +14565,24 @@ var TaskAnalyzer = class {
14066
14565
  feedback;
14067
14566
  bias;
14068
14567
  lastProfile = null;
14069
- lastSelectedModels = /* @__PURE__ */ new Map();
14568
+ /** Models chosen by the run currently in flight. Cleared when it completes. */
14569
+ currentRunSelections = /* @__PURE__ */ new Map();
14570
+ /**
14571
+ * Immutable snapshot of the last COMPLETED run: its selections AND the task
14572
+ * type they were chosen for.
14573
+ *
14574
+ * The task type has to travel WITH the selections. `lastProfile` is overwritten
14575
+ * by the next analyze(), so if run B started before the user rated run A, A's
14576
+ * models were credited under B's task type — teaching the router that a coding
14577
+ * model is good at creative writing, from a rating that never said so.
14578
+ *
14579
+ * Explicit ratings arrive after the run finishes — rateLastRun() is by nature
14580
+ * a reaction to a finished answer — but the run finalizer calls
14581
+ * recordRunOutcome() first, and that used to clear the only map there was. So
14582
+ * every explicit rating iterated an empty map, recorded nothing, and returned
14583
+ * false: the 3x-weighted user signal never reached the tracker at all.
14584
+ */
14585
+ lastCompletedRun;
14070
14586
  constructor(tracker, bias = "balanced") {
14071
14587
  this.tracker = tracker;
14072
14588
  this.bias = bias;
@@ -14095,7 +14611,7 @@ var TaskAnalyzer = class {
14095
14611
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
14096
14612
  */
14097
14613
  async analyze(prompt) {
14098
- const cacheKey = prompt.slice(0, 200);
14614
+ const cacheKey = hashPrompt(prompt);
14099
14615
  const cached = analysisCache.get(cacheKey);
14100
14616
  if (cached) {
14101
14617
  this.lastProfile = cached;
@@ -14113,11 +14629,15 @@ var TaskAnalyzer = class {
14113
14629
  */
14114
14630
  async selectModel(prompt, tier, selector, opts) {
14115
14631
  const profile = await this.analyze(prompt);
14632
+ const recordSelection = (model) => {
14633
+ if (model) this.currentRunSelections.set(tier, model);
14634
+ return model;
14635
+ };
14116
14636
  if (profile.requiresVision) {
14117
- return selector.selectVisionModel();
14637
+ return recordSelection(selector.selectVisionModel());
14118
14638
  }
14119
14639
  let candidates = selector.getCandidatesForTier(tier);
14120
- if (candidates.length === 0) return selector.selectForTier(tier);
14640
+ if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
14121
14641
  if (opts?.requiresToolUse) {
14122
14642
  const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
14123
14643
  if (toolCapable.length > 0) candidates = toolCapable;
@@ -14127,9 +14647,7 @@ var TaskAnalyzer = class {
14127
14647
  score: this.scoreModel(m, profile)
14128
14648
  }));
14129
14649
  scored.sort((a, b) => b.score - a.score);
14130
- const best = scored[0]?.model ?? selector.selectForTier(tier);
14131
- if (best) this.lastSelectedModels.set(tier, best);
14132
- return best;
14650
+ return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
14133
14651
  }
14134
14652
  /**
14135
14653
  * Record the outcome of a completed run across all tiers that were selected
@@ -14138,25 +14656,38 @@ var TaskAnalyzer = class {
14138
14656
  recordRunOutcome(outcome, costByTier, contextTokens = 0) {
14139
14657
  if (!this.tracker || !this.lastProfile) return;
14140
14658
  const taskType = this.lastProfile.type;
14141
- for (const [tier, model] of this.lastSelectedModels) {
14659
+ for (const [tier, model] of this.currentRunSelections) {
14142
14660
  const cost = costByTier[tier] ?? 0;
14143
14661
  this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
14144
14662
  }
14145
- this.lastSelectedModels.clear();
14663
+ this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
14664
+ this.currentRunSelections.clear();
14146
14665
  void this.tracker.save();
14147
14666
  }
14148
14667
  /**
14149
- * Record an explicit user rating (good/bad) for the last run's selected models.
14668
+ * Record an explicit user rating (good/bad) for the last COMPLETED run's models.
14150
14669
  * Explicit ratings carry 3× the weight of auto-detected outcomes.
14151
- * Does NOT clear lastSelectedModels — the auto record already did that.
14670
+ *
14671
+ * Reads the completed-run snapshot, not the in-flight map: by the time a user
14672
+ * can rate an answer, the finalizer has already run and the in-flight map is
14673
+ * empty.
14674
+ *
14675
+ * The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
14676
+ * three samples, so a double submit would inject six — a stutter on the
14677
+ * button would count as two opinions. Consuming makes a second call a no-op
14678
+ * that reports false, which is what "rate this run" should mean.
14152
14679
  */
14153
14680
  recordExplicitRating(rating) {
14154
- if (!this.tracker || !this.lastProfile) return false;
14155
- const taskType = this.lastProfile.type;
14156
- for (const [, model] of this.lastSelectedModels) {
14157
- this.tracker.recordExplicit(model.id, taskType, rating, 0);
14158
- }
14159
- return this.lastSelectedModels.size > 0;
14681
+ const snapshot = this.lastCompletedRun;
14682
+ if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
14683
+ const ratedModels = /* @__PURE__ */ new Set();
14684
+ for (const [, model] of snapshot.selections) {
14685
+ if (ratedModels.has(model.id)) continue;
14686
+ ratedModels.add(model.id);
14687
+ this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
14688
+ }
14689
+ this.lastCompletedRun = void 0;
14690
+ return true;
14160
14691
  }
14161
14692
  scoreModel(model, profile) {
14162
14693
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
@@ -14194,7 +14725,7 @@ var TaskAnalyzer = class {
14194
14725
  analysisCache.clear();
14195
14726
  }
14196
14727
  };
14197
- var DEFAULT_STATS_FILE = path25.join(os6.homedir(), ".cascade", "model-perf.json");
14728
+ var DEFAULT_STATS_FILE = path26.join(os6.homedir(), ".cascade", "model-perf.json");
14198
14729
  var ModelPerformanceTracker = class {
14199
14730
  stats = /* @__PURE__ */ new Map();
14200
14731
  featureStats = /* @__PURE__ */ new Map();
@@ -14234,7 +14765,7 @@ var ModelPerformanceTracker = class {
14234
14765
  async save() {
14235
14766
  if (this.readOnly) return;
14236
14767
  try {
14237
- await fs5.mkdir(path25.dirname(this.statsFile), { recursive: true });
14768
+ await fs5.mkdir(path26.dirname(this.statsFile), { recursive: true });
14238
14769
  const modelsObj = {};
14239
14770
  const featuresObj = {};
14240
14771
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -14782,7 +15313,7 @@ Required capability: ${description.slice(0, 300)}`;
14782
15313
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
14783
15314
  async loadPersistedTools() {
14784
15315
  if (!this.workspacePath || !this.persistEnabled) return;
14785
- const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
15316
+ const file = path26.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
14786
15317
  try {
14787
15318
  const raw = await fs5.readFile(file, "utf-8");
14788
15319
  const specs = JSON.parse(raw);
@@ -14806,8 +15337,8 @@ Required capability: ${description.slice(0, 300)}`;
14806
15337
  }
14807
15338
  async persist() {
14808
15339
  if (!this.workspacePath || !this.persistEnabled) return;
14809
- const dir = path25.join(this.workspacePath, ".cascade");
14810
- const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
15340
+ const dir = path26.join(this.workspacePath, ".cascade");
15341
+ const file = path26.join(dir, DYNAMIC_TOOLS_FILE);
14811
15342
  try {
14812
15343
  await fs5.mkdir(dir, { recursive: true });
14813
15344
  await fs5.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -14827,12 +15358,12 @@ var WorldStateDB = class {
14827
15358
  constructor(workspacePath, debugMode = false) {
14828
15359
  this.workspacePath = workspacePath;
14829
15360
  this.debugMode = debugMode;
14830
- const cascadeDir = path25.join(workspacePath, ".cascade");
14831
- if (!fs24.existsSync(cascadeDir)) {
14832
- fs24.mkdirSync(cascadeDir, { recursive: true });
15361
+ const cascadeDir = path26.join(workspacePath, ".cascade");
15362
+ if (!fs25.existsSync(cascadeDir)) {
15363
+ fs25.mkdirSync(cascadeDir, { recursive: true });
14833
15364
  }
14834
- this.keyPath = path25.join(cascadeDir, "world_state.key");
14835
- this.dbPath = path25.join(cascadeDir, "world_state.db");
15365
+ this.keyPath = path26.join(cascadeDir, "world_state.key");
15366
+ this.dbPath = path26.join(cascadeDir, "world_state.db");
14836
15367
  this.initEncryptionKey();
14837
15368
  this.db = new Database(this.dbPath);
14838
15369
  this.db.pragma("journal_mode = WAL");
@@ -14889,11 +15420,11 @@ var WorldStateDB = class {
14889
15420
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
14890
15421
  }
14891
15422
  initEncryptionKey() {
14892
- if (fs24.existsSync(this.keyPath)) {
14893
- this.encryptionKey = fs24.readFileSync(this.keyPath);
15423
+ if (fs25.existsSync(this.keyPath)) {
15424
+ this.encryptionKey = fs25.readFileSync(this.keyPath);
14894
15425
  } else {
14895
15426
  this.encryptionKey = crypto4.randomBytes(32);
14896
- fs24.writeFileSync(this.keyPath, this.encryptionKey);
15427
+ fs25.writeFileSync(this.keyPath, this.encryptionKey);
14897
15428
  }
14898
15429
  }
14899
15430
  encrypt(text) {
@@ -15179,9 +15710,9 @@ var WorldStateDB = class {
15179
15710
  dumpDebugIfNeeded() {
15180
15711
  if (!this.debugMode) return;
15181
15712
  try {
15182
- const dumpPath = path25.join(os6.tmpdir(), "cascade_world_state_debug.json");
15713
+ const dumpPath = path26.join(os6.tmpdir(), "cascade_world_state_debug.json");
15183
15714
  const entries = this.getAllEntries();
15184
- fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15715
+ fs25.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15185
15716
  } catch (err) {
15186
15717
  console.error("Failed to dump debug world state", err);
15187
15718
  }
@@ -15190,6 +15721,212 @@ var WorldStateDB = class {
15190
15721
  this.db.close();
15191
15722
  }
15192
15723
  };
15724
+ var RESUME_SCHEMA_VERSION = 1;
15725
+ var DEFAULT_MAX_CHECKPOINTS = 5;
15726
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
15727
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
15728
+ var CLAIM_SUFFIX = ".claimed";
15729
+ var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
15730
+ function safeId(taskId) {
15731
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
15732
+ return cleaned.slice(0, 100) || "run";
15733
+ }
15734
+ function isCheckpoint(value) {
15735
+ if (typeof value !== "object" || value === null) return false;
15736
+ const c = value;
15737
+ 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);
15738
+ }
15739
+ var ResumeStore = class {
15740
+ dir;
15741
+ maxCheckpoints;
15742
+ maxAgeMs;
15743
+ now;
15744
+ claimTimeoutMs;
15745
+ constructor(options) {
15746
+ this.dir = options.dir;
15747
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
15748
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
15749
+ this.now = options.now ?? Date.now;
15750
+ this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
15751
+ }
15752
+ /**
15753
+ * Persist a checkpoint and prune.
15754
+ *
15755
+ * Never throws — failing to save a resume file must not turn a recoverable
15756
+ * stop into a crash — but DOES report whether the atomic rename landed.
15757
+ * Callers decide whether an original claim is safe to discard based on that
15758
+ * answer, so "I swallowed the error" and "the checkpoint is on disk" must not
15759
+ * look alike to them.
15760
+ *
15761
+ * @returns true only when the checkpoint is durably on disk.
15762
+ */
15763
+ async save(checkpoint) {
15764
+ try {
15765
+ const timestamp = this.now();
15766
+ const record = {
15767
+ ...checkpoint,
15768
+ version: RESUME_SCHEMA_VERSION,
15769
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
15770
+ };
15771
+ await promises.mkdir(this.dir, { recursive: true });
15772
+ const finalPath = path26.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
15773
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
15774
+ await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
15775
+ await promises.rename(tempPath, finalPath);
15776
+ await this.prune();
15777
+ return true;
15778
+ } catch {
15779
+ return false;
15780
+ }
15781
+ }
15782
+ /** Valid, unexpired checkpoints, newest first. */
15783
+ async list() {
15784
+ let entries;
15785
+ try {
15786
+ entries = await promises.readdir(this.dir);
15787
+ } catch {
15788
+ return [];
15789
+ }
15790
+ const cutoff = this.now() - this.maxAgeMs;
15791
+ 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);
15792
+ const checkpoints = [];
15793
+ for (const candidate of candidates) {
15794
+ try {
15795
+ const raw = await promises.readFile(path26.join(this.dir, candidate.name), "utf-8");
15796
+ const parsed = JSON.parse(raw);
15797
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
15798
+ } catch {
15799
+ continue;
15800
+ }
15801
+ }
15802
+ return checkpoints;
15803
+ }
15804
+ /** The most recent resumable run, or null. */
15805
+ async latest() {
15806
+ return (await this.list())[0] ?? null;
15807
+ }
15808
+ /**
15809
+ * Claim a checkpoint for a resume attempt.
15810
+ *
15811
+ * This used to DELETE the file and return it, which opened a window with no
15812
+ * recovery record at all: between preparing the continuation and the resumed
15813
+ * run writing its own checkpoint, a crash lost the only copy — and a crash is
15814
+ * precisely what this store exists to survive. Deleting on read optimised for
15815
+ * "never resume twice" at the cost of "never lose the work", which is the
15816
+ * wrong trade for a recovery mechanism.
15817
+ *
15818
+ * Instead the file is RENAMED to `.claimed`, which still hides it from
15819
+ * `list()` (so a concurrent resume cannot take the same work) but keeps it on
15820
+ * disk. The caller then either `release`s it — restoring it for another
15821
+ * attempt — or `settle`s it once the resumed run is safely under way. A claim
15822
+ * abandoned by a dead process is reclaimed after `claimTimeoutMs`.
15823
+ */
15824
+ async claim(taskId) {
15825
+ const all = await this.list();
15826
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
15827
+ if (!target) return null;
15828
+ try {
15829
+ const entries = await promises.readdir(this.dir);
15830
+ const wanted = safeId(target.taskId);
15831
+ const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
15832
+ if (!source) return null;
15833
+ const claimId = `${this.now()}-${safeId(target.taskId)}`;
15834
+ await promises.rename(path26.join(this.dir, source), path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
15835
+ return { checkpoint: target, claimId };
15836
+ } catch {
15837
+ return null;
15838
+ }
15839
+ }
15840
+ /** Give a claim back, so the work stays recoverable after a failed start. */
15841
+ async release(claimId) {
15842
+ try {
15843
+ await promises.rename(
15844
+ path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
15845
+ path26.join(this.dir, `${claimId}.json`)
15846
+ );
15847
+ } catch {
15848
+ }
15849
+ }
15850
+ /** Discard a claim for good — the resumed run has taken ownership. */
15851
+ async settle(claimId) {
15852
+ try {
15853
+ await promises.rm(path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
15854
+ } catch {
15855
+ }
15856
+ }
15857
+ /**
15858
+ * Claim-and-discard in one step. Kept for callers that genuinely want the old
15859
+ * semantics; prefer claim/settle so a crash mid-resume stays recoverable.
15860
+ */
15861
+ async consume(taskId) {
15862
+ const claimed = await this.claim(taskId);
15863
+ if (!claimed) return null;
15864
+ await this.settle(claimed.claimId);
15865
+ return claimed.checkpoint;
15866
+ }
15867
+ /**
15868
+ * Return claims abandoned by a process that died mid-resume. Without this a
15869
+ * crash between claim and settle would strand the checkpoint invisibly —
15870
+ * trading one loss window for another.
15871
+ */
15872
+ async reclaimStale() {
15873
+ let reclaimed = 0;
15874
+ try {
15875
+ const entries = await promises.readdir(this.dir);
15876
+ const cutoff = this.now() - this.claimTimeoutMs;
15877
+ for (const name of entries) {
15878
+ if (!name.endsWith(CLAIM_SUFFIX)) continue;
15879
+ const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
15880
+ if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
15881
+ await promises.rename(
15882
+ path26.join(this.dir, name),
15883
+ path26.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
15884
+ );
15885
+ reclaimed++;
15886
+ }
15887
+ } catch {
15888
+ }
15889
+ return reclaimed;
15890
+ }
15891
+ /** Remove every checkpoint for a task id. */
15892
+ async delete(taskId) {
15893
+ try {
15894
+ const entries = await promises.readdir(this.dir);
15895
+ const wanted = safeId(taskId);
15896
+ await Promise.all(entries.map(async (name) => {
15897
+ const match = FILE_PATTERN.exec(name);
15898
+ if (match?.[2] === wanted) {
15899
+ await promises.rm(path26.join(this.dir, name), { force: true });
15900
+ }
15901
+ }));
15902
+ } catch {
15903
+ }
15904
+ }
15905
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
15906
+ async prune() {
15907
+ try {
15908
+ const entries = await promises.readdir(this.dir);
15909
+ const cutoff = this.now() - this.maxAgeMs;
15910
+ 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);
15911
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
15912
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
15913
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
15914
+ (name) => promises.rm(path26.join(this.dir, name), { force: true })
15915
+ ));
15916
+ } catch {
15917
+ }
15918
+ }
15919
+ };
15920
+ function summarizeCompleted(completed, perNodeChars = 500) {
15921
+ if (!completed.length) return "";
15922
+ const lines = completed.map((node) => {
15923
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
15924
+ return `- [${node.status}] ${node.title}
15925
+ ${body.replace(/\n/g, "\n ")}`;
15926
+ });
15927
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
15928
+ ${lines.join("\n")}`;
15929
+ }
15193
15930
  var ignore2 = ignoreFactory.default ?? ignoreFactory;
15194
15931
  var PrivacyPaths = class {
15195
15932
  localOnly;
@@ -15236,7 +15973,7 @@ var CascadeIgnore = class {
15236
15973
  ]);
15237
15974
  }
15238
15975
  async load(workspacePath) {
15239
- const filePath = path25.join(workspacePath, ".cascadeignore");
15976
+ const filePath = path26.join(workspacePath, ".cascadeignore");
15240
15977
  try {
15241
15978
  const content = await fs5.readFile(filePath, "utf-8");
15242
15979
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -15247,7 +15984,7 @@ var CascadeIgnore = class {
15247
15984
  }
15248
15985
  isIgnored(filePath, workspacePath) {
15249
15986
  try {
15250
- const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
15987
+ const relative = workspacePath ? path26.relative(workspacePath, filePath) : filePath;
15251
15988
  return this.ig.ignores(relative);
15252
15989
  } catch {
15253
15990
  return false;
@@ -15590,7 +16327,7 @@ var WorkspaceIndex = class {
15590
16327
  isIgnored;
15591
16328
  chunker;
15592
16329
  constructor(opts) {
15593
- this.root = path25.resolve(opts.root);
16330
+ this.root = path26.resolve(opts.root);
15594
16331
  this.db = opts.db;
15595
16332
  this.namespace = opts.namespace ?? "code";
15596
16333
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -15622,14 +16359,14 @@ var WorkspaceIndex = class {
15622
16359
  return;
15623
16360
  }
15624
16361
  for (const entry of entries) {
15625
- const abs = path25.join(dir, entry.name);
16362
+ const abs = path26.join(dir, entry.name);
15626
16363
  if (entry.isDirectory()) {
15627
16364
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
15628
16365
  await walk(abs);
15629
16366
  } else if (entry.isFile()) {
15630
- const ext = path25.extname(entry.name).slice(1).toLowerCase();
16367
+ const ext = path26.extname(entry.name).slice(1).toLowerCase();
15631
16368
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
15632
- out.push(path25.relative(this.root, abs));
16369
+ out.push(path26.relative(this.root, abs));
15633
16370
  }
15634
16371
  }
15635
16372
  };
@@ -15659,7 +16396,7 @@ var WorkspaceIndex = class {
15659
16396
  const entries = [];
15660
16397
  for (const rel of rels) {
15661
16398
  try {
15662
- const abs = path25.join(this.root, rel);
16399
+ const abs = path26.join(this.root, rel);
15663
16400
  const stat = await fs5.stat(abs);
15664
16401
  if (stat.size > this.maxFileBytes) continue;
15665
16402
  const buf = await fs5.readFile(abs);
@@ -16197,8 +16934,35 @@ var Cascade = class _Cascade extends EventEmitter {
16197
16934
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
16198
16935
  decisionLog = [];
16199
16936
  initialized = false;
16200
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
16937
+ /**
16938
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
16939
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
16940
+ * so a crash or an exit no longer throws away everything already finished.
16941
+ */
16201
16942
  lastInterruptedRun;
16943
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
16944
+ resumeStore;
16945
+ /** Claim held by an in-flight resume, settled once the run owns the work. */
16946
+ claimedResumeId;
16947
+ /**
16948
+ * Facts inherited from the checkpoint this run is resuming, so its own
16949
+ * checkpoint is cumulative rather than only covering this attempt.
16950
+ */
16951
+ resumedFrom;
16952
+ /** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
16953
+ wroteCheckpointThisRun = false;
16954
+ /**
16955
+ * Whether the run reached the end of its normal path.
16956
+ *
16957
+ * NOT the same as `runError == null`: the cancellation and budget handlers
16958
+ * both null `runError` deliberately (an intentional stop is not a telemetry
16959
+ * error) and the breaker path never sets it at all. Treating that as success
16960
+ * meant an immediate cancel — no output, no replacement checkpoint — deleted
16961
+ * the original claim and the finished sections with it.
16962
+ */
16963
+ completedNormally = false;
16964
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
16965
+ currentRunCompleted = [];
16202
16966
  initPromise;
16203
16967
  store;
16204
16968
  audit;
@@ -16231,10 +16995,16 @@ var Cascade = class _Cascade extends EventEmitter {
16231
16995
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16232
16996
  try {
16233
16997
  this.router.setDeadModelStore(new DeadModelStore(
16234
- fileDeadModelPersistence(path25.join(workspacePath, ".cascade", "dead-models.json"))
16998
+ fileDeadModelPersistence(path26.join(workspacePath, ".cascade", "dead-models.json"))
16235
16999
  ));
16236
17000
  } catch {
16237
17001
  }
17002
+ try {
17003
+ this.resumeStore = new ResumeStore({
17004
+ dir: path26.join(workspacePath, ".cascade", "resume")
17005
+ });
17006
+ } catch {
17007
+ }
16238
17008
  this.multimodal = new MultimodalRegistry(
16239
17009
  (this.config.providers ?? []).map((p) => p.type)
16240
17010
  );
@@ -16319,8 +17089,8 @@ var Cascade = class _Cascade extends EventEmitter {
16319
17089
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
16320
17090
  return;
16321
17091
  }
16322
- const dbPath = ci.dbPath || path25.join(this.workspacePath, ".cascade", "code-index.db");
16323
- fs24.mkdirSync(path25.dirname(dbPath), { recursive: true });
17092
+ const dbPath = ci.dbPath || path26.join(this.workspacePath, ".cascade", "code-index.db");
17093
+ fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
16324
17094
  const db = new Database(dbPath);
16325
17095
  db.pragma("journal_mode = WAL");
16326
17096
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -16700,10 +17470,57 @@ ${input}` }],
16700
17470
  setFeedbackSource(source) {
16701
17471
  this.router.setFeedbackSource(source);
16702
17472
  }
16703
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
17473
+ /**
17474
+ * Write a durable checkpoint for an interrupted run.
17475
+ *
17476
+ * Best-effort throughout: a run is already ending badly on every path that
17477
+ * calls this, and failing to record a resume file must not make that ending
17478
+ * worse. Nothing here is allowed to throw.
17479
+ */
17480
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
17481
+ const inherited = this.resumedFrom?.completed ?? [];
17482
+ const merged = /* @__PURE__ */ new Map();
17483
+ for (const node of inherited) merged.set(node.id, node);
17484
+ for (const node of this.currentRunCompleted) {
17485
+ const existing = merged.get(node.id);
17486
+ if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
17487
+ merged.set(node.id, node);
17488
+ }
17489
+ const completed = [...merged.values()];
17490
+ const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
17491
+ const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
17492
+ if (!completed.length && !carriedPartial) return false;
17493
+ try {
17494
+ const saved = await this.resumeStore?.save({
17495
+ taskId,
17496
+ prompt: canonicalPrompt,
17497
+ reason,
17498
+ ...detail ? { detail } : {},
17499
+ partialOutput: carriedPartial,
17500
+ completed
17501
+ });
17502
+ this.wroteCheckpointThisRun = saved === true;
17503
+ return saved === true;
17504
+ } catch {
17505
+ return false;
17506
+ }
17507
+ }
17508
+ /** True when a task can be resumed via /continue (this process or a previous one). */
16704
17509
  hasResumableRun() {
16705
17510
  return this.lastInterruptedRun != null;
16706
17511
  }
17512
+ /**
17513
+ * True when a checkpoint from ANY process is resumable. Async because the
17514
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
17515
+ * answer for callers already on a hot path.
17516
+ */
17517
+ async hasDurableResume() {
17518
+ return await this.resumeStore?.latest() != null;
17519
+ }
17520
+ /** Interrupted runs available to resume, newest first. */
17521
+ async listResumableRuns() {
17522
+ return await this.resumeStore?.list() ?? [];
17523
+ }
16707
17524
  /**
16708
17525
  * Raise the per-run token budget for a resume and return the continuation
16709
17526
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -16714,24 +17531,105 @@ ${input}` }],
16714
17531
  const last = this.lastInterruptedRun;
16715
17532
  if (!last) return null;
16716
17533
  this.lastInterruptedRun = void 0;
16717
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
17534
+ this.raiseBudgetForResume(opts.maxTokens);
17535
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
17536
+ }
17537
+ /**
17538
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
17539
+ * process, including one that crashed before it could return. Consumes the
17540
+ * checkpoint, so the same finished work is never restored into two runs.
17541
+ */
17542
+ async prepareDurableResume(opts = {}) {
17543
+ await this.resumeStore?.reclaimStale();
17544
+ const claimed = await this.resumeStore?.claim(opts.taskId);
17545
+ if (!claimed) return null;
17546
+ try {
17547
+ this.lastInterruptedRun = void 0;
17548
+ this.raiseBudgetForResume(opts.maxTokens);
17549
+ const prompt = this.buildResumePrompt(
17550
+ claimed.checkpoint.prompt,
17551
+ claimed.checkpoint.partialOutput,
17552
+ claimed.checkpoint.completed
17553
+ );
17554
+ this.resumedFrom = {
17555
+ prompt: claimed.checkpoint.prompt,
17556
+ completed: claimed.checkpoint.completed,
17557
+ partialOutput: claimed.checkpoint.partialOutput
17558
+ };
17559
+ this.claimedResumeId = claimed.claimId;
17560
+ return prompt;
17561
+ } catch (err) {
17562
+ await this.resumeStore?.release(claimed.claimId);
17563
+ throw err;
17564
+ }
17565
+ }
17566
+ /**
17567
+ * Discard the claimed checkpoint once the resumed run has taken ownership of
17568
+ * the work (it has produced its own checkpoint, or finished). Called by the
17569
+ * run loop; safe to call when nothing is claimed.
17570
+ */
17571
+ async settleClaimedResume(succeeded) {
17572
+ const claimId = this.claimedResumeId;
17573
+ if (!claimId) return;
17574
+ this.claimedResumeId = void 0;
17575
+ if (succeeded || this.wroteCheckpointThisRun) {
17576
+ await this.resumeStore?.settle(claimId);
17577
+ } else {
17578
+ await this.resumeStore?.release(claimId);
17579
+ }
17580
+ }
17581
+ /**
17582
+ * Hand back a claim taken by prepareDurableResume() when the caller never
17583
+ * gets as far as run(). The REPL prepares the continuation and submits it
17584
+ * separately, so a failure in between would otherwise strand the checkpoint
17585
+ * for the whole lease timeout before reclaimStale() could recover it.
17586
+ */
17587
+ async releaseClaimedResume() {
17588
+ const claimId = this.claimedResumeId;
17589
+ if (!claimId) return;
17590
+ this.claimedResumeId = void 0;
17591
+ await this.resumeStore?.release(claimId);
17592
+ }
17593
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
17594
+ raiseBudgetForResume(maxTokens) {
17595
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
16718
17596
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
16719
17597
  this.router.setMaxTokensPerRun(raised);
16720
- 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.
16721
-
16722
- Original task: ${last.prompt}` + (last.partialOutput ? `
16723
-
17598
+ }
17599
+ /**
17600
+ * The resume prompt. Completed sections are named explicitly rather than
17601
+ * gestured at: "do not recreate the files" is an instruction a planner can
17602
+ * only guess at, whereas a list of what is already done, and what it produced,
17603
+ * is something it can act on. That difference is the entire reason node
17604
+ * results are kept — a re-plan that cannot see the finished work will plan the
17605
+ * finished work again, and the user pays for it a second time.
17606
+ */
17607
+ buildResumePrompt(prompt, partialOutput, completed) {
17608
+ const done = summarizeCompleted(completed);
17609
+ return [
17610
+ "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.",
17611
+ "",
17612
+ `Original task: ${prompt}`,
17613
+ done ? `
17614
+ ${done}` : "",
17615
+ partialOutput ? `
16724
17616
  Partial result so far:
16725
- ${last.partialOutput}` : "");
17617
+ ${partialOutput}` : ""
17618
+ ].filter(Boolean).join("\n");
16726
17619
  }
16727
17620
  /**
16728
17621
  * Resume the last budget-capped task with a raised budget (SDK/headless).
16729
17622
  * Returns null when there is nothing to resume.
16730
17623
  */
16731
17624
  async resumeRun(opts = {}) {
16732
- const prompt = this.prepareResume(opts);
17625
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
16733
17626
  if (!prompt) return null;
16734
- return this.run({ prompt });
17627
+ try {
17628
+ return await this.run({ prompt });
17629
+ } catch (err) {
17630
+ await this.releaseClaimedResume();
17631
+ throw err;
17632
+ }
16735
17633
  }
16736
17634
  getWorkspacePath() {
16737
17635
  return this.workspacePath;
@@ -17100,6 +17998,10 @@ ${prompt}` : prompt;
17100
17998
  const startMs = Date.now();
17101
17999
  const taskId = randomUUID();
17102
18000
  this.decisionLog = [];
18001
+ this.currentRunCompleted = [];
18002
+ this.wroteCheckpointThisRun = false;
18003
+ this.completedNormally = false;
18004
+ if (!this.claimedResumeId) this.resumedFrom = void 0;
17103
18005
  if (options.fastAnswer) {
17104
18006
  return this.runFastAnswer(options, startMs, taskId);
17105
18007
  }
@@ -17214,6 +18116,11 @@ ${prompt}` : prompt;
17214
18116
  });
17215
18117
  tier.on("log", (e) => this.emit("log", e));
17216
18118
  tier.on("tier:status", (e) => this.emit("tier:status", e));
18119
+ tier.on("section:complete", (e) => {
18120
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
18121
+ this.currentRunCompleted.push(e);
18122
+ this.emit("section:complete", e);
18123
+ });
17217
18124
  tier.on("tool:call", (e) => this.emit("tool:call", e));
17218
18125
  tier.on("tool:result", (e) => this.emit("tool:result", e));
17219
18126
  tier.on("tool:approval-request", async (request) => {
@@ -17360,6 +18267,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17360
18267
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
17361
18268
  const trip = runBreaker.reason();
17362
18269
  if (trip) {
18270
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
17363
18271
  finalOutput = [
17364
18272
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
17365
18273
  "",
@@ -17372,6 +18280,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17372
18280
  ${finalOutput}` : ""
17373
18281
  ].join("\n").trimEnd();
17374
18282
  }
18283
+ this.completedNormally = trip == null;
17375
18284
  } catch (err) {
17376
18285
  if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
17377
18286
  this.emit("run:cancelled", {
@@ -17379,6 +18288,13 @@ ${finalOutput}` : ""
17379
18288
  reason: err instanceof Error ? err.message : "Task cancelled",
17380
18289
  partialOutput: finalOutput || ""
17381
18290
  });
18291
+ await this.checkpointRun(
18292
+ taskId,
18293
+ options.prompt,
18294
+ finalOutput || "",
18295
+ "cancelled",
18296
+ err instanceof Error ? err.message : "Task cancelled"
18297
+ );
17382
18298
  runError = null;
17383
18299
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
17384
18300
  this.emit("run:budget-exceeded", {
@@ -17387,9 +18303,17 @@ ${finalOutput}` : ""
17387
18303
  partialOutput: finalOutput || ""
17388
18304
  });
17389
18305
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
18306
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
17390
18307
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
17391
18308
  runError = null;
17392
18309
  } else {
18310
+ await this.checkpointRun(
18311
+ taskId,
18312
+ options.prompt,
18313
+ finalOutput || "",
18314
+ "error",
18315
+ err instanceof Error ? err.message : String(err)
18316
+ );
17393
18317
  runError = err;
17394
18318
  throw err;
17395
18319
  }
@@ -17398,6 +18322,10 @@ ${finalOutput}` : ""
17398
18322
  escalator.cancelAllPending();
17399
18323
  } catch {
17400
18324
  }
18325
+ try {
18326
+ await this.settleClaimedResume(this.completedNormally);
18327
+ } catch {
18328
+ }
17401
18329
  try {
17402
18330
  this.releasePendingEscalations();
17403
18331
  } catch {
@@ -17637,7 +18565,7 @@ var Keystore = class {
17637
18565
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
17638
18566
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
17639
18567
  this.backend = "keytar";
17640
- if (password && fs24.existsSync(this.storePath)) {
18568
+ if (password && fs25.existsSync(this.storePath)) {
17641
18569
  try {
17642
18570
  const fileEntries = this.decryptFile(password);
17643
18571
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -17656,7 +18584,7 @@ var Keystore = class {
17656
18584
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
17657
18585
  );
17658
18586
  }
17659
- if (!fs24.existsSync(this.storePath)) {
18587
+ if (!fs25.existsSync(this.storePath)) {
17660
18588
  const salt = crypto4.randomBytes(SALT_LEN);
17661
18589
  this.masterKey = this.deriveKey(password, salt);
17662
18590
  this.writeWithSalt({}, salt);
@@ -17670,7 +18598,7 @@ var Keystore = class {
17670
18598
  }
17671
18599
  /** Synchronous legacy unlock kept for AES-only environments. */
17672
18600
  unlockSync(password) {
17673
- if (!fs24.existsSync(this.storePath)) {
18601
+ if (!fs25.existsSync(this.storePath)) {
17674
18602
  const salt = crypto4.randomBytes(SALT_LEN);
17675
18603
  this.masterKey = this.deriveKey(password, salt);
17676
18604
  this.writeWithSalt({}, salt);
@@ -17728,7 +18656,7 @@ var Keystore = class {
17728
18656
  }
17729
18657
  }
17730
18658
  decryptFile(password, knownSalt) {
17731
- if (!fs24.existsSync(this.storePath)) return {};
18659
+ if (!fs25.existsSync(this.storePath)) return {};
17732
18660
  try {
17733
18661
  const { salt, ciphertext, iv, tag } = this.readRaw();
17734
18662
  const useSalt = knownSalt ?? salt;
@@ -17750,8 +18678,8 @@ var Keystore = class {
17750
18678
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17751
18679
  const tag = cipher.getAuthTag();
17752
18680
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
17753
- fs24.mkdirSync(path25.dirname(this.storePath), { recursive: true });
17754
- fs24.writeFileSync(this.storePath, out, { mode: 384 });
18681
+ fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
18682
+ fs25.writeFileSync(this.storePath, out, { mode: 384 });
17755
18683
  }
17756
18684
  writeWithSalt(data, salt) {
17757
18685
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -17761,11 +18689,11 @@ var Keystore = class {
17761
18689
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17762
18690
  const tag = cipher.getAuthTag();
17763
18691
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
17764
- fs24.mkdirSync(path25.dirname(this.storePath), { recursive: true });
17765
- fs24.writeFileSync(this.storePath, out, { mode: 384 });
18692
+ fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
18693
+ fs25.writeFileSync(this.storePath, out, { mode: 384 });
17766
18694
  }
17767
18695
  readRaw() {
17768
- const buf = fs24.readFileSync(this.storePath);
18696
+ const buf = fs25.readFileSync(this.storePath);
17769
18697
  let offset = 0;
17770
18698
  const salt = buf.subarray(offset, offset + SALT_LEN);
17771
18699
  offset += SALT_LEN;
@@ -17781,7 +18709,7 @@ var Keystore = class {
17781
18709
  }
17782
18710
  };
17783
18711
  async function loadCascadeMd(workspacePath) {
17784
- const filePath = path25.join(workspacePath, "CASCADE.md");
18712
+ const filePath = path26.join(workspacePath, "CASCADE.md");
17785
18713
  try {
17786
18714
  const raw = await fs5.readFile(filePath, "utf-8");
17787
18715
  return parseCascadeMd(raw);
@@ -17812,7 +18740,7 @@ ${raw.trim()}`;
17812
18740
  var MemoryStore = class _MemoryStore {
17813
18741
  db;
17814
18742
  constructor(dbPath) {
17815
- fs24.mkdirSync(path25.dirname(dbPath), { recursive: true });
18743
+ fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
17816
18744
  try {
17817
18745
  this.db = new Database(dbPath, { timeout: 5e3 });
17818
18746
  this.db.pragma("journal_mode = WAL");
@@ -18648,7 +19576,7 @@ Original error: ${err.message}`
18648
19576
  }
18649
19577
  };
18650
19578
  function credentialsPath(globalDir2) {
18651
- return path25.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
19579
+ return path26.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
18652
19580
  }
18653
19581
  function providerKey(p) {
18654
19582
  if (p.type === "azure") {
@@ -18661,7 +19589,7 @@ function isPersistable(p) {
18661
19589
  }
18662
19590
  function loadGlobalCredentials(globalDir2) {
18663
19591
  try {
18664
- const raw = fs24.readFileSync(credentialsPath(globalDir2), "utf-8");
19592
+ const raw = fs25.readFileSync(credentialsPath(globalDir2), "utf-8");
18665
19593
  const parsed = JSON.parse(raw);
18666
19594
  if (!Array.isArray(parsed.providers)) return [];
18667
19595
  return parsed.providers.filter(
@@ -18673,13 +19601,13 @@ function loadGlobalCredentials(globalDir2) {
18673
19601
  }
18674
19602
  function saveGlobalCredentials(globalDir2, providers) {
18675
19603
  const filePath = credentialsPath(globalDir2);
18676
- fs24.mkdirSync(globalDir2, { recursive: true, mode: 448 });
19604
+ fs25.mkdirSync(globalDir2, { recursive: true, mode: 448 });
18677
19605
  const body = { version: 1, providers: providers.filter(isPersistable) };
18678
19606
  const tmp = `${filePath}.tmp`;
18679
- fs24.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
18680
- fs24.renameSync(tmp, filePath);
19607
+ fs25.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
19608
+ fs25.renameSync(tmp, filePath);
18681
19609
  try {
18682
- fs24.chmodSync(filePath, 384);
19610
+ fs25.chmodSync(filePath, 384);
18683
19611
  } catch {
18684
19612
  }
18685
19613
  }
@@ -18714,7 +19642,7 @@ var ConfigManager = class {
18714
19642
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
18715
19643
  constructor(workspacePath = process.cwd(), globalDirOverride) {
18716
19644
  this.workspacePath = workspacePath;
18717
- this.globalDir = globalDirOverride ?? path25.join(os6.homedir(), GLOBAL_CONFIG_DIR);
19645
+ this.globalDir = globalDirOverride ?? path26.join(os6.homedir(), GLOBAL_CONFIG_DIR);
18718
19646
  }
18719
19647
  async load() {
18720
19648
  this.config = await this.loadConfig();
@@ -18728,8 +19656,8 @@ var ConfigManager = class {
18728
19656
  this.ignore = new CascadeIgnore();
18729
19657
  await this.ignore.load(this.workspacePath);
18730
19658
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
18731
- this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
18732
- this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
19659
+ this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19660
+ this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
18733
19661
  await this.injectEnvKeys();
18734
19662
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
18735
19663
  await this.ensureDefaultIdentity();
@@ -18754,8 +19682,8 @@ var ConfigManager = class {
18754
19682
  return this.workspacePath;
18755
19683
  }
18756
19684
  async save() {
18757
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
18758
- await fs5.mkdir(path25.dirname(configPath), { recursive: true });
19685
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
19686
+ await fs5.mkdir(path26.dirname(configPath), { recursive: true });
18759
19687
  await fs5.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
18760
19688
  try {
18761
19689
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -18791,7 +19719,7 @@ var ConfigManager = class {
18791
19719
  return configProvider?.apiKey;
18792
19720
  }
18793
19721
  async loadConfig() {
18794
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
19722
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
18795
19723
  try {
18796
19724
  const raw = await fs5.readFile(configPath, "utf-8");
18797
19725
  return validateConfig(JSON.parse(raw));
@@ -19026,8 +19954,14 @@ var DashboardSocket = class {
19026
19954
  emitCascadeEvent(ev) {
19027
19955
  this.io.emit("cascade:event", ev);
19028
19956
  }
19029
- emitTierStatus(tierId, role, status, sessionId, action) {
19030
- const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId };
19957
+ /**
19958
+ * `extra` exists because this signature is positional: every field the tier
19959
+ * emits that is not named here is silently dropped on this path, while the
19960
+ * other two forwarders spread the whole event through. That asymmetry is how
19961
+ * a field can reach the web dashboard from one code path and not another.
19962
+ */
19963
+ emitTierStatus(tierId, role, status, sessionId, action, extra) {
19964
+ const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
19031
19965
  this.io.emit("tier:status", payload);
19032
19966
  this.io.to(`session:${sessionId}`).emit("tier:status", payload);
19033
19967
  }
@@ -19323,7 +20257,7 @@ function aggregateCostStats(sessions, opts = {}) {
19323
20257
  }
19324
20258
 
19325
20259
  // src/dashboard/server.ts
19326
- var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
20260
+ var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
19327
20261
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
19328
20262
  var DashboardServer = class {
19329
20263
  app;
@@ -19603,14 +20537,14 @@ var DashboardServer = class {
19603
20537
  */
19604
20538
  persistConfig() {
19605
20539
  try {
19606
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
19607
- fs24.mkdirSync(path25.dirname(configPath), { recursive: true });
19608
- fs24.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
20540
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
20541
+ fs25.mkdirSync(path26.dirname(configPath), { recursive: true });
20542
+ fs25.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
19609
20543
  } catch (err) {
19610
20544
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
19611
20545
  }
19612
20546
  try {
19613
- saveGlobalCredentials(path25.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
20547
+ saveGlobalCredentials(path26.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
19614
20548
  } catch (err) {
19615
20549
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
19616
20550
  }
@@ -19625,15 +20559,15 @@ var DashboardServer = class {
19625
20559
  resolveDashboardSecret() {
19626
20560
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
19627
20561
  if (fromConfig) return fromConfig;
19628
- const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
20562
+ const secretPath = path26.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
19629
20563
  try {
19630
- if (fs24.existsSync(secretPath)) {
19631
- const existing = fs24.readFileSync(secretPath, "utf-8").trim();
20564
+ if (fs25.existsSync(secretPath)) {
20565
+ const existing = fs25.readFileSync(secretPath, "utf-8").trim();
19632
20566
  if (existing.length >= 16) return existing;
19633
20567
  }
19634
20568
  const generated = randomUUID();
19635
- fs24.mkdirSync(path25.dirname(secretPath), { recursive: true });
19636
- fs24.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
20569
+ fs25.mkdirSync(path26.dirname(secretPath), { recursive: true });
20570
+ fs25.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
19637
20571
  if (this.config.dashboard.auth) {
19638
20572
  console.warn(
19639
20573
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -19660,7 +20594,7 @@ var DashboardServer = class {
19660
20594
  // ── Setup ─────────────────────────────────────
19661
20595
  getGlobalStore() {
19662
20596
  if (!this.globalStore) {
19663
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20597
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
19664
20598
  this.globalStore = new MemoryStore(globalDbPath);
19665
20599
  }
19666
20600
  return this.globalStore;
@@ -20040,12 +20974,12 @@ ${prompt}`;
20040
20974
  }
20041
20975
  }
20042
20976
  watchRuntimeChanges() {
20043
- const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
20044
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20977
+ const workspaceDbPath = path26.join(this.workspacePath, CASCADE_DB_FILE);
20978
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20045
20979
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
20046
20980
  for (const watchPath of watchPaths) {
20047
- if (!fs24.existsSync(watchPath)) continue;
20048
- fs24.watchFile(watchPath, { interval: 3e3 }, () => {
20981
+ if (!fs25.existsSync(watchPath)) continue;
20982
+ fs25.watchFile(watchPath, { interval: 3e3 }, () => {
20049
20983
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
20050
20984
  });
20051
20985
  }
@@ -20167,7 +21101,7 @@ ${prompt}`;
20167
21101
  const sessionId = req.params.id;
20168
21102
  this.store.deleteSession(sessionId);
20169
21103
  this.store.deleteRuntimeSession(sessionId);
20170
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21104
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20171
21105
  const globalStore = new MemoryStore(globalDbPath);
20172
21106
  try {
20173
21107
  globalStore.deleteRuntimeSession(sessionId);
@@ -20260,7 +21194,7 @@ ${prompt}`;
20260
21194
  });
20261
21195
  this.app.delete("/api/sessions", auth2, (req, res) => {
20262
21196
  const body = req.body;
20263
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21197
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20264
21198
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
20265
21199
  const globalStore = new MemoryStore(globalDbPath);
20266
21200
  try {
@@ -20283,7 +21217,7 @@ ${prompt}`;
20283
21217
  });
20284
21218
  this.app.delete("/api/runtime", auth2, (_req, res) => {
20285
21219
  this.store.deleteAllRuntimeNodes();
20286
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21220
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20287
21221
  const globalStore = new MemoryStore(globalDbPath);
20288
21222
  try {
20289
21223
  globalStore.deleteAllRuntimeNodes();
@@ -20369,12 +21303,12 @@ ${prompt}`;
20369
21303
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
20370
21304
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
20371
21305
  try {
20372
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
20373
- const existing = fs24.existsSync(configPath) ? JSON.parse(fs24.readFileSync(configPath, "utf-8")) : {};
21306
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
21307
+ const existing = fs25.existsSync(configPath) ? JSON.parse(fs25.readFileSync(configPath, "utf-8")) : {};
20374
21308
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
20375
21309
  const tmp = configPath + ".tmp";
20376
- fs24.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20377
- fs24.renameSync(tmp, configPath);
21310
+ fs25.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
21311
+ fs25.renameSync(tmp, configPath);
20378
21312
  res.json({ ok: true });
20379
21313
  } catch (err) {
20380
21314
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -20402,7 +21336,7 @@ ${prompt}`;
20402
21336
  this.app.get("/api/runtime", auth2, (req, res) => {
20403
21337
  const scope = req.query["scope"] ?? "workspace";
20404
21338
  if (scope === "global") {
20405
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
21339
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20406
21340
  const globalStore = new MemoryStore(globalDbPath);
20407
21341
  try {
20408
21342
  res.json({
@@ -20743,13 +21677,13 @@ ${prompt}`;
20743
21677
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
20744
21678
  }
20745
21679
  });
20746
- const prodPath = path25.resolve(__dirname$1, "../web/dist");
20747
- const devPath = path25.resolve(__dirname$1, "../../web/dist");
20748
- const webDistPath = fs24.existsSync(prodPath) ? prodPath : devPath;
20749
- if (fs24.existsSync(webDistPath)) {
21680
+ const prodPath = path26.resolve(__dirname$1, "../web/dist");
21681
+ const devPath = path26.resolve(__dirname$1, "../../web/dist");
21682
+ const webDistPath = fs25.existsSync(prodPath) ? prodPath : devPath;
21683
+ if (fs25.existsSync(webDistPath)) {
20750
21684
  this.app.use(express.static(webDistPath));
20751
21685
  this.app.get("*", (_req, res) => {
20752
- res.sendFile(path25.join(webDistPath, "index.html"));
21686
+ res.sendFile(path26.join(webDistPath, "index.html"));
20753
21687
  });
20754
21688
  } else {
20755
21689
  this.app.get("/", (_req, res) => {
@@ -20936,14 +21870,14 @@ function planRetrieval(ctx) {
20936
21870
  }
20937
21871
  var CLOUD_SESSION_FILE = "cloud-session.json";
20938
21872
  function globalDir(homeDir = os6.homedir()) {
20939
- return path25.join(homeDir, GLOBAL_CONFIG_DIR);
21873
+ return path26.join(homeDir, GLOBAL_CONFIG_DIR);
20940
21874
  }
20941
21875
  function cloudSessionPath(dir = globalDir()) {
20942
- return path25.join(dir, CLOUD_SESSION_FILE);
21876
+ return path26.join(dir, CLOUD_SESSION_FILE);
20943
21877
  }
20944
21878
  function loadCloudSession(dir = globalDir()) {
20945
21879
  try {
20946
- const raw = fs24.readFileSync(cloudSessionPath(dir), "utf-8");
21880
+ const raw = fs25.readFileSync(cloudSessionPath(dir), "utf-8");
20947
21881
  const parsed = JSON.parse(raw);
20948
21882
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20949
21883
  return parsed;
@@ -20952,17 +21886,17 @@ function loadCloudSession(dir = globalDir()) {
20952
21886
  }
20953
21887
  }
20954
21888
  function saveCloudSession(session, dir = globalDir()) {
20955
- fs24.mkdirSync(dir, { recursive: true, mode: 448 });
21889
+ fs25.mkdirSync(dir, { recursive: true, mode: 448 });
20956
21890
  const file = cloudSessionPath(dir);
20957
- fs24.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21891
+ fs25.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20958
21892
  try {
20959
- fs24.chmodSync(file, 384);
21893
+ fs25.chmodSync(file, 384);
20960
21894
  } catch {
20961
21895
  }
20962
21896
  }
20963
21897
  function clearCloudSession(dir = globalDir()) {
20964
21898
  try {
20965
- fs24.rmSync(cloudSessionPath(dir));
21899
+ fs25.rmSync(cloudSessionPath(dir));
20966
21900
  } catch {
20967
21901
  }
20968
21902
  }
@@ -21134,16 +22068,16 @@ var CloudClient = class _CloudClient {
21134
22068
  return (await this.refresh()).accessToken;
21135
22069
  }
21136
22070
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
21137
- async authed(path27, init = {}) {
21138
- const call = (token) => fetch(this.url(path27), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
22071
+ async authed(path28, init = {}) {
22072
+ const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21139
22073
  let res = await call(await this.accessToken());
21140
22074
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21141
22075
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21142
22076
  const text = await res.text();
21143
22077
  return text ? JSON.parse(text) : {};
21144
22078
  }
21145
- authedGet(path27) {
21146
- return this.authed(path27);
22079
+ authedGet(path28) {
22080
+ return this.authed(path28);
21147
22081
  }
21148
22082
  async fetchMe(token) {
21149
22083
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21480,6 +22414,6 @@ function applySyncBundle(bundle, config) {
21480
22414
  return next;
21481
22415
  }
21482
22416
 
21483
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, extractCharts, fileExt, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, uniqueMcpServerName };
22417
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, evaluateAcceptance, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
21484
22418
  //# sourceMappingURL=index.js.map
21485
22419
  //# sourceMappingURL=index.js.map