cascade-ai 0.54.0 → 0.56.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/cli.cjs +793 -64
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +793 -64
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +833 -55
- package/dist/index.cjs +827 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +435 -4
- package/dist/index.d.ts +435 -4
- package/dist/index.js +818 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1897,7 +1897,7 @@ function reconcilePrice(model, live) {
|
|
|
1897
1897
|
}
|
|
1898
1898
|
|
|
1899
1899
|
// src/constants.ts
|
|
1900
|
-
var CASCADE_VERSION = "0.
|
|
1900
|
+
var CASCADE_VERSION = "0.56.0";
|
|
1901
1901
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1902
1902
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1903
1903
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -4835,6 +4835,23 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4835
4835
|
*/
|
|
4836
4836
|
budgetState = "ok";
|
|
4837
4837
|
budgetExceededReason;
|
|
4838
|
+
/**
|
|
4839
|
+
* Whether the run/session budget is permanently tripped, and why.
|
|
4840
|
+
*
|
|
4841
|
+
* A tier or worker that catches `BudgetExceededError` internally (T3Worker
|
|
4842
|
+
* does, so one dead-end subtask doesn't crash the whole run) absorbs the
|
|
4843
|
+
* exception into a normal FAILED/PARTIAL result — the tier returns as if
|
|
4844
|
+
* nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
|
|
4845
|
+
* budget-exceeded handling (the `run:budget-exceeded` event and
|
|
4846
|
+
* `lastInterruptedRun` for `/continue`). This lets a caller check the
|
|
4847
|
+
* router's own state directly after a tier returns, regardless of how that
|
|
4848
|
+
* tier chose to report the error internally.
|
|
4849
|
+
*/
|
|
4850
|
+
budgetExceededInfo() {
|
|
4851
|
+
if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
|
|
4852
|
+
if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
|
|
4853
|
+
return null;
|
|
4854
|
+
}
|
|
4838
4855
|
tpmLimiter;
|
|
4839
4856
|
localQueue;
|
|
4840
4857
|
taskAnalyzer;
|
|
@@ -6492,6 +6509,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
6492
6509
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
6493
6510
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6494
6511
|
}
|
|
6512
|
+
if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
6513
|
+
issues.push(errMsg);
|
|
6514
|
+
const stopped = output || errMsg;
|
|
6515
|
+
this.setStatus("FAILED", stopped);
|
|
6516
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
|
|
6517
|
+
return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6518
|
+
}
|
|
6495
6519
|
issues.push(`Execution error: ${errMsg}`);
|
|
6496
6520
|
const finalOutput = output || errMsg;
|
|
6497
6521
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -7513,6 +7537,26 @@ var RedactionLayer = class {
|
|
|
7513
7537
|
}
|
|
7514
7538
|
};
|
|
7515
7539
|
|
|
7540
|
+
// src/core/tiers/escalation-policy.ts
|
|
7541
|
+
function sectionNeedsDecision(results) {
|
|
7542
|
+
return results.some((r) => r.status === "ESCALATED");
|
|
7543
|
+
}
|
|
7544
|
+
function settledEscalationStatus(results) {
|
|
7545
|
+
const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
|
|
7546
|
+
return hasWork ? "PARTIAL" : "FAILED";
|
|
7547
|
+
}
|
|
7548
|
+
function composeRootSectionOutput(t2Result, completedOutputs) {
|
|
7549
|
+
let output = `${t2Result.sectionSummary}
|
|
7550
|
+
|
|
7551
|
+
${completedOutputs.join("\n\n")}`;
|
|
7552
|
+
if (t2Result.status === "FAILED" && t2Result.issues?.length) {
|
|
7553
|
+
output += `
|
|
7554
|
+
|
|
7555
|
+
_(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
7556
|
+
}
|
|
7557
|
+
return output;
|
|
7558
|
+
}
|
|
7559
|
+
|
|
7516
7560
|
// src/core/tiers/t2-manager.ts
|
|
7517
7561
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
7518
7562
|
return [
|
|
@@ -7538,6 +7582,19 @@ var T2Manager = class extends BaseTier {
|
|
|
7538
7582
|
t2PeerBus;
|
|
7539
7583
|
permissionEscalator;
|
|
7540
7584
|
toolCreator;
|
|
7585
|
+
/**
|
|
7586
|
+
* Asked when a section ends ESCALATED — i.e. a worker hit something it could
|
|
7587
|
+
* not decide alone. Without this the escalation was a dead end: the status
|
|
7588
|
+
* said "needs a decision" and no decision was ever requested, so an MCP run
|
|
7589
|
+
* that legitimately needed input just stopped there.
|
|
7590
|
+
*/
|
|
7591
|
+
escalationCallback;
|
|
7592
|
+
/**
|
|
7593
|
+
* The one allowed retry has been spent. Retrying is bounded because an
|
|
7594
|
+
* escalation loop that can re-ask forever burns a budget on a question that
|
|
7595
|
+
* is not resolving; a second escalation is terminal instead.
|
|
7596
|
+
*/
|
|
7597
|
+
escalationRetryUsed = false;
|
|
7541
7598
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
7542
7599
|
planApprovalCallback;
|
|
7543
7600
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -7586,6 +7643,10 @@ var T2Manager = class extends BaseTier {
|
|
|
7586
7643
|
setToolCreator(creator) {
|
|
7587
7644
|
this.toolCreator = creator;
|
|
7588
7645
|
}
|
|
7646
|
+
/** Ask the user what to do when a section escalates. */
|
|
7647
|
+
setEscalationCallback(cb) {
|
|
7648
|
+
this.escalationCallback = cb;
|
|
7649
|
+
}
|
|
7589
7650
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
7590
7651
|
setPlanApprovalCallback(cb) {
|
|
7591
7652
|
this.planApprovalCallback = cb;
|
|
@@ -7690,9 +7751,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7690
7751
|
currentAction: "Aggregating T3 results",
|
|
7691
7752
|
status: "IN_PROGRESS"
|
|
7692
7753
|
});
|
|
7693
|
-
|
|
7754
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
7694
7755
|
const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
|
|
7695
|
-
|
|
7756
|
+
let overallStatus = this.determineStatus(t3Results);
|
|
7757
|
+
let userSkipped = false;
|
|
7758
|
+
const hasEscalated = sectionNeedsDecision(t3Results);
|
|
7759
|
+
const settleEscalated = async (reason) => {
|
|
7760
|
+
if (reason) issues.push(reason);
|
|
7761
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7762
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7763
|
+
};
|
|
7764
|
+
if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
|
|
7765
|
+
await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
|
|
7766
|
+
} else if (hasEscalated && !this.escalationCallback) {
|
|
7767
|
+
await settleEscalated();
|
|
7768
|
+
} else if (hasEscalated && this.escalationCallback) {
|
|
7769
|
+
this.sendStatusUpdate({
|
|
7770
|
+
progressPct: 95,
|
|
7771
|
+
currentAction: "Escalated \u2014 waiting for your decision",
|
|
7772
|
+
status: "ESCALATING"
|
|
7773
|
+
});
|
|
7774
|
+
const decision = await this.escalationCallback({
|
|
7775
|
+
sectionId: assignment.sectionId,
|
|
7776
|
+
sectionTitle: assignment.sectionTitle,
|
|
7777
|
+
issues,
|
|
7778
|
+
summary
|
|
7779
|
+
});
|
|
7780
|
+
this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
|
|
7781
|
+
if (decision.action === "skip") {
|
|
7782
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7783
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7784
|
+
userSkipped = decision.automatic !== true;
|
|
7785
|
+
} else if (decision.action === "retry" || decision.action === "guidance") {
|
|
7786
|
+
this.escalationRetryUsed = true;
|
|
7787
|
+
const guided = decision.action === "guidance" && decision.note?.trim() ? {
|
|
7788
|
+
...assignment,
|
|
7789
|
+
description: `${assignment.description}
|
|
7790
|
+
|
|
7791
|
+
Guidance (must be followed): ${decision.note.trim()}`,
|
|
7792
|
+
// Drop T1's preplanned subtasks so the retry re-decomposes with
|
|
7793
|
+
// the guidance in hand. The workers execute from their subtask
|
|
7794
|
+
// slices, not from this description — leaving the slices in
|
|
7795
|
+
// place gave "Retry with guidance" byte-identical worker
|
|
7796
|
+
// prompts to "Retry as-is", making the note do nothing at all.
|
|
7797
|
+
t3Subtasks: []
|
|
7798
|
+
} : assignment;
|
|
7799
|
+
return await this.execute(guided, taskId, signal);
|
|
7800
|
+
} else {
|
|
7801
|
+
issues.push("Escalated, but no decision was received in time.");
|
|
7802
|
+
overallStatus = "FAILED";
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7696
7805
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7697
7806
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7698
7807
|
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";
|
|
@@ -7703,7 +7812,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7703
7812
|
status: overallStatus,
|
|
7704
7813
|
t3Results,
|
|
7705
7814
|
sectionSummary: summary,
|
|
7706
|
-
issues
|
|
7815
|
+
issues,
|
|
7816
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
7707
7817
|
};
|
|
7708
7818
|
this.publishSectionOutput(result);
|
|
7709
7819
|
return result;
|
|
@@ -7762,7 +7872,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7762
7872
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
7763
7873
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
7764
7874
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
7765
|
-
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
7875
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle, constraints: a.constraints ?? [] }));
|
|
7766
7876
|
} catch {
|
|
7767
7877
|
return [{
|
|
7768
7878
|
subtaskId: crypto4.randomUUID(),
|
|
@@ -8137,8 +8247,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8137
8247
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
8138
8248
|
);
|
|
8139
8249
|
}
|
|
8140
|
-
async aggregateResults(assignment, results) {
|
|
8141
|
-
const completed = results.filter(
|
|
8250
|
+
async aggregateResults(assignment, results, opts = {}) {
|
|
8251
|
+
const completed = results.filter(
|
|
8252
|
+
(r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
|
|
8253
|
+
);
|
|
8142
8254
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
8143
8255
|
const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
|
|
8144
8256
|
const peerContext = peerOutputs ? `
|
|
@@ -8344,6 +8456,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
8344
8456
|
escalations = [];
|
|
8345
8457
|
store;
|
|
8346
8458
|
runBreaker;
|
|
8459
|
+
escalationCallback;
|
|
8347
8460
|
t2PeerBus = new PeerBus();
|
|
8348
8461
|
permissionEscalator;
|
|
8349
8462
|
toolCreator;
|
|
@@ -8358,6 +8471,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
8358
8471
|
this.toolRegistry = toolRegistry;
|
|
8359
8472
|
this.config = config;
|
|
8360
8473
|
}
|
|
8474
|
+
/** Ask the user what to do when any section escalates. */
|
|
8475
|
+
setEscalationCallback(cb) {
|
|
8476
|
+
this.escalationCallback = cb;
|
|
8477
|
+
}
|
|
8361
8478
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
8362
8479
|
setRunBreaker(breaker) {
|
|
8363
8480
|
this.runBreaker = breaker;
|
|
@@ -8517,8 +8634,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
8517
8634
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
|
|
8518
8635
|
};
|
|
8519
8636
|
}
|
|
8520
|
-
const sectionsText = t2Results.map((r) =>
|
|
8521
|
-
|
|
8637
|
+
const sectionsText = t2Results.map((r) => {
|
|
8638
|
+
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.]" : "";
|
|
8639
|
+
return `**${r.sectionTitle}**
|
|
8640
|
+
${r.sectionSummary}${skipNote}`;
|
|
8641
|
+
}).join("\n\n");
|
|
8522
8642
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
8523
8643
|
Review the following execution outputs against the original user prompt.
|
|
8524
8644
|
|
|
@@ -8528,6 +8648,7 @@ T2 Manager Summaries:
|
|
|
8528
8648
|
${sectionsText}
|
|
8529
8649
|
|
|
8530
8650
|
Does the current state of the workspace and the outputs fully satisfy the user's request?
|
|
8651
|
+
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.
|
|
8531
8652
|
If yes, reply with exactly: "APPROVED".
|
|
8532
8653
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
8533
8654
|
try {
|
|
@@ -8709,6 +8830,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8709
8830
|
if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
|
|
8710
8831
|
throw new Error("Invalid plan: no sections");
|
|
8711
8832
|
}
|
|
8833
|
+
for (const section of plan.sections) {
|
|
8834
|
+
section.constraints ??= [];
|
|
8835
|
+
for (const subtask of section.t3Subtasks ?? []) {
|
|
8836
|
+
subtask.constraints ??= [];
|
|
8837
|
+
}
|
|
8838
|
+
}
|
|
8712
8839
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
8713
8840
|
if (plan.sections.length < min) ;
|
|
8714
8841
|
}
|
|
@@ -8725,6 +8852,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8725
8852
|
const managers = sections.map((section) => {
|
|
8726
8853
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8727
8854
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8855
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
8728
8856
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8729
8857
|
if (this.store) {
|
|
8730
8858
|
manager.setStore(this.store);
|
|
@@ -8934,7 +9062,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8934
9062
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
8935
9063
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
8936
9064
|
if (!completedSections.length) {
|
|
8937
|
-
const allIssues =
|
|
9065
|
+
const allIssues = [
|
|
9066
|
+
...t2Results.flatMap((r) => r.issues ?? []),
|
|
9067
|
+
...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
|
|
9068
|
+
].filter(Boolean);
|
|
8938
9069
|
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
8939
9070
|
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
8940
9071
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -9743,6 +9874,102 @@ var RunBreaker = class {
|
|
|
9743
9874
|
return out;
|
|
9744
9875
|
}
|
|
9745
9876
|
};
|
|
9877
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
9878
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
9879
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
9880
|
+
function boundedSegment(raw) {
|
|
9881
|
+
const clean = sanitizeToolNameSegment(raw);
|
|
9882
|
+
if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
|
|
9883
|
+
const hash = crypto4.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
9884
|
+
return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
9885
|
+
}
|
|
9886
|
+
function isStableSegment(raw) {
|
|
9887
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
9888
|
+
}
|
|
9889
|
+
function sanitizeToolNameSegment(segment) {
|
|
9890
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
9891
|
+
let start = 0;
|
|
9892
|
+
let end = cleaned.length;
|
|
9893
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
9894
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
9895
|
+
return cleaned.slice(start, end) || "_";
|
|
9896
|
+
}
|
|
9897
|
+
function mcpToolName(serverName, toolName) {
|
|
9898
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
9899
|
+
}
|
|
9900
|
+
function isMcpToolName(name) {
|
|
9901
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
9902
|
+
}
|
|
9903
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
9904
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
9905
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
9906
|
+
for (let n = 2; n < 1e3; n++) {
|
|
9907
|
+
const candidate = `${desired} (${n})`;
|
|
9908
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
9909
|
+
}
|
|
9910
|
+
return `${desired} (${crypto4.randomUUID().slice(0, 8)})`;
|
|
9911
|
+
}
|
|
9912
|
+
function disambiguateMcpServerNames(servers) {
|
|
9913
|
+
const seenNames = [];
|
|
9914
|
+
const renames = [];
|
|
9915
|
+
const result = servers.map((s) => {
|
|
9916
|
+
const name = uniqueMcpServerName(s.name, seenNames);
|
|
9917
|
+
seenNames.push(name);
|
|
9918
|
+
if (name === s.name) return s;
|
|
9919
|
+
renames.push({ from: s.name, to: name });
|
|
9920
|
+
return { ...s, name };
|
|
9921
|
+
});
|
|
9922
|
+
return { servers: renames.length ? result : servers, renames };
|
|
9923
|
+
}
|
|
9924
|
+
function assignMcpToolNames(tools) {
|
|
9925
|
+
const names = new Array(tools.length);
|
|
9926
|
+
const bases = tools.map((t) => mcpToolName(t.server, t.tool));
|
|
9927
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9928
|
+
bases.forEach((base, i) => {
|
|
9929
|
+
const g = groups.get(base);
|
|
9930
|
+
if (g) g.push(i);
|
|
9931
|
+
else groups.set(base, [i]);
|
|
9932
|
+
});
|
|
9933
|
+
const used = new Set(bases);
|
|
9934
|
+
for (const [base, idx] of groups) {
|
|
9935
|
+
if (idx.length === 1) {
|
|
9936
|
+
names[idx[0]] = base;
|
|
9937
|
+
continue;
|
|
9938
|
+
}
|
|
9939
|
+
const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
|
|
9940
|
+
const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
|
|
9941
|
+
const ordered = [...idx].sort((a, b) => {
|
|
9942
|
+
const la = isClean(a) ? 0 : 1;
|
|
9943
|
+
const lb = isClean(b) ? 0 : 1;
|
|
9944
|
+
if (la !== lb) return la - lb;
|
|
9945
|
+
const ra = raw(a);
|
|
9946
|
+
const rb = raw(b);
|
|
9947
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
9948
|
+
});
|
|
9949
|
+
names[ordered[0]] = base;
|
|
9950
|
+
let n = 2;
|
|
9951
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
9952
|
+
let candidate = `${base}_${n}`;
|
|
9953
|
+
while (used.has(candidate)) {
|
|
9954
|
+
n++;
|
|
9955
|
+
candidate = `${base}_${n}`;
|
|
9956
|
+
}
|
|
9957
|
+
names[ordered[rank3]] = candidate;
|
|
9958
|
+
used.add(candidate);
|
|
9959
|
+
n++;
|
|
9960
|
+
}
|
|
9961
|
+
}
|
|
9962
|
+
return names;
|
|
9963
|
+
}
|
|
9964
|
+
function mcpServerPrefix(serverName) {
|
|
9965
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
9966
|
+
}
|
|
9967
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
9968
|
+
if (!disabledTools?.length) return disabledTools;
|
|
9969
|
+
const prefix = mcpServerPrefix(serverName);
|
|
9970
|
+
const kept = disabledTools.filter((t) => !t.startsWith(prefix));
|
|
9971
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
9972
|
+
}
|
|
9746
9973
|
var execAsync = util.promisify(child_process.exec);
|
|
9747
9974
|
var ShellTool = class extends BaseTool {
|
|
9748
9975
|
name = "shell";
|
|
@@ -11158,20 +11385,6 @@ ${text}`;
|
|
|
11158
11385
|
}
|
|
11159
11386
|
};
|
|
11160
11387
|
|
|
11161
|
-
// src/tools/tool-name.ts
|
|
11162
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
11163
|
-
function sanitizeToolNameSegment(segment) {
|
|
11164
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
11165
|
-
let start = 0;
|
|
11166
|
-
let end = cleaned.length;
|
|
11167
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
11168
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
11169
|
-
return cleaned.slice(start, end) || "_";
|
|
11170
|
-
}
|
|
11171
|
-
function mcpToolName(serverName, toolName) {
|
|
11172
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
11173
|
-
}
|
|
11174
|
-
|
|
11175
11388
|
// src/tools/mcp.ts
|
|
11176
11389
|
var McpToolWrapper = class extends BaseTool {
|
|
11177
11390
|
name;
|
|
@@ -11180,12 +11393,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
11180
11393
|
mcpClient;
|
|
11181
11394
|
serverName;
|
|
11182
11395
|
toolName;
|
|
11183
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
11396
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
11184
11397
|
super();
|
|
11185
11398
|
this.mcpClient = mcpClient;
|
|
11186
11399
|
this.serverName = serverName;
|
|
11187
11400
|
this.toolName = toolName;
|
|
11188
|
-
this.name = mcpToolName(serverName, toolName);
|
|
11401
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
11189
11402
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
11190
11403
|
this.inputSchema = inputSchema;
|
|
11191
11404
|
}
|
|
@@ -11256,22 +11469,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
11256
11469
|
getRegisteredPlugins() {
|
|
11257
11470
|
return Array.from(this.plugins.keys());
|
|
11258
11471
|
}
|
|
11259
|
-
/**
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11472
|
+
/**
|
|
11473
|
+
* Registers all tools from an MCP client.
|
|
11474
|
+
*
|
|
11475
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
11476
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
11477
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
11478
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
11479
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
11480
|
+
* never counted against the tool budget, and never has to be refused.
|
|
11481
|
+
*/
|
|
11482
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
11483
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
11484
|
+
const [, server, tool] = def.name.split("::");
|
|
11485
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
11486
|
+
});
|
|
11487
|
+
const names = assignMcpToolNames(parsed);
|
|
11488
|
+
parsed.forEach(({ def, server, tool }, i) => {
|
|
11265
11489
|
const wrapper = new McpToolWrapper(
|
|
11266
11490
|
mcpClient,
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
def.description.replace(`[MCP:${
|
|
11270
|
-
def.inputSchema
|
|
11491
|
+
server,
|
|
11492
|
+
tool,
|
|
11493
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
11494
|
+
def.inputSchema,
|
|
11495
|
+
names[i]
|
|
11271
11496
|
);
|
|
11497
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
11272
11498
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
11273
11499
|
this.register(wrapper);
|
|
11274
|
-
}
|
|
11500
|
+
});
|
|
11275
11501
|
}
|
|
11276
11502
|
setIgnoredPaths(patterns) {
|
|
11277
11503
|
this.ignoreMatcher = ignore().add(patterns);
|
|
@@ -11608,6 +11834,17 @@ function fileOAuthProvider(storePath) {
|
|
|
11608
11834
|
}
|
|
11609
11835
|
});
|
|
11610
11836
|
}
|
|
11837
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
11838
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
11839
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
11840
|
+
const run = prev.then(fn, fn);
|
|
11841
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
11842
|
+
oauthStoreLocks.set(storePath, tail);
|
|
11843
|
+
void tail.then(() => {
|
|
11844
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
11845
|
+
});
|
|
11846
|
+
return run;
|
|
11847
|
+
}
|
|
11611
11848
|
function beginMcpOAuth(provider, serverUrl) {
|
|
11612
11849
|
return auth_js.auth(provider, { serverUrl });
|
|
11613
11850
|
}
|
|
@@ -14546,7 +14783,50 @@ var GuidanceQueue = class {
|
|
|
14546
14783
|
}
|
|
14547
14784
|
};
|
|
14548
14785
|
|
|
14786
|
+
// src/tools/current-page.ts
|
|
14787
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
14788
|
+
var CurrentPageTool = class extends BaseTool {
|
|
14789
|
+
name = "read_current_page";
|
|
14790
|
+
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.';
|
|
14791
|
+
inputSchema = {
|
|
14792
|
+
type: "object",
|
|
14793
|
+
properties: {},
|
|
14794
|
+
required: []
|
|
14795
|
+
};
|
|
14796
|
+
provider;
|
|
14797
|
+
constructor(provider) {
|
|
14798
|
+
super();
|
|
14799
|
+
this.provider = provider;
|
|
14800
|
+
}
|
|
14801
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
14802
|
+
// nothing they have not already opened themselves.
|
|
14803
|
+
isDangerous() {
|
|
14804
|
+
return false;
|
|
14805
|
+
}
|
|
14806
|
+
async execute(_input, _options) {
|
|
14807
|
+
let page2;
|
|
14808
|
+
try {
|
|
14809
|
+
page2 = await this.provider();
|
|
14810
|
+
} catch (err) {
|
|
14811
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
14812
|
+
}
|
|
14813
|
+
if (!page2) {
|
|
14814
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
14815
|
+
}
|
|
14816
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
14817
|
+
|
|
14818
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
14819
|
+
return [
|
|
14820
|
+
`URL: ${page2.url}`,
|
|
14821
|
+
`Title: ${page2.title}`,
|
|
14822
|
+
"",
|
|
14823
|
+
text.trim() || "[the page rendered no readable text]"
|
|
14824
|
+
].join("\n");
|
|
14825
|
+
}
|
|
14826
|
+
};
|
|
14827
|
+
|
|
14549
14828
|
// src/core/cascade.ts
|
|
14829
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
14550
14830
|
function buildContextualPrompt(prompt, history = []) {
|
|
14551
14831
|
const recent = history.slice(-6);
|
|
14552
14832
|
if (recent.length === 0) return prompt;
|
|
@@ -14769,6 +15049,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
14769
15049
|
});
|
|
14770
15050
|
});
|
|
14771
15051
|
}
|
|
15052
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
15053
|
+
//
|
|
15054
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
15055
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
15056
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
15057
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
15058
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
15059
|
+
//
|
|
15060
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
15061
|
+
// timeout because the plan is already the model's considered proposal;
|
|
15062
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
15063
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
15064
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
15065
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
15066
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
15067
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
15068
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
15069
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
15070
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
15071
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
15072
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
15073
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
15074
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
15075
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
15076
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
15077
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
15078
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
15079
|
+
const requestId = crypto4.randomUUID();
|
|
15080
|
+
return await new Promise((resolve2) => {
|
|
15081
|
+
let timeout;
|
|
15082
|
+
const settle = (decision) => {
|
|
15083
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
15084
|
+
if (timeout) clearTimeout(timeout);
|
|
15085
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15086
|
+
resolve2(decision);
|
|
15087
|
+
};
|
|
15088
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
15089
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15090
|
+
timeout = setTimeout(() => {
|
|
15091
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
15092
|
+
settle({ action: "timeout" });
|
|
15093
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
15094
|
+
this.pendingEscalations.set(requestId, settle);
|
|
15095
|
+
this.emit("escalation:decision-required", {
|
|
15096
|
+
taskId,
|
|
15097
|
+
requestId,
|
|
15098
|
+
sectionId: ctx.sectionId,
|
|
15099
|
+
sectionTitle: ctx.sectionTitle,
|
|
15100
|
+
issues: ctx.issues,
|
|
15101
|
+
summary: ctx.summary,
|
|
15102
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
15103
|
+
});
|
|
15104
|
+
});
|
|
15105
|
+
}
|
|
15106
|
+
/**
|
|
15107
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
15108
|
+
*
|
|
15109
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
15110
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
15111
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
15112
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
15113
|
+
* which is correct whenever there is exactly one.
|
|
15114
|
+
*
|
|
15115
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
15116
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
15117
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
15118
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
15119
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
15120
|
+
* see EscalationDecision.automatic.
|
|
15121
|
+
*/
|
|
15122
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
15123
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
15124
|
+
if (requestId) {
|
|
15125
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
15126
|
+
return;
|
|
15127
|
+
}
|
|
15128
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
15129
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
15130
|
+
}
|
|
15131
|
+
/**
|
|
15132
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
15133
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
15134
|
+
* same system-decided case as the early-return skips above, not a person
|
|
15135
|
+
* reviewing and accepting that section's output.
|
|
15136
|
+
*/
|
|
15137
|
+
releasePendingEscalations(action = "skip") {
|
|
15138
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
15139
|
+
}
|
|
14772
15140
|
/**
|
|
14773
15141
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
14774
15142
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -14902,6 +15270,20 @@ ${input}` }],
|
|
|
14902
15270
|
this.mediaSink = sink;
|
|
14903
15271
|
this.registerMediaTools(this.workspacePath);
|
|
14904
15272
|
}
|
|
15273
|
+
/**
|
|
15274
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
15275
|
+
*
|
|
15276
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
15277
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
15278
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
15279
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
15280
|
+
* touch the machine, and reading a page the user already opened touches
|
|
15281
|
+
* nothing. `disabledTools` still removes it.
|
|
15282
|
+
*/
|
|
15283
|
+
setCurrentPageProvider(provider) {
|
|
15284
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
15285
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
15286
|
+
}
|
|
14905
15287
|
registerMediaTools(workspacePath) {
|
|
14906
15288
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14907
15289
|
const permitted = (name) => !denied.has(name);
|
|
@@ -15034,8 +15416,12 @@ ${last.partialOutput}` : "");
|
|
|
15034
15416
|
for (const server of this.config.tools.mcpServers) {
|
|
15035
15417
|
try {
|
|
15036
15418
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
15037
|
-
|
|
15038
|
-
|
|
15419
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
15420
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
15421
|
+
this.toolRegistry.registerMcpTools(
|
|
15422
|
+
this.mcpClient,
|
|
15423
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
15424
|
+
);
|
|
15039
15425
|
} catch (err) {
|
|
15040
15426
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
15041
15427
|
}
|
|
@@ -15500,6 +15886,7 @@ ${prompt}` : prompt;
|
|
|
15500
15886
|
} else if (complexity === "Moderate") {
|
|
15501
15887
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
15502
15888
|
t2.setRunBreaker(runBreaker);
|
|
15889
|
+
t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15503
15890
|
t2.setPresenter(true);
|
|
15504
15891
|
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.");
|
|
15505
15892
|
if (identityPrompt) {
|
|
@@ -15544,11 +15931,17 @@ ${prompt}` : prompt;
|
|
|
15544
15931
|
t2Results = [t2Result];
|
|
15545
15932
|
const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
|
|
15546
15933
|
if (completed.length > 0) {
|
|
15547
|
-
finalOutput =
|
|
15934
|
+
finalOutput = composeRootSectionOutput(
|
|
15935
|
+
t2Result,
|
|
15936
|
+
completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
|
|
15937
|
+
);
|
|
15548
15938
|
} else {
|
|
15549
15939
|
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
15550
15940
|
const reasons = Array.from(
|
|
15551
|
-
new Set(
|
|
15941
|
+
new Set([
|
|
15942
|
+
...t2Result.issues ?? [],
|
|
15943
|
+
...t2Result.t3Results.flatMap((r) => r.issues ?? [])
|
|
15944
|
+
].filter(Boolean))
|
|
15552
15945
|
);
|
|
15553
15946
|
if (partial) {
|
|
15554
15947
|
finalOutput = reasons.length ? `${partial}
|
|
@@ -15561,6 +15954,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15561
15954
|
} else {
|
|
15562
15955
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
15563
15956
|
t1.setRunBreaker(runBreaker);
|
|
15957
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15564
15958
|
t1.setPresenter(true);
|
|
15565
15959
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
15566
15960
|
if (identityPrompt) {
|
|
@@ -15585,6 +15979,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15585
15979
|
finalOutput = result.output;
|
|
15586
15980
|
t2Results = result.t2Results;
|
|
15587
15981
|
}
|
|
15982
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
15983
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
15588
15984
|
const trip = runBreaker.reason();
|
|
15589
15985
|
if (trip) {
|
|
15590
15986
|
finalOutput = [
|
|
@@ -15625,6 +16021,10 @@ ${finalOutput}` : ""
|
|
|
15625
16021
|
escalator.cancelAllPending();
|
|
15626
16022
|
} catch {
|
|
15627
16023
|
}
|
|
16024
|
+
try {
|
|
16025
|
+
this.releasePendingEscalations();
|
|
16026
|
+
} catch {
|
|
16027
|
+
}
|
|
15628
16028
|
this.router.restoreTierModels();
|
|
15629
16029
|
this.router.setRunSignal(void 0);
|
|
15630
16030
|
if (this.taskAnalyzer) {
|
|
@@ -16941,6 +17341,13 @@ var ConfigManager = class {
|
|
|
16941
17341
|
}
|
|
16942
17342
|
async load() {
|
|
16943
17343
|
this.config = await this.loadConfig();
|
|
17344
|
+
const servers = this.config.tools?.mcpServers;
|
|
17345
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
17346
|
+
const mcpNamesChanged = renames.length > 0;
|
|
17347
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
17348
|
+
this.config.tools.mcpServers = disambiguated;
|
|
17349
|
+
this.renameMcpServerReferences(renames);
|
|
17350
|
+
}
|
|
16944
17351
|
this.ignore = new CascadeIgnore();
|
|
16945
17352
|
await this.ignore.load(this.workspacePath);
|
|
16946
17353
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -16949,6 +17356,7 @@ var ConfigManager = class {
|
|
|
16949
17356
|
await this.injectEnvKeys();
|
|
16950
17357
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
16951
17358
|
await this.ensureDefaultIdentity();
|
|
17359
|
+
if (mcpNamesChanged) await this.save();
|
|
16952
17360
|
}
|
|
16953
17361
|
getConfig() {
|
|
16954
17362
|
return this.config;
|
|
@@ -17032,6 +17440,66 @@ var ConfigManager = class {
|
|
|
17032
17440
|
this.config.providers.push({ type: "ollama" });
|
|
17033
17441
|
}
|
|
17034
17442
|
}
|
|
17443
|
+
/**
|
|
17444
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
17445
|
+
*
|
|
17446
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
17447
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
17448
|
+
*
|
|
17449
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
17450
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
17451
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
17452
|
+
* move with it. A plain rewrite is correct here.
|
|
17453
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
17454
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
17455
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
17456
|
+
* already been updated to the disambiguated list by the time this runs
|
|
17457
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
17458
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
17459
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
17460
|
+
* AND granted to the renamed identity, not moved.
|
|
17461
|
+
*
|
|
17462
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
17463
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
17464
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
17465
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
17466
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
17467
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
17468
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
17469
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
17470
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
17471
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
17472
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
17473
|
+
* together, in the one pass that still sees the original entry.
|
|
17474
|
+
*
|
|
17475
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
17476
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
17477
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
17478
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
17479
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
17480
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
17481
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
17482
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
17483
|
+
* would do the opposite — move a denial that already worked for the
|
|
17484
|
+
* survivor onto a server it may never have meant to cover.
|
|
17485
|
+
*/
|
|
17486
|
+
renameMcpServerReferences(renames) {
|
|
17487
|
+
const tools = this.config.tools;
|
|
17488
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
17489
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
|
|
17490
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
17491
|
+
for (const { from, to } of renames) {
|
|
17492
|
+
const tos = byFrom.get(from);
|
|
17493
|
+
if (tos) tos.push(to);
|
|
17494
|
+
else byFrom.set(from, [to]);
|
|
17495
|
+
}
|
|
17496
|
+
let trusted = tools.mcpTrusted;
|
|
17497
|
+
for (const [from, tos] of byFrom) {
|
|
17498
|
+
if (!trusted.includes(from)) continue;
|
|
17499
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
|
|
17500
|
+
}
|
|
17501
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
17502
|
+
}
|
|
17035
17503
|
async ensureDefaultIdentity() {
|
|
17036
17504
|
const existing = this.store.getDefaultIdentity();
|
|
17037
17505
|
if (existing) return;
|
|
@@ -17154,6 +17622,14 @@ var DashboardSocket = class {
|
|
|
17154
17622
|
this.secret = options.secret;
|
|
17155
17623
|
this.setupHandlers();
|
|
17156
17624
|
}
|
|
17625
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
17626
|
+
roomSize(room) {
|
|
17627
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
17628
|
+
}
|
|
17629
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
17630
|
+
hasSocket(socketId) {
|
|
17631
|
+
return this.io.sockets.sockets.has(socketId);
|
|
17632
|
+
}
|
|
17157
17633
|
broadcastToRoom(room, event, data) {
|
|
17158
17634
|
this.io.to(room).emit(event, data);
|
|
17159
17635
|
}
|
|
@@ -17225,6 +17701,22 @@ var DashboardSocket = class {
|
|
|
17225
17701
|
});
|
|
17226
17702
|
});
|
|
17227
17703
|
}
|
|
17704
|
+
/**
|
|
17705
|
+
* A client subscribed to a session's room.
|
|
17706
|
+
*
|
|
17707
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
17708
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
17709
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
17710
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
17711
|
+
*/
|
|
17712
|
+
onJoinSession(callback) {
|
|
17713
|
+
this.io.on("connection", (socket) => {
|
|
17714
|
+
socket.on("join:session", (payload) => {
|
|
17715
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
17716
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
17717
|
+
});
|
|
17718
|
+
});
|
|
17719
|
+
}
|
|
17228
17720
|
onSessionRate(callback) {
|
|
17229
17721
|
this.io.on("connection", (socket) => {
|
|
17230
17722
|
socket.on("session:rate", (payload) => {
|
|
@@ -17262,6 +17754,31 @@ var DashboardSocket = class {
|
|
|
17262
17754
|
});
|
|
17263
17755
|
});
|
|
17264
17756
|
}
|
|
17757
|
+
/**
|
|
17758
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
17759
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
17760
|
+
* answers here.
|
|
17761
|
+
*
|
|
17762
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
17763
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
17764
|
+
*/
|
|
17765
|
+
onEscalationDecision(callback) {
|
|
17766
|
+
this.io.on("connection", (socket) => {
|
|
17767
|
+
socket.on("escalation:decide", (payload) => {
|
|
17768
|
+
const action = payload?.action;
|
|
17769
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
17770
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
17771
|
+
callback({
|
|
17772
|
+
sessionId: payload.sessionId,
|
|
17773
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
17774
|
+
// run concurrently, so more than one can be waiting.
|
|
17775
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
17776
|
+
action,
|
|
17777
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
17778
|
+
});
|
|
17779
|
+
});
|
|
17780
|
+
});
|
|
17781
|
+
}
|
|
17265
17782
|
onSessionSteer(callback) {
|
|
17266
17783
|
this.io.on("connection", (socket) => {
|
|
17267
17784
|
socket.on("session:steer", (payload) => {
|
|
@@ -17420,6 +17937,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
17420
17937
|
|
|
17421
17938
|
// src/dashboard/server.ts
|
|
17422
17939
|
var __dirname$1 = path24__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
17940
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
17423
17941
|
var DashboardServer = class {
|
|
17424
17942
|
app;
|
|
17425
17943
|
httpServer;
|
|
@@ -17430,6 +17948,13 @@ var DashboardServer = class {
|
|
|
17430
17948
|
globalStore = null;
|
|
17431
17949
|
broadcastTimer = null;
|
|
17432
17950
|
activeSessions = /* @__PURE__ */ new Map();
|
|
17951
|
+
/**
|
|
17952
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
17953
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
17954
|
+
* browser — so the tool is simply not registered rather than present and
|
|
17955
|
+
* always failing.
|
|
17956
|
+
*/
|
|
17957
|
+
currentPageProvider;
|
|
17433
17958
|
activeControllers = /* @__PURE__ */ new Map();
|
|
17434
17959
|
/**
|
|
17435
17960
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -17444,6 +17969,17 @@ var DashboardServer = class {
|
|
|
17444
17969
|
* map is how that answer reaches the run that's blocked on it.
|
|
17445
17970
|
*/
|
|
17446
17971
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
17972
|
+
/**
|
|
17973
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
17974
|
+
* delivered to a client that was not reachable when it was raised.
|
|
17975
|
+
*
|
|
17976
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
17977
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
17978
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
17979
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
17980
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
17981
|
+
*/
|
|
17982
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
17447
17983
|
/**
|
|
17448
17984
|
* The orchestration decision trail ("why") of each session's most recent
|
|
17449
17985
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -17521,20 +18057,29 @@ var DashboardServer = class {
|
|
|
17521
18057
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
17522
18058
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
17523
18059
|
this.activeSessions.set(sessionId, cascade);
|
|
18060
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
17524
18061
|
cascade.on("stream:token", (e) => {
|
|
17525
|
-
this.
|
|
18062
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
17526
18063
|
});
|
|
17527
18064
|
cascade.on("tier:status", (e) => {
|
|
17528
|
-
this.
|
|
18065
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
|
|
17529
18066
|
});
|
|
17530
18067
|
cascade.on("permission:user-required", (e) => {
|
|
17531
|
-
this.
|
|
18068
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
|
|
17532
18069
|
});
|
|
17533
18070
|
cascade.on("peer:message", (e) => {
|
|
17534
18071
|
this.socket.emitPeerMessage(e);
|
|
17535
18072
|
});
|
|
17536
18073
|
cascade.on("plan:approval-required", (e) => {
|
|
17537
|
-
this.
|
|
18074
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
|
|
18075
|
+
});
|
|
18076
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
18077
|
+
this.raiseEscalation(sessionId, socketId, e);
|
|
18078
|
+
});
|
|
18079
|
+
cascade.on("escalation:timeout", (e) => {
|
|
18080
|
+
const ev = e;
|
|
18081
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
18082
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
17538
18083
|
});
|
|
17539
18084
|
try {
|
|
17540
18085
|
const result = await cascade.run({
|
|
@@ -17545,7 +18090,7 @@ var DashboardServer = class {
|
|
|
17545
18090
|
this.recordSessionTask(sessionId, result.taskId);
|
|
17546
18091
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17547
18092
|
this.captureWhy(sessionId, cascade, result);
|
|
17548
|
-
this.
|
|
18093
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
17549
18094
|
this.socket.broadcast("cost:update", {
|
|
17550
18095
|
sessionId,
|
|
17551
18096
|
totalTokens: result.usage.totalTokens,
|
|
@@ -17558,7 +18103,7 @@ var DashboardServer = class {
|
|
|
17558
18103
|
} catch (err) {
|
|
17559
18104
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17560
18105
|
this.captureWhy(sessionId, cascade);
|
|
17561
|
-
this.
|
|
18106
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
17562
18107
|
sessionId,
|
|
17563
18108
|
error: err instanceof Error ? err.message : String(err)
|
|
17564
18109
|
});
|
|
@@ -17566,6 +18111,7 @@ var DashboardServer = class {
|
|
|
17566
18111
|
this.activeSessions.delete(sessionId);
|
|
17567
18112
|
this.activeControllers.delete(sessionId);
|
|
17568
18113
|
this.denyPendingApprovals(sessionId);
|
|
18114
|
+
this.clearSessionEscalations(sessionId);
|
|
17569
18115
|
}
|
|
17570
18116
|
});
|
|
17571
18117
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -17575,10 +18121,20 @@ var DashboardServer = class {
|
|
|
17575
18121
|
editedPlan
|
|
17576
18122
|
);
|
|
17577
18123
|
});
|
|
18124
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
18125
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
18126
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
18127
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
18128
|
+
});
|
|
18129
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
18130
|
+
this.replayEscalations(sessionId, socketId);
|
|
18131
|
+
});
|
|
17578
18132
|
this.socket.onSessionHalt((sessionId) => {
|
|
17579
18133
|
this.activeControllers.get(sessionId)?.abort();
|
|
17580
18134
|
this.denyPendingApprovals(sessionId);
|
|
17581
18135
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
18136
|
+
this.clearSessionEscalations(sessionId);
|
|
18137
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
17582
18138
|
});
|
|
17583
18139
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
17584
18140
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -17593,6 +18149,10 @@ var DashboardServer = class {
|
|
|
17593
18149
|
}
|
|
17594
18150
|
});
|
|
17595
18151
|
}
|
|
18152
|
+
/** Wire the host's browser view in (desktop only). */
|
|
18153
|
+
setCurrentPageProvider(provider) {
|
|
18154
|
+
this.currentPageProvider = provider;
|
|
18155
|
+
}
|
|
17596
18156
|
async start() {
|
|
17597
18157
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
17598
18158
|
if (!isLoopback) {
|
|
@@ -17881,6 +18441,116 @@ var DashboardServer = class {
|
|
|
17881
18441
|
this.activeSessions.delete(sessionId);
|
|
17882
18442
|
}
|
|
17883
18443
|
}
|
|
18444
|
+
/**
|
|
18445
|
+
* Is anyone able to answer for this session right now?
|
|
18446
|
+
*
|
|
18447
|
+
* The originating socket first — that connection is definitely watching this
|
|
18448
|
+
* run — then the session room, which is where a client lands after a
|
|
18449
|
+
* reconnect issues it a new socket id.
|
|
18450
|
+
*/
|
|
18451
|
+
hasResponder(sessionId, socketId) {
|
|
18452
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
18453
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
18454
|
+
}
|
|
18455
|
+
/**
|
|
18456
|
+
* Send a run event to whoever is watching this session.
|
|
18457
|
+
*
|
|
18458
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
18459
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
18460
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
18461
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
18462
|
+
* client that is both the originator and a room member from seeing every
|
|
18463
|
+
* token twice.
|
|
18464
|
+
*/
|
|
18465
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
18466
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
18467
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
18468
|
+
}
|
|
18469
|
+
/**
|
|
18470
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
18471
|
+
*
|
|
18472
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
18473
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
18474
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
18475
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
18476
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
18477
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
18478
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
18479
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
18480
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
18481
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
18482
|
+
* mistaken for an absent user.
|
|
18483
|
+
*/
|
|
18484
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
18485
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto4.randomUUID();
|
|
18486
|
+
const payload = { sessionId, ...event };
|
|
18487
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
18488
|
+
this.pendingEscalations.set(requestId, entry);
|
|
18489
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
18490
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
18491
|
+
return;
|
|
18492
|
+
}
|
|
18493
|
+
entry.graceTimer = setTimeout(() => {
|
|
18494
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
18495
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
18496
|
+
this.pendingEscalations.delete(requestId);
|
|
18497
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
18498
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
18499
|
+
}
|
|
18500
|
+
/**
|
|
18501
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
18502
|
+
*
|
|
18503
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
18504
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
18505
|
+
* its gates and the clients queue theirs.
|
|
18506
|
+
*
|
|
18507
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
18508
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
18509
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
18510
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
18511
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
18512
|
+
* sides never have to agree on a wall clock.
|
|
18513
|
+
*/
|
|
18514
|
+
replayEscalations(sessionId, socketId) {
|
|
18515
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
18516
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18517
|
+
if (entry.graceTimer) {
|
|
18518
|
+
clearTimeout(entry.graceTimer);
|
|
18519
|
+
entry.graceTimer = void 0;
|
|
18520
|
+
}
|
|
18521
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
18522
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
18523
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
18524
|
+
}
|
|
18525
|
+
}
|
|
18526
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
18527
|
+
clearPendingEscalation(requestId) {
|
|
18528
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
18529
|
+
if (!entry) return;
|
|
18530
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18531
|
+
this.pendingEscalations.delete(requestId);
|
|
18532
|
+
}
|
|
18533
|
+
/**
|
|
18534
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
18535
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
18536
|
+
* Insertion order makes `values()` oldest-first.
|
|
18537
|
+
*/
|
|
18538
|
+
clearOldestSessionEscalation(sessionId) {
|
|
18539
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18540
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18541
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18542
|
+
this.pendingEscalations.delete(id);
|
|
18543
|
+
return;
|
|
18544
|
+
}
|
|
18545
|
+
}
|
|
18546
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
18547
|
+
clearSessionEscalations(sessionId) {
|
|
18548
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18549
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18550
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18551
|
+
this.pendingEscalations.delete(id);
|
|
18552
|
+
}
|
|
18553
|
+
}
|
|
17884
18554
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
17885
18555
|
denyPendingApprovals(sessionId) {
|
|
17886
18556
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -18381,6 +19051,7 @@ ${prompt}`;
|
|
|
18381
19051
|
void (async () => {
|
|
18382
19052
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
18383
19053
|
this.activeSessions.set(sessionId, cascade);
|
|
19054
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
18384
19055
|
cascade.on("stream:token", (e) => {
|
|
18385
19056
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
18386
19057
|
});
|
|
@@ -18396,6 +19067,14 @@ ${prompt}`;
|
|
|
18396
19067
|
cascade.on("plan:approval-required", (e) => {
|
|
18397
19068
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
18398
19069
|
});
|
|
19070
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
19071
|
+
this.raiseEscalation(sessionId, void 0, e);
|
|
19072
|
+
});
|
|
19073
|
+
cascade.on("escalation:timeout", (e) => {
|
|
19074
|
+
const ev = e;
|
|
19075
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
19076
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
19077
|
+
});
|
|
18399
19078
|
try {
|
|
18400
19079
|
const result = await cascade.run({
|
|
18401
19080
|
prompt: runPrompt,
|
|
@@ -18423,6 +19102,7 @@ ${prompt}`;
|
|
|
18423
19102
|
} finally {
|
|
18424
19103
|
this.activeSessions.delete(sessionId);
|
|
18425
19104
|
this.denyPendingApprovals(sessionId);
|
|
19105
|
+
this.clearSessionEscalations(sessionId);
|
|
18426
19106
|
}
|
|
18427
19107
|
})();
|
|
18428
19108
|
});
|
|
@@ -18755,6 +19435,41 @@ var HooksRunner = class {
|
|
|
18755
19435
|
}
|
|
18756
19436
|
};
|
|
18757
19437
|
|
|
19438
|
+
// src/mcp/discover.ts
|
|
19439
|
+
async function discoverMcpTools(servers) {
|
|
19440
|
+
const results = [];
|
|
19441
|
+
for (const server of servers) {
|
|
19442
|
+
const client = new McpClient({ trustedServers: [server.name] });
|
|
19443
|
+
try {
|
|
19444
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
19445
|
+
const connect = () => client.connect(server, authProvider ? { authProvider } : {});
|
|
19446
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
19447
|
+
const parsed = client.getToolDefinitions().flatMap((def) => {
|
|
19448
|
+
const [, serverName, toolName] = def.name.split("::");
|
|
19449
|
+
return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
|
|
19450
|
+
});
|
|
19451
|
+
const names = assignMcpToolNames(parsed);
|
|
19452
|
+
const tools = parsed.map(({ def, server: s, tool }, i) => ({
|
|
19453
|
+
server: s,
|
|
19454
|
+
tool,
|
|
19455
|
+
name: names[i],
|
|
19456
|
+
description: def.description.replace(`[MCP:${s}] `, "")
|
|
19457
|
+
}));
|
|
19458
|
+
results.push({ server: server.name, tools });
|
|
19459
|
+
} catch (err) {
|
|
19460
|
+
results.push({
|
|
19461
|
+
server: server.name,
|
|
19462
|
+
tools: [],
|
|
19463
|
+
error: err instanceof Error ? err.message : String(err)
|
|
19464
|
+
});
|
|
19465
|
+
} finally {
|
|
19466
|
+
await client.disconnectAll().catch(() => {
|
|
19467
|
+
});
|
|
19468
|
+
}
|
|
19469
|
+
}
|
|
19470
|
+
return results;
|
|
19471
|
+
}
|
|
19472
|
+
|
|
18758
19473
|
// src/retrieval/chunk.ts
|
|
18759
19474
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
18760
19475
|
function splitLongBlock(block, max) {
|
|
@@ -19293,11 +20008,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
19293
20008
|
for (const i of incoming) map.set(key(i), i);
|
|
19294
20009
|
return [...map.values()];
|
|
19295
20010
|
}
|
|
20011
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
20012
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
20013
|
+
let n = 2;
|
|
20014
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
|
|
20015
|
+
return `${desired} ${n}`;
|
|
20016
|
+
}
|
|
20017
|
+
function mergeMcpServers(local, incoming) {
|
|
20018
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
20019
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
20020
|
+
const byName = /* @__PURE__ */ new Map();
|
|
20021
|
+
for (const l of local ?? []) byName.set(l.name, l);
|
|
20022
|
+
const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
|
|
20023
|
+
const synced = [];
|
|
20024
|
+
for (const inc of incoming) {
|
|
20025
|
+
if (byName.has(inc.name)) {
|
|
20026
|
+
byName.set(inc.name, inc);
|
|
20027
|
+
synced.push(inc);
|
|
20028
|
+
continue;
|
|
20029
|
+
}
|
|
20030
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
20031
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
20032
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
20033
|
+
byName.set(name, server);
|
|
20034
|
+
synced.push(server);
|
|
20035
|
+
prefixes.add(mcpServerPrefix(name));
|
|
20036
|
+
}
|
|
20037
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
20038
|
+
}
|
|
20039
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
20040
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
20041
|
+
return tools.map((t) => {
|
|
20042
|
+
for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
|
|
20043
|
+
return t;
|
|
20044
|
+
});
|
|
20045
|
+
}
|
|
20046
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
20047
|
+
const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
|
|
20048
|
+
const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
|
|
20049
|
+
const kept = (local ?? []).filter((t) => !covered(t));
|
|
20050
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
20051
|
+
return merged.length ? merged : void 0;
|
|
20052
|
+
}
|
|
19296
20053
|
function gatherSyncBundle(config) {
|
|
19297
|
-
const bundle = { v:
|
|
20054
|
+
const bundle = { v: 2 };
|
|
19298
20055
|
if (config.providers?.length) bundle.providers = config.providers;
|
|
19299
20056
|
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
19300
20057
|
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
20058
|
+
const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
|
|
20059
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
19301
20060
|
if (config.models) bundle.models = config.models;
|
|
19302
20061
|
if (config.budget) bundle.budget = config.budget;
|
|
19303
20062
|
const prefs = {};
|
|
@@ -19312,7 +20071,16 @@ function applySyncBundle(bundle, config) {
|
|
|
19312
20071
|
const next = { ...config, tools: { ...config.tools } };
|
|
19313
20072
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
19314
20073
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
19315
|
-
|
|
20074
|
+
const mcp = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
|
|
20075
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
|
|
20076
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
20077
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
20078
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
20079
|
+
config.tools?.disabledTools,
|
|
20080
|
+
rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
|
|
20081
|
+
bundle.mcpServers ? mcp.synced : void 0
|
|
20082
|
+
);
|
|
20083
|
+
}
|
|
19316
20084
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
19317
20085
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
19318
20086
|
if (bundle.prefs) {
|
|
@@ -19347,6 +20115,7 @@ exports.CascadeToolError = CascadeToolError;
|
|
|
19347
20115
|
exports.CloudClient = CloudClient;
|
|
19348
20116
|
exports.CodeSearchTool = CodeSearchTool;
|
|
19349
20117
|
exports.ConfigManager = ConfigManager;
|
|
20118
|
+
exports.CurrentPageTool = CurrentPageTool;
|
|
19350
20119
|
exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
|
|
19351
20120
|
exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;
|
|
19352
20121
|
exports.DEFAULT_AUTO_SUMMARIZE_AT = DEFAULT_AUTO_SUMMARIZE_AT;
|
|
@@ -19369,6 +20138,8 @@ exports.HooksRunner = HooksRunner;
|
|
|
19369
20138
|
exports.Keystore = Keystore;
|
|
19370
20139
|
exports.LLMReranker = LLMReranker;
|
|
19371
20140
|
exports.LM_STUDIO_BASE_URL = LM_STUDIO_BASE_URL;
|
|
20141
|
+
exports.MAX_PAGE_TEXT_CHARS = MAX_PAGE_TEXT_CHARS;
|
|
20142
|
+
exports.MCP_TOOL_PREFIX = MCP_TOOL_PREFIX;
|
|
19372
20143
|
exports.MODELS = MODELS;
|
|
19373
20144
|
exports.McpClient = McpClient;
|
|
19374
20145
|
exports.McpOAuthProvider = McpOAuthProvider;
|
|
@@ -19405,7 +20176,9 @@ exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
|
|
|
19405
20176
|
exports.createCascade = createCascade;
|
|
19406
20177
|
exports.decryptSyncBlob = decryptJSON;
|
|
19407
20178
|
exports.diffManifest = diffManifest;
|
|
20179
|
+
exports.disambiguateMcpServerNames = disambiguateMcpServerNames;
|
|
19408
20180
|
exports.discoverMcpAuthServer = discoverMcpAuthServer;
|
|
20181
|
+
exports.discoverMcpTools = discoverMcpTools;
|
|
19409
20182
|
exports.distillSessionFacts = distillSessionFacts;
|
|
19410
20183
|
exports.embedderFromProviders = embedderFromProviders;
|
|
19411
20184
|
exports.encryptSyncBlob = encryptJSON;
|
|
@@ -19414,14 +20187,19 @@ exports.gatherSyncBundle = gatherSyncBundle;
|
|
|
19414
20187
|
exports.hashContent = hashContent;
|
|
19415
20188
|
exports.heuristicCodeChunker = heuristicCodeChunker;
|
|
19416
20189
|
exports.inferAzureBaseModel = inferAzureBaseModel;
|
|
20190
|
+
exports.isMcpToolName = isMcpToolName;
|
|
20191
|
+
exports.mcpServerPrefix = mcpServerPrefix;
|
|
20192
|
+
exports.mcpToolName = mcpToolName;
|
|
19417
20193
|
exports.nodeHttpFetch = nodeHttpFetch;
|
|
19418
20194
|
exports.parseRankOrder = parseRankOrder;
|
|
19419
20195
|
exports.planRetrieval = planRetrieval;
|
|
19420
20196
|
exports.preferIpv4Host = preferIpv4Host;
|
|
19421
20197
|
exports.reciprocalRankFusion = reciprocalRankFusion;
|
|
19422
20198
|
exports.refreshMcpToken = refreshMcpToken;
|
|
20199
|
+
exports.removeMcpServerDenials = removeMcpServerDenials;
|
|
19423
20200
|
exports.runCascade = runCascade;
|
|
19424
20201
|
exports.sessionWorthRemembering = sessionWorthRemembering;
|
|
19425
20202
|
exports.streamCascade = streamCascade;
|
|
20203
|
+
exports.uniqueMcpServerName = uniqueMcpServerName;
|
|
19426
20204
|
//# sourceMappingURL=index.cjs.map
|
|
19427
20205
|
//# sourceMappingURL=index.cjs.map
|