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.
@@ -18237,7 +18237,7 @@ var require_gaxios = __commonJS({
18237
18237
  var retry_js_1 = require_retry3();
18238
18238
  var stream_1 = __require("stream");
18239
18239
  var interceptor_js_1 = require_interceptor();
18240
- var randomUUID10 = async () => globalThis.crypto?.randomUUID() || (await import('crypto')).randomUUID();
18240
+ var randomUUID11 = async () => globalThis.crypto?.randomUUID() || (await import('crypto')).randomUUID();
18241
18241
  var HTTP_STATUS_NO_CONTENT = 204;
18242
18242
  var Gaxios = class {
18243
18243
  agentCache = /* @__PURE__ */ new Map();
@@ -18510,7 +18510,7 @@ var require_gaxios = __commonJS({
18510
18510
  */
18511
18511
  ["Blob", "File", "FormData"].includes(opts.data?.constructor?.name || "");
18512
18512
  if (opts.multipart?.length) {
18513
- const boundary = await randomUUID10();
18513
+ const boundary = await randomUUID11();
18514
18514
  preparedHeaders.set("content-type", `multipart/related; boundary=${boundary}`);
18515
18515
  opts.body = stream_1.Readable.from(this.getMultipartRequest(opts.multipart, boundary));
18516
18516
  } else if (shouldDirectlyPassData) {
@@ -30587,7 +30587,7 @@ var require_websocket = __commonJS({
30587
30587
  var http7 = __require("http");
30588
30588
  var net2 = __require("net");
30589
30589
  var tls = __require("tls");
30590
- var { randomBytes: randomBytes4, createHash: createHash4 } = __require("crypto");
30590
+ var { randomBytes: randomBytes4, createHash: createHash5 } = __require("crypto");
30591
30591
  var { Duplex, Readable: Readable5 } = __require("stream");
30592
30592
  var { URL: URL4 } = __require("url");
30593
30593
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -31255,7 +31255,7 @@ var require_websocket = __commonJS({
31255
31255
  abortHandshake(websocket, socket, "Invalid Upgrade header");
31256
31256
  return;
31257
31257
  }
31258
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
31258
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
31259
31259
  if (res.headers["sec-websocket-accept"] !== digest) {
31260
31260
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
31261
31261
  return;
@@ -31621,7 +31621,7 @@ var require_websocket_server = __commonJS({
31621
31621
  var EventEmitter9 = __require("events");
31622
31622
  var http7 = __require("http");
31623
31623
  var { Duplex } = __require("stream");
31624
- var { createHash: createHash4 } = __require("crypto");
31624
+ var { createHash: createHash5 } = __require("crypto");
31625
31625
  var extension2 = require_extension();
31626
31626
  var PerMessageDeflate2 = require_permessage_deflate();
31627
31627
  var subprotocol2 = require_subprotocol();
@@ -31928,7 +31928,7 @@ var require_websocket_server = __commonJS({
31928
31928
  );
31929
31929
  }
31930
31930
  if (this._state > RUNNING) return abortHandshake(socket, 503);
31931
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
31931
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
31932
31932
  const headers = [
31933
31933
  "HTTP/1.1 101 Switching Protocols",
31934
31934
  "Upgrade: websocket",
@@ -224247,7 +224247,7 @@ function reconcilePrice(model, live) {
224247
224247
  }
224248
224248
 
224249
224249
  // src/constants.ts
224250
- var CASCADE_VERSION = "0.54.0";
224250
+ var CASCADE_VERSION = "0.55.0";
224251
224251
  var CASCADE_CONFIG_DIR = ".cascade";
224252
224252
  var CASCADE_MD_FILE = "CASCADE.md";
224253
224253
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -253088,6 +253088,23 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
253088
253088
  */
253089
253089
  budgetState = "ok";
253090
253090
  budgetExceededReason;
253091
+ /**
253092
+ * Whether the run/session budget is permanently tripped, and why.
253093
+ *
253094
+ * A tier or worker that catches `BudgetExceededError` internally (T3Worker
253095
+ * does, so one dead-end subtask doesn't crash the whole run) absorbs the
253096
+ * exception into a normal FAILED/PARTIAL result — the tier returns as if
253097
+ * nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
253098
+ * budget-exceeded handling (the `run:budget-exceeded` event and
253099
+ * `lastInterruptedRun` for `/continue`). This lets a caller check the
253100
+ * router's own state directly after a tier returns, regardless of how that
253101
+ * tier chose to report the error internally.
253102
+ */
253103
+ budgetExceededInfo() {
253104
+ if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
253105
+ if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
253106
+ return null;
253107
+ }
253091
253108
  tpmLimiter;
253092
253109
  localQueue;
253093
253110
  taskAnalyzer;
@@ -254745,6 +254762,13 @@ Now execute your subtask using this context where relevant.`
254745
254762
  this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
254746
254763
  return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
254747
254764
  }
254765
+ if (err instanceof Error && err.name === "BudgetExceededError") {
254766
+ issues.push(errMsg);
254767
+ const stopped = output || errMsg;
254768
+ this.setStatus("FAILED", stopped);
254769
+ this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
254770
+ return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
254771
+ }
254748
254772
  issues.push(`Execution error: ${errMsg}`);
254749
254773
  const finalOutput = output || errMsg;
254750
254774
  this.setStatus("FAILED", finalOutput);
@@ -255766,6 +255790,26 @@ var RedactionLayer = class {
255766
255790
  }
255767
255791
  };
255768
255792
 
255793
+ // src/core/tiers/escalation-policy.ts
255794
+ function sectionNeedsDecision(results) {
255795
+ return results.some((r4) => r4.status === "ESCALATED");
255796
+ }
255797
+ function settledEscalationStatus(results) {
255798
+ const hasWork = results.some((r4) => r4.status === "COMPLETED" || !!r4.output);
255799
+ return hasWork ? "PARTIAL" : "FAILED";
255800
+ }
255801
+ function composeRootSectionOutput(t2Result, completedOutputs) {
255802
+ let output = `${t2Result.sectionSummary}
255803
+
255804
+ ${completedOutputs.join("\n\n")}`;
255805
+ if (t2Result.status === "FAILED" && t2Result.issues?.length) {
255806
+ output += `
255807
+
255808
+ _(incomplete: ${t2Result.issues.join("; ")})_`;
255809
+ }
255810
+ return output;
255811
+ }
255812
+
255769
255813
  // src/core/tiers/t2-manager.ts
255770
255814
  function buildT2SystemPrompt(hasPeerMessage) {
255771
255815
  return [
@@ -255791,6 +255835,19 @@ var T2Manager = class extends BaseTier {
255791
255835
  t2PeerBus;
255792
255836
  permissionEscalator;
255793
255837
  toolCreator;
255838
+ /**
255839
+ * Asked when a section ends ESCALATED — i.e. a worker hit something it could
255840
+ * not decide alone. Without this the escalation was a dead end: the status
255841
+ * said "needs a decision" and no decision was ever requested, so an MCP run
255842
+ * that legitimately needed input just stopped there.
255843
+ */
255844
+ escalationCallback;
255845
+ /**
255846
+ * The one allowed retry has been spent. Retrying is bounded because an
255847
+ * escalation loop that can re-ask forever burns a budget on a question that
255848
+ * is not resolving; a second escalation is terminal instead.
255849
+ */
255850
+ escalationRetryUsed = false;
255794
255851
  /** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
255795
255852
  planApprovalCallback;
255796
255853
  /** AbortController for the current T3 wave — aborted on cancel-and-respawn */
@@ -255839,6 +255896,10 @@ var T2Manager = class extends BaseTier {
255839
255896
  setToolCreator(creator) {
255840
255897
  this.toolCreator = creator;
255841
255898
  }
255899
+ /** Ask the user what to do when a section escalates. */
255900
+ setEscalationCallback(cb) {
255901
+ this.escalationCallback = cb;
255902
+ }
255842
255903
  /** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
255843
255904
  setPlanApprovalCallback(cb) {
255844
255905
  this.planApprovalCallback = cb;
@@ -255943,9 +256004,57 @@ Guidance (must be followed): ${decision.note}`
255943
256004
  currentAction: "Aggregating T3 results",
255944
256005
  status: "IN_PROGRESS"
255945
256006
  });
255946
- const summary = await this.aggregateResults(assignment, t3Results);
256007
+ let summary = await this.aggregateResults(assignment, t3Results);
255947
256008
  const issues = t3Results.filter((r4) => r4.status !== "COMPLETED").flatMap((r4) => r4.issues);
255948
- const overallStatus = this.determineStatus(t3Results);
256009
+ let overallStatus = this.determineStatus(t3Results);
256010
+ let userSkipped = false;
256011
+ const hasEscalated = sectionNeedsDecision(t3Results);
256012
+ const settleEscalated = async (reason) => {
256013
+ if (reason) issues.push(reason);
256014
+ summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
256015
+ overallStatus = settledEscalationStatus(t3Results);
256016
+ };
256017
+ if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
256018
+ await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
256019
+ } else if (hasEscalated && !this.escalationCallback) {
256020
+ await settleEscalated();
256021
+ } else if (hasEscalated && this.escalationCallback) {
256022
+ this.sendStatusUpdate({
256023
+ progressPct: 95,
256024
+ currentAction: "Escalated \u2014 waiting for your decision",
256025
+ status: "ESCALATING"
256026
+ });
256027
+ const decision = await this.escalationCallback({
256028
+ sectionId: assignment.sectionId,
256029
+ sectionTitle: assignment.sectionTitle,
256030
+ issues,
256031
+ summary
256032
+ });
256033
+ this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
256034
+ if (decision.action === "skip") {
256035
+ summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
256036
+ overallStatus = settledEscalationStatus(t3Results);
256037
+ userSkipped = decision.automatic !== true;
256038
+ } else if (decision.action === "retry" || decision.action === "guidance") {
256039
+ this.escalationRetryUsed = true;
256040
+ const guided = decision.action === "guidance" && decision.note?.trim() ? {
256041
+ ...assignment,
256042
+ description: `${assignment.description}
256043
+
256044
+ Guidance (must be followed): ${decision.note.trim()}`,
256045
+ // Drop T1's preplanned subtasks so the retry re-decomposes with
256046
+ // the guidance in hand. The workers execute from their subtask
256047
+ // slices, not from this description — leaving the slices in
256048
+ // place gave "Retry with guidance" byte-identical worker
256049
+ // prompts to "Retry as-is", making the note do nothing at all.
256050
+ t3Subtasks: []
256051
+ } : assignment;
256052
+ return await this.execute(guided, taskId, signal);
256053
+ } else {
256054
+ issues.push("Escalated, but no decision was received in time.");
256055
+ overallStatus = "FAILED";
256056
+ }
256057
+ }
255949
256058
  const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
255950
256059
  this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
255951
256060
  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";
@@ -255956,7 +256065,8 @@ Guidance (must be followed): ${decision.note}`
255956
256065
  status: overallStatus,
255957
256066
  t3Results,
255958
256067
  sectionSummary: summary,
255959
- issues
256068
+ issues,
256069
+ ...userSkipped ? { userSkipped: true } : {}
255960
256070
  };
255961
256071
  this.publishSectionOutput(result);
255962
256072
  return result;
@@ -256390,8 +256500,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
256390
256500
  result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
256391
256501
  );
256392
256502
  }
256393
- async aggregateResults(assignment, results) {
256394
- const completed = results.filter((r4) => r4.status === "COMPLETED");
256503
+ async aggregateResults(assignment, results, opts = {}) {
256504
+ const completed = results.filter(
256505
+ (r4) => r4.status === "COMPLETED" || opts.includeEscalated && r4.status === "ESCALATED" && r4.output
256506
+ );
256395
256507
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
256396
256508
  const peerOutputs = this.peerSyncBuffer.filter((p3) => p3.content?.type === "T2_SECTION_OUTPUT").map((p3) => `[Peer ${p3.fromId} Output]: ${p3.content.output}`).join("\n\n");
256397
256509
  const peerContext = peerOutputs ? `
@@ -256597,6 +256709,7 @@ var T1Administrator = class extends BaseTier {
256597
256709
  escalations = [];
256598
256710
  store;
256599
256711
  runBreaker;
256712
+ escalationCallback;
256600
256713
  t2PeerBus = new PeerBus();
256601
256714
  permissionEscalator;
256602
256715
  toolCreator;
@@ -256611,6 +256724,10 @@ var T1Administrator = class extends BaseTier {
256611
256724
  this.toolRegistry = toolRegistry;
256612
256725
  this.config = config2;
256613
256726
  }
256727
+ /** Ask the user what to do when any section escalates. */
256728
+ setEscalationCallback(cb) {
256729
+ this.escalationCallback = cb;
256730
+ }
256614
256731
  /** Share the run-wide circuit breaker with every section manager. */
256615
256732
  setRunBreaker(breaker) {
256616
256733
  this.runBreaker = breaker;
@@ -256770,8 +256887,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
256770
256887
  reason: `Some T2 managers failed entirely: ${failedSections.map((s3) => s3.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s3) => s3.issues).join("; ")}`
256771
256888
  };
256772
256889
  }
256773
- const sectionsText = t2Results.map((r4) => `**${r4.sectionTitle}**
256774
- ${r4.sectionSummary}`).join("\n\n");
256890
+ const sectionsText = t2Results.map((r4) => {
256891
+ const skipNote = r4.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.]" : "";
256892
+ return `**${r4.sectionTitle}**
256893
+ ${r4.sectionSummary}${skipNote}`;
256894
+ }).join("\n\n");
256775
256895
  const prompt = `You are a strict QA Reviewer for the Cascade AI system.
256776
256896
  Review the following execution outputs against the original user prompt.
256777
256897
 
@@ -256781,6 +256901,7 @@ T2 Manager Summaries:
256781
256901
  ${sectionsText}
256782
256902
 
256783
256903
  Does the current state of the workspace and the outputs fully satisfy the user's request?
256904
+ 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.
256784
256905
  If yes, reply with exactly: "APPROVED".
256785
256906
  If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
256786
256907
  try {
@@ -256978,6 +257099,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
256978
257099
  const managers = sections.map((section) => {
256979
257100
  const manager = new T2Manager(this.router, this.toolRegistry, this.id);
256980
257101
  if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
257102
+ if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
256981
257103
  manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
256982
257104
  if (this.store) {
256983
257105
  manager.setStore(this.store);
@@ -257187,7 +257309,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
257187
257309
  async compileFinalOutput(originalPrompt, plan, t2Results) {
257188
257310
  const completedSections = t2Results.filter((r4) => r4.status !== "FAILED");
257189
257311
  if (!completedSections.length) {
257190
- const allIssues = t2Results.flatMap((r4) => r4.t3Results.flatMap((t4) => t4.issues));
257312
+ const allIssues = [
257313
+ ...t2Results.flatMap((r4) => r4.issues ?? []),
257314
+ ...t2Results.flatMap((r4) => r4.t3Results.flatMap((t4) => t4.issues))
257315
+ ].filter(Boolean);
257191
257316
  const critical = allIssues.find((i4) => i4.includes("[CRITICAL_TOOL_ERROR]"));
257192
257317
  const stalled = allIssues.find((i4) => /^Stalled:/.test(i4));
257193
257318
  const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
@@ -257999,6 +258124,102 @@ var RunBreaker = class {
257999
258124
 
258000
258125
  // src/tools/registry.ts
258001
258126
  var import_ignore = __toESM(require_ignore(), 1);
258127
+ var MCP_TOOL_PREFIX = "mcp__";
258128
+ var MAX_SEGMENT_LENGTH = 24;
258129
+ var SEGMENT_HASH_LENGTH = 8;
258130
+ function boundedSegment(raw) {
258131
+ const clean3 = sanitizeToolNameSegment(raw);
258132
+ if (clean3.length <= MAX_SEGMENT_LENGTH) return clean3;
258133
+ const hash = crypto6.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
258134
+ return `${clean3.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
258135
+ }
258136
+ function isStableSegment(raw) {
258137
+ return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
258138
+ }
258139
+ function sanitizeToolNameSegment(segment) {
258140
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
258141
+ let start = 0;
258142
+ let end = cleaned.length;
258143
+ while (start < end && cleaned[start] === "_") start++;
258144
+ while (end > start && cleaned[end - 1] === "_") end--;
258145
+ return cleaned.slice(start, end) || "_";
258146
+ }
258147
+ function mcpToolName(serverName, toolName) {
258148
+ return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
258149
+ }
258150
+ function isMcpToolName(name) {
258151
+ return name.startsWith(MCP_TOOL_PREFIX);
258152
+ }
258153
+ function uniqueMcpServerName(desired, existingNames) {
258154
+ const taken = new Set(existingNames.map(mcpServerPrefix));
258155
+ if (!taken.has(mcpServerPrefix(desired))) return desired;
258156
+ for (let n8 = 2; n8 < 1e3; n8++) {
258157
+ const candidate = `${desired} (${n8})`;
258158
+ if (!taken.has(mcpServerPrefix(candidate))) return candidate;
258159
+ }
258160
+ return `${desired} (${crypto6.randomUUID().slice(0, 8)})`;
258161
+ }
258162
+ function disambiguateMcpServerNames(servers) {
258163
+ const seenNames = [];
258164
+ const renames = [];
258165
+ const result = servers.map((s3) => {
258166
+ const name = uniqueMcpServerName(s3.name, seenNames);
258167
+ seenNames.push(name);
258168
+ if (name === s3.name) return s3;
258169
+ renames.push({ from: s3.name, to: name });
258170
+ return { ...s3, name };
258171
+ });
258172
+ return { servers: renames.length ? result : servers, renames };
258173
+ }
258174
+ function assignMcpToolNames(tools) {
258175
+ const names = new Array(tools.length);
258176
+ const bases = tools.map((t4) => mcpToolName(t4.server, t4.tool));
258177
+ const groups = /* @__PURE__ */ new Map();
258178
+ bases.forEach((base, i4) => {
258179
+ const g2 = groups.get(base);
258180
+ if (g2) g2.push(i4);
258181
+ else groups.set(base, [i4]);
258182
+ });
258183
+ const used = new Set(bases);
258184
+ for (const [base, idx] of groups) {
258185
+ if (idx.length === 1) {
258186
+ names[idx[0]] = base;
258187
+ continue;
258188
+ }
258189
+ const raw = (i4) => `${tools[i4].server}::${tools[i4].tool}`;
258190
+ const isClean = (i4) => isStableSegment(tools[i4].server) && isStableSegment(tools[i4].tool);
258191
+ const ordered = [...idx].sort((a2, b3) => {
258192
+ const la = isClean(a2) ? 0 : 1;
258193
+ const lb = isClean(b3) ? 0 : 1;
258194
+ if (la !== lb) return la - lb;
258195
+ const ra = raw(a2);
258196
+ const rb = raw(b3);
258197
+ return ra < rb ? -1 : ra > rb ? 1 : 0;
258198
+ });
258199
+ names[ordered[0]] = base;
258200
+ let n8 = 2;
258201
+ for (let rank3 = 1; rank3 < ordered.length; rank3++) {
258202
+ let candidate = `${base}_${n8}`;
258203
+ while (used.has(candidate)) {
258204
+ n8++;
258205
+ candidate = `${base}_${n8}`;
258206
+ }
258207
+ names[ordered[rank3]] = candidate;
258208
+ used.add(candidate);
258209
+ n8++;
258210
+ }
258211
+ }
258212
+ return names;
258213
+ }
258214
+ function mcpServerPrefix(serverName) {
258215
+ return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
258216
+ }
258217
+ function removeMcpServerDenials(disabledTools, serverName) {
258218
+ if (!disabledTools?.length) return disabledTools;
258219
+ const prefix = mcpServerPrefix(serverName);
258220
+ const kept = disabledTools.filter((t4) => !t4.startsWith(prefix));
258221
+ return kept.length === disabledTools.length ? disabledTools : kept;
258222
+ }
258002
258223
  var execAsync = util$1.promisify(child_process.exec);
258003
258224
  var ShellTool = class extends BaseTool {
258004
258225
  name = "shell";
@@ -286300,20 +286521,6 @@ ${text}`;
286300
286521
  }
286301
286522
  };
286302
286523
 
286303
- // src/tools/tool-name.ts
286304
- var MCP_TOOL_PREFIX = "mcp__";
286305
- function sanitizeToolNameSegment(segment) {
286306
- const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
286307
- let start = 0;
286308
- let end = cleaned.length;
286309
- while (start < end && cleaned[start] === "_") start++;
286310
- while (end > start && cleaned[end - 1] === "_") end--;
286311
- return cleaned.slice(start, end) || "_";
286312
- }
286313
- function mcpToolName(serverName, toolName) {
286314
- return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
286315
- }
286316
-
286317
286524
  // src/tools/mcp.ts
286318
286525
  var McpToolWrapper = class extends BaseTool {
286319
286526
  name;
@@ -286322,12 +286529,12 @@ var McpToolWrapper = class extends BaseTool {
286322
286529
  mcpClient;
286323
286530
  serverName;
286324
286531
  toolName;
286325
- constructor(mcpClient, serverName, toolName, description, inputSchema) {
286532
+ constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
286326
286533
  super();
286327
286534
  this.mcpClient = mcpClient;
286328
286535
  this.serverName = serverName;
286329
286536
  this.toolName = toolName;
286330
- this.name = mcpToolName(serverName, toolName);
286537
+ this.name = registeredName ?? mcpToolName(serverName, toolName);
286331
286538
  this.description = `[MCP:${serverName}] ${description}`;
286332
286539
  this.inputSchema = inputSchema;
286333
286540
  }
@@ -286398,22 +286605,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
286398
286605
  getRegisteredPlugins() {
286399
286606
  return Array.from(this.plugins.keys());
286400
286607
  }
286401
- /** Registers all tools from an MCP client */
286402
- registerMcpTools(mcpClient) {
286403
- const definitions = mcpClient.getToolDefinitions();
286404
- for (const def of definitions) {
286405
- const [, serverName, toolName] = def.name.split("::");
286406
- if (!serverName || !toolName) continue;
286608
+ /**
286609
+ * Registers all tools from an MCP client.
286610
+ *
286611
+ * `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
286612
+ * same string the user sees and the same one that reaches the provider — so
286613
+ * a per-tool opt-out in Settings can be expressed as a plain name list.
286614
+ * Filtering here rather than at call time means a deselected tool is genuinely
286615
+ * absent from the definitions handed to the model, so it is never proposed,
286616
+ * never counted against the tool budget, and never has to be refused.
286617
+ */
286618
+ registerMcpTools(mcpClient, isAllowed) {
286619
+ const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
286620
+ const [, server, tool] = def.name.split("::");
286621
+ return server && tool ? [{ def, server, tool }] : [];
286622
+ });
286623
+ const names = assignMcpToolNames(parsed);
286624
+ parsed.forEach(({ def, server, tool }, i4) => {
286407
286625
  const wrapper = new McpToolWrapper(
286408
286626
  mcpClient,
286409
- serverName,
286410
- toolName,
286411
- def.description.replace(`[MCP:${serverName}] `, ""),
286412
- def.inputSchema
286627
+ server,
286628
+ tool,
286629
+ def.description.replace(`[MCP:${server}] `, ""),
286630
+ def.inputSchema,
286631
+ names[i4]
286413
286632
  );
286633
+ if (isAllowed && !isAllowed(wrapper.name)) return;
286414
286634
  wrapper.setWorkspaceRoot(this.workspaceRoot);
286415
286635
  this.register(wrapper);
286416
- }
286636
+ });
286417
286637
  }
286418
286638
  setIgnoredPaths(patterns) {
286419
286639
  this.ignoreMatcher = ignore().add(patterns);
@@ -300153,6 +300373,17 @@ function fileOAuthProvider(storePath) {
300153
300373
  }
300154
300374
  });
300155
300375
  }
300376
+ var oauthStoreLocks = /* @__PURE__ */ new Map();
300377
+ function withOAuthStoreLock(storePath, fn) {
300378
+ const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
300379
+ const run = prev.then(fn, fn);
300380
+ const tail = run.then(() => void 0, () => void 0);
300381
+ oauthStoreLocks.set(storePath, tail);
300382
+ void tail.then(() => {
300383
+ if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
300384
+ });
300385
+ return run;
300386
+ }
300156
300387
  function beginMcpOAuth(provider, serverUrl) {
300157
300388
  return auth(provider, { serverUrl });
300158
300389
  }
@@ -303103,7 +303334,50 @@ var GuidanceQueue = class {
303103
303334
  }
303104
303335
  };
303105
303336
 
303337
+ // src/tools/current-page.ts
303338
+ var MAX_PAGE_TEXT_CHARS = 4e4;
303339
+ var CurrentPageTool = class extends BaseTool {
303340
+ name = "read_current_page";
303341
+ 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.';
303342
+ inputSchema = {
303343
+ type: "object",
303344
+ properties: {},
303345
+ required: []
303346
+ };
303347
+ provider;
303348
+ constructor(provider) {
303349
+ super();
303350
+ this.provider = provider;
303351
+ }
303352
+ // Reading what is already on the user's screen changes nothing and reaches
303353
+ // nothing they have not already opened themselves.
303354
+ isDangerous() {
303355
+ return false;
303356
+ }
303357
+ async execute(_input, _options) {
303358
+ let page2;
303359
+ try {
303360
+ page2 = await this.provider();
303361
+ } catch (err) {
303362
+ return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
303363
+ }
303364
+ if (!page2) {
303365
+ return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
303366
+ }
303367
+ const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
303368
+
303369
+ [truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
303370
+ return [
303371
+ `URL: ${page2.url}`,
303372
+ `Title: ${page2.title}`,
303373
+ "",
303374
+ text.trim() || "[the page rendered no readable text]"
303375
+ ].join("\n");
303376
+ }
303377
+ };
303378
+
303106
303379
  // src/core/cascade.ts
303380
+ var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
303107
303381
  function buildContextualPrompt(prompt, history = []) {
303108
303382
  const recent = history.slice(-6);
303109
303383
  if (recent.length === 0) return prompt;
@@ -303326,6 +303600,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
303326
303600
  });
303327
303601
  });
303328
303602
  }
303603
+ // ── Escalation decisions ────────────────────────────────────────────
303604
+ //
303605
+ // A section that ends ESCALATED used to be a dead end: the Cockpit showed
303606
+ // "Section escalated — needs a decision" and no decision was ever requested,
303607
+ // so the run stopped there having spent a full orchestration. This is the
303608
+ // same round-trip shape as plan approval, with one deliberate difference —
303609
+ // an unanswered escalation FAILS the section instead of proceeding.
303610
+ //
303611
+ // That direction is the safe one here. Plan approval defaults to "yes" on
303612
+ // timeout because the plan is already the model's considered proposal;
303613
+ // an escalation exists precisely BECAUSE the worker was not confident, so
303614
+ // acting unattended is the option most likely to be wrong. And in cloud a
303615
+ // waiting run holds server resources, so hanging is not free either.
303616
+ // Keyed by request id, NOT a single slot. A Complex run dispatches each wave
303617
+ // of ready sections with `Promise.all` (t1-administrator.ts), so two sections
303618
+ // can be waiting on a decision at the same moment. A single field made that
303619
+ // case fail hard rather than merely confusingly: the second escalation
303620
+ // overwrote the first's resolver, the user's answer resolved only the second,
303621
+ // and then the FIRST section's timer found the slot already empty and
303622
+ // returned without resolving anything — parking the run forever, past its own
303623
+ // timeout. Same class of bug as the twelve-failover dead-model race: a wave
303624
+ // is concurrent, so per-run singletons are never safe inside it.
303625
+ pendingEscalations = /* @__PURE__ */ new Map();
303626
+ async requestEscalationDecision(ctx, taskId, signal) {
303627
+ if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
303628
+ if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
303629
+ if (signal?.aborted) return { action: "skip", automatic: true };
303630
+ const requestId = crypto6.randomUUID();
303631
+ return await new Promise((resolve2) => {
303632
+ let timeout;
303633
+ const settle = (decision) => {
303634
+ if (!this.pendingEscalations.delete(requestId)) return;
303635
+ if (timeout) clearTimeout(timeout);
303636
+ signal?.removeEventListener("abort", onAbort);
303637
+ resolve2(decision);
303638
+ };
303639
+ const onAbort = () => settle({ action: "skip", automatic: true });
303640
+ signal?.addEventListener("abort", onAbort, { once: true });
303641
+ timeout = setTimeout(() => {
303642
+ this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
303643
+ settle({ action: "timeout" });
303644
+ }, ESCALATION_DECISION_TIMEOUT_MS);
303645
+ this.pendingEscalations.set(requestId, settle);
303646
+ this.emit("escalation:decision-required", {
303647
+ taskId,
303648
+ requestId,
303649
+ sectionId: ctx.sectionId,
303650
+ sectionTitle: ctx.sectionTitle,
303651
+ issues: ctx.issues,
303652
+ summary: ctx.summary,
303653
+ timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
303654
+ });
303655
+ });
303656
+ }
303657
+ /**
303658
+ * Resolve a pending escalation from a REPL / dashboard / cloud listener.
303659
+ *
303660
+ * `requestId` comes back from the `escalation:decision-required` event and is
303661
+ * what makes an answer land on the section that asked. It is optional so a
303662
+ * host with only one escalation in flight (and any caller that predates the
303663
+ * id) still works: without it the oldest outstanding request is answered,
303664
+ * which is correct whenever there is exactly one.
303665
+ *
303666
+ * `automatic` defaults to false — a call into this method is normally a real
303667
+ * person answering the prompt. Pass `true` from a host that is itself
303668
+ * settling the gate on nobody's behalf (a REST caller who never had a
303669
+ * chance to connect, a session halt with no per-section review behind it),
303670
+ * so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
303671
+ * see EscalationDecision.automatic.
303672
+ */
303673
+ resolveEscalation(action, note, requestId, automatic) {
303674
+ const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
303675
+ if (requestId) {
303676
+ this.pendingEscalations.get(requestId)?.(decision);
303677
+ return;
303678
+ }
303679
+ const oldest = this.pendingEscalations.keys().next();
303680
+ if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
303681
+ }
303682
+ /**
303683
+ * Release every parked escalation — run teardown, so none outlive the run.
303684
+ * `automatic: true`: teardown resolving a section nobody answered is the
303685
+ * same system-decided case as the early-return skips above, not a person
303686
+ * reviewing and accepting that section's output.
303687
+ */
303688
+ releasePendingEscalations(action = "skip") {
303689
+ for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
303690
+ }
303329
303691
  /**
303330
303692
  * Resolve a pending boardroom plan approval from a REPL / dashboard listener.
303331
303693
  * An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
@@ -303459,6 +303821,20 @@ ${input}` }],
303459
303821
  this.mediaSink = sink;
303460
303822
  this.registerMediaTools(this.workspacePath);
303461
303823
  }
303824
+ /**
303825
+ * Let the agent read the page the user has open in the host's browser.
303826
+ *
303827
+ * Host-supplied because only a host WITH a browser can answer it — so the
303828
+ * tool is simply absent in the CLI and in hosted runs, rather than present
303829
+ * and always failing. Registered outside `enabledTools` for the same reason
303830
+ * media tools are: that allowlist is a blast-radius control for tools that
303831
+ * touch the machine, and reading a page the user already opened touches
303832
+ * nothing. `disabledTools` still removes it.
303833
+ */
303834
+ setCurrentPageProvider(provider) {
303835
+ if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
303836
+ this.toolRegistry.register(new CurrentPageTool(provider));
303837
+ }
303462
303838
  registerMediaTools(workspacePath) {
303463
303839
  const denied = new Set(this.config.tools?.disabledTools ?? []);
303464
303840
  const permitted = (name) => !denied.has(name);
@@ -303591,8 +303967,12 @@ ${last3.partialOutput}` : "");
303591
303967
  for (const server of this.config.tools.mcpServers) {
303592
303968
  try {
303593
303969
  const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
303594
- await this.mcpClient.connect(server, authProvider ? { authProvider } : {});
303595
- this.toolRegistry.registerMcpTools(this.mcpClient);
303970
+ const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
303971
+ await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
303972
+ this.toolRegistry.registerMcpTools(
303973
+ this.mcpClient,
303974
+ (name) => !(this.config.tools?.disabledTools ?? []).includes(name)
303975
+ );
303596
303976
  } catch (err) {
303597
303977
  console.error(`Failed to connect to MCP server "${server.name}":`, err);
303598
303978
  }
@@ -304057,6 +304437,7 @@ ${prompt}` : prompt;
304057
304437
  } else if (complexity === "Moderate") {
304058
304438
  const t22 = new T2Manager(this.router, this.toolRegistry, "root");
304059
304439
  t22.setRunBreaker(runBreaker);
304440
+ t22.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
304060
304441
  t22.setPresenter(true);
304061
304442
  t22.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.");
304062
304443
  if (identityPrompt) {
@@ -304101,11 +304482,17 @@ ${prompt}` : prompt;
304101
304482
  t2Results = [t2Result];
304102
304483
  const completed = t2Result.t3Results.filter((r4) => r4.status === "COMPLETED");
304103
304484
  if (completed.length > 0) {
304104
- finalOutput = t2Result.sectionSummary + "\n\n" + completed.map((r4) => r4.output).join("\n\n");
304485
+ finalOutput = composeRootSectionOutput(
304486
+ t2Result,
304487
+ completed.map((r4) => typeof r4.output === "string" ? r4.output : JSON.stringify(r4.output))
304488
+ );
304105
304489
  } else {
304106
304490
  const partial2 = t2Result.t3Results.map((r4) => typeof r4.output === "string" ? r4.output : "").filter((o3) => o3.trim()).sort((a2, b3) => b3.length - a2.length)[0];
304107
304491
  const reasons = Array.from(
304108
- new Set(t2Result.t3Results.flatMap((r4) => r4.issues ?? []).filter(Boolean))
304492
+ new Set([
304493
+ ...t2Result.issues ?? [],
304494
+ ...t2Result.t3Results.flatMap((r4) => r4.issues ?? [])
304495
+ ].filter(Boolean))
304109
304496
  );
304110
304497
  if (partial2) {
304111
304498
  finalOutput = reasons.length ? `${partial2}
@@ -304118,6 +304505,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
304118
304505
  } else {
304119
304506
  const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
304120
304507
  t1.setRunBreaker(runBreaker);
304508
+ t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
304121
304509
  t1.setPresenter(true);
304122
304510
  t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
304123
304511
  if (identityPrompt) {
@@ -304142,6 +304530,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
304142
304530
  finalOutput = result.output;
304143
304531
  t2Results = result.t2Results;
304144
304532
  }
304533
+ const budgetInfo = this.router.budgetExceededInfo();
304534
+ if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
304145
304535
  const trip = runBreaker.reason();
304146
304536
  if (trip) {
304147
304537
  finalOutput = [
@@ -304182,6 +304572,10 @@ ${finalOutput}` : ""
304182
304572
  escalator.cancelAllPending();
304183
304573
  } catch {
304184
304574
  }
304575
+ try {
304576
+ this.releasePendingEscalations();
304577
+ } catch {
304578
+ }
304185
304579
  this.router.restoreTierModels();
304186
304580
  this.router.setRunSignal(void 0);
304187
304581
  if (this.taskAnalyzer) {
@@ -305498,6 +305892,13 @@ var ConfigManager = class {
305498
305892
  }
305499
305893
  async load() {
305500
305894
  this.config = await this.loadConfig();
305895
+ const servers = this.config.tools?.mcpServers;
305896
+ const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
305897
+ const mcpNamesChanged = renames.length > 0;
305898
+ if (mcpNamesChanged && this.config.tools) {
305899
+ this.config.tools.mcpServers = disambiguated;
305900
+ this.renameMcpServerReferences(renames);
305901
+ }
305501
305902
  this.ignore = new CascadeIgnore();
305502
305903
  await this.ignore.load(this.workspacePath);
305503
305904
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
@@ -305506,6 +305907,7 @@ var ConfigManager = class {
305506
305907
  await this.injectEnvKeys();
305507
305908
  this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
305508
305909
  await this.ensureDefaultIdentity();
305910
+ if (mcpNamesChanged) await this.save();
305509
305911
  }
305510
305912
  getConfig() {
305511
305913
  return this.config;
@@ -305589,6 +305991,66 @@ var ConfigManager = class {
305589
305991
  this.config.providers.push({ type: "ollama" });
305590
305992
  }
305591
305993
  }
305994
+ /**
305995
+ * Follow a disambiguation rename into `tools.mcpTrusted`.
305996
+ *
305997
+ * `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
305998
+ * of rename reach here, and they need OPPOSITE treatment:
305999
+ *
306000
+ * - Distinct raw names that only collide via sanitizing (`foo bar` /
306001
+ * `foo@bar`): each had its OWN trust entry, and the old string now belongs
306002
+ * to nothing — the server that used it moved away, so the entry has to
306003
+ * move with it. A plain rewrite is correct here.
306004
+ * - Literally identical names (a hand-edited or duplicated config: two rows
306005
+ * both named `foo`): there is only ONE trust entry for both, since
306006
+ * `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
306007
+ * already been updated to the disambiguated list by the time this runs
306008
+ * (see `load()`), so the untouched survivor is STILL named `foo` — a plain
306009
+ * rewrite would move the one entry entirely onto the renamed row and
306010
+ * leave the survivor untrusted. The entry must be kept for the survivor
306011
+ * AND granted to the renamed identity, not moved.
306012
+ *
306013
+ * Renames are grouped by `from` before either treatment is applied, and
306014
+ * each group is settled in one step. Two DIFFERENT rows can start out with
306015
+ * the exact same raw name and BOTH get renamed away (e.g. three servers
306016
+ * named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
306017
+ * with `foo bar` and with each other, so both are renamed to distinct
306018
+ * identities while `foo bar` survives untouched). Processing renames one at
306019
+ * a time in that shape drops coverage: the first rename's REPLACE branch
306020
+ * would erase `from` from `trusted` entirely, so the guard on the second
306021
+ * rename with the same `from` sees it already gone and silently skips —
306022
+ * granting trust to only one of the two renamed identities. Grouping first
306023
+ * means every renamed identity that shares a `from` is added or substituted
306024
+ * together, in the one pass that still sees the original entry.
306025
+ *
306026
+ * `tools.disabledTools` deliberately gets no equivalent treatment. It's
306027
+ * matched by sanitized PREFIX, and a prefix collision is exactly what made
306028
+ * an existing entry ambiguous between the two servers in the first place —
306029
+ * there is no way to tell, from the stored string alone, which of the two
306030
+ * a denial was meant for. Leaving those entries untouched resolves that
306031
+ * ambiguity for free: the survivor keeps the original (now unique) prefix,
306032
+ * so an old entry keeps applying to it, while the renamed server starts
306033
+ * clean under its new prefix. Rewriting the entry to follow the rename
306034
+ * would do the opposite — move a denial that already worked for the
306035
+ * survivor onto a server it may never have meant to cover.
306036
+ */
306037
+ renameMcpServerReferences(renames) {
306038
+ const tools = this.config.tools;
306039
+ if (!tools?.mcpTrusted?.length || !renames.length) return;
306040
+ const currentNames = new Set((tools.mcpServers ?? []).map((s3) => s3.name));
306041
+ const byFrom = /* @__PURE__ */ new Map();
306042
+ for (const { from, to } of renames) {
306043
+ const tos = byFrom.get(from);
306044
+ if (tos) tos.push(to);
306045
+ else byFrom.set(from, [to]);
306046
+ }
306047
+ let trusted = tools.mcpTrusted;
306048
+ for (const [from, tos] of byFrom) {
306049
+ if (!trusted.includes(from)) continue;
306050
+ trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n8) => n8 !== from), ...tos];
306051
+ }
306052
+ tools.mcpTrusted = Array.from(new Set(trusted));
306053
+ }
305592
306054
  async ensureDefaultIdentity() {
305593
306055
  const existing = this.store.getDefaultIdentity();
305594
306056
  if (existing) return;
@@ -306492,6 +306954,14 @@ var DashboardSocket = class {
306492
306954
  this.secret = options.secret;
306493
306955
  this.setupHandlers();
306494
306956
  }
306957
+ /** How many sockets are currently in a room — 0 means nobody can answer. */
306958
+ roomSize(room) {
306959
+ return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
306960
+ }
306961
+ /** Is this exact connection still live? A reconnect issues a NEW socket id. */
306962
+ hasSocket(socketId) {
306963
+ return this.io.sockets.sockets.has(socketId);
306964
+ }
306495
306965
  broadcastToRoom(room, event, data) {
306496
306966
  this.io.to(room).emit(event, data);
306497
306967
  }
@@ -306563,6 +307033,22 @@ var DashboardSocket = class {
306563
307033
  });
306564
307034
  });
306565
307035
  }
307036
+ /**
307037
+ * A client subscribed to a session's room.
307038
+ *
307039
+ * The host uses this to replay a gate the run is already parked on. Socket.IO
307040
+ * does not buffer room emissions, so a prompt broadcast while the renderer was
307041
+ * reconnecting is simply gone; without a replay the section waits out its full
307042
+ * timeout with the user sitting in front of a screen that never asked.
307043
+ */
307044
+ onJoinSession(callback) {
307045
+ this.io.on("connection", (socket) => {
307046
+ socket.on("join:session", (payload) => {
307047
+ const { sessionId } = normalizeSessionSubscriptionPayload(payload);
307048
+ if (sessionId) callback(sessionId, socket.id);
307049
+ });
307050
+ });
307051
+ }
306566
307052
  onSessionRate(callback) {
306567
307053
  this.io.on("connection", (socket) => {
306568
307054
  socket.on("session:rate", (payload) => {
@@ -306600,6 +307086,31 @@ var DashboardSocket = class {
306600
307086
  });
306601
307087
  });
306602
307088
  }
307089
+ /**
307090
+ * A section escalated and the run is parked waiting for an answer. The
307091
+ * desktop shows the escalation modal on `escalation:decision-required` and
307092
+ * answers here.
307093
+ *
307094
+ * Unlike a plan decision, silence is NOT taken as consent — the SDK fails
307095
+ * the section on timeout — so this handler only ever forwards a real choice.
307096
+ */
307097
+ onEscalationDecision(callback) {
307098
+ this.io.on("connection", (socket) => {
307099
+ socket.on("escalation:decide", (payload) => {
307100
+ const action = payload?.action;
307101
+ if (typeof payload?.sessionId !== "string") return;
307102
+ if (action !== "retry" && action !== "skip" && action !== "guidance") return;
307103
+ callback({
307104
+ sessionId: payload.sessionId,
307105
+ // Identifies WHICH parked section this answers — sections in a wave
307106
+ // run concurrently, so more than one can be waiting.
307107
+ requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
307108
+ action,
307109
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
307110
+ });
307111
+ });
307112
+ });
307113
+ }
306603
307114
  onSessionSteer(callback) {
306604
307115
  this.io.on("connection", (socket) => {
306605
307116
  socket.on("session:steer", (payload) => {
@@ -306761,6 +307272,7 @@ function aggregateCostStats(sessions, opts = {}) {
306761
307272
 
306762
307273
  // src/dashboard/server.ts
306763
307274
  var __dirname2 = path26__namespace.default.dirname(Url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('desktop-core.cjs', document.baseURI).href))));
307275
+ var ORPHANED_ESCALATION_GRACE_MS = 3e4;
306764
307276
  var DashboardServer = class {
306765
307277
  app;
306766
307278
  httpServer;
@@ -306771,6 +307283,13 @@ var DashboardServer = class {
306771
307283
  globalStore = null;
306772
307284
  broadcastTimer = null;
306773
307285
  activeSessions = /* @__PURE__ */ new Map();
307286
+ /**
307287
+ * Supplied by the desktop shell so a run can read the page the user has
307288
+ * open in the built-in browser. Absent in the CLI, where there is no
307289
+ * browser — so the tool is simply not registered rather than present and
307290
+ * always failing.
307291
+ */
307292
+ currentPageProvider;
306774
307293
  activeControllers = /* @__PURE__ */ new Map();
306775
307294
  /**
306776
307295
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -306785,6 +307304,17 @@ var DashboardServer = class {
306785
307304
  * map is how that answer reaches the run that's blocked on it.
306786
307305
  */
306787
307306
  pendingApprovals = /* @__PURE__ */ new Map();
307307
+ /**
307308
+ * The escalation prompt a session is currently parked on, kept so it can be
307309
+ * delivered to a client that was not reachable when it was raised.
307310
+ *
307311
+ * Socket.IO buffers nothing: a prompt emitted while the renderer is mid
307312
+ * reconnect is gone, and the section then waits out its full five minutes
307313
+ * with the user in front of a screen that never asked them anything. Holding
307314
+ * the payload lets `join:session` replay it, and lets a run with no reachable
307315
+ * client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
307316
+ */
307317
+ pendingEscalations = /* @__PURE__ */ new Map();
306788
307318
  /**
306789
307319
  * The orchestration decision trail ("why") of each session's most recent
306790
307320
  * run — captured when the run ends so the desktop's Why panel can show it
@@ -306862,20 +307392,29 @@ var DashboardServer = class {
306862
307392
  if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
306863
307393
  const cascade = new Cascade(cfg, this.workspacePath, this.store);
306864
307394
  this.activeSessions.set(sessionId, cascade);
307395
+ if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
306865
307396
  cascade.on("stream:token", (e3) => {
306866
- this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
307397
+ this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
306867
307398
  });
306868
307399
  cascade.on("tier:status", (e3) => {
306869
- this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e3 });
307400
+ this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e3 });
306870
307401
  });
306871
307402
  cascade.on("permission:user-required", (e3) => {
306872
- this.socket.emitToSocket(socketId, "permission:user-required", { sessionId, ...e3 });
307403
+ this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e3 });
306873
307404
  });
306874
307405
  cascade.on("peer:message", (e3) => {
306875
307406
  this.socket.emitPeerMessage(e3);
306876
307407
  });
306877
307408
  cascade.on("plan:approval-required", (e3) => {
306878
- this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e3 });
307409
+ this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e3 });
307410
+ });
307411
+ cascade.on("escalation:decision-required", (e3) => {
307412
+ this.raiseEscalation(sessionId, socketId, e3);
307413
+ });
307414
+ cascade.on("escalation:timeout", (e3) => {
307415
+ const ev = e3;
307416
+ if (ev.requestId) this.clearPendingEscalation(ev.requestId);
307417
+ this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e3 });
306879
307418
  });
306880
307419
  try {
306881
307420
  const result = await cascade.run({
@@ -306886,7 +307425,7 @@ var DashboardServer = class {
306886
307425
  this.recordSessionTask(sessionId, result.taskId);
306887
307426
  this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
306888
307427
  this.captureWhy(sessionId, cascade, result);
306889
- this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
307428
+ this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
306890
307429
  this.socket.broadcast("cost:update", {
306891
307430
  sessionId,
306892
307431
  totalTokens: result.usage.totalTokens,
@@ -306899,7 +307438,7 @@ var DashboardServer = class {
306899
307438
  } catch (err) {
306900
307439
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
306901
307440
  this.captureWhy(sessionId, cascade);
306902
- this.socket.emitToSocket(socketId, "session:error", {
307441
+ this.emitToSessionClients(socketId, sessionId, "session:error", {
306903
307442
  sessionId,
306904
307443
  error: err instanceof Error ? err.message : String(err)
306905
307444
  });
@@ -306907,6 +307446,7 @@ var DashboardServer = class {
306907
307446
  this.activeSessions.delete(sessionId);
306908
307447
  this.activeControllers.delete(sessionId);
306909
307448
  this.denyPendingApprovals(sessionId);
307449
+ this.clearSessionEscalations(sessionId);
306910
307450
  }
306911
307451
  });
306912
307452
  this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
@@ -306916,10 +307456,20 @@ var DashboardServer = class {
306916
307456
  editedPlan
306917
307457
  );
306918
307458
  });
307459
+ this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
307460
+ if (requestId) this.clearPendingEscalation(requestId);
307461
+ else this.clearOldestSessionEscalation(sessionId);
307462
+ this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
307463
+ });
307464
+ this.socket.onJoinSession((sessionId, socketId) => {
307465
+ this.replayEscalations(sessionId, socketId);
307466
+ });
306919
307467
  this.socket.onSessionHalt((sessionId) => {
306920
307468
  this.activeControllers.get(sessionId)?.abort();
306921
307469
  this.denyPendingApprovals(sessionId);
306922
307470
  this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
307471
+ this.clearSessionEscalations(sessionId);
307472
+ this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
306923
307473
  });
306924
307474
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
306925
307475
  const pending = this.pendingApprovals.get(requestId);
@@ -306934,6 +307484,10 @@ var DashboardServer = class {
306934
307484
  }
306935
307485
  });
306936
307486
  }
307487
+ /** Wire the host's browser view in (desktop only). */
307488
+ setCurrentPageProvider(provider) {
307489
+ this.currentPageProvider = provider;
307490
+ }
306937
307491
  async start() {
306938
307492
  const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
306939
307493
  if (!isLoopback) {
@@ -307222,6 +307776,116 @@ var DashboardServer = class {
307222
307776
  this.activeSessions.delete(sessionId);
307223
307777
  }
307224
307778
  }
307779
+ /**
307780
+ * Is anyone able to answer for this session right now?
307781
+ *
307782
+ * The originating socket first — that connection is definitely watching this
307783
+ * run — then the session room, which is where a client lands after a
307784
+ * reconnect issues it a new socket id.
307785
+ */
307786
+ hasResponder(sessionId, socketId) {
307787
+ if (socketId && this.socket.hasSocket(socketId)) return true;
307788
+ return this.socket.roomSize(`session:${sessionId}`) > 0;
307789
+ }
307790
+ /**
307791
+ * Send a run event to whoever is watching this session.
307792
+ *
307793
+ * A captured socket id goes stale the moment Socket.IO reconnects — the
307794
+ * client is back, but under a new id, and `emitToSocket` on the old one is a
307795
+ * silent no-op. Falling back to the session room keeps the run addressable
307796
+ * across a reconnect; emitting to one OR the other (never both) keeps a
307797
+ * client that is both the originator and a room member from seeing every
307798
+ * token twice.
307799
+ */
307800
+ emitToSessionClients(socketId, sessionId, event, data) {
307801
+ if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
307802
+ else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
307803
+ }
307804
+ /**
307805
+ * Raise an escalation prompt, and make sure it can still be answered.
307806
+ *
307807
+ * Held in `pendingEscalations` — keyed by requestId, because a Complex wave
307808
+ * dispatches sections concurrently and two can be parked at once — so a
307809
+ * client that joins late (or rejoins after a reconnect) gets every waiting
307810
+ * prompt replayed. Keying by session would drop the first of a pair: its
307811
+ * grace timer would be cleared by the second, leaving it parked for the full
307812
+ * timeout with nobody able to answer it. If nobody is reachable at all, the gate is
307813
+ * settled as `skip` after a short grace period rather than parked for the
307814
+ * full timeout: that matches the SDK's policy when no listener exists, and
307815
+ * five minutes of a held run is a real cost for a REST caller who has already
307816
+ * hung up. The grace period exists so an ordinary two-second reconnect is not
307817
+ * mistaken for an absent user.
307818
+ */
307819
+ raiseEscalation(sessionId, socketId, event) {
307820
+ const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto6.randomUUID();
307821
+ const payload = { sessionId, ...event };
307822
+ const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
307823
+ this.pendingEscalations.set(requestId, entry);
307824
+ if (this.hasResponder(sessionId, socketId)) {
307825
+ this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
307826
+ return;
307827
+ }
307828
+ entry.graceTimer = setTimeout(() => {
307829
+ if (this.pendingEscalations.get(requestId) !== entry) return;
307830
+ if (this.hasResponder(sessionId, socketId)) return;
307831
+ this.pendingEscalations.delete(requestId);
307832
+ this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
307833
+ }, ORPHANED_ESCALATION_GRACE_MS);
307834
+ }
307835
+ /**
307836
+ * Replay EVERY parked prompt to a client that just subscribed to the session.
307837
+ *
307838
+ * Plural because a Complex run dispatches sections concurrently, so a wave can
307839
+ * leave more than one waiting at the same time — the same reason the SDK keys
307840
+ * its gates and the clients queue theirs.
307841
+ *
307842
+ * `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
307843
+ * has been running since the prompt was raised. Sent verbatim, a client
307844
+ * reconnecting four minutes in would show a fresh five-minute countdown and
307845
+ * then watch the section fail with four of those minutes still on the clock.
307846
+ * A remaining duration is used rather than an absolute deadline so the two
307847
+ * sides never have to agree on a wall clock.
307848
+ */
307849
+ replayEscalations(sessionId, socketId) {
307850
+ for (const entry of this.pendingEscalations.values()) {
307851
+ if (entry.sessionId !== sessionId) continue;
307852
+ if (entry.graceTimer) {
307853
+ clearTimeout(entry.graceTimer);
307854
+ entry.graceTimer = void 0;
307855
+ }
307856
+ const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
307857
+ const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
307858
+ this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
307859
+ }
307860
+ }
307861
+ /** Forget ONE parked prompt — the one that was answered or timed out. */
307862
+ clearPendingEscalation(requestId) {
307863
+ const entry = this.pendingEscalations.get(requestId);
307864
+ if (!entry) return;
307865
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
307866
+ this.pendingEscalations.delete(requestId);
307867
+ }
307868
+ /**
307869
+ * Forget the session's OLDEST prompt — for a client that answered without a
307870
+ * requestId, which is exactly the gate `resolveEscalation` will settle.
307871
+ * Insertion order makes `values()` oldest-first.
307872
+ */
307873
+ clearOldestSessionEscalation(sessionId) {
307874
+ for (const [id, entry] of this.pendingEscalations) {
307875
+ if (entry.sessionId !== sessionId) continue;
307876
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
307877
+ this.pendingEscalations.delete(id);
307878
+ return;
307879
+ }
307880
+ }
307881
+ /** Forget every prompt a session was parked on (run ended, or halted). */
307882
+ clearSessionEscalations(sessionId) {
307883
+ for (const [id, entry] of this.pendingEscalations) {
307884
+ if (entry.sessionId !== sessionId) continue;
307885
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
307886
+ this.pendingEscalations.delete(id);
307887
+ }
307888
+ }
307225
307889
  /** Deny + clear any approvals still pending for a session (run end / abort). */
307226
307890
  denyPendingApprovals(sessionId) {
307227
307891
  for (const [id, pending] of this.pendingApprovals) {
@@ -307722,6 +308386,7 @@ ${prompt}`;
307722
308386
  void (async () => {
307723
308387
  const cascade = new Cascade(this.config, this.workspacePath, this.store);
307724
308388
  this.activeSessions.set(sessionId, cascade);
308389
+ if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
307725
308390
  cascade.on("stream:token", (e3) => {
307726
308391
  this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
307727
308392
  });
@@ -307737,6 +308402,14 @@ ${prompt}`;
307737
308402
  cascade.on("plan:approval-required", (e3) => {
307738
308403
  this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e3 });
307739
308404
  });
308405
+ cascade.on("escalation:decision-required", (e3) => {
308406
+ this.raiseEscalation(sessionId, void 0, e3);
308407
+ });
308408
+ cascade.on("escalation:timeout", (e3) => {
308409
+ const ev = e3;
308410
+ if (ev.requestId) this.clearPendingEscalation(ev.requestId);
308411
+ this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e3 });
308412
+ });
307740
308413
  try {
307741
308414
  const result = await cascade.run({
307742
308415
  prompt: runPrompt,
@@ -307764,6 +308437,7 @@ ${prompt}`;
307764
308437
  } finally {
307765
308438
  this.activeSessions.delete(sessionId);
307766
308439
  this.denyPendingApprovals(sessionId);
308440
+ this.clearSessionEscalations(sessionId);
307767
308441
  }
307768
308442
  })();
307769
308443
  });
@@ -308096,6 +308770,41 @@ var HooksRunner = class {
308096
308770
  }
308097
308771
  };
308098
308772
 
308773
+ // src/mcp/discover.ts
308774
+ async function discoverMcpTools(servers) {
308775
+ const results = [];
308776
+ for (const server of servers) {
308777
+ const client = new McpClient({ trustedServers: [server.name] });
308778
+ try {
308779
+ const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
308780
+ const connect = () => client.connect(server, authProvider ? { authProvider } : {});
308781
+ await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
308782
+ const parsed = client.getToolDefinitions().flatMap((def) => {
308783
+ const [, serverName, toolName] = def.name.split("::");
308784
+ return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
308785
+ });
308786
+ const names = assignMcpToolNames(parsed);
308787
+ const tools = parsed.map(({ def, server: s3, tool }, i4) => ({
308788
+ server: s3,
308789
+ tool,
308790
+ name: names[i4],
308791
+ description: def.description.replace(`[MCP:${s3}] `, "")
308792
+ }));
308793
+ results.push({ server: server.name, tools });
308794
+ } catch (err) {
308795
+ results.push({
308796
+ server: server.name,
308797
+ tools: [],
308798
+ error: err instanceof Error ? err.message : String(err)
308799
+ });
308800
+ } finally {
308801
+ await client.disconnectAll().catch(() => {
308802
+ });
308803
+ }
308804
+ }
308805
+ return results;
308806
+ }
308807
+
308099
308808
  // src/retrieval/chunk.ts
308100
308809
  var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
308101
308810
  function splitLongBlock(block, max) {
@@ -308634,11 +309343,55 @@ function mergeByKey(local, incoming, key) {
308634
309343
  for (const i4 of incoming) map.set(key(i4), i4);
308635
309344
  return [...map.values()];
308636
309345
  }
309346
+ function uniqueServerName(desired, takenPrefixes) {
309347
+ if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
309348
+ let n8 = 2;
309349
+ while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n8}`))) n8++;
309350
+ return `${desired} ${n8}`;
309351
+ }
309352
+ function mergeMcpServers(local, incoming) {
309353
+ const rewrite = /* @__PURE__ */ new Map();
309354
+ if (!incoming?.length) return { servers: local, synced: [], rewrite };
309355
+ const byName = /* @__PURE__ */ new Map();
309356
+ for (const l2 of local ?? []) byName.set(l2.name, l2);
309357
+ const prefixes = new Set([...byName.values()].map((m3) => mcpServerPrefix(m3.name)));
309358
+ const synced = [];
309359
+ for (const inc of incoming) {
309360
+ if (byName.has(inc.name)) {
309361
+ byName.set(inc.name, inc);
309362
+ synced.push(inc);
309363
+ continue;
309364
+ }
309365
+ const name = uniqueServerName(inc.name, prefixes);
309366
+ if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
309367
+ const server = name === inc.name ? inc : { ...inc, name };
309368
+ byName.set(name, server);
309369
+ synced.push(server);
309370
+ prefixes.add(mcpServerPrefix(name));
309371
+ }
309372
+ return { servers: [...byName.values()], synced, rewrite };
309373
+ }
309374
+ function rewriteDisabledTools(tools, rewrite) {
309375
+ if (!tools?.length || rewrite.size === 0) return tools;
309376
+ return tools.map((t4) => {
309377
+ for (const [from, to] of rewrite) if (t4.startsWith(from)) return to + t4.slice(from.length);
309378
+ return t4;
309379
+ });
309380
+ }
309381
+ function mergeDisabledTools(local, incoming, syncedServers) {
309382
+ const prefixes = (syncedServers ?? []).map((m3) => mcpServerPrefix(m3.name));
309383
+ const covered = (tool) => prefixes.some((p3) => tool.startsWith(p3));
309384
+ const kept = (local ?? []).filter((t4) => !covered(t4));
309385
+ const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
309386
+ return merged.length ? merged : void 0;
309387
+ }
308637
309388
  function gatherSyncBundle(config2) {
308638
- const bundle = { v: 1 };
309389
+ const bundle = { v: 2 };
308639
309390
  if (config2.providers?.length) bundle.providers = config2.providers;
308640
309391
  if (config2.tools?.webSearch) bundle.webSearch = config2.tools.webSearch;
308641
309392
  if (config2.tools?.mcpServers?.length) bundle.mcpServers = config2.tools.mcpServers;
309393
+ const syncedDisabledTools = config2.tools?.disabledTools?.filter(isMcpToolName);
309394
+ if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
308642
309395
  if (config2.models) bundle.models = config2.models;
308643
309396
  if (config2.budget) bundle.budget = config2.budget;
308644
309397
  const prefs = {};
@@ -308653,7 +309406,16 @@ function applySyncBundle(bundle, config2) {
308653
309406
  const next = { ...config2, tools: { ...config2.tools } };
308654
309407
  if (bundle.providers) next.providers = mergeByKey(config2.providers, bundle.providers, providerSig) ?? [];
308655
309408
  if (bundle.webSearch) next.tools.webSearch = { ...config2.tools?.webSearch, ...bundle.webSearch };
308656
- if (bundle.mcpServers) next.tools.mcpServers = mergeByKey(config2.tools?.mcpServers, bundle.mcpServers, (m3) => m3.name);
309409
+ const mcp = mergeMcpServers(config2.tools?.mcpServers, bundle.mcpServers);
309410
+ if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
309411
+ const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
309412
+ if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
309413
+ next.tools.disabledTools = mergeDisabledTools(
309414
+ config2.tools?.disabledTools,
309415
+ rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
309416
+ bundle.mcpServers ? mcp.synced : void 0
309417
+ );
309418
+ }
308657
309419
  if (bundle.models) next.models = { ...config2.models, ...bundle.models };
308658
309420
  if (bundle.budget) next.budget = { ...config2.budget, ...bundle.budget };
308659
309421
  if (bundle.prefs) {
@@ -309088,6 +309850,7 @@ exports.CascadeToolError = CascadeToolError;
309088
309850
  exports.CloudClient = CloudClient;
309089
309851
  exports.CodeSearchTool = CodeSearchTool;
309090
309852
  exports.ConfigManager = ConfigManager;
309853
+ exports.CurrentPageTool = CurrentPageTool;
309091
309854
  exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
309092
309855
  exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;
309093
309856
  exports.DEFAULT_AUTO_SUMMARIZE_AT = DEFAULT_AUTO_SUMMARIZE_AT;
@@ -309110,6 +309873,8 @@ exports.HooksRunner = HooksRunner;
309110
309873
  exports.Keystore = Keystore;
309111
309874
  exports.LLMReranker = LLMReranker;
309112
309875
  exports.LM_STUDIO_BASE_URL = LM_STUDIO_BASE_URL;
309876
+ exports.MAX_PAGE_TEXT_CHARS = MAX_PAGE_TEXT_CHARS;
309877
+ exports.MCP_TOOL_PREFIX = MCP_TOOL_PREFIX;
309113
309878
  exports.MODELS = MODELS;
309114
309879
  exports.McpClient = McpClient;
309115
309880
  exports.McpOAuthProvider = McpOAuthProvider;
@@ -309146,7 +309911,9 @@ exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
309146
309911
  exports.createCascade = createCascade;
309147
309912
  exports.decryptSyncBlob = decryptJSON;
309148
309913
  exports.diffManifest = diffManifest;
309914
+ exports.disambiguateMcpServerNames = disambiguateMcpServerNames;
309149
309915
  exports.discoverMcpAuthServer = discoverMcpAuthServer;
309916
+ exports.discoverMcpTools = discoverMcpTools;
309150
309917
  exports.distillSessionFacts = distillSessionFacts;
309151
309918
  exports.embedderFromProviders = embedderFromProviders;
309152
309919
  exports.encryptSyncBlob = encryptJSON;
@@ -309155,12 +309922,17 @@ exports.gatherSyncBundle = gatherSyncBundle;
309155
309922
  exports.hashContent = hashContent;
309156
309923
  exports.heuristicCodeChunker = heuristicCodeChunker;
309157
309924
  exports.inferAzureBaseModel = inferAzureBaseModel;
309925
+ exports.isMcpToolName = isMcpToolName;
309926
+ exports.mcpServerPrefix = mcpServerPrefix;
309927
+ exports.mcpToolName = mcpToolName;
309158
309928
  exports.nodeHttpFetch = nodeHttpFetch;
309159
309929
  exports.parseRankOrder = parseRankOrder;
309160
309930
  exports.planRetrieval = planRetrieval;
309161
309931
  exports.preferIpv4Host = preferIpv4Host;
309162
309932
  exports.reciprocalRankFusion = reciprocalRankFusion;
309163
309933
  exports.refreshMcpToken = refreshMcpToken;
309934
+ exports.removeMcpServerDenials = removeMcpServerDenials;
309164
309935
  exports.runCascade = runCascade;
309165
309936
  exports.sessionWorthRemembering = sessionWorthRemembering;
309166
309937
  exports.streamCascade = streamCascade;
309938
+ exports.uniqueMcpServerName = uniqueMcpServerName;