cascade-ai 0.54.0 → 0.55.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
@@ -1851,7 +1851,7 @@ function reconcilePrice(model, live) {
1851
1851
  }
1852
1852
 
1853
1853
  // src/constants.ts
1854
- var CASCADE_VERSION = "0.54.0";
1854
+ var CASCADE_VERSION = "0.55.0";
1855
1855
  var CASCADE_CONFIG_DIR = ".cascade";
1856
1856
  var CASCADE_MD_FILE = "CASCADE.md";
1857
1857
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -4789,6 +4789,23 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4789
4789
  */
4790
4790
  budgetState = "ok";
4791
4791
  budgetExceededReason;
4792
+ /**
4793
+ * Whether the run/session budget is permanently tripped, and why.
4794
+ *
4795
+ * A tier or worker that catches `BudgetExceededError` internally (T3Worker
4796
+ * does, so one dead-end subtask doesn't crash the whole run) absorbs the
4797
+ * exception into a normal FAILED/PARTIAL result — the tier returns as if
4798
+ * nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
4799
+ * budget-exceeded handling (the `run:budget-exceeded` event and
4800
+ * `lastInterruptedRun` for `/continue`). This lets a caller check the
4801
+ * router's own state directly after a tier returns, regardless of how that
4802
+ * tier chose to report the error internally.
4803
+ */
4804
+ budgetExceededInfo() {
4805
+ if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
4806
+ if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
4807
+ return null;
4808
+ }
4792
4809
  tpmLimiter;
4793
4810
  localQueue;
4794
4811
  taskAnalyzer;
@@ -6446,6 +6463,13 @@ Now execute your subtask using this context where relevant.`
6446
6463
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
6447
6464
  return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
6448
6465
  }
6466
+ if (err instanceof Error && err.name === "BudgetExceededError") {
6467
+ issues.push(errMsg);
6468
+ const stopped = output || errMsg;
6469
+ this.setStatus("FAILED", stopped);
6470
+ this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
6471
+ return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
6472
+ }
6449
6473
  issues.push(`Execution error: ${errMsg}`);
6450
6474
  const finalOutput = output || errMsg;
6451
6475
  this.setStatus("FAILED", finalOutput);
@@ -7467,6 +7491,26 @@ var RedactionLayer = class {
7467
7491
  }
7468
7492
  };
7469
7493
 
7494
+ // src/core/tiers/escalation-policy.ts
7495
+ function sectionNeedsDecision(results) {
7496
+ return results.some((r) => r.status === "ESCALATED");
7497
+ }
7498
+ function settledEscalationStatus(results) {
7499
+ const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
7500
+ return hasWork ? "PARTIAL" : "FAILED";
7501
+ }
7502
+ function composeRootSectionOutput(t2Result, completedOutputs) {
7503
+ let output = `${t2Result.sectionSummary}
7504
+
7505
+ ${completedOutputs.join("\n\n")}`;
7506
+ if (t2Result.status === "FAILED" && t2Result.issues?.length) {
7507
+ output += `
7508
+
7509
+ _(incomplete: ${t2Result.issues.join("; ")})_`;
7510
+ }
7511
+ return output;
7512
+ }
7513
+
7470
7514
  // src/core/tiers/t2-manager.ts
7471
7515
  function buildT2SystemPrompt(hasPeerMessage) {
7472
7516
  return [
@@ -7492,6 +7536,19 @@ var T2Manager = class extends BaseTier {
7492
7536
  t2PeerBus;
7493
7537
  permissionEscalator;
7494
7538
  toolCreator;
7539
+ /**
7540
+ * Asked when a section ends ESCALATED — i.e. a worker hit something it could
7541
+ * not decide alone. Without this the escalation was a dead end: the status
7542
+ * said "needs a decision" and no decision was ever requested, so an MCP run
7543
+ * that legitimately needed input just stopped there.
7544
+ */
7545
+ escalationCallback;
7546
+ /**
7547
+ * The one allowed retry has been spent. Retrying is bounded because an
7548
+ * escalation loop that can re-ask forever burns a budget on a question that
7549
+ * is not resolving; a second escalation is terminal instead.
7550
+ */
7551
+ escalationRetryUsed = false;
7495
7552
  /** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
7496
7553
  planApprovalCallback;
7497
7554
  /** AbortController for the current T3 wave — aborted on cancel-and-respawn */
@@ -7540,6 +7597,10 @@ var T2Manager = class extends BaseTier {
7540
7597
  setToolCreator(creator) {
7541
7598
  this.toolCreator = creator;
7542
7599
  }
7600
+ /** Ask the user what to do when a section escalates. */
7601
+ setEscalationCallback(cb) {
7602
+ this.escalationCallback = cb;
7603
+ }
7543
7604
  /** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
7544
7605
  setPlanApprovalCallback(cb) {
7545
7606
  this.planApprovalCallback = cb;
@@ -7644,9 +7705,57 @@ Guidance (must be followed): ${decision.note}`
7644
7705
  currentAction: "Aggregating T3 results",
7645
7706
  status: "IN_PROGRESS"
7646
7707
  });
7647
- const summary = await this.aggregateResults(assignment, t3Results);
7708
+ let summary = await this.aggregateResults(assignment, t3Results);
7648
7709
  const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
7649
- const overallStatus = this.determineStatus(t3Results);
7710
+ let overallStatus = this.determineStatus(t3Results);
7711
+ let userSkipped = false;
7712
+ const hasEscalated = sectionNeedsDecision(t3Results);
7713
+ const settleEscalated = async (reason) => {
7714
+ if (reason) issues.push(reason);
7715
+ summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
7716
+ overallStatus = settledEscalationStatus(t3Results);
7717
+ };
7718
+ if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
7719
+ await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
7720
+ } else if (hasEscalated && !this.escalationCallback) {
7721
+ await settleEscalated();
7722
+ } else if (hasEscalated && this.escalationCallback) {
7723
+ this.sendStatusUpdate({
7724
+ progressPct: 95,
7725
+ currentAction: "Escalated \u2014 waiting for your decision",
7726
+ status: "ESCALATING"
7727
+ });
7728
+ const decision = await this.escalationCallback({
7729
+ sectionId: assignment.sectionId,
7730
+ sectionTitle: assignment.sectionTitle,
7731
+ issues,
7732
+ summary
7733
+ });
7734
+ this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
7735
+ if (decision.action === "skip") {
7736
+ summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
7737
+ overallStatus = settledEscalationStatus(t3Results);
7738
+ userSkipped = decision.automatic !== true;
7739
+ } else if (decision.action === "retry" || decision.action === "guidance") {
7740
+ this.escalationRetryUsed = true;
7741
+ const guided = decision.action === "guidance" && decision.note?.trim() ? {
7742
+ ...assignment,
7743
+ description: `${assignment.description}
7744
+
7745
+ Guidance (must be followed): ${decision.note.trim()}`,
7746
+ // Drop T1's preplanned subtasks so the retry re-decomposes with
7747
+ // the guidance in hand. The workers execute from their subtask
7748
+ // slices, not from this description — leaving the slices in
7749
+ // place gave "Retry with guidance" byte-identical worker
7750
+ // prompts to "Retry as-is", making the note do nothing at all.
7751
+ t3Subtasks: []
7752
+ } : assignment;
7753
+ return await this.execute(guided, taskId, signal);
7754
+ } else {
7755
+ issues.push("Escalated, but no decision was received in time.");
7756
+ overallStatus = "FAILED";
7757
+ }
7758
+ }
7650
7759
  const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
7651
7760
  this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
7652
7761
  const sectionAction = overallStatus === "COMPLETED" ? "Section complete" : overallStatus === "PARTIAL" ? "Section partially complete \u2014 some subtasks failed" : overallStatus === "ESCALATED" ? "Section escalated \u2014 needs a decision" : "Section failed \u2014 no subtask completed";
@@ -7657,7 +7766,8 @@ Guidance (must be followed): ${decision.note}`
7657
7766
  status: overallStatus,
7658
7767
  t3Results,
7659
7768
  sectionSummary: summary,
7660
- issues
7769
+ issues,
7770
+ ...userSkipped ? { userSkipped: true } : {}
7661
7771
  };
7662
7772
  this.publishSectionOutput(result);
7663
7773
  return result;
@@ -8091,8 +8201,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8091
8201
  result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
8092
8202
  );
8093
8203
  }
8094
- async aggregateResults(assignment, results) {
8095
- const completed = results.filter((r) => r.status === "COMPLETED");
8204
+ async aggregateResults(assignment, results, opts = {}) {
8205
+ const completed = results.filter(
8206
+ (r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
8207
+ );
8096
8208
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
8097
8209
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
8098
8210
  const peerContext = peerOutputs ? `
@@ -8298,6 +8410,7 @@ var T1Administrator = class extends BaseTier {
8298
8410
  escalations = [];
8299
8411
  store;
8300
8412
  runBreaker;
8413
+ escalationCallback;
8301
8414
  t2PeerBus = new PeerBus();
8302
8415
  permissionEscalator;
8303
8416
  toolCreator;
@@ -8312,6 +8425,10 @@ var T1Administrator = class extends BaseTier {
8312
8425
  this.toolRegistry = toolRegistry;
8313
8426
  this.config = config;
8314
8427
  }
8428
+ /** Ask the user what to do when any section escalates. */
8429
+ setEscalationCallback(cb) {
8430
+ this.escalationCallback = cb;
8431
+ }
8315
8432
  /** Share the run-wide circuit breaker with every section manager. */
8316
8433
  setRunBreaker(breaker) {
8317
8434
  this.runBreaker = breaker;
@@ -8471,8 +8588,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
8471
8588
  reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
8472
8589
  };
8473
8590
  }
8474
- const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
8475
- ${r.sectionSummary}`).join("\n\n");
8591
+ const sectionsText = t2Results.map((r) => {
8592
+ const skipNote = r.userSkipped ? "\n[The user was asked to retry or skip this section and explicitly chose to SKIP it. This is an intentional decision, not a failure \u2014 do not cite it as a reason to reject.]" : "";
8593
+ return `**${r.sectionTitle}**
8594
+ ${r.sectionSummary}${skipNote}`;
8595
+ }).join("\n\n");
8476
8596
  const prompt = `You are a strict QA Reviewer for the Cascade AI system.
8477
8597
  Review the following execution outputs against the original user prompt.
8478
8598
 
@@ -8482,6 +8602,7 @@ T2 Manager Summaries:
8482
8602
  ${sectionsText}
8483
8603
 
8484
8604
  Does the current state of the workspace and the outputs fully satisfy the user's request?
8605
+ A section marked as user-skipped above is accepted as-is by the user's own choice \u2014 treat it as satisfied, not as a gap.
8485
8606
  If yes, reply with exactly: "APPROVED".
8486
8607
  If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
8487
8608
  try {
@@ -8679,6 +8800,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
8679
8800
  const managers = sections.map((section) => {
8680
8801
  const manager = new T2Manager(this.router, this.toolRegistry, this.id);
8681
8802
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
8803
+ if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
8682
8804
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
8683
8805
  if (this.store) {
8684
8806
  manager.setStore(this.store);
@@ -8888,7 +9010,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
8888
9010
  async compileFinalOutput(originalPrompt, plan, t2Results) {
8889
9011
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
8890
9012
  if (!completedSections.length) {
8891
- const allIssues = t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues));
9013
+ const allIssues = [
9014
+ ...t2Results.flatMap((r) => r.issues ?? []),
9015
+ ...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
9016
+ ].filter(Boolean);
8892
9017
  const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
8893
9018
  const stalled = allIssues.find((i) => /^Stalled:/.test(i));
8894
9019
  const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
@@ -9697,6 +9822,102 @@ var RunBreaker = class {
9697
9822
  return out;
9698
9823
  }
9699
9824
  };
9825
+ var MCP_TOOL_PREFIX = "mcp__";
9826
+ var MAX_SEGMENT_LENGTH = 24;
9827
+ var SEGMENT_HASH_LENGTH = 8;
9828
+ function boundedSegment(raw) {
9829
+ const clean = sanitizeToolNameSegment(raw);
9830
+ if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
9831
+ const hash = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
9832
+ return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
9833
+ }
9834
+ function isStableSegment(raw) {
9835
+ return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
9836
+ }
9837
+ function sanitizeToolNameSegment(segment) {
9838
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
9839
+ let start = 0;
9840
+ let end = cleaned.length;
9841
+ while (start < end && cleaned[start] === "_") start++;
9842
+ while (end > start && cleaned[end - 1] === "_") end--;
9843
+ return cleaned.slice(start, end) || "_";
9844
+ }
9845
+ function mcpToolName(serverName, toolName) {
9846
+ return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
9847
+ }
9848
+ function isMcpToolName(name) {
9849
+ return name.startsWith(MCP_TOOL_PREFIX);
9850
+ }
9851
+ function uniqueMcpServerName(desired, existingNames) {
9852
+ const taken = new Set(existingNames.map(mcpServerPrefix));
9853
+ if (!taken.has(mcpServerPrefix(desired))) return desired;
9854
+ for (let n = 2; n < 1e3; n++) {
9855
+ const candidate = `${desired} (${n})`;
9856
+ if (!taken.has(mcpServerPrefix(candidate))) return candidate;
9857
+ }
9858
+ return `${desired} (${randomUUID().slice(0, 8)})`;
9859
+ }
9860
+ function disambiguateMcpServerNames(servers) {
9861
+ const seenNames = [];
9862
+ const renames = [];
9863
+ const result = servers.map((s) => {
9864
+ const name = uniqueMcpServerName(s.name, seenNames);
9865
+ seenNames.push(name);
9866
+ if (name === s.name) return s;
9867
+ renames.push({ from: s.name, to: name });
9868
+ return { ...s, name };
9869
+ });
9870
+ return { servers: renames.length ? result : servers, renames };
9871
+ }
9872
+ function assignMcpToolNames(tools) {
9873
+ const names = new Array(tools.length);
9874
+ const bases = tools.map((t) => mcpToolName(t.server, t.tool));
9875
+ const groups = /* @__PURE__ */ new Map();
9876
+ bases.forEach((base, i) => {
9877
+ const g = groups.get(base);
9878
+ if (g) g.push(i);
9879
+ else groups.set(base, [i]);
9880
+ });
9881
+ const used = new Set(bases);
9882
+ for (const [base, idx] of groups) {
9883
+ if (idx.length === 1) {
9884
+ names[idx[0]] = base;
9885
+ continue;
9886
+ }
9887
+ const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
9888
+ const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
9889
+ const ordered = [...idx].sort((a, b) => {
9890
+ const la = isClean(a) ? 0 : 1;
9891
+ const lb = isClean(b) ? 0 : 1;
9892
+ if (la !== lb) return la - lb;
9893
+ const ra = raw(a);
9894
+ const rb = raw(b);
9895
+ return ra < rb ? -1 : ra > rb ? 1 : 0;
9896
+ });
9897
+ names[ordered[0]] = base;
9898
+ let n = 2;
9899
+ for (let rank3 = 1; rank3 < ordered.length; rank3++) {
9900
+ let candidate = `${base}_${n}`;
9901
+ while (used.has(candidate)) {
9902
+ n++;
9903
+ candidate = `${base}_${n}`;
9904
+ }
9905
+ names[ordered[rank3]] = candidate;
9906
+ used.add(candidate);
9907
+ n++;
9908
+ }
9909
+ }
9910
+ return names;
9911
+ }
9912
+ function mcpServerPrefix(serverName) {
9913
+ return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
9914
+ }
9915
+ function removeMcpServerDenials(disabledTools, serverName) {
9916
+ if (!disabledTools?.length) return disabledTools;
9917
+ const prefix = mcpServerPrefix(serverName);
9918
+ const kept = disabledTools.filter((t) => !t.startsWith(prefix));
9919
+ return kept.length === disabledTools.length ? disabledTools : kept;
9920
+ }
9700
9921
  var execAsync = promisify(exec);
9701
9922
  var ShellTool = class extends BaseTool {
9702
9923
  name = "shell";
@@ -11112,20 +11333,6 @@ ${text}`;
11112
11333
  }
11113
11334
  };
11114
11335
 
11115
- // src/tools/tool-name.ts
11116
- var MCP_TOOL_PREFIX = "mcp__";
11117
- function sanitizeToolNameSegment(segment) {
11118
- const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
11119
- let start = 0;
11120
- let end = cleaned.length;
11121
- while (start < end && cleaned[start] === "_") start++;
11122
- while (end > start && cleaned[end - 1] === "_") end--;
11123
- return cleaned.slice(start, end) || "_";
11124
- }
11125
- function mcpToolName(serverName, toolName) {
11126
- return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
11127
- }
11128
-
11129
11336
  // src/tools/mcp.ts
11130
11337
  var McpToolWrapper = class extends BaseTool {
11131
11338
  name;
@@ -11134,12 +11341,12 @@ var McpToolWrapper = class extends BaseTool {
11134
11341
  mcpClient;
11135
11342
  serverName;
11136
11343
  toolName;
11137
- constructor(mcpClient, serverName, toolName, description, inputSchema) {
11344
+ constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
11138
11345
  super();
11139
11346
  this.mcpClient = mcpClient;
11140
11347
  this.serverName = serverName;
11141
11348
  this.toolName = toolName;
11142
- this.name = mcpToolName(serverName, toolName);
11349
+ this.name = registeredName ?? mcpToolName(serverName, toolName);
11143
11350
  this.description = `[MCP:${serverName}] ${description}`;
11144
11351
  this.inputSchema = inputSchema;
11145
11352
  }
@@ -11210,22 +11417,35 @@ var ToolRegistry = class extends EventEmitter {
11210
11417
  getRegisteredPlugins() {
11211
11418
  return Array.from(this.plugins.keys());
11212
11419
  }
11213
- /** Registers all tools from an MCP client */
11214
- registerMcpTools(mcpClient) {
11215
- const definitions = mcpClient.getToolDefinitions();
11216
- for (const def of definitions) {
11217
- const [, serverName, toolName] = def.name.split("::");
11218
- if (!serverName || !toolName) continue;
11420
+ /**
11421
+ * Registers all tools from an MCP client.
11422
+ *
11423
+ * `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
11424
+ * same string the user sees and the same one that reaches the provider — so
11425
+ * a per-tool opt-out in Settings can be expressed as a plain name list.
11426
+ * Filtering here rather than at call time means a deselected tool is genuinely
11427
+ * absent from the definitions handed to the model, so it is never proposed,
11428
+ * never counted against the tool budget, and never has to be refused.
11429
+ */
11430
+ registerMcpTools(mcpClient, isAllowed) {
11431
+ const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
11432
+ const [, server, tool] = def.name.split("::");
11433
+ return server && tool ? [{ def, server, tool }] : [];
11434
+ });
11435
+ const names = assignMcpToolNames(parsed);
11436
+ parsed.forEach(({ def, server, tool }, i) => {
11219
11437
  const wrapper = new McpToolWrapper(
11220
11438
  mcpClient,
11221
- serverName,
11222
- toolName,
11223
- def.description.replace(`[MCP:${serverName}] `, ""),
11224
- def.inputSchema
11439
+ server,
11440
+ tool,
11441
+ def.description.replace(`[MCP:${server}] `, ""),
11442
+ def.inputSchema,
11443
+ names[i]
11225
11444
  );
11445
+ if (isAllowed && !isAllowed(wrapper.name)) return;
11226
11446
  wrapper.setWorkspaceRoot(this.workspaceRoot);
11227
11447
  this.register(wrapper);
11228
- }
11448
+ });
11229
11449
  }
11230
11450
  setIgnoredPaths(patterns) {
11231
11451
  this.ignoreMatcher = ignore().add(patterns);
@@ -11562,6 +11782,17 @@ function fileOAuthProvider(storePath) {
11562
11782
  }
11563
11783
  });
11564
11784
  }
11785
+ var oauthStoreLocks = /* @__PURE__ */ new Map();
11786
+ function withOAuthStoreLock(storePath, fn) {
11787
+ const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
11788
+ const run = prev.then(fn, fn);
11789
+ const tail = run.then(() => void 0, () => void 0);
11790
+ oauthStoreLocks.set(storePath, tail);
11791
+ void tail.then(() => {
11792
+ if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
11793
+ });
11794
+ return run;
11795
+ }
11565
11796
  function beginMcpOAuth(provider, serverUrl) {
11566
11797
  return auth(provider, { serverUrl });
11567
11798
  }
@@ -14500,7 +14731,50 @@ var GuidanceQueue = class {
14500
14731
  }
14501
14732
  };
14502
14733
 
14734
+ // src/tools/current-page.ts
14735
+ var MAX_PAGE_TEXT_CHARS = 4e4;
14736
+ var CurrentPageTool = class extends BaseTool {
14737
+ name = "read_current_page";
14738
+ description = 'Read the web page the user currently has open in the built-in browser \u2014 its URL, title, and visible text. Use this whenever the user refers to "this page", "what I am looking at", or asks about something on screen. Prefer it over web_fetch for the open page: it sees content behind a login and content rendered by JavaScript, which web_fetch cannot.';
14739
+ inputSchema = {
14740
+ type: "object",
14741
+ properties: {},
14742
+ required: []
14743
+ };
14744
+ provider;
14745
+ constructor(provider) {
14746
+ super();
14747
+ this.provider = provider;
14748
+ }
14749
+ // Reading what is already on the user's screen changes nothing and reaches
14750
+ // nothing they have not already opened themselves.
14751
+ isDangerous() {
14752
+ return false;
14753
+ }
14754
+ async execute(_input, _options) {
14755
+ let page2;
14756
+ try {
14757
+ page2 = await this.provider();
14758
+ } catch (err) {
14759
+ return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
14760
+ }
14761
+ if (!page2) {
14762
+ return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
14763
+ }
14764
+ const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
14765
+
14766
+ [truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
14767
+ return [
14768
+ `URL: ${page2.url}`,
14769
+ `Title: ${page2.title}`,
14770
+ "",
14771
+ text.trim() || "[the page rendered no readable text]"
14772
+ ].join("\n");
14773
+ }
14774
+ };
14775
+
14503
14776
  // src/core/cascade.ts
14777
+ var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
14504
14778
  function buildContextualPrompt(prompt, history = []) {
14505
14779
  const recent = history.slice(-6);
14506
14780
  if (recent.length === 0) return prompt;
@@ -14723,6 +14997,94 @@ var Cascade = class _Cascade extends EventEmitter {
14723
14997
  });
14724
14998
  });
14725
14999
  }
15000
+ // ── Escalation decisions ────────────────────────────────────────────
15001
+ //
15002
+ // A section that ends ESCALATED used to be a dead end: the Cockpit showed
15003
+ // "Section escalated — needs a decision" and no decision was ever requested,
15004
+ // so the run stopped there having spent a full orchestration. This is the
15005
+ // same round-trip shape as plan approval, with one deliberate difference —
15006
+ // an unanswered escalation FAILS the section instead of proceeding.
15007
+ //
15008
+ // That direction is the safe one here. Plan approval defaults to "yes" on
15009
+ // timeout because the plan is already the model's considered proposal;
15010
+ // an escalation exists precisely BECAUSE the worker was not confident, so
15011
+ // acting unattended is the option most likely to be wrong. And in cloud a
15012
+ // waiting run holds server resources, so hanging is not free either.
15013
+ // Keyed by request id, NOT a single slot. A Complex run dispatches each wave
15014
+ // of ready sections with `Promise.all` (t1-administrator.ts), so two sections
15015
+ // can be waiting on a decision at the same moment. A single field made that
15016
+ // case fail hard rather than merely confusingly: the second escalation
15017
+ // overwrote the first's resolver, the user's answer resolved only the second,
15018
+ // and then the FIRST section's timer found the slot already empty and
15019
+ // returned without resolving anything — parking the run forever, past its own
15020
+ // timeout. Same class of bug as the twelve-failover dead-model race: a wave
15021
+ // is concurrent, so per-run singletons are never safe inside it.
15022
+ pendingEscalations = /* @__PURE__ */ new Map();
15023
+ async requestEscalationDecision(ctx, taskId, signal) {
15024
+ if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
15025
+ if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
15026
+ if (signal?.aborted) return { action: "skip", automatic: true };
15027
+ const requestId = randomUUID();
15028
+ return await new Promise((resolve2) => {
15029
+ let timeout;
15030
+ const settle = (decision) => {
15031
+ if (!this.pendingEscalations.delete(requestId)) return;
15032
+ if (timeout) clearTimeout(timeout);
15033
+ signal?.removeEventListener("abort", onAbort);
15034
+ resolve2(decision);
15035
+ };
15036
+ const onAbort = () => settle({ action: "skip", automatic: true });
15037
+ signal?.addEventListener("abort", onAbort, { once: true });
15038
+ timeout = setTimeout(() => {
15039
+ this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
15040
+ settle({ action: "timeout" });
15041
+ }, ESCALATION_DECISION_TIMEOUT_MS);
15042
+ this.pendingEscalations.set(requestId, settle);
15043
+ this.emit("escalation:decision-required", {
15044
+ taskId,
15045
+ requestId,
15046
+ sectionId: ctx.sectionId,
15047
+ sectionTitle: ctx.sectionTitle,
15048
+ issues: ctx.issues,
15049
+ summary: ctx.summary,
15050
+ timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
15051
+ });
15052
+ });
15053
+ }
15054
+ /**
15055
+ * Resolve a pending escalation from a REPL / dashboard / cloud listener.
15056
+ *
15057
+ * `requestId` comes back from the `escalation:decision-required` event and is
15058
+ * what makes an answer land on the section that asked. It is optional so a
15059
+ * host with only one escalation in flight (and any caller that predates the
15060
+ * id) still works: without it the oldest outstanding request is answered,
15061
+ * which is correct whenever there is exactly one.
15062
+ *
15063
+ * `automatic` defaults to false — a call into this method is normally a real
15064
+ * person answering the prompt. Pass `true` from a host that is itself
15065
+ * settling the gate on nobody's behalf (a REST caller who never had a
15066
+ * chance to connect, a session halt with no per-section review behind it),
15067
+ * so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
15068
+ * see EscalationDecision.automatic.
15069
+ */
15070
+ resolveEscalation(action, note, requestId, automatic) {
15071
+ const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
15072
+ if (requestId) {
15073
+ this.pendingEscalations.get(requestId)?.(decision);
15074
+ return;
15075
+ }
15076
+ const oldest = this.pendingEscalations.keys().next();
15077
+ if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
15078
+ }
15079
+ /**
15080
+ * Release every parked escalation — run teardown, so none outlive the run.
15081
+ * `automatic: true`: teardown resolving a section nobody answered is the
15082
+ * same system-decided case as the early-return skips above, not a person
15083
+ * reviewing and accepting that section's output.
15084
+ */
15085
+ releasePendingEscalations(action = "skip") {
15086
+ for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
15087
+ }
14726
15088
  /**
14727
15089
  * Resolve a pending boardroom plan approval from a REPL / dashboard listener.
14728
15090
  * An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
@@ -14856,6 +15218,20 @@ ${input}` }],
14856
15218
  this.mediaSink = sink;
14857
15219
  this.registerMediaTools(this.workspacePath);
14858
15220
  }
15221
+ /**
15222
+ * Let the agent read the page the user has open in the host's browser.
15223
+ *
15224
+ * Host-supplied because only a host WITH a browser can answer it — so the
15225
+ * tool is simply absent in the CLI and in hosted runs, rather than present
15226
+ * and always failing. Registered outside `enabledTools` for the same reason
15227
+ * media tools are: that allowlist is a blast-radius control for tools that
15228
+ * touch the machine, and reading a page the user already opened touches
15229
+ * nothing. `disabledTools` still removes it.
15230
+ */
15231
+ setCurrentPageProvider(provider) {
15232
+ if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
15233
+ this.toolRegistry.register(new CurrentPageTool(provider));
15234
+ }
14859
15235
  registerMediaTools(workspacePath) {
14860
15236
  const denied = new Set(this.config.tools?.disabledTools ?? []);
14861
15237
  const permitted = (name) => !denied.has(name);
@@ -14988,8 +15364,12 @@ ${last.partialOutput}` : "");
14988
15364
  for (const server of this.config.tools.mcpServers) {
14989
15365
  try {
14990
15366
  const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
14991
- await this.mcpClient.connect(server, authProvider ? { authProvider } : {});
14992
- this.toolRegistry.registerMcpTools(this.mcpClient);
15367
+ const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
15368
+ await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
15369
+ this.toolRegistry.registerMcpTools(
15370
+ this.mcpClient,
15371
+ (name) => !(this.config.tools?.disabledTools ?? []).includes(name)
15372
+ );
14993
15373
  } catch (err) {
14994
15374
  console.error(`Failed to connect to MCP server "${server.name}":`, err);
14995
15375
  }
@@ -15454,6 +15834,7 @@ ${prompt}` : prompt;
15454
15834
  } else if (complexity === "Moderate") {
15455
15835
  const t2 = new T2Manager(this.router, this.toolRegistry, "root");
15456
15836
  t2.setRunBreaker(runBreaker);
15837
+ t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
15457
15838
  t2.setPresenter(true);
15458
15839
  t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
15459
15840
  if (identityPrompt) {
@@ -15498,11 +15879,17 @@ ${prompt}` : prompt;
15498
15879
  t2Results = [t2Result];
15499
15880
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
15500
15881
  if (completed.length > 0) {
15501
- finalOutput = t2Result.sectionSummary + "\n\n" + completed.map((r) => r.output).join("\n\n");
15882
+ finalOutput = composeRootSectionOutput(
15883
+ t2Result,
15884
+ completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
15885
+ );
15502
15886
  } else {
15503
15887
  const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
15504
15888
  const reasons = Array.from(
15505
- new Set(t2Result.t3Results.flatMap((r) => r.issues ?? []).filter(Boolean))
15889
+ new Set([
15890
+ ...t2Result.issues ?? [],
15891
+ ...t2Result.t3Results.flatMap((r) => r.issues ?? [])
15892
+ ].filter(Boolean))
15506
15893
  );
15507
15894
  if (partial) {
15508
15895
  finalOutput = reasons.length ? `${partial}
@@ -15515,6 +15902,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
15515
15902
  } else {
15516
15903
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
15517
15904
  t1.setRunBreaker(runBreaker);
15905
+ t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
15518
15906
  t1.setPresenter(true);
15519
15907
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
15520
15908
  if (identityPrompt) {
@@ -15539,6 +15927,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
15539
15927
  finalOutput = result.output;
15540
15928
  t2Results = result.t2Results;
15541
15929
  }
15930
+ const budgetInfo = this.router.budgetExceededInfo();
15931
+ if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
15542
15932
  const trip = runBreaker.reason();
15543
15933
  if (trip) {
15544
15934
  finalOutput = [
@@ -15579,6 +15969,10 @@ ${finalOutput}` : ""
15579
15969
  escalator.cancelAllPending();
15580
15970
  } catch {
15581
15971
  }
15972
+ try {
15973
+ this.releasePendingEscalations();
15974
+ } catch {
15975
+ }
15582
15976
  this.router.restoreTierModels();
15583
15977
  this.router.setRunSignal(void 0);
15584
15978
  if (this.taskAnalyzer) {
@@ -16895,6 +17289,13 @@ var ConfigManager = class {
16895
17289
  }
16896
17290
  async load() {
16897
17291
  this.config = await this.loadConfig();
17292
+ const servers = this.config.tools?.mcpServers;
17293
+ const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
17294
+ const mcpNamesChanged = renames.length > 0;
17295
+ if (mcpNamesChanged && this.config.tools) {
17296
+ this.config.tools.mcpServers = disambiguated;
17297
+ this.renameMcpServerReferences(renames);
17298
+ }
16898
17299
  this.ignore = new CascadeIgnore();
16899
17300
  await this.ignore.load(this.workspacePath);
16900
17301
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
@@ -16903,6 +17304,7 @@ var ConfigManager = class {
16903
17304
  await this.injectEnvKeys();
16904
17305
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
16905
17306
  await this.ensureDefaultIdentity();
17307
+ if (mcpNamesChanged) await this.save();
16906
17308
  }
16907
17309
  getConfig() {
16908
17310
  return this.config;
@@ -16986,6 +17388,66 @@ var ConfigManager = class {
16986
17388
  this.config.providers.push({ type: "ollama" });
16987
17389
  }
16988
17390
  }
17391
+ /**
17392
+ * Follow a disambiguation rename into `tools.mcpTrusted`.
17393
+ *
17394
+ * `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
17395
+ * of rename reach here, and they need OPPOSITE treatment:
17396
+ *
17397
+ * - Distinct raw names that only collide via sanitizing (`foo bar` /
17398
+ * `foo@bar`): each had its OWN trust entry, and the old string now belongs
17399
+ * to nothing — the server that used it moved away, so the entry has to
17400
+ * move with it. A plain rewrite is correct here.
17401
+ * - Literally identical names (a hand-edited or duplicated config: two rows
17402
+ * both named `foo`): there is only ONE trust entry for both, since
17403
+ * `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
17404
+ * already been updated to the disambiguated list by the time this runs
17405
+ * (see `load()`), so the untouched survivor is STILL named `foo` — a plain
17406
+ * rewrite would move the one entry entirely onto the renamed row and
17407
+ * leave the survivor untrusted. The entry must be kept for the survivor
17408
+ * AND granted to the renamed identity, not moved.
17409
+ *
17410
+ * Renames are grouped by `from` before either treatment is applied, and
17411
+ * each group is settled in one step. Two DIFFERENT rows can start out with
17412
+ * the exact same raw name and BOTH get renamed away (e.g. three servers
17413
+ * named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
17414
+ * with `foo bar` and with each other, so both are renamed to distinct
17415
+ * identities while `foo bar` survives untouched). Processing renames one at
17416
+ * a time in that shape drops coverage: the first rename's REPLACE branch
17417
+ * would erase `from` from `trusted` entirely, so the guard on the second
17418
+ * rename with the same `from` sees it already gone and silently skips —
17419
+ * granting trust to only one of the two renamed identities. Grouping first
17420
+ * means every renamed identity that shares a `from` is added or substituted
17421
+ * together, in the one pass that still sees the original entry.
17422
+ *
17423
+ * `tools.disabledTools` deliberately gets no equivalent treatment. It's
17424
+ * matched by sanitized PREFIX, and a prefix collision is exactly what made
17425
+ * an existing entry ambiguous between the two servers in the first place —
17426
+ * there is no way to tell, from the stored string alone, which of the two
17427
+ * a denial was meant for. Leaving those entries untouched resolves that
17428
+ * ambiguity for free: the survivor keeps the original (now unique) prefix,
17429
+ * so an old entry keeps applying to it, while the renamed server starts
17430
+ * clean under its new prefix. Rewriting the entry to follow the rename
17431
+ * would do the opposite — move a denial that already worked for the
17432
+ * survivor onto a server it may never have meant to cover.
17433
+ */
17434
+ renameMcpServerReferences(renames) {
17435
+ const tools = this.config.tools;
17436
+ if (!tools?.mcpTrusted?.length || !renames.length) return;
17437
+ const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
17438
+ const byFrom = /* @__PURE__ */ new Map();
17439
+ for (const { from, to } of renames) {
17440
+ const tos = byFrom.get(from);
17441
+ if (tos) tos.push(to);
17442
+ else byFrom.set(from, [to]);
17443
+ }
17444
+ let trusted = tools.mcpTrusted;
17445
+ for (const [from, tos] of byFrom) {
17446
+ if (!trusted.includes(from)) continue;
17447
+ trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
17448
+ }
17449
+ tools.mcpTrusted = Array.from(new Set(trusted));
17450
+ }
16989
17451
  async ensureDefaultIdentity() {
16990
17452
  const existing = this.store.getDefaultIdentity();
16991
17453
  if (existing) return;
@@ -17108,6 +17570,14 @@ var DashboardSocket = class {
17108
17570
  this.secret = options.secret;
17109
17571
  this.setupHandlers();
17110
17572
  }
17573
+ /** How many sockets are currently in a room — 0 means nobody can answer. */
17574
+ roomSize(room) {
17575
+ return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
17576
+ }
17577
+ /** Is this exact connection still live? A reconnect issues a NEW socket id. */
17578
+ hasSocket(socketId) {
17579
+ return this.io.sockets.sockets.has(socketId);
17580
+ }
17111
17581
  broadcastToRoom(room, event, data) {
17112
17582
  this.io.to(room).emit(event, data);
17113
17583
  }
@@ -17179,6 +17649,22 @@ var DashboardSocket = class {
17179
17649
  });
17180
17650
  });
17181
17651
  }
17652
+ /**
17653
+ * A client subscribed to a session's room.
17654
+ *
17655
+ * The host uses this to replay a gate the run is already parked on. Socket.IO
17656
+ * does not buffer room emissions, so a prompt broadcast while the renderer was
17657
+ * reconnecting is simply gone; without a replay the section waits out its full
17658
+ * timeout with the user sitting in front of a screen that never asked.
17659
+ */
17660
+ onJoinSession(callback) {
17661
+ this.io.on("connection", (socket) => {
17662
+ socket.on("join:session", (payload) => {
17663
+ const { sessionId } = normalizeSessionSubscriptionPayload(payload);
17664
+ if (sessionId) callback(sessionId, socket.id);
17665
+ });
17666
+ });
17667
+ }
17182
17668
  onSessionRate(callback) {
17183
17669
  this.io.on("connection", (socket) => {
17184
17670
  socket.on("session:rate", (payload) => {
@@ -17216,6 +17702,31 @@ var DashboardSocket = class {
17216
17702
  });
17217
17703
  });
17218
17704
  }
17705
+ /**
17706
+ * A section escalated and the run is parked waiting for an answer. The
17707
+ * desktop shows the escalation modal on `escalation:decision-required` and
17708
+ * answers here.
17709
+ *
17710
+ * Unlike a plan decision, silence is NOT taken as consent — the SDK fails
17711
+ * the section on timeout — so this handler only ever forwards a real choice.
17712
+ */
17713
+ onEscalationDecision(callback) {
17714
+ this.io.on("connection", (socket) => {
17715
+ socket.on("escalation:decide", (payload) => {
17716
+ const action = payload?.action;
17717
+ if (typeof payload?.sessionId !== "string") return;
17718
+ if (action !== "retry" && action !== "skip" && action !== "guidance") return;
17719
+ callback({
17720
+ sessionId: payload.sessionId,
17721
+ // Identifies WHICH parked section this answers — sections in a wave
17722
+ // run concurrently, so more than one can be waiting.
17723
+ requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
17724
+ action,
17725
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
17726
+ });
17727
+ });
17728
+ });
17729
+ }
17219
17730
  onSessionSteer(callback) {
17220
17731
  this.io.on("connection", (socket) => {
17221
17732
  socket.on("session:steer", (payload) => {
@@ -17374,6 +17885,7 @@ function aggregateCostStats(sessions, opts = {}) {
17374
17885
 
17375
17886
  // src/dashboard/server.ts
17376
17887
  var __dirname$1 = path24.dirname(fileURLToPath(import.meta.url));
17888
+ var ORPHANED_ESCALATION_GRACE_MS = 3e4;
17377
17889
  var DashboardServer = class {
17378
17890
  app;
17379
17891
  httpServer;
@@ -17384,6 +17896,13 @@ var DashboardServer = class {
17384
17896
  globalStore = null;
17385
17897
  broadcastTimer = null;
17386
17898
  activeSessions = /* @__PURE__ */ new Map();
17899
+ /**
17900
+ * Supplied by the desktop shell so a run can read the page the user has
17901
+ * open in the built-in browser. Absent in the CLI, where there is no
17902
+ * browser — so the tool is simply not registered rather than present and
17903
+ * always failing.
17904
+ */
17905
+ currentPageProvider;
17387
17906
  activeControllers = /* @__PURE__ */ new Map();
17388
17907
  /**
17389
17908
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -17398,6 +17917,17 @@ var DashboardServer = class {
17398
17917
  * map is how that answer reaches the run that's blocked on it.
17399
17918
  */
17400
17919
  pendingApprovals = /* @__PURE__ */ new Map();
17920
+ /**
17921
+ * The escalation prompt a session is currently parked on, kept so it can be
17922
+ * delivered to a client that was not reachable when it was raised.
17923
+ *
17924
+ * Socket.IO buffers nothing: a prompt emitted while the renderer is mid
17925
+ * reconnect is gone, and the section then waits out its full five minutes
17926
+ * with the user in front of a screen that never asked them anything. Holding
17927
+ * the payload lets `join:session` replay it, and lets a run with no reachable
17928
+ * client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
17929
+ */
17930
+ pendingEscalations = /* @__PURE__ */ new Map();
17401
17931
  /**
17402
17932
  * The orchestration decision trail ("why") of each session's most recent
17403
17933
  * run — captured when the run ends so the desktop's Why panel can show it
@@ -17475,20 +18005,29 @@ var DashboardServer = class {
17475
18005
  if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
17476
18006
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
17477
18007
  this.activeSessions.set(sessionId, cascade);
18008
+ if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
17478
18009
  cascade.on("stream:token", (e) => {
17479
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
18010
+ this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
17480
18011
  });
17481
18012
  cascade.on("tier:status", (e) => {
17482
- this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
18013
+ this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
17483
18014
  });
17484
18015
  cascade.on("permission:user-required", (e) => {
17485
- this.socket.emitToSocket(socketId, "permission:user-required", { sessionId, ...e });
18016
+ this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
17486
18017
  });
17487
18018
  cascade.on("peer:message", (e) => {
17488
18019
  this.socket.emitPeerMessage(e);
17489
18020
  });
17490
18021
  cascade.on("plan:approval-required", (e) => {
17491
- this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
18022
+ this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
18023
+ });
18024
+ cascade.on("escalation:decision-required", (e) => {
18025
+ this.raiseEscalation(sessionId, socketId, e);
18026
+ });
18027
+ cascade.on("escalation:timeout", (e) => {
18028
+ const ev = e;
18029
+ if (ev.requestId) this.clearPendingEscalation(ev.requestId);
18030
+ this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
17492
18031
  });
17493
18032
  try {
17494
18033
  const result = await cascade.run({
@@ -17499,7 +18038,7 @@ var DashboardServer = class {
17499
18038
  this.recordSessionTask(sessionId, result.taskId);
17500
18039
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
17501
18040
  this.captureWhy(sessionId, cascade, result);
17502
- this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
18041
+ this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
17503
18042
  this.socket.broadcast("cost:update", {
17504
18043
  sessionId,
17505
18044
  totalTokens: result.usage.totalTokens,
@@ -17512,7 +18051,7 @@ var DashboardServer = class {
17512
18051
  } catch (err) {
17513
18052
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
17514
18053
  this.captureWhy(sessionId, cascade);
17515
- this.socket.emitToSocket(socketId, "session:error", {
18054
+ this.emitToSessionClients(socketId, sessionId, "session:error", {
17516
18055
  sessionId,
17517
18056
  error: err instanceof Error ? err.message : String(err)
17518
18057
  });
@@ -17520,6 +18059,7 @@ var DashboardServer = class {
17520
18059
  this.activeSessions.delete(sessionId);
17521
18060
  this.activeControllers.delete(sessionId);
17522
18061
  this.denyPendingApprovals(sessionId);
18062
+ this.clearSessionEscalations(sessionId);
17523
18063
  }
17524
18064
  });
17525
18065
  this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
@@ -17529,10 +18069,20 @@ var DashboardServer = class {
17529
18069
  editedPlan
17530
18070
  );
17531
18071
  });
18072
+ this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
18073
+ if (requestId) this.clearPendingEscalation(requestId);
18074
+ else this.clearOldestSessionEscalation(sessionId);
18075
+ this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
18076
+ });
18077
+ this.socket.onJoinSession((sessionId, socketId) => {
18078
+ this.replayEscalations(sessionId, socketId);
18079
+ });
17532
18080
  this.socket.onSessionHalt((sessionId) => {
17533
18081
  this.activeControllers.get(sessionId)?.abort();
17534
18082
  this.denyPendingApprovals(sessionId);
17535
18083
  this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
18084
+ this.clearSessionEscalations(sessionId);
18085
+ this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
17536
18086
  });
17537
18087
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
17538
18088
  const pending = this.pendingApprovals.get(requestId);
@@ -17547,6 +18097,10 @@ var DashboardServer = class {
17547
18097
  }
17548
18098
  });
17549
18099
  }
18100
+ /** Wire the host's browser view in (desktop only). */
18101
+ setCurrentPageProvider(provider) {
18102
+ this.currentPageProvider = provider;
18103
+ }
17550
18104
  async start() {
17551
18105
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
17552
18106
  if (!isLoopback) {
@@ -17835,6 +18389,116 @@ var DashboardServer = class {
17835
18389
  this.activeSessions.delete(sessionId);
17836
18390
  }
17837
18391
  }
18392
+ /**
18393
+ * Is anyone able to answer for this session right now?
18394
+ *
18395
+ * The originating socket first — that connection is definitely watching this
18396
+ * run — then the session room, which is where a client lands after a
18397
+ * reconnect issues it a new socket id.
18398
+ */
18399
+ hasResponder(sessionId, socketId) {
18400
+ if (socketId && this.socket.hasSocket(socketId)) return true;
18401
+ return this.socket.roomSize(`session:${sessionId}`) > 0;
18402
+ }
18403
+ /**
18404
+ * Send a run event to whoever is watching this session.
18405
+ *
18406
+ * A captured socket id goes stale the moment Socket.IO reconnects — the
18407
+ * client is back, but under a new id, and `emitToSocket` on the old one is a
18408
+ * silent no-op. Falling back to the session room keeps the run addressable
18409
+ * across a reconnect; emitting to one OR the other (never both) keeps a
18410
+ * client that is both the originator and a room member from seeing every
18411
+ * token twice.
18412
+ */
18413
+ emitToSessionClients(socketId, sessionId, event, data) {
18414
+ if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
18415
+ else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
18416
+ }
18417
+ /**
18418
+ * Raise an escalation prompt, and make sure it can still be answered.
18419
+ *
18420
+ * Held in `pendingEscalations` — keyed by requestId, because a Complex wave
18421
+ * dispatches sections concurrently and two can be parked at once — so a
18422
+ * client that joins late (or rejoins after a reconnect) gets every waiting
18423
+ * prompt replayed. Keying by session would drop the first of a pair: its
18424
+ * grace timer would be cleared by the second, leaving it parked for the full
18425
+ * timeout with nobody able to answer it. If nobody is reachable at all, the gate is
18426
+ * settled as `skip` after a short grace period rather than parked for the
18427
+ * full timeout: that matches the SDK's policy when no listener exists, and
18428
+ * five minutes of a held run is a real cost for a REST caller who has already
18429
+ * hung up. The grace period exists so an ordinary two-second reconnect is not
18430
+ * mistaken for an absent user.
18431
+ */
18432
+ raiseEscalation(sessionId, socketId, event) {
18433
+ const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : randomUUID();
18434
+ const payload = { sessionId, ...event };
18435
+ const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
18436
+ this.pendingEscalations.set(requestId, entry);
18437
+ if (this.hasResponder(sessionId, socketId)) {
18438
+ this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
18439
+ return;
18440
+ }
18441
+ entry.graceTimer = setTimeout(() => {
18442
+ if (this.pendingEscalations.get(requestId) !== entry) return;
18443
+ if (this.hasResponder(sessionId, socketId)) return;
18444
+ this.pendingEscalations.delete(requestId);
18445
+ this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
18446
+ }, ORPHANED_ESCALATION_GRACE_MS);
18447
+ }
18448
+ /**
18449
+ * Replay EVERY parked prompt to a client that just subscribed to the session.
18450
+ *
18451
+ * Plural because a Complex run dispatches sections concurrently, so a wave can
18452
+ * leave more than one waiting at the same time — the same reason the SDK keys
18453
+ * its gates and the clients queue theirs.
18454
+ *
18455
+ * `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
18456
+ * has been running since the prompt was raised. Sent verbatim, a client
18457
+ * reconnecting four minutes in would show a fresh five-minute countdown and
18458
+ * then watch the section fail with four of those minutes still on the clock.
18459
+ * A remaining duration is used rather than an absolute deadline so the two
18460
+ * sides never have to agree on a wall clock.
18461
+ */
18462
+ replayEscalations(sessionId, socketId) {
18463
+ for (const entry of this.pendingEscalations.values()) {
18464
+ if (entry.sessionId !== sessionId) continue;
18465
+ if (entry.graceTimer) {
18466
+ clearTimeout(entry.graceTimer);
18467
+ entry.graceTimer = void 0;
18468
+ }
18469
+ const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
18470
+ const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
18471
+ this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
18472
+ }
18473
+ }
18474
+ /** Forget ONE parked prompt — the one that was answered or timed out. */
18475
+ clearPendingEscalation(requestId) {
18476
+ const entry = this.pendingEscalations.get(requestId);
18477
+ if (!entry) return;
18478
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
18479
+ this.pendingEscalations.delete(requestId);
18480
+ }
18481
+ /**
18482
+ * Forget the session's OLDEST prompt — for a client that answered without a
18483
+ * requestId, which is exactly the gate `resolveEscalation` will settle.
18484
+ * Insertion order makes `values()` oldest-first.
18485
+ */
18486
+ clearOldestSessionEscalation(sessionId) {
18487
+ for (const [id, entry] of this.pendingEscalations) {
18488
+ if (entry.sessionId !== sessionId) continue;
18489
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
18490
+ this.pendingEscalations.delete(id);
18491
+ return;
18492
+ }
18493
+ }
18494
+ /** Forget every prompt a session was parked on (run ended, or halted). */
18495
+ clearSessionEscalations(sessionId) {
18496
+ for (const [id, entry] of this.pendingEscalations) {
18497
+ if (entry.sessionId !== sessionId) continue;
18498
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
18499
+ this.pendingEscalations.delete(id);
18500
+ }
18501
+ }
17838
18502
  /** Deny + clear any approvals still pending for a session (run end / abort). */
17839
18503
  denyPendingApprovals(sessionId) {
17840
18504
  for (const [id, pending] of this.pendingApprovals) {
@@ -18335,6 +18999,7 @@ ${prompt}`;
18335
18999
  void (async () => {
18336
19000
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
18337
19001
  this.activeSessions.set(sessionId, cascade);
19002
+ if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
18338
19003
  cascade.on("stream:token", (e) => {
18339
19004
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
18340
19005
  });
@@ -18350,6 +19015,14 @@ ${prompt}`;
18350
19015
  cascade.on("plan:approval-required", (e) => {
18351
19016
  this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
18352
19017
  });
19018
+ cascade.on("escalation:decision-required", (e) => {
19019
+ this.raiseEscalation(sessionId, void 0, e);
19020
+ });
19021
+ cascade.on("escalation:timeout", (e) => {
19022
+ const ev = e;
19023
+ if (ev.requestId) this.clearPendingEscalation(ev.requestId);
19024
+ this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
19025
+ });
18353
19026
  try {
18354
19027
  const result = await cascade.run({
18355
19028
  prompt: runPrompt,
@@ -18377,6 +19050,7 @@ ${prompt}`;
18377
19050
  } finally {
18378
19051
  this.activeSessions.delete(sessionId);
18379
19052
  this.denyPendingApprovals(sessionId);
19053
+ this.clearSessionEscalations(sessionId);
18380
19054
  }
18381
19055
  })();
18382
19056
  });
@@ -18709,6 +19383,41 @@ var HooksRunner = class {
18709
19383
  }
18710
19384
  };
18711
19385
 
19386
+ // src/mcp/discover.ts
19387
+ async function discoverMcpTools(servers) {
19388
+ const results = [];
19389
+ for (const server of servers) {
19390
+ const client = new McpClient({ trustedServers: [server.name] });
19391
+ try {
19392
+ const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
19393
+ const connect = () => client.connect(server, authProvider ? { authProvider } : {});
19394
+ await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
19395
+ const parsed = client.getToolDefinitions().flatMap((def) => {
19396
+ const [, serverName, toolName] = def.name.split("::");
19397
+ return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
19398
+ });
19399
+ const names = assignMcpToolNames(parsed);
19400
+ const tools = parsed.map(({ def, server: s, tool }, i) => ({
19401
+ server: s,
19402
+ tool,
19403
+ name: names[i],
19404
+ description: def.description.replace(`[MCP:${s}] `, "")
19405
+ }));
19406
+ results.push({ server: server.name, tools });
19407
+ } catch (err) {
19408
+ results.push({
19409
+ server: server.name,
19410
+ tools: [],
19411
+ error: err instanceof Error ? err.message : String(err)
19412
+ });
19413
+ } finally {
19414
+ await client.disconnectAll().catch(() => {
19415
+ });
19416
+ }
19417
+ }
19418
+ return results;
19419
+ }
19420
+
18712
19421
  // src/retrieval/chunk.ts
18713
19422
  var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
18714
19423
  function splitLongBlock(block, max) {
@@ -19247,11 +19956,55 @@ function mergeByKey(local, incoming, key) {
19247
19956
  for (const i of incoming) map.set(key(i), i);
19248
19957
  return [...map.values()];
19249
19958
  }
19959
+ function uniqueServerName(desired, takenPrefixes) {
19960
+ if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
19961
+ let n = 2;
19962
+ while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
19963
+ return `${desired} ${n}`;
19964
+ }
19965
+ function mergeMcpServers(local, incoming) {
19966
+ const rewrite = /* @__PURE__ */ new Map();
19967
+ if (!incoming?.length) return { servers: local, synced: [], rewrite };
19968
+ const byName = /* @__PURE__ */ new Map();
19969
+ for (const l of local ?? []) byName.set(l.name, l);
19970
+ const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
19971
+ const synced = [];
19972
+ for (const inc of incoming) {
19973
+ if (byName.has(inc.name)) {
19974
+ byName.set(inc.name, inc);
19975
+ synced.push(inc);
19976
+ continue;
19977
+ }
19978
+ const name = uniqueServerName(inc.name, prefixes);
19979
+ if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
19980
+ const server = name === inc.name ? inc : { ...inc, name };
19981
+ byName.set(name, server);
19982
+ synced.push(server);
19983
+ prefixes.add(mcpServerPrefix(name));
19984
+ }
19985
+ return { servers: [...byName.values()], synced, rewrite };
19986
+ }
19987
+ function rewriteDisabledTools(tools, rewrite) {
19988
+ if (!tools?.length || rewrite.size === 0) return tools;
19989
+ return tools.map((t) => {
19990
+ for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
19991
+ return t;
19992
+ });
19993
+ }
19994
+ function mergeDisabledTools(local, incoming, syncedServers) {
19995
+ const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
19996
+ const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
19997
+ const kept = (local ?? []).filter((t) => !covered(t));
19998
+ const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
19999
+ return merged.length ? merged : void 0;
20000
+ }
19250
20001
  function gatherSyncBundle(config) {
19251
- const bundle = { v: 1 };
20002
+ const bundle = { v: 2 };
19252
20003
  if (config.providers?.length) bundle.providers = config.providers;
19253
20004
  if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
19254
20005
  if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
20006
+ const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
20007
+ if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
19255
20008
  if (config.models) bundle.models = config.models;
19256
20009
  if (config.budget) bundle.budget = config.budget;
19257
20010
  const prefs = {};
@@ -19266,7 +20019,16 @@ function applySyncBundle(bundle, config) {
19266
20019
  const next = { ...config, tools: { ...config.tools } };
19267
20020
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
19268
20021
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
19269
- if (bundle.mcpServers) next.tools.mcpServers = mergeByKey(config.tools?.mcpServers, bundle.mcpServers, (m) => m.name);
20022
+ const mcp = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
20023
+ if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
20024
+ const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
20025
+ if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
20026
+ next.tools.disabledTools = mergeDisabledTools(
20027
+ config.tools?.disabledTools,
20028
+ rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
20029
+ bundle.mcpServers ? mcp.synced : void 0
20030
+ );
20031
+ }
19270
20032
  if (bundle.models) next.models = { ...config.models, ...bundle.models };
19271
20033
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
19272
20034
  if (bundle.prefs) {
@@ -19279,6 +20041,6 @@ function applySyncBundle(bundle, config) {
19279
20041
  return next;
19280
20042
  }
19281
20043
 
19282
- 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, 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, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, 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, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, discoverMcpAuthServer, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, runCascade, sessionWorthRemembering, streamCascade };
20044
+ 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, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, 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, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, isMcpToolName, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, runCascade, sessionWorthRemembering, streamCascade, uniqueMcpServerName };
19283
20045
  //# sourceMappingURL=index.js.map
19284
20046
  //# sourceMappingURL=index.js.map