cascade-ai 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.68.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,
@@ -6556,6 +6556,79 @@ function describeProviderError(c, modelId) {
6556
6556
  }
6557
6557
  }
6558
6558
 
6559
+ // src/core/verification/acceptance.ts
6560
+ var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
6561
+ var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
6562
+ var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
6563
+ var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
6564
+ var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
6565
+ function targetFile(criterion, ownedFiles) {
6566
+ const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
6567
+ if (mentioned.length > 1) return null;
6568
+ if (mentioned.length === 1) {
6569
+ const candidate = mentioned[0];
6570
+ return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
6571
+ }
6572
+ return ownedFiles.length === 1 ? ownedFiles[0] : null;
6573
+ }
6574
+ async function evaluateAcceptance(criteria, ownedFiles, probe) {
6575
+ const results = [];
6576
+ for (const criterion of criteria) {
6577
+ const text = criterion.trim();
6578
+ if (!text) {
6579
+ results.push({ criterion, verdict: "undecidable" });
6580
+ continue;
6581
+ }
6582
+ if (NEGATION.test(text)) {
6583
+ results.push({ criterion, verdict: "undecidable" });
6584
+ continue;
6585
+ }
6586
+ const needle = text.match(QUOTED_NEEDLE)?.[1];
6587
+ const wantsExists = EXISTS.test(text);
6588
+ const wantsNonEmpty = NON_EMPTY.test(text);
6589
+ if (!needle && !wantsExists && !wantsNonEmpty) {
6590
+ results.push({ criterion, verdict: "undecidable" });
6591
+ continue;
6592
+ }
6593
+ const file = targetFile(text, ownedFiles);
6594
+ if (!file) {
6595
+ results.push({ criterion, verdict: "undecidable" });
6596
+ continue;
6597
+ }
6598
+ const stat = await probe.stat(file);
6599
+ if (!stat) {
6600
+ results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
6601
+ continue;
6602
+ }
6603
+ if (wantsNonEmpty && stat.size === 0) {
6604
+ results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
6605
+ continue;
6606
+ }
6607
+ if (needle) {
6608
+ const content = await probe.read(file);
6609
+ if (content === null) {
6610
+ results.push({ criterion, verdict: "undecidable" });
6611
+ continue;
6612
+ }
6613
+ const found = content.includes(needle);
6614
+ results.push({
6615
+ criterion,
6616
+ verdict: found ? "passed" : "failed",
6617
+ detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
6618
+ });
6619
+ continue;
6620
+ }
6621
+ results.push({ criterion, verdict: "passed", detail: `${file} exists` });
6622
+ }
6623
+ return results;
6624
+ }
6625
+ function undecided(results) {
6626
+ return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
6627
+ }
6628
+ function failures(results) {
6629
+ return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
6630
+ }
6631
+
6559
6632
  // src/core/tiers/t3-worker.ts
6560
6633
  var CriticalToolError = class extends Error {
6561
6634
  constructor(message, toolName) {
@@ -6872,7 +6945,7 @@ Now execute your subtask using this context where relevant.`
6872
6945
  }
6873
6946
  const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
6874
6947
  if (!artifactCheck.ok) {
6875
- correctionAttempts = 1;
6948
+ correctionAttempts++;
6876
6949
  issues.push(...artifactCheck.issues);
6877
6950
  output = await this.correctOutput(output, artifactCheck.issues);
6878
6951
  const retryArtifactCheck = await this.verifyArtifacts(assignment);
@@ -6890,18 +6963,40 @@ Now execute your subtask using this context where relevant.`
6890
6963
  this.tools.map((t) => t.name)
6891
6964
  );
6892
6965
  if (visualIssue) {
6893
- correctionAttempts = 1;
6966
+ correctionAttempts++;
6894
6967
  issues.push(visualIssue);
6895
6968
  this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
6896
6969
  output = await this.correctOutput(output, [visualIssue]);
6897
6970
  }
6971
+ const acceptanceResults = await this.checkAcceptance(assignment);
6972
+ const acceptanceFailures = failures(acceptanceResults);
6973
+ if (acceptanceFailures.length > 0) {
6974
+ correctionAttempts++;
6975
+ issues.push(...acceptanceFailures);
6976
+ this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
6977
+ output = await this.correctOutput(output, acceptanceFailures);
6978
+ const recheck = failures(await this.checkAcceptance(assignment));
6979
+ if (recheck.length > 0) {
6980
+ issues.push(...recheck);
6981
+ checksRun.push(...acceptanceResults.map((r) => r.criterion));
6982
+ failed.push(...recheck);
6983
+ this.setStatus("FAILED");
6984
+ this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
6985
+ return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
6986
+ }
6987
+ }
6988
+ for (const result2 of acceptanceResults) {
6989
+ if (result2.verdict === "undecidable") continue;
6990
+ checksRun.push(result2.criterion);
6991
+ if (result2.verdict === "passed") passed.push(result2.criterion);
6992
+ }
6898
6993
  this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
6899
- const testResult = await this.selfTest(assignment, output);
6994
+ const testResult = await this.selfTest(assignment, output, undecided(acceptanceResults));
6900
6995
  checksRun.push(...testResult.checksRun);
6901
6996
  passed.push(...testResult.passed);
6902
6997
  failed.push(...testResult.failed);
6903
6998
  if (testResult.failed.length > 0) {
6904
- correctionAttempts = 1;
6999
+ correctionAttempts++;
6905
7000
  issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
6906
7001
  const corrected = await this.correctOutput(output, testResult.failed);
6907
7002
  output = corrected;
@@ -7209,8 +7304,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
7209
7304
  // Media generation can run for a minute; without this a cancelled run
7210
7305
  // still pays for an image nobody will see.
7211
7306
  ...this.signal ? { signal: this.signal } : {},
7212
- saveSnapshot: async (path27, content) => {
7213
- this.store?.addFileSnapshot(this.taskId, path27, content);
7307
+ saveSnapshot: async (path28, content) => {
7308
+ this.store?.addFileSnapshot(this.taskId, path28, content);
7214
7309
  },
7215
7310
  sendPeerSync: (to, syncType, content) => {
7216
7311
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -7397,7 +7492,7 @@ ${assignment.expectedOutput}`;
7397
7492
  const { promisify: promisify4 } = await import('util');
7398
7493
  const execAsync2 = promisify4(exec2);
7399
7494
  for (const artifactPath of artifactPaths) {
7400
- const absolutePath = path25.resolve(process.cwd(), artifactPath);
7495
+ const absolutePath = path26.resolve(process.cwd(), artifactPath);
7401
7496
  try {
7402
7497
  const stat = await fs5.stat(absolutePath);
7403
7498
  if (!stat.isFile()) {
@@ -7432,7 +7527,7 @@ ${assignment.expectedOutput}`;
7432
7527
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
7433
7528
  continue;
7434
7529
  }
7435
- const ext = path25.extname(absolutePath).toLowerCase();
7530
+ const ext = path26.extname(absolutePath).toLowerCase();
7436
7531
  try {
7437
7532
  if (ext === ".ts" || ext === ".tsx") {
7438
7533
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -7518,14 +7613,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7518
7613
  }
7519
7614
  return current;
7520
7615
  }
7521
- async selfTest(assignment, output) {
7616
+ /**
7617
+ * Run the acceptance criteria that can be settled by looking at the workspace.
7618
+ * Reads go through the same workspace-relative resolution verifyArtifacts uses.
7619
+ */
7620
+ async checkAcceptance(assignment) {
7621
+ const criteria = assignment.acceptance ?? [];
7622
+ if (!criteria.length) return [];
7623
+ const resolve2 = (target) => path26.resolve(process.cwd(), target);
7624
+ return evaluateAcceptance(criteria, assignment.files ?? [], {
7625
+ stat: async (target) => {
7626
+ try {
7627
+ const stat = await fs5.stat(resolve2(target));
7628
+ return stat.isFile() ? { size: stat.size } : null;
7629
+ } catch {
7630
+ return null;
7631
+ }
7632
+ },
7633
+ read: async (target) => {
7634
+ try {
7635
+ const content = await fs5.readFile(resolve2(target), "utf-8");
7636
+ return content.includes("\uFFFD") ? null : content;
7637
+ } catch {
7638
+ return null;
7639
+ }
7640
+ }
7641
+ });
7642
+ }
7643
+ async selfTest(assignment, output, pendingAcceptance) {
7644
+ const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
7522
7645
  const prompt = `Self-test this output against the assignment requirements.
7523
7646
 
7524
7647
  Assignment: ${assignment.description}
7525
7648
  Expected output: ${assignment.expectedOutput}
7526
7649
  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")}
7650
+ ${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
7651
+ ${acceptance.map((a) => `- ${a}`).join("\n")}
7529
7652
  ` : ""}
7530
7653
  Output to test:
7531
7654
  ${output}
@@ -7592,8 +7715,8 @@ ${output.slice(0, 4e3)}`;
7592
7715
  } catch {
7593
7716
  }
7594
7717
  }
7595
- async correctOutput(originalOutput, failures) {
7596
- const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
7718
+ async correctOutput(originalOutput, failures2) {
7719
+ const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
7597
7720
 
7598
7721
  Original output:
7599
7722
  ${originalOutput}
@@ -8254,12 +8377,220 @@ function describeGenerationForPlanner(has) {
8254
8377
  return lines.join("\n");
8255
8378
  }
8256
8379
 
8380
+ // src/core/orchestration/task-graph.ts
8381
+ var TaskGraphValidationError = class extends Error {
8382
+ constructor(issues) {
8383
+ super(issues.map((issue) => issue.message).join("; "));
8384
+ this.issues = issues;
8385
+ this.name = "TaskGraphValidationError";
8386
+ }
8387
+ issues;
8388
+ };
8389
+ function normalizeDependencies(input) {
8390
+ if (!input?.length) return [];
8391
+ return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
8392
+ }
8393
+ function cycleNodes(nodes) {
8394
+ const adjacency = new Map(
8395
+ nodes.map((node) => [node.id, node.dependsOn])
8396
+ );
8397
+ const index = /* @__PURE__ */ new Map();
8398
+ const lowlink = /* @__PURE__ */ new Map();
8399
+ const onStack = /* @__PURE__ */ new Set();
8400
+ const stack = [];
8401
+ const inCycle = /* @__PURE__ */ new Set();
8402
+ let counter = 0;
8403
+ const open = (id) => {
8404
+ index.set(id, counter);
8405
+ lowlink.set(id, counter);
8406
+ counter++;
8407
+ stack.push(id);
8408
+ onStack.add(id);
8409
+ };
8410
+ for (const root of nodes) {
8411
+ if (index.has(root.id)) continue;
8412
+ open(root.id);
8413
+ const frames = [{ id: root.id, edge: 0 }];
8414
+ while (frames.length > 0) {
8415
+ const frame = frames[frames.length - 1];
8416
+ const neighbours = adjacency.get(frame.id) ?? [];
8417
+ if (frame.edge < neighbours.length) {
8418
+ const next = neighbours[frame.edge];
8419
+ frame.edge++;
8420
+ if (!adjacency.has(next)) continue;
8421
+ if (!index.has(next)) {
8422
+ open(next);
8423
+ frames.push({ id: next, edge: 0 });
8424
+ } else if (onStack.has(next)) {
8425
+ lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
8426
+ }
8427
+ continue;
8428
+ }
8429
+ frames.pop();
8430
+ const parent = frames[frames.length - 1];
8431
+ if (parent) {
8432
+ lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
8433
+ }
8434
+ if (lowlink.get(frame.id) === index.get(frame.id)) {
8435
+ const component = [];
8436
+ for (; ; ) {
8437
+ const member = stack.pop();
8438
+ onStack.delete(member);
8439
+ component.push(member);
8440
+ if (member === frame.id) break;
8441
+ }
8442
+ const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
8443
+ if (cyclic) for (const member of component) inCycle.add(member);
8444
+ }
8445
+ }
8446
+ }
8447
+ return inCycle;
8448
+ }
8449
+ function throwIfStrict(mode, issues) {
8450
+ if (mode === "strict" && issues.length > 0) {
8451
+ throw new TaskGraphValidationError(issues);
8452
+ }
8453
+ }
8454
+ function compileTaskGraph(inputNodes, options = {}) {
8455
+ const mode = options.mode ?? "strict";
8456
+ const issues = [];
8457
+ if (inputNodes.length === 0) {
8458
+ issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
8459
+ throwIfStrict(mode, issues);
8460
+ }
8461
+ const seen = /* @__PURE__ */ new Set();
8462
+ const nodes = [];
8463
+ for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
8464
+ const input = inputNodes[ordinal];
8465
+ const id = input.id?.trim();
8466
+ const title = input.title?.trim();
8467
+ if (!id || !title) {
8468
+ issues.push({
8469
+ code: "INVALID_NODE",
8470
+ nodeId: id || void 0,
8471
+ message: `Task graph node at index ${ordinal} requires a non-empty id and title`
8472
+ });
8473
+ continue;
8474
+ }
8475
+ if (seen.has(id)) {
8476
+ issues.push({
8477
+ code: "DUPLICATE_NODE_ID",
8478
+ nodeId: id,
8479
+ message: `Duplicate task graph node id: ${id}`
8480
+ });
8481
+ continue;
8482
+ }
8483
+ seen.add(id);
8484
+ nodes.push({
8485
+ id,
8486
+ title,
8487
+ kind: input.kind,
8488
+ dependsOn: normalizeDependencies(input.dependsOn),
8489
+ payload: input.payload,
8490
+ ordinal
8491
+ });
8492
+ }
8493
+ throwIfStrict(mode, issues);
8494
+ const ids = new Set(nodes.map((node) => node.id));
8495
+ const sanitized = nodes.map((node) => {
8496
+ const dependsOn = [];
8497
+ for (const dependencyId of node.dependsOn) {
8498
+ if (dependencyId === node.id) {
8499
+ issues.push({
8500
+ code: "SELF_DEPENDENCY",
8501
+ nodeId: node.id,
8502
+ dependencyId,
8503
+ message: `Task graph node ${node.id} cannot depend on itself`
8504
+ });
8505
+ continue;
8506
+ }
8507
+ if (!ids.has(dependencyId)) {
8508
+ issues.push({
8509
+ code: "UNKNOWN_DEPENDENCY",
8510
+ nodeId: node.id,
8511
+ dependencyId,
8512
+ message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
8513
+ });
8514
+ continue;
8515
+ }
8516
+ dependsOn.push(dependencyId);
8517
+ }
8518
+ return { ...node, dependsOn };
8519
+ });
8520
+ throwIfStrict(mode, issues);
8521
+ const cyclic = cycleNodes(sanitized);
8522
+ let repaired = sanitized;
8523
+ if (cyclic.size > 0) {
8524
+ const cycleIssues = [];
8525
+ repaired = sanitized.map((node) => {
8526
+ if (!cyclic.has(node.id)) return node;
8527
+ const dependsOn = node.dependsOn.filter((dependencyId) => {
8528
+ if (!cyclic.has(dependencyId)) return true;
8529
+ cycleIssues.push({
8530
+ code: "CYCLE_EDGE_REMOVED",
8531
+ nodeId: node.id,
8532
+ dependencyId,
8533
+ message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
8534
+ });
8535
+ return false;
8536
+ });
8537
+ return { ...node, dependsOn };
8538
+ });
8539
+ issues.push(...cycleIssues);
8540
+ throwIfStrict(mode, cycleIssues);
8541
+ }
8542
+ const remainingCycle = cycleNodes(repaired);
8543
+ if (remainingCycle.size > 0) {
8544
+ throw new TaskGraphValidationError([
8545
+ {
8546
+ code: "INVALID_NODE",
8547
+ message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
8548
+ }
8549
+ ]);
8550
+ }
8551
+ return {
8552
+ graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
8553
+ issues
8554
+ };
8555
+ }
8556
+
8557
+ // src/core/orchestration/adapters.ts
8558
+ function compileSectionGraph(sections) {
8559
+ return compileTaskGraph(
8560
+ sections.map((section) => ({
8561
+ id: section.sectionId,
8562
+ title: section.sectionTitle,
8563
+ kind: "section",
8564
+ dependsOn: section.dependsOn,
8565
+ payload: section
8566
+ })),
8567
+ { mode: "repair" }
8568
+ );
8569
+ }
8570
+ function compileSubtaskGraph(subtasks) {
8571
+ return compileTaskGraph(
8572
+ subtasks.map((subtask) => ({
8573
+ id: subtask.subtaskId,
8574
+ title: subtask.subtaskTitle,
8575
+ kind: "subtask",
8576
+ dependsOn: subtask.dependsOn,
8577
+ payload: subtask
8578
+ })),
8579
+ { mode: "repair" }
8580
+ );
8581
+ }
8582
+
8257
8583
  // src/core/tiers/t2-manager.ts
8258
8584
  function buildT2SystemPrompt(has) {
8259
8585
  const generation = describeGenerationForPlanner(has);
8260
8586
  return [
8261
8587
  "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.",
8588
+ // Must agree with the decomposition prompt in decomposeSection(), which is
8589
+ // the one carrying the actual instruction. It said 1-4 and "the FEWEST that
8590
+ // fully cover it"; this said 2-5. The model received both, so its floor was
8591
+ // simultaneously one and two, and the cheaper reading — pad to two — costs a
8592
+ // real worker call on every small section.
8593
+ "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
8594
  'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
8264
8595
  has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
8265
8596
  "Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
@@ -8655,21 +8986,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8655
8986
  * then reduce in-degrees of their dependents and repeat.
8656
8987
  */
8657
8988
  async runWithDependencies(assignments, workerMap, taskId) {
8989
+ const compiled = compileSubtaskGraph(assignments);
8990
+ for (const issue of compiled.issues) {
8991
+ this.log(`\u26A0 Subtask graph: ${issue.message}`);
8992
+ }
8993
+ const sanitizedAssignments = compiled.graph.nodes.map((node) => {
8994
+ const assignment = node.payload;
8995
+ const original = assignment.dependsOn ?? [];
8996
+ const repaired = node.dependsOn;
8997
+ const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
8998
+ return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
8999
+ });
8658
9000
  const adj = /* @__PURE__ */ new Map();
8659
9001
  const inDegree = /* @__PURE__ */ new Map();
8660
9002
  const resultMap = /* @__PURE__ */ new Map();
8661
- for (const a of assignments) {
9003
+ for (const a of sanitizedAssignments) {
8662
9004
  if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
8663
9005
  inDegree.set(a.subtaskId, 0);
8664
9006
  }
8665
- for (const a of assignments) {
8666
- const deps = a.dependsOn ?? [];
8667
- for (const dep of deps) {
9007
+ for (const a of sanitizedAssignments) {
9008
+ for (const dep of a.dependsOn ?? []) {
8668
9009
  adj.get(dep).add(a.subtaskId);
8669
9010
  inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
8670
9011
  }
8671
9012
  }
8672
- const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
8673
9013
  let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
8674
9014
  let wave = 0;
8675
9015
  let respawnBudget = 1;
@@ -8876,49 +9216,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8876
9216
  }
8877
9217
  return [...resultMap.values()];
8878
9218
  }
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
9219
  async retryT3(assignment, taskId) {
8923
9220
  this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
8924
9221
  const worker = new T3Worker(this.router, this.toolRegistry, this.id);
@@ -9054,6 +9351,57 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
9054
9351
  }
9055
9352
  };
9056
9353
 
9354
+ // src/core/orchestration/scheduler.ts
9355
+ var DependencyScheduler = class {
9356
+ constructor(graph, hooks, options = {}) {
9357
+ this.graph = graph;
9358
+ this.hooks = hooks;
9359
+ this.options = options;
9360
+ }
9361
+ graph;
9362
+ hooks;
9363
+ options;
9364
+ async run() {
9365
+ const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
9366
+ const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
9367
+ const dependents = /* @__PURE__ */ new Map();
9368
+ for (const node of nodes) dependents.set(node.id, []);
9369
+ for (const node of nodes) {
9370
+ for (const dependencyId of node.dependsOn) {
9371
+ dependents.get(dependencyId)?.push(node.id);
9372
+ }
9373
+ }
9374
+ const completed = /* @__PURE__ */ new Set();
9375
+ const results = /* @__PURE__ */ new Map();
9376
+ const waves = [];
9377
+ let wave = 0;
9378
+ while (completed.size < nodes.length) {
9379
+ const ready = nodes.filter((node) => !completed.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
9380
+ if (ready.length === 0) {
9381
+ throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
9382
+ }
9383
+ waves.push(ready.map((node) => node.id));
9384
+ await this.hooks.onWaveStart?.(ready, wave);
9385
+ const runOne = async (node) => {
9386
+ const result = await this.hooks.execute(node);
9387
+ results.set(node.id, result);
9388
+ completed.add(node.id);
9389
+ for (const dependentId of dependents.get(node.id) ?? []) {
9390
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
9391
+ }
9392
+ await this.hooks.onNodeComplete?.(node, result);
9393
+ };
9394
+ if ((this.options.mode ?? "parallel") === "sequential") {
9395
+ for (const node of ready) await runOne(node);
9396
+ } else {
9397
+ await Promise.all(ready.map(runOne));
9398
+ }
9399
+ wave++;
9400
+ }
9401
+ return { results, waves };
9402
+ }
9403
+ };
9404
+
9057
9405
  // src/utils/json-extract.ts
9058
9406
  function extractFirstJsonObject(input) {
9059
9407
  if (!input) return null;
@@ -9125,7 +9473,7 @@ function buildT1SystemPrompt(has) {
9125
9473
  Your responsibilities:
9126
9474
  1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
9127
9475
  2. Decompose the task into logical sections (one per T2 Manager)
9128
- 3. For each section, define 2-5 subtasks for T3 Workers
9476
+ 3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
9129
9477
  4. Return a structured plan as JSON
9130
9478
 
9131
9479
  CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
@@ -9540,7 +9888,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9540
9888
  }
9541
9889
  }
9542
9890
  const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
9543
- if (plan.sections.length < min) ;
9891
+ if (plan.sections.length < min || plan.sections.length > max) {
9892
+ this.log(
9893
+ `\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.`
9894
+ );
9895
+ }
9544
9896
  }
9545
9897
  async dispatchT2Managers(sections) {
9546
9898
  for (const section of sections) {
@@ -9652,114 +10004,91 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
9652
10004
  return t2Results;
9653
10005
  }
9654
10006
  /**
9655
- * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
10007
+ * Runs T2 managers respecting dependsOn declarations.
10008
+ *
10009
+ * Graph construction, validation and cycle repair now live in
10010
+ * compileSectionGraph (orchestration/adapters.ts), and wave execution in
10011
+ * DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
10012
+ * ordered by one implementation rather than two that drifted.
10013
+ *
10014
+ * The hand-rolled Kahn pass this replaces treated every node it could not
10015
+ * reach as part of a cycle. That set also contains everything DOWNSTREAM of a
10016
+ * cycle, whose in-degree can never fall to zero while its dependency is stuck,
10017
+ * so breaking a cycle here also cut the dependencies of innocent later
10018
+ * sections and started them immediately — before the sections they consume.
10019
+ * compileTaskGraph uses strongly connected components, so only real cycle
10020
+ * members are touched.
9656
10021
  */
9657
10022
  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));
10023
+ const compiled = compileSectionGraph(sections);
10024
+ for (const issue of compiled.issues) {
10025
+ this.log(`\u26A0 Section graph: ${issue.message}`);
9666
10026
  }
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
- }
10027
+ const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
10028
+ for (const section of sections) {
10029
+ const node = byId.get(section.sectionId);
10030
+ if (node) section.dependsOn = [...node.dependsOn];
9699
10031
  }
9700
- const totalSections = sections.length;
10032
+ const resultMap = /* @__PURE__ */ new Map();
10033
+ const totalSections = compiled.graph.nodes.length;
9701
10034
  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}`,
10035
+ const scheduler = new DependencyScheduler(
10036
+ compiled.graph,
10037
+ {
10038
+ execute: async (node) => {
10039
+ const section = node.payload;
10040
+ const manager = managers[node.ordinal];
10041
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
10042
+ this.sendStatusUpdate({
10043
+ progressPct,
10044
+ currentAction: `T2 working on: ${section.sectionTitle}`,
10045
+ status: "IN_PROGRESS"
10046
+ });
10047
+ this.throwIfCancelled();
10048
+ try {
10049
+ const result = await manager.execute(section, taskId, this.signal);
10050
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
10051
+ if (result.status === "ESCALATED") {
10052
+ this.escalations.push({
10053
+ raisedBy: `T2_${section.sectionId}`,
10054
+ sectionId: section.sectionId,
10055
+ attempted: result.issues,
10056
+ blocker: result.issues.join("; "),
10057
+ needs: "Human review required"
10058
+ });
10059
+ }
10060
+ return result;
10061
+ } catch (err) {
10062
+ return {
9729
10063
  sectionId: section.sectionId,
9730
- attempted: result.issues,
9731
- blocker: result.issues.join("; "),
9732
- needs: "Human review required"
9733
- });
10064
+ sectionTitle: section.sectionTitle,
10065
+ status: "FAILED",
10066
+ t3Results: [],
10067
+ sectionSummary: "",
10068
+ issues: [err instanceof Error ? err.message : String(err)]
10069
+ };
9734
10070
  }
9735
- } catch (err) {
9736
- result = {
9737
- sectionId: section.sectionId,
9738
- sectionTitle: section.sectionTitle,
9739
- status: "FAILED",
9740
- t3Results: [],
9741
- 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);
10071
+ },
10072
+ onNodeComplete: (node, result) => {
10073
+ resultMap.set(node.id, result);
10074
+ completedSections++;
10075
+ this.emit("section:complete", {
10076
+ id: node.id,
10077
+ title: result.sectionTitle,
10078
+ status: result.status,
10079
+ output: result.sectionSummary
10080
+ });
9754
10081
  }
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();
10082
+ },
10083
+ {
10084
+ // Cross-section concurrency respects the same t3Execution mode as the
10085
+ // intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
10086
+ // serialized T3 workers within a single section while every section's
10087
+ // T2Manager (and its T3 workers) still ran in parallel here.
10088
+ mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
9760
10089
  }
9761
- };
9762
- await executeWave();
10090
+ );
10091
+ await scheduler.run();
9763
10092
  return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
9764
10093
  }
9765
10094
  async compileFinalOutput(originalPrompt, plan, t2Results) {
@@ -10389,16 +10718,16 @@ function resolveInWorkspace(workspaceRoot, input) {
10389
10718
  if (typeof input !== "string" || input.length === 0) {
10390
10719
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
10391
10720
  }
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)) {
10721
+ const root = path26.resolve(workspaceRoot);
10722
+ const abs = path26.isAbsolute(input) ? path26.resolve(input) : path26.resolve(root, input);
10723
+ const rel = path26.relative(root, abs);
10724
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26.isAbsolute(rel)) {
10396
10725
  throw new WorkspaceSandboxError(input, root);
10397
10726
  }
10398
10727
  try {
10399
- const real = fs24.realpathSync(abs);
10400
- const realRel = path25.relative(root, real);
10401
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
10728
+ const real = fs25.realpathSync(abs);
10729
+ const realRel = path26.relative(root, real);
10730
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26.isAbsolute(realRel))) {
10402
10731
  throw new WorkspaceSandboxError(input, root);
10403
10732
  }
10404
10733
  } catch (e) {
@@ -11056,7 +11385,7 @@ var GenerateDocumentTool = class extends BaseTool {
11056
11385
  }
11057
11386
  }
11058
11387
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
11059
- await fs5.mkdir(path25.dirname(absPath), { recursive: true });
11388
+ await fs5.mkdir(path26.dirname(absPath), { recursive: true });
11060
11389
  await fs5.writeFile(absPath, bytes);
11061
11390
  const charts = countCharts(content);
11062
11391
  return [
@@ -11451,7 +11780,7 @@ var FileWriteTool = class extends BaseTool {
11451
11780
  } catch {
11452
11781
  }
11453
11782
  }
11454
- await fs5.mkdir(path25.dirname(absPath), { recursive: true });
11783
+ await fs5.mkdir(path26.dirname(absPath), { recursive: true });
11455
11784
  await fs5.writeFile(absPath, content, "utf-8");
11456
11785
  return `Written ${content.length} characters to ${filePath}`;
11457
11786
  }
@@ -11949,7 +12278,7 @@ var ImageAnalyzeTool = class extends BaseTool {
11949
12278
  };
11950
12279
  async function fileToImageAttachment(filePath) {
11951
12280
  const data = await fs5.readFile(filePath);
11952
- const ext = path25.extname(filePath).toLowerCase();
12281
+ const ext = path26.extname(filePath).toLowerCase();
11953
12282
  const mimeMap = {
11954
12283
  ".jpg": "image/jpeg",
11955
12284
  ".jpeg": "image/jpeg",
@@ -11983,14 +12312,14 @@ var PDFCreateTool = class extends BaseTool {
11983
12312
  const filePath = input["path"];
11984
12313
  const content = input["content"];
11985
12314
  const title = input["title"];
11986
- const dir = path25.dirname(filePath);
11987
- if (!fs24.existsSync(dir)) {
11988
- fs24.mkdirSync(dir, { recursive: true });
12315
+ const dir = path26.dirname(filePath);
12316
+ if (!fs25.existsSync(dir)) {
12317
+ fs25.mkdirSync(dir, { recursive: true });
11989
12318
  }
11990
12319
  return new Promise((resolve2, reject) => {
11991
12320
  try {
11992
12321
  const doc = new PDFDocument({ margin: 50 });
11993
- const stream = fs24.createWriteStream(filePath);
12322
+ const stream = fs25.createWriteStream(filePath);
11994
12323
  doc.pipe(stream);
11995
12324
  if (title) {
11996
12325
  doc.info["Title"] = title;
@@ -12068,22 +12397,22 @@ var CodeInterpreterTool = class extends BaseTool {
12068
12397
  }
12069
12398
  cmdPrefix = NODE_CMD;
12070
12399
  }
12071
- const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
12072
- if (!fs24.existsSync(tmpDir)) {
12073
- fs24.mkdirSync(tmpDir, { recursive: true });
12400
+ const tmpDir = path26.join(this.workspaceRoot, ".cascade", "tmp");
12401
+ if (!fs25.existsSync(tmpDir)) {
12402
+ fs25.mkdirSync(tmpDir, { recursive: true });
12074
12403
  }
12075
12404
  const extension = language === "python" ? "py" : "js";
12076
12405
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
12077
- const filePath = path25.join(tmpDir, fileName);
12078
- fs24.writeFileSync(filePath, code, "utf-8");
12406
+ const filePath = path26.join(tmpDir, fileName);
12407
+ fs25.writeFileSync(filePath, code, "utf-8");
12079
12408
  const execArgs = [filePath, ...args];
12080
12409
  return new Promise((resolve2) => {
12081
12410
  const startMs = Date.now();
12082
12411
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
12083
12412
  const duration = Date.now() - startMs;
12084
12413
  try {
12085
- if (fs24.existsSync(filePath)) {
12086
- fs24.unlinkSync(filePath);
12414
+ if (fs25.existsSync(filePath)) {
12415
+ fs25.unlinkSync(filePath);
12087
12416
  }
12088
12417
  } catch (cleanupErr) {
12089
12418
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -12395,7 +12724,7 @@ var GlobTool = class extends BaseTool {
12395
12724
  };
12396
12725
  async execute(input, _options) {
12397
12726
  const pattern = input["pattern"];
12398
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12727
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12399
12728
  const matches = await glob(pattern, {
12400
12729
  cwd: searchPath,
12401
12730
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -12408,7 +12737,7 @@ var GlobTool = class extends BaseTool {
12408
12737
  const withMtime = await Promise.all(
12409
12738
  matches.map(async (rel) => {
12410
12739
  try {
12411
- const stat = await fs5.stat(path25.join(searchPath, rel));
12740
+ const stat = await fs5.stat(path26.join(searchPath, rel));
12412
12741
  return { rel, mtime: stat.mtimeMs };
12413
12742
  } catch {
12414
12743
  return { rel, mtime: 0 };
@@ -12457,7 +12786,7 @@ var GrepTool = class extends BaseTool {
12457
12786
  };
12458
12787
  async execute(input, _options) {
12459
12788
  const pattern = input["pattern"];
12460
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12789
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
12461
12790
  const globPattern = input["glob"];
12462
12791
  const outputMode = input["output_mode"] ?? "content";
12463
12792
  const context = input["context"] ?? 0;
@@ -12511,12 +12840,12 @@ var GrepTool = class extends BaseTool {
12511
12840
  nodir: true
12512
12841
  });
12513
12842
  } catch {
12514
- files = [path25.relative(searchPath, searchPath) || "."];
12843
+ files = [path26.relative(searchPath, searchPath) || "."];
12515
12844
  }
12516
12845
  const results = [];
12517
12846
  let totalCount = 0;
12518
12847
  for (const rel of files) {
12519
- const abs = path25.join(searchPath, rel);
12848
+ const abs = path26.join(searchPath, rel);
12520
12849
  let content;
12521
12850
  try {
12522
12851
  content = await fs5.readFile(abs, "utf-8");
@@ -12903,10 +13232,10 @@ var ToolRegistry = class extends EventEmitter {
12903
13232
  }
12904
13233
  isIgnored(filePath) {
12905
13234
  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("/");
13235
+ const abs = path26.resolve(this.workspaceRoot, filePath);
13236
+ const rel = path26.relative(this.workspaceRoot, abs);
13237
+ if (!rel || rel.startsWith("..") || path26.isAbsolute(rel)) return true;
13238
+ const posixRel = rel.split(path26.sep).join("/");
12910
13239
  return this.ignoreMatcher.ignores(posixRel);
12911
13240
  }
12912
13241
  };
@@ -13140,22 +13469,22 @@ var FileMcpOAuthStore = class {
13140
13469
  filePath;
13141
13470
  load() {
13142
13471
  try {
13143
- return JSON.parse(fs24.readFileSync(this.filePath, "utf-8"));
13472
+ return JSON.parse(fs25.readFileSync(this.filePath, "utf-8"));
13144
13473
  } catch {
13145
13474
  return void 0;
13146
13475
  }
13147
13476
  }
13148
13477
  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 });
13478
+ fs25.mkdirSync(path26.dirname(this.filePath), { recursive: true, mode: 448 });
13479
+ fs25.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
13151
13480
  try {
13152
- fs24.chmodSync(this.filePath, 384);
13481
+ fs25.chmodSync(this.filePath, 384);
13153
13482
  } catch {
13154
13483
  }
13155
13484
  }
13156
13485
  clear() {
13157
13486
  try {
13158
- fs24.rmSync(this.filePath);
13487
+ fs25.rmSync(this.filePath);
13159
13488
  } catch {
13160
13489
  }
13161
13490
  }
@@ -14052,6 +14381,14 @@ function heuristicAnalyze(prompt) {
14052
14381
  const estimatedTokens = wordCount * 5;
14053
14382
  return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
14054
14383
  }
14384
+ function hashPrompt(prompt) {
14385
+ let hash = 2166136261;
14386
+ for (let index = 0; index < prompt.length; index++) {
14387
+ hash ^= prompt.charCodeAt(index);
14388
+ hash = Math.imul(hash, 16777619) >>> 0;
14389
+ }
14390
+ return `${prompt.length}:${hash.toString(36)}`;
14391
+ }
14055
14392
  var analysisCache = /* @__PURE__ */ new Map();
14056
14393
  var TASK_TYPE_TAGS = {
14057
14394
  code: ["code", "instruction"],
@@ -14095,7 +14432,7 @@ var TaskAnalyzer = class {
14095
14432
  * Low confidence prompts fall back to a conservative mixed/moderate profile.
14096
14433
  */
14097
14434
  async analyze(prompt) {
14098
- const cacheKey = prompt.slice(0, 200);
14435
+ const cacheKey = hashPrompt(prompt);
14099
14436
  const cached = analysisCache.get(cacheKey);
14100
14437
  if (cached) {
14101
14438
  this.lastProfile = cached;
@@ -14194,7 +14531,7 @@ var TaskAnalyzer = class {
14194
14531
  analysisCache.clear();
14195
14532
  }
14196
14533
  };
14197
- var DEFAULT_STATS_FILE = path25.join(os6.homedir(), ".cascade", "model-perf.json");
14534
+ var DEFAULT_STATS_FILE = path26.join(os6.homedir(), ".cascade", "model-perf.json");
14198
14535
  var ModelPerformanceTracker = class {
14199
14536
  stats = /* @__PURE__ */ new Map();
14200
14537
  featureStats = /* @__PURE__ */ new Map();
@@ -14234,7 +14571,7 @@ var ModelPerformanceTracker = class {
14234
14571
  async save() {
14235
14572
  if (this.readOnly) return;
14236
14573
  try {
14237
- await fs5.mkdir(path25.dirname(this.statsFile), { recursive: true });
14574
+ await fs5.mkdir(path26.dirname(this.statsFile), { recursive: true });
14238
14575
  const modelsObj = {};
14239
14576
  const featuresObj = {};
14240
14577
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -14782,7 +15119,7 @@ Required capability: ${description.slice(0, 300)}`;
14782
15119
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
14783
15120
  async loadPersistedTools() {
14784
15121
  if (!this.workspacePath || !this.persistEnabled) return;
14785
- const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
15122
+ const file = path26.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
14786
15123
  try {
14787
15124
  const raw = await fs5.readFile(file, "utf-8");
14788
15125
  const specs = JSON.parse(raw);
@@ -14806,8 +15143,8 @@ Required capability: ${description.slice(0, 300)}`;
14806
15143
  }
14807
15144
  async persist() {
14808
15145
  if (!this.workspacePath || !this.persistEnabled) return;
14809
- const dir = path25.join(this.workspacePath, ".cascade");
14810
- const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
15146
+ const dir = path26.join(this.workspacePath, ".cascade");
15147
+ const file = path26.join(dir, DYNAMIC_TOOLS_FILE);
14811
15148
  try {
14812
15149
  await fs5.mkdir(dir, { recursive: true });
14813
15150
  await fs5.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -14827,12 +15164,12 @@ var WorldStateDB = class {
14827
15164
  constructor(workspacePath, debugMode = false) {
14828
15165
  this.workspacePath = workspacePath;
14829
15166
  this.debugMode = debugMode;
14830
- const cascadeDir = path25.join(workspacePath, ".cascade");
14831
- if (!fs24.existsSync(cascadeDir)) {
14832
- fs24.mkdirSync(cascadeDir, { recursive: true });
15167
+ const cascadeDir = path26.join(workspacePath, ".cascade");
15168
+ if (!fs25.existsSync(cascadeDir)) {
15169
+ fs25.mkdirSync(cascadeDir, { recursive: true });
14833
15170
  }
14834
- this.keyPath = path25.join(cascadeDir, "world_state.key");
14835
- this.dbPath = path25.join(cascadeDir, "world_state.db");
15171
+ this.keyPath = path26.join(cascadeDir, "world_state.key");
15172
+ this.dbPath = path26.join(cascadeDir, "world_state.db");
14836
15173
  this.initEncryptionKey();
14837
15174
  this.db = new Database(this.dbPath);
14838
15175
  this.db.pragma("journal_mode = WAL");
@@ -14889,11 +15226,11 @@ var WorldStateDB = class {
14889
15226
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
14890
15227
  }
14891
15228
  initEncryptionKey() {
14892
- if (fs24.existsSync(this.keyPath)) {
14893
- this.encryptionKey = fs24.readFileSync(this.keyPath);
15229
+ if (fs25.existsSync(this.keyPath)) {
15230
+ this.encryptionKey = fs25.readFileSync(this.keyPath);
14894
15231
  } else {
14895
15232
  this.encryptionKey = crypto4.randomBytes(32);
14896
- fs24.writeFileSync(this.keyPath, this.encryptionKey);
15233
+ fs25.writeFileSync(this.keyPath, this.encryptionKey);
14897
15234
  }
14898
15235
  }
14899
15236
  encrypt(text) {
@@ -15179,9 +15516,9 @@ var WorldStateDB = class {
15179
15516
  dumpDebugIfNeeded() {
15180
15517
  if (!this.debugMode) return;
15181
15518
  try {
15182
- const dumpPath = path25.join(os6.tmpdir(), "cascade_world_state_debug.json");
15519
+ const dumpPath = path26.join(os6.tmpdir(), "cascade_world_state_debug.json");
15183
15520
  const entries = this.getAllEntries();
15184
- fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15521
+ fs25.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
15185
15522
  } catch (err) {
15186
15523
  console.error("Failed to dump debug world state", err);
15187
15524
  }
@@ -15190,6 +15527,128 @@ var WorldStateDB = class {
15190
15527
  this.db.close();
15191
15528
  }
15192
15529
  };
15530
+ var RESUME_SCHEMA_VERSION = 1;
15531
+ var DEFAULT_MAX_CHECKPOINTS = 5;
15532
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
15533
+ var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
15534
+ function safeId(taskId) {
15535
+ const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
15536
+ return cleaned.slice(0, 100) || "run";
15537
+ }
15538
+ function isCheckpoint(value) {
15539
+ if (typeof value !== "object" || value === null) return false;
15540
+ const c = value;
15541
+ 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);
15542
+ }
15543
+ var ResumeStore = class {
15544
+ dir;
15545
+ maxCheckpoints;
15546
+ maxAgeMs;
15547
+ now;
15548
+ constructor(options) {
15549
+ this.dir = options.dir;
15550
+ this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
15551
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
15552
+ this.now = options.now ?? Date.now;
15553
+ }
15554
+ /**
15555
+ * Persist a checkpoint and prune. Never throws: failing to save a resume file
15556
+ * must not turn a recoverable stop into a crash, which would be a strictly
15557
+ * worse outcome than the one this store exists to improve.
15558
+ */
15559
+ async save(checkpoint) {
15560
+ try {
15561
+ const timestamp = this.now();
15562
+ const record = {
15563
+ ...checkpoint,
15564
+ version: RESUME_SCHEMA_VERSION,
15565
+ createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
15566
+ };
15567
+ await promises.mkdir(this.dir, { recursive: true });
15568
+ const finalPath = path26.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
15569
+ const tempPath = `${finalPath}.${process.pid}.tmp`;
15570
+ await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
15571
+ await promises.rename(tempPath, finalPath);
15572
+ await this.prune();
15573
+ } catch {
15574
+ }
15575
+ }
15576
+ /** Valid, unexpired checkpoints, newest first. */
15577
+ async list() {
15578
+ let entries;
15579
+ try {
15580
+ entries = await promises.readdir(this.dir);
15581
+ } catch {
15582
+ return [];
15583
+ }
15584
+ const cutoff = this.now() - this.maxAgeMs;
15585
+ 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);
15586
+ const checkpoints = [];
15587
+ for (const candidate of candidates) {
15588
+ try {
15589
+ const raw = await promises.readFile(path26.join(this.dir, candidate.name), "utf-8");
15590
+ const parsed = JSON.parse(raw);
15591
+ if (isCheckpoint(parsed)) checkpoints.push(parsed);
15592
+ } catch {
15593
+ continue;
15594
+ }
15595
+ }
15596
+ return checkpoints;
15597
+ }
15598
+ /** The most recent resumable run, or null. */
15599
+ async latest() {
15600
+ return (await this.list())[0] ?? null;
15601
+ }
15602
+ /**
15603
+ * Load a checkpoint and delete it in the same step. Resuming twice from one
15604
+ * checkpoint would re-run restored work, so consumption is part of reading.
15605
+ */
15606
+ async consume(taskId) {
15607
+ const all = await this.list();
15608
+ const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
15609
+ if (!target) return null;
15610
+ await this.delete(target.taskId);
15611
+ return target;
15612
+ }
15613
+ /** Remove every checkpoint for a task id. */
15614
+ async delete(taskId) {
15615
+ try {
15616
+ const entries = await promises.readdir(this.dir);
15617
+ const wanted = safeId(taskId);
15618
+ await Promise.all(entries.map(async (name) => {
15619
+ const match = FILE_PATTERN.exec(name);
15620
+ if (match?.[2] === wanted) {
15621
+ await promises.rm(path26.join(this.dir, name), { force: true });
15622
+ }
15623
+ }));
15624
+ } catch {
15625
+ }
15626
+ }
15627
+ /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
15628
+ async prune() {
15629
+ try {
15630
+ const entries = await promises.readdir(this.dir);
15631
+ const cutoff = this.now() - this.maxAgeMs;
15632
+ 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);
15633
+ const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
15634
+ const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
15635
+ await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
15636
+ (name) => promises.rm(path26.join(this.dir, name), { force: true })
15637
+ ));
15638
+ } catch {
15639
+ }
15640
+ }
15641
+ };
15642
+ function summarizeCompleted(completed, perNodeChars = 500) {
15643
+ if (!completed.length) return "";
15644
+ const lines = completed.map((node) => {
15645
+ const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
15646
+ return `- [${node.status}] ${node.title}
15647
+ ${body.replace(/\n/g, "\n ")}`;
15648
+ });
15649
+ return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
15650
+ ${lines.join("\n")}`;
15651
+ }
15193
15652
  var ignore2 = ignoreFactory.default ?? ignoreFactory;
15194
15653
  var PrivacyPaths = class {
15195
15654
  localOnly;
@@ -15236,7 +15695,7 @@ var CascadeIgnore = class {
15236
15695
  ]);
15237
15696
  }
15238
15697
  async load(workspacePath) {
15239
- const filePath = path25.join(workspacePath, ".cascadeignore");
15698
+ const filePath = path26.join(workspacePath, ".cascadeignore");
15240
15699
  try {
15241
15700
  const content = await fs5.readFile(filePath, "utf-8");
15242
15701
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -15247,7 +15706,7 @@ var CascadeIgnore = class {
15247
15706
  }
15248
15707
  isIgnored(filePath, workspacePath) {
15249
15708
  try {
15250
- const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
15709
+ const relative = workspacePath ? path26.relative(workspacePath, filePath) : filePath;
15251
15710
  return this.ig.ignores(relative);
15252
15711
  } catch {
15253
15712
  return false;
@@ -15590,7 +16049,7 @@ var WorkspaceIndex = class {
15590
16049
  isIgnored;
15591
16050
  chunker;
15592
16051
  constructor(opts) {
15593
- this.root = path25.resolve(opts.root);
16052
+ this.root = path26.resolve(opts.root);
15594
16053
  this.db = opts.db;
15595
16054
  this.namespace = opts.namespace ?? "code";
15596
16055
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -15622,14 +16081,14 @@ var WorkspaceIndex = class {
15622
16081
  return;
15623
16082
  }
15624
16083
  for (const entry of entries) {
15625
- const abs = path25.join(dir, entry.name);
16084
+ const abs = path26.join(dir, entry.name);
15626
16085
  if (entry.isDirectory()) {
15627
16086
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
15628
16087
  await walk(abs);
15629
16088
  } else if (entry.isFile()) {
15630
- const ext = path25.extname(entry.name).slice(1).toLowerCase();
16089
+ const ext = path26.extname(entry.name).slice(1).toLowerCase();
15631
16090
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
15632
- out.push(path25.relative(this.root, abs));
16091
+ out.push(path26.relative(this.root, abs));
15633
16092
  }
15634
16093
  }
15635
16094
  };
@@ -15659,7 +16118,7 @@ var WorkspaceIndex = class {
15659
16118
  const entries = [];
15660
16119
  for (const rel of rels) {
15661
16120
  try {
15662
- const abs = path25.join(this.root, rel);
16121
+ const abs = path26.join(this.root, rel);
15663
16122
  const stat = await fs5.stat(abs);
15664
16123
  if (stat.size > this.maxFileBytes) continue;
15665
16124
  const buf = await fs5.readFile(abs);
@@ -16197,8 +16656,16 @@ var Cascade = class _Cascade extends EventEmitter {
16197
16656
  /** Orchestration decisions for the CURRENT run — cleared on each run(). */
16198
16657
  decisionLog = [];
16199
16658
  initialized = false;
16200
- /** Last task that stopped at the budget cap — powers /continue (resumeRun). */
16659
+ /**
16660
+ * Last interrupted task — powers /continue (resumeRun). Kept in memory as a
16661
+ * fast path for the same-process case; the durable copy lives in `resumeStore`
16662
+ * so a crash or an exit no longer throws away everything already finished.
16663
+ */
16201
16664
  lastInterruptedRun;
16665
+ /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
16666
+ resumeStore;
16667
+ /** Section results of the CURRENT run, so an interruption can checkpoint them. */
16668
+ currentRunCompleted = [];
16202
16669
  initPromise;
16203
16670
  store;
16204
16671
  audit;
@@ -16231,10 +16698,16 @@ var Cascade = class _Cascade extends EventEmitter {
16231
16698
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16232
16699
  try {
16233
16700
  this.router.setDeadModelStore(new DeadModelStore(
16234
- fileDeadModelPersistence(path25.join(workspacePath, ".cascade", "dead-models.json"))
16701
+ fileDeadModelPersistence(path26.join(workspacePath, ".cascade", "dead-models.json"))
16235
16702
  ));
16236
16703
  } catch {
16237
16704
  }
16705
+ try {
16706
+ this.resumeStore = new ResumeStore({
16707
+ dir: path26.join(workspacePath, ".cascade", "resume")
16708
+ });
16709
+ } catch {
16710
+ }
16238
16711
  this.multimodal = new MultimodalRegistry(
16239
16712
  (this.config.providers ?? []).map((p) => p.type)
16240
16713
  );
@@ -16319,8 +16792,8 @@ var Cascade = class _Cascade extends EventEmitter {
16319
16792
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
16320
16793
  return;
16321
16794
  }
16322
- const dbPath = ci.dbPath || path25.join(this.workspacePath, ".cascade", "code-index.db");
16323
- fs24.mkdirSync(path25.dirname(dbPath), { recursive: true });
16795
+ const dbPath = ci.dbPath || path26.join(this.workspacePath, ".cascade", "code-index.db");
16796
+ fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
16324
16797
  const db = new Database(dbPath);
16325
16798
  db.pragma("journal_mode = WAL");
16326
16799
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -16700,10 +17173,43 @@ ${input}` }],
16700
17173
  setFeedbackSource(source) {
16701
17174
  this.router.setFeedbackSource(source);
16702
17175
  }
16703
- /** True when a task stopped at the budget cap and can be resumed via /continue. */
17176
+ /**
17177
+ * Write a durable checkpoint for an interrupted run.
17178
+ *
17179
+ * Best-effort throughout: a run is already ending badly on every path that
17180
+ * calls this, and failing to record a resume file must not make that ending
17181
+ * worse. Nothing here is allowed to throw.
17182
+ */
17183
+ async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
17184
+ if (!this.currentRunCompleted.length && !partialOutput) return;
17185
+ try {
17186
+ await this.resumeStore?.save({
17187
+ taskId,
17188
+ prompt,
17189
+ reason,
17190
+ ...detail ? { detail } : {},
17191
+ partialOutput,
17192
+ completed: [...this.currentRunCompleted]
17193
+ });
17194
+ } catch {
17195
+ }
17196
+ }
17197
+ /** True when a task can be resumed via /continue (this process or a previous one). */
16704
17198
  hasResumableRun() {
16705
17199
  return this.lastInterruptedRun != null;
16706
17200
  }
17201
+ /**
17202
+ * True when a checkpoint from ANY process is resumable. Async because the
17203
+ * durable check touches disk; `hasResumableRun` remains the sync in-memory
17204
+ * answer for callers already on a hot path.
17205
+ */
17206
+ async hasDurableResume() {
17207
+ return await this.resumeStore?.latest() != null;
17208
+ }
17209
+ /** Interrupted runs available to resume, newest first. */
17210
+ async listResumableRuns() {
17211
+ return await this.resumeStore?.list() ?? [];
17212
+ }
16707
17213
  /**
16708
17214
  * Raise the per-run token budget for a resume and return the continuation
16709
17215
  * prompt (or null when nothing is resumable). Consumes the interrupted-run
@@ -16714,22 +17220,54 @@ ${input}` }],
16714
17220
  const last = this.lastInterruptedRun;
16715
17221
  if (!last) return null;
16716
17222
  this.lastInterruptedRun = void 0;
16717
- const raised = opts.maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
17223
+ this.raiseBudgetForResume(opts.maxTokens);
17224
+ return this.buildResumePrompt(last.prompt, last.partialOutput, []);
17225
+ }
17226
+ /**
17227
+ * Durable counterpart to prepareResume: recovers a run interrupted in ANY
17228
+ * process, including one that crashed before it could return. Consumes the
17229
+ * checkpoint, so the same finished work is never restored into two runs.
17230
+ */
17231
+ async prepareDurableResume(opts = {}) {
17232
+ const checkpoint = await this.resumeStore?.consume(opts.taskId);
17233
+ if (!checkpoint) return null;
17234
+ this.lastInterruptedRun = void 0;
17235
+ this.raiseBudgetForResume(opts.maxTokens);
17236
+ return this.buildResumePrompt(checkpoint.prompt, checkpoint.partialOutput, checkpoint.completed);
17237
+ }
17238
+ /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
17239
+ raiseBudgetForResume(maxTokens) {
17240
+ const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
16718
17241
  this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
16719
17242
  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
-
17243
+ }
17244
+ /**
17245
+ * The resume prompt. Completed sections are named explicitly rather than
17246
+ * gestured at: "do not recreate the files" is an instruction a planner can
17247
+ * only guess at, whereas a list of what is already done, and what it produced,
17248
+ * is something it can act on. That difference is the entire reason node
17249
+ * results are kept — a re-plan that cannot see the finished work will plan the
17250
+ * finished work again, and the user pays for it a second time.
17251
+ */
17252
+ buildResumePrompt(prompt, partialOutput, completed) {
17253
+ const done = summarizeCompleted(completed);
17254
+ return [
17255
+ "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.",
17256
+ "",
17257
+ `Original task: ${prompt}`,
17258
+ done ? `
17259
+ ${done}` : "",
17260
+ partialOutput ? `
16724
17261
  Partial result so far:
16725
- ${last.partialOutput}` : "");
17262
+ ${partialOutput}` : ""
17263
+ ].filter(Boolean).join("\n");
16726
17264
  }
16727
17265
  /**
16728
17266
  * Resume the last budget-capped task with a raised budget (SDK/headless).
16729
17267
  * Returns null when there is nothing to resume.
16730
17268
  */
16731
17269
  async resumeRun(opts = {}) {
16732
- const prompt = this.prepareResume(opts);
17270
+ const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
16733
17271
  if (!prompt) return null;
16734
17272
  return this.run({ prompt });
16735
17273
  }
@@ -17100,6 +17638,7 @@ ${prompt}` : prompt;
17100
17638
  const startMs = Date.now();
17101
17639
  const taskId = randomUUID();
17102
17640
  this.decisionLog = [];
17641
+ this.currentRunCompleted = [];
17103
17642
  if (options.fastAnswer) {
17104
17643
  return this.runFastAnswer(options, startMs, taskId);
17105
17644
  }
@@ -17214,6 +17753,11 @@ ${prompt}` : prompt;
17214
17753
  });
17215
17754
  tier.on("log", (e) => this.emit("log", e));
17216
17755
  tier.on("tier:status", (e) => this.emit("tier:status", e));
17756
+ tier.on("section:complete", (e) => {
17757
+ if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
17758
+ this.currentRunCompleted.push(e);
17759
+ this.emit("section:complete", e);
17760
+ });
17217
17761
  tier.on("tool:call", (e) => this.emit("tool:call", e));
17218
17762
  tier.on("tool:result", (e) => this.emit("tool:result", e));
17219
17763
  tier.on("tool:approval-request", async (request) => {
@@ -17360,6 +17904,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
17360
17904
  if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
17361
17905
  const trip = runBreaker.reason();
17362
17906
  if (trip) {
17907
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
17363
17908
  finalOutput = [
17364
17909
  `\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
17365
17910
  "",
@@ -17379,6 +17924,13 @@ ${finalOutput}` : ""
17379
17924
  reason: err instanceof Error ? err.message : "Task cancelled",
17380
17925
  partialOutput: finalOutput || ""
17381
17926
  });
17927
+ await this.checkpointRun(
17928
+ taskId,
17929
+ options.prompt,
17930
+ finalOutput || "",
17931
+ "cancelled",
17932
+ err instanceof Error ? err.message : "Task cancelled"
17933
+ );
17382
17934
  runError = null;
17383
17935
  } else if (err instanceof Error && err.name === "BudgetExceededError") {
17384
17936
  this.emit("run:budget-exceeded", {
@@ -17387,9 +17939,17 @@ ${finalOutput}` : ""
17387
17939
  partialOutput: finalOutput || ""
17388
17940
  });
17389
17941
  this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
17942
+ await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
17390
17943
  if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
17391
17944
  runError = null;
17392
17945
  } else {
17946
+ await this.checkpointRun(
17947
+ taskId,
17948
+ options.prompt,
17949
+ finalOutput || "",
17950
+ "error",
17951
+ err instanceof Error ? err.message : String(err)
17952
+ );
17393
17953
  runError = err;
17394
17954
  throw err;
17395
17955
  }
@@ -17637,7 +18197,7 @@ var Keystore = class {
17637
18197
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
17638
18198
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
17639
18199
  this.backend = "keytar";
17640
- if (password && fs24.existsSync(this.storePath)) {
18200
+ if (password && fs25.existsSync(this.storePath)) {
17641
18201
  try {
17642
18202
  const fileEntries = this.decryptFile(password);
17643
18203
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -17656,7 +18216,7 @@ var Keystore = class {
17656
18216
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
17657
18217
  );
17658
18218
  }
17659
- if (!fs24.existsSync(this.storePath)) {
18219
+ if (!fs25.existsSync(this.storePath)) {
17660
18220
  const salt = crypto4.randomBytes(SALT_LEN);
17661
18221
  this.masterKey = this.deriveKey(password, salt);
17662
18222
  this.writeWithSalt({}, salt);
@@ -17670,7 +18230,7 @@ var Keystore = class {
17670
18230
  }
17671
18231
  /** Synchronous legacy unlock kept for AES-only environments. */
17672
18232
  unlockSync(password) {
17673
- if (!fs24.existsSync(this.storePath)) {
18233
+ if (!fs25.existsSync(this.storePath)) {
17674
18234
  const salt = crypto4.randomBytes(SALT_LEN);
17675
18235
  this.masterKey = this.deriveKey(password, salt);
17676
18236
  this.writeWithSalt({}, salt);
@@ -17728,7 +18288,7 @@ var Keystore = class {
17728
18288
  }
17729
18289
  }
17730
18290
  decryptFile(password, knownSalt) {
17731
- if (!fs24.existsSync(this.storePath)) return {};
18291
+ if (!fs25.existsSync(this.storePath)) return {};
17732
18292
  try {
17733
18293
  const { salt, ciphertext, iv, tag } = this.readRaw();
17734
18294
  const useSalt = knownSalt ?? salt;
@@ -17750,8 +18310,8 @@ var Keystore = class {
17750
18310
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17751
18311
  const tag = cipher.getAuthTag();
17752
18312
  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 });
18313
+ fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
18314
+ fs25.writeFileSync(this.storePath, out, { mode: 384 });
17755
18315
  }
17756
18316
  writeWithSalt(data, salt) {
17757
18317
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -17761,11 +18321,11 @@ var Keystore = class {
17761
18321
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17762
18322
  const tag = cipher.getAuthTag();
17763
18323
  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 });
18324
+ fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
18325
+ fs25.writeFileSync(this.storePath, out, { mode: 384 });
17766
18326
  }
17767
18327
  readRaw() {
17768
- const buf = fs24.readFileSync(this.storePath);
18328
+ const buf = fs25.readFileSync(this.storePath);
17769
18329
  let offset = 0;
17770
18330
  const salt = buf.subarray(offset, offset + SALT_LEN);
17771
18331
  offset += SALT_LEN;
@@ -17781,7 +18341,7 @@ var Keystore = class {
17781
18341
  }
17782
18342
  };
17783
18343
  async function loadCascadeMd(workspacePath) {
17784
- const filePath = path25.join(workspacePath, "CASCADE.md");
18344
+ const filePath = path26.join(workspacePath, "CASCADE.md");
17785
18345
  try {
17786
18346
  const raw = await fs5.readFile(filePath, "utf-8");
17787
18347
  return parseCascadeMd(raw);
@@ -17812,7 +18372,7 @@ ${raw.trim()}`;
17812
18372
  var MemoryStore = class _MemoryStore {
17813
18373
  db;
17814
18374
  constructor(dbPath) {
17815
- fs24.mkdirSync(path25.dirname(dbPath), { recursive: true });
18375
+ fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
17816
18376
  try {
17817
18377
  this.db = new Database(dbPath, { timeout: 5e3 });
17818
18378
  this.db.pragma("journal_mode = WAL");
@@ -18648,7 +19208,7 @@ Original error: ${err.message}`
18648
19208
  }
18649
19209
  };
18650
19210
  function credentialsPath(globalDir2) {
18651
- return path25.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
19211
+ return path26.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
18652
19212
  }
18653
19213
  function providerKey(p) {
18654
19214
  if (p.type === "azure") {
@@ -18661,7 +19221,7 @@ function isPersistable(p) {
18661
19221
  }
18662
19222
  function loadGlobalCredentials(globalDir2) {
18663
19223
  try {
18664
- const raw = fs24.readFileSync(credentialsPath(globalDir2), "utf-8");
19224
+ const raw = fs25.readFileSync(credentialsPath(globalDir2), "utf-8");
18665
19225
  const parsed = JSON.parse(raw);
18666
19226
  if (!Array.isArray(parsed.providers)) return [];
18667
19227
  return parsed.providers.filter(
@@ -18673,13 +19233,13 @@ function loadGlobalCredentials(globalDir2) {
18673
19233
  }
18674
19234
  function saveGlobalCredentials(globalDir2, providers) {
18675
19235
  const filePath = credentialsPath(globalDir2);
18676
- fs24.mkdirSync(globalDir2, { recursive: true, mode: 448 });
19236
+ fs25.mkdirSync(globalDir2, { recursive: true, mode: 448 });
18677
19237
  const body = { version: 1, providers: providers.filter(isPersistable) };
18678
19238
  const tmp = `${filePath}.tmp`;
18679
- fs24.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
18680
- fs24.renameSync(tmp, filePath);
19239
+ fs25.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
19240
+ fs25.renameSync(tmp, filePath);
18681
19241
  try {
18682
- fs24.chmodSync(filePath, 384);
19242
+ fs25.chmodSync(filePath, 384);
18683
19243
  } catch {
18684
19244
  }
18685
19245
  }
@@ -18714,7 +19274,7 @@ var ConfigManager = class {
18714
19274
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
18715
19275
  constructor(workspacePath = process.cwd(), globalDirOverride) {
18716
19276
  this.workspacePath = workspacePath;
18717
- this.globalDir = globalDirOverride ?? path25.join(os6.homedir(), GLOBAL_CONFIG_DIR);
19277
+ this.globalDir = globalDirOverride ?? path26.join(os6.homedir(), GLOBAL_CONFIG_DIR);
18718
19278
  }
18719
19279
  async load() {
18720
19280
  this.config = await this.loadConfig();
@@ -18728,8 +19288,8 @@ var ConfigManager = class {
18728
19288
  this.ignore = new CascadeIgnore();
18729
19289
  await this.ignore.load(this.workspacePath);
18730
19290
  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));
19291
+ this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
19292
+ this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
18733
19293
  await this.injectEnvKeys();
18734
19294
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
18735
19295
  await this.ensureDefaultIdentity();
@@ -18754,8 +19314,8 @@ var ConfigManager = class {
18754
19314
  return this.workspacePath;
18755
19315
  }
18756
19316
  async save() {
18757
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
18758
- await fs5.mkdir(path25.dirname(configPath), { recursive: true });
19317
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
19318
+ await fs5.mkdir(path26.dirname(configPath), { recursive: true });
18759
19319
  await fs5.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
18760
19320
  try {
18761
19321
  saveGlobalCredentials(this.globalDir, this.config.providers);
@@ -18791,7 +19351,7 @@ var ConfigManager = class {
18791
19351
  return configProvider?.apiKey;
18792
19352
  }
18793
19353
  async loadConfig() {
18794
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
19354
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
18795
19355
  try {
18796
19356
  const raw = await fs5.readFile(configPath, "utf-8");
18797
19357
  return validateConfig(JSON.parse(raw));
@@ -19323,7 +19883,7 @@ function aggregateCostStats(sessions, opts = {}) {
19323
19883
  }
19324
19884
 
19325
19885
  // src/dashboard/server.ts
19326
- var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
19886
+ var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
19327
19887
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
19328
19888
  var DashboardServer = class {
19329
19889
  app;
@@ -19603,14 +20163,14 @@ var DashboardServer = class {
19603
20163
  */
19604
20164
  persistConfig() {
19605
20165
  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");
20166
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
20167
+ fs25.mkdirSync(path26.dirname(configPath), { recursive: true });
20168
+ fs25.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
19609
20169
  } catch (err) {
19610
20170
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
19611
20171
  }
19612
20172
  try {
19613
- saveGlobalCredentials(path25.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
20173
+ saveGlobalCredentials(path26.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
19614
20174
  } catch (err) {
19615
20175
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
19616
20176
  }
@@ -19625,15 +20185,15 @@ var DashboardServer = class {
19625
20185
  resolveDashboardSecret() {
19626
20186
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
19627
20187
  if (fromConfig) return fromConfig;
19628
- const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
20188
+ const secretPath = path26.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
19629
20189
  try {
19630
- if (fs24.existsSync(secretPath)) {
19631
- const existing = fs24.readFileSync(secretPath, "utf-8").trim();
20190
+ if (fs25.existsSync(secretPath)) {
20191
+ const existing = fs25.readFileSync(secretPath, "utf-8").trim();
19632
20192
  if (existing.length >= 16) return existing;
19633
20193
  }
19634
20194
  const generated = randomUUID();
19635
- fs24.mkdirSync(path25.dirname(secretPath), { recursive: true });
19636
- fs24.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
20195
+ fs25.mkdirSync(path26.dirname(secretPath), { recursive: true });
20196
+ fs25.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
19637
20197
  if (this.config.dashboard.auth) {
19638
20198
  console.warn(
19639
20199
  `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 +20220,7 @@ var DashboardServer = class {
19660
20220
  // ── Setup ─────────────────────────────────────
19661
20221
  getGlobalStore() {
19662
20222
  if (!this.globalStore) {
19663
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20223
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
19664
20224
  this.globalStore = new MemoryStore(globalDbPath);
19665
20225
  }
19666
20226
  return this.globalStore;
@@ -20040,12 +20600,12 @@ ${prompt}`;
20040
20600
  }
20041
20601
  }
20042
20602
  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);
20603
+ const workspaceDbPath = path26.join(this.workspacePath, CASCADE_DB_FILE);
20604
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20045
20605
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
20046
20606
  for (const watchPath of watchPaths) {
20047
- if (!fs24.existsSync(watchPath)) continue;
20048
- fs24.watchFile(watchPath, { interval: 3e3 }, () => {
20607
+ if (!fs25.existsSync(watchPath)) continue;
20608
+ fs25.watchFile(watchPath, { interval: 3e3 }, () => {
20049
20609
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
20050
20610
  });
20051
20611
  }
@@ -20167,7 +20727,7 @@ ${prompt}`;
20167
20727
  const sessionId = req.params.id;
20168
20728
  this.store.deleteSession(sessionId);
20169
20729
  this.store.deleteRuntimeSession(sessionId);
20170
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20730
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20171
20731
  const globalStore = new MemoryStore(globalDbPath);
20172
20732
  try {
20173
20733
  globalStore.deleteRuntimeSession(sessionId);
@@ -20260,7 +20820,7 @@ ${prompt}`;
20260
20820
  });
20261
20821
  this.app.delete("/api/sessions", auth2, (req, res) => {
20262
20822
  const body = req.body;
20263
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20823
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20264
20824
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
20265
20825
  const globalStore = new MemoryStore(globalDbPath);
20266
20826
  try {
@@ -20283,7 +20843,7 @@ ${prompt}`;
20283
20843
  });
20284
20844
  this.app.delete("/api/runtime", auth2, (_req, res) => {
20285
20845
  this.store.deleteAllRuntimeNodes();
20286
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20846
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20287
20847
  const globalStore = new MemoryStore(globalDbPath);
20288
20848
  try {
20289
20849
  globalStore.deleteAllRuntimeNodes();
@@ -20369,12 +20929,12 @@ ${prompt}`;
20369
20929
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
20370
20930
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
20371
20931
  try {
20372
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
20373
- const existing = fs24.existsSync(configPath) ? JSON.parse(fs24.readFileSync(configPath, "utf-8")) : {};
20932
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
20933
+ const existing = fs25.existsSync(configPath) ? JSON.parse(fs25.readFileSync(configPath, "utf-8")) : {};
20374
20934
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
20375
20935
  const tmp = configPath + ".tmp";
20376
- fs24.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20377
- fs24.renameSync(tmp, configPath);
20936
+ fs25.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
20937
+ fs25.renameSync(tmp, configPath);
20378
20938
  res.json({ ok: true });
20379
20939
  } catch (err) {
20380
20940
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -20402,7 +20962,7 @@ ${prompt}`;
20402
20962
  this.app.get("/api/runtime", auth2, (req, res) => {
20403
20963
  const scope = req.query["scope"] ?? "workspace";
20404
20964
  if (scope === "global") {
20405
- const globalDbPath = path25.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20965
+ const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
20406
20966
  const globalStore = new MemoryStore(globalDbPath);
20407
20967
  try {
20408
20968
  res.json({
@@ -20743,13 +21303,13 @@ ${prompt}`;
20743
21303
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
20744
21304
  }
20745
21305
  });
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)) {
21306
+ const prodPath = path26.resolve(__dirname$1, "../web/dist");
21307
+ const devPath = path26.resolve(__dirname$1, "../../web/dist");
21308
+ const webDistPath = fs25.existsSync(prodPath) ? prodPath : devPath;
21309
+ if (fs25.existsSync(webDistPath)) {
20750
21310
  this.app.use(express.static(webDistPath));
20751
21311
  this.app.get("*", (_req, res) => {
20752
- res.sendFile(path25.join(webDistPath, "index.html"));
21312
+ res.sendFile(path26.join(webDistPath, "index.html"));
20753
21313
  });
20754
21314
  } else {
20755
21315
  this.app.get("/", (_req, res) => {
@@ -20936,14 +21496,14 @@ function planRetrieval(ctx) {
20936
21496
  }
20937
21497
  var CLOUD_SESSION_FILE = "cloud-session.json";
20938
21498
  function globalDir(homeDir = os6.homedir()) {
20939
- return path25.join(homeDir, GLOBAL_CONFIG_DIR);
21499
+ return path26.join(homeDir, GLOBAL_CONFIG_DIR);
20940
21500
  }
20941
21501
  function cloudSessionPath(dir = globalDir()) {
20942
- return path25.join(dir, CLOUD_SESSION_FILE);
21502
+ return path26.join(dir, CLOUD_SESSION_FILE);
20943
21503
  }
20944
21504
  function loadCloudSession(dir = globalDir()) {
20945
21505
  try {
20946
- const raw = fs24.readFileSync(cloudSessionPath(dir), "utf-8");
21506
+ const raw = fs25.readFileSync(cloudSessionPath(dir), "utf-8");
20947
21507
  const parsed = JSON.parse(raw);
20948
21508
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20949
21509
  return parsed;
@@ -20952,17 +21512,17 @@ function loadCloudSession(dir = globalDir()) {
20952
21512
  }
20953
21513
  }
20954
21514
  function saveCloudSession(session, dir = globalDir()) {
20955
- fs24.mkdirSync(dir, { recursive: true, mode: 448 });
21515
+ fs25.mkdirSync(dir, { recursive: true, mode: 448 });
20956
21516
  const file = cloudSessionPath(dir);
20957
- fs24.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21517
+ fs25.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20958
21518
  try {
20959
- fs24.chmodSync(file, 384);
21519
+ fs25.chmodSync(file, 384);
20960
21520
  } catch {
20961
21521
  }
20962
21522
  }
20963
21523
  function clearCloudSession(dir = globalDir()) {
20964
21524
  try {
20965
- fs24.rmSync(cloudSessionPath(dir));
21525
+ fs25.rmSync(cloudSessionPath(dir));
20966
21526
  } catch {
20967
21527
  }
20968
21528
  }
@@ -21134,16 +21694,16 @@ var CloudClient = class _CloudClient {
21134
21694
  return (await this.refresh()).accessToken;
21135
21695
  }
21136
21696
  /** 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}` } });
21697
+ async authed(path28, init = {}) {
21698
+ const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21139
21699
  let res = await call(await this.accessToken());
21140
21700
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21141
21701
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21142
21702
  const text = await res.text();
21143
21703
  return text ? JSON.parse(text) : {};
21144
21704
  }
21145
- authedGet(path27) {
21146
- return this.authed(path27);
21705
+ authedGet(path28) {
21706
+ return this.authed(path28);
21147
21707
  }
21148
21708
  async fetchMe(token) {
21149
21709
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21480,6 +22040,6 @@ function applySyncBundle(bundle, config) {
21480
22040
  return next;
21481
22041
  }
21482
22042
 
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 };
22043
+ 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
22044
  //# sourceMappingURL=index.js.map
21485
22045
  //# sourceMappingURL=index.js.map