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/desktop-core.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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.
|
|
224250
|
+
var CASCADE_VERSION = "0.56.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
|
-
|
|
256007
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
255947
256008
|
const issues = t3Results.filter((r4) => r4.status !== "COMPLETED").flatMap((r4) => r4.issues);
|
|
255948
|
-
|
|
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;
|
|
@@ -256015,7 +256125,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
256015
256125
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
256016
256126
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
256017
256127
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
256018
|
-
return parsed.map((a2) => ({ ...a2, sectionTitle: assignment.sectionTitle }));
|
|
256128
|
+
return parsed.map((a2) => ({ ...a2, sectionTitle: assignment.sectionTitle, constraints: a2.constraints ?? [] }));
|
|
256019
256129
|
} catch {
|
|
256020
256130
|
return [{
|
|
256021
256131
|
subtaskId: crypto6.randomUUID(),
|
|
@@ -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(
|
|
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) =>
|
|
256774
|
-
|
|
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 {
|
|
@@ -256962,6 +257083,12 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
256962
257083
|
if (!plan.sections || !Array.isArray(plan.sections) || plan.sections.length === 0) {
|
|
256963
257084
|
throw new Error("Invalid plan: no sections");
|
|
256964
257085
|
}
|
|
257086
|
+
for (const section of plan.sections) {
|
|
257087
|
+
section.constraints ??= [];
|
|
257088
|
+
for (const subtask of section.t3Subtasks ?? []) {
|
|
257089
|
+
subtask.constraints ??= [];
|
|
257090
|
+
}
|
|
257091
|
+
}
|
|
256965
257092
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
256966
257093
|
if (plan.sections.length < min) ;
|
|
256967
257094
|
}
|
|
@@ -256978,6 +257105,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
256978
257105
|
const managers = sections.map((section) => {
|
|
256979
257106
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
256980
257107
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
257108
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
256981
257109
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
256982
257110
|
if (this.store) {
|
|
256983
257111
|
manager.setStore(this.store);
|
|
@@ -257187,7 +257315,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
257187
257315
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
257188
257316
|
const completedSections = t2Results.filter((r4) => r4.status !== "FAILED");
|
|
257189
257317
|
if (!completedSections.length) {
|
|
257190
|
-
const allIssues =
|
|
257318
|
+
const allIssues = [
|
|
257319
|
+
...t2Results.flatMap((r4) => r4.issues ?? []),
|
|
257320
|
+
...t2Results.flatMap((r4) => r4.t3Results.flatMap((t4) => t4.issues))
|
|
257321
|
+
].filter(Boolean);
|
|
257191
257322
|
const critical = allIssues.find((i4) => i4.includes("[CRITICAL_TOOL_ERROR]"));
|
|
257192
257323
|
const stalled = allIssues.find((i4) => /^Stalled:/.test(i4));
|
|
257193
257324
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -257999,6 +258130,102 @@ var RunBreaker = class {
|
|
|
257999
258130
|
|
|
258000
258131
|
// src/tools/registry.ts
|
|
258001
258132
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
258133
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
258134
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
258135
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
258136
|
+
function boundedSegment(raw) {
|
|
258137
|
+
const clean3 = sanitizeToolNameSegment(raw);
|
|
258138
|
+
if (clean3.length <= MAX_SEGMENT_LENGTH) return clean3;
|
|
258139
|
+
const hash = crypto6.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
258140
|
+
return `${clean3.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
258141
|
+
}
|
|
258142
|
+
function isStableSegment(raw) {
|
|
258143
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
258144
|
+
}
|
|
258145
|
+
function sanitizeToolNameSegment(segment) {
|
|
258146
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
258147
|
+
let start = 0;
|
|
258148
|
+
let end = cleaned.length;
|
|
258149
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
258150
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
258151
|
+
return cleaned.slice(start, end) || "_";
|
|
258152
|
+
}
|
|
258153
|
+
function mcpToolName(serverName, toolName) {
|
|
258154
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
258155
|
+
}
|
|
258156
|
+
function isMcpToolName(name) {
|
|
258157
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
258158
|
+
}
|
|
258159
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
258160
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
258161
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
258162
|
+
for (let n8 = 2; n8 < 1e3; n8++) {
|
|
258163
|
+
const candidate = `${desired} (${n8})`;
|
|
258164
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
258165
|
+
}
|
|
258166
|
+
return `${desired} (${crypto6.randomUUID().slice(0, 8)})`;
|
|
258167
|
+
}
|
|
258168
|
+
function disambiguateMcpServerNames(servers) {
|
|
258169
|
+
const seenNames = [];
|
|
258170
|
+
const renames = [];
|
|
258171
|
+
const result = servers.map((s3) => {
|
|
258172
|
+
const name = uniqueMcpServerName(s3.name, seenNames);
|
|
258173
|
+
seenNames.push(name);
|
|
258174
|
+
if (name === s3.name) return s3;
|
|
258175
|
+
renames.push({ from: s3.name, to: name });
|
|
258176
|
+
return { ...s3, name };
|
|
258177
|
+
});
|
|
258178
|
+
return { servers: renames.length ? result : servers, renames };
|
|
258179
|
+
}
|
|
258180
|
+
function assignMcpToolNames(tools) {
|
|
258181
|
+
const names = new Array(tools.length);
|
|
258182
|
+
const bases = tools.map((t4) => mcpToolName(t4.server, t4.tool));
|
|
258183
|
+
const groups = /* @__PURE__ */ new Map();
|
|
258184
|
+
bases.forEach((base, i4) => {
|
|
258185
|
+
const g2 = groups.get(base);
|
|
258186
|
+
if (g2) g2.push(i4);
|
|
258187
|
+
else groups.set(base, [i4]);
|
|
258188
|
+
});
|
|
258189
|
+
const used = new Set(bases);
|
|
258190
|
+
for (const [base, idx] of groups) {
|
|
258191
|
+
if (idx.length === 1) {
|
|
258192
|
+
names[idx[0]] = base;
|
|
258193
|
+
continue;
|
|
258194
|
+
}
|
|
258195
|
+
const raw = (i4) => `${tools[i4].server}::${tools[i4].tool}`;
|
|
258196
|
+
const isClean = (i4) => isStableSegment(tools[i4].server) && isStableSegment(tools[i4].tool);
|
|
258197
|
+
const ordered = [...idx].sort((a2, b3) => {
|
|
258198
|
+
const la = isClean(a2) ? 0 : 1;
|
|
258199
|
+
const lb = isClean(b3) ? 0 : 1;
|
|
258200
|
+
if (la !== lb) return la - lb;
|
|
258201
|
+
const ra = raw(a2);
|
|
258202
|
+
const rb = raw(b3);
|
|
258203
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
258204
|
+
});
|
|
258205
|
+
names[ordered[0]] = base;
|
|
258206
|
+
let n8 = 2;
|
|
258207
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
258208
|
+
let candidate = `${base}_${n8}`;
|
|
258209
|
+
while (used.has(candidate)) {
|
|
258210
|
+
n8++;
|
|
258211
|
+
candidate = `${base}_${n8}`;
|
|
258212
|
+
}
|
|
258213
|
+
names[ordered[rank3]] = candidate;
|
|
258214
|
+
used.add(candidate);
|
|
258215
|
+
n8++;
|
|
258216
|
+
}
|
|
258217
|
+
}
|
|
258218
|
+
return names;
|
|
258219
|
+
}
|
|
258220
|
+
function mcpServerPrefix(serverName) {
|
|
258221
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
258222
|
+
}
|
|
258223
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
258224
|
+
if (!disabledTools?.length) return disabledTools;
|
|
258225
|
+
const prefix = mcpServerPrefix(serverName);
|
|
258226
|
+
const kept = disabledTools.filter((t4) => !t4.startsWith(prefix));
|
|
258227
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
258228
|
+
}
|
|
258002
258229
|
var execAsync = util$1.promisify(child_process.exec);
|
|
258003
258230
|
var ShellTool = class extends BaseTool {
|
|
258004
258231
|
name = "shell";
|
|
@@ -286300,20 +286527,6 @@ ${text}`;
|
|
|
286300
286527
|
}
|
|
286301
286528
|
};
|
|
286302
286529
|
|
|
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
286530
|
// src/tools/mcp.ts
|
|
286318
286531
|
var McpToolWrapper = class extends BaseTool {
|
|
286319
286532
|
name;
|
|
@@ -286322,12 +286535,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
286322
286535
|
mcpClient;
|
|
286323
286536
|
serverName;
|
|
286324
286537
|
toolName;
|
|
286325
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
286538
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
286326
286539
|
super();
|
|
286327
286540
|
this.mcpClient = mcpClient;
|
|
286328
286541
|
this.serverName = serverName;
|
|
286329
286542
|
this.toolName = toolName;
|
|
286330
|
-
this.name = mcpToolName(serverName, toolName);
|
|
286543
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
286331
286544
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
286332
286545
|
this.inputSchema = inputSchema;
|
|
286333
286546
|
}
|
|
@@ -286398,22 +286611,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
286398
286611
|
getRegisteredPlugins() {
|
|
286399
286612
|
return Array.from(this.plugins.keys());
|
|
286400
286613
|
}
|
|
286401
|
-
/**
|
|
286402
|
-
|
|
286403
|
-
|
|
286404
|
-
|
|
286405
|
-
|
|
286406
|
-
|
|
286614
|
+
/**
|
|
286615
|
+
* Registers all tools from an MCP client.
|
|
286616
|
+
*
|
|
286617
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
286618
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
286619
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
286620
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
286621
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
286622
|
+
* never counted against the tool budget, and never has to be refused.
|
|
286623
|
+
*/
|
|
286624
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
286625
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
286626
|
+
const [, server, tool] = def.name.split("::");
|
|
286627
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
286628
|
+
});
|
|
286629
|
+
const names = assignMcpToolNames(parsed);
|
|
286630
|
+
parsed.forEach(({ def, server, tool }, i4) => {
|
|
286407
286631
|
const wrapper = new McpToolWrapper(
|
|
286408
286632
|
mcpClient,
|
|
286409
|
-
|
|
286410
|
-
|
|
286411
|
-
def.description.replace(`[MCP:${
|
|
286412
|
-
def.inputSchema
|
|
286633
|
+
server,
|
|
286634
|
+
tool,
|
|
286635
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
286636
|
+
def.inputSchema,
|
|
286637
|
+
names[i4]
|
|
286413
286638
|
);
|
|
286639
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
286414
286640
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
286415
286641
|
this.register(wrapper);
|
|
286416
|
-
}
|
|
286642
|
+
});
|
|
286417
286643
|
}
|
|
286418
286644
|
setIgnoredPaths(patterns) {
|
|
286419
286645
|
this.ignoreMatcher = ignore().add(patterns);
|
|
@@ -300153,6 +300379,17 @@ function fileOAuthProvider(storePath) {
|
|
|
300153
300379
|
}
|
|
300154
300380
|
});
|
|
300155
300381
|
}
|
|
300382
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
300383
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
300384
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
300385
|
+
const run = prev.then(fn, fn);
|
|
300386
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
300387
|
+
oauthStoreLocks.set(storePath, tail);
|
|
300388
|
+
void tail.then(() => {
|
|
300389
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
300390
|
+
});
|
|
300391
|
+
return run;
|
|
300392
|
+
}
|
|
300156
300393
|
function beginMcpOAuth(provider, serverUrl) {
|
|
300157
300394
|
return auth(provider, { serverUrl });
|
|
300158
300395
|
}
|
|
@@ -303103,7 +303340,50 @@ var GuidanceQueue = class {
|
|
|
303103
303340
|
}
|
|
303104
303341
|
};
|
|
303105
303342
|
|
|
303343
|
+
// src/tools/current-page.ts
|
|
303344
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
303345
|
+
var CurrentPageTool = class extends BaseTool {
|
|
303346
|
+
name = "read_current_page";
|
|
303347
|
+
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.';
|
|
303348
|
+
inputSchema = {
|
|
303349
|
+
type: "object",
|
|
303350
|
+
properties: {},
|
|
303351
|
+
required: []
|
|
303352
|
+
};
|
|
303353
|
+
provider;
|
|
303354
|
+
constructor(provider) {
|
|
303355
|
+
super();
|
|
303356
|
+
this.provider = provider;
|
|
303357
|
+
}
|
|
303358
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
303359
|
+
// nothing they have not already opened themselves.
|
|
303360
|
+
isDangerous() {
|
|
303361
|
+
return false;
|
|
303362
|
+
}
|
|
303363
|
+
async execute(_input, _options) {
|
|
303364
|
+
let page2;
|
|
303365
|
+
try {
|
|
303366
|
+
page2 = await this.provider();
|
|
303367
|
+
} catch (err) {
|
|
303368
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
303369
|
+
}
|
|
303370
|
+
if (!page2) {
|
|
303371
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
303372
|
+
}
|
|
303373
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
303374
|
+
|
|
303375
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
303376
|
+
return [
|
|
303377
|
+
`URL: ${page2.url}`,
|
|
303378
|
+
`Title: ${page2.title}`,
|
|
303379
|
+
"",
|
|
303380
|
+
text.trim() || "[the page rendered no readable text]"
|
|
303381
|
+
].join("\n");
|
|
303382
|
+
}
|
|
303383
|
+
};
|
|
303384
|
+
|
|
303106
303385
|
// src/core/cascade.ts
|
|
303386
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
303107
303387
|
function buildContextualPrompt(prompt, history = []) {
|
|
303108
303388
|
const recent = history.slice(-6);
|
|
303109
303389
|
if (recent.length === 0) return prompt;
|
|
@@ -303326,6 +303606,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
303326
303606
|
});
|
|
303327
303607
|
});
|
|
303328
303608
|
}
|
|
303609
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
303610
|
+
//
|
|
303611
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
303612
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
303613
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
303614
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
303615
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
303616
|
+
//
|
|
303617
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
303618
|
+
// timeout because the plan is already the model's considered proposal;
|
|
303619
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
303620
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
303621
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
303622
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
303623
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
303624
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
303625
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
303626
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
303627
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
303628
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
303629
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
303630
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
303631
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
303632
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
303633
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
303634
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
303635
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
303636
|
+
const requestId = crypto6.randomUUID();
|
|
303637
|
+
return await new Promise((resolve2) => {
|
|
303638
|
+
let timeout;
|
|
303639
|
+
const settle = (decision) => {
|
|
303640
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
303641
|
+
if (timeout) clearTimeout(timeout);
|
|
303642
|
+
signal?.removeEventListener("abort", onAbort);
|
|
303643
|
+
resolve2(decision);
|
|
303644
|
+
};
|
|
303645
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
303646
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
303647
|
+
timeout = setTimeout(() => {
|
|
303648
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
303649
|
+
settle({ action: "timeout" });
|
|
303650
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
303651
|
+
this.pendingEscalations.set(requestId, settle);
|
|
303652
|
+
this.emit("escalation:decision-required", {
|
|
303653
|
+
taskId,
|
|
303654
|
+
requestId,
|
|
303655
|
+
sectionId: ctx.sectionId,
|
|
303656
|
+
sectionTitle: ctx.sectionTitle,
|
|
303657
|
+
issues: ctx.issues,
|
|
303658
|
+
summary: ctx.summary,
|
|
303659
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
303660
|
+
});
|
|
303661
|
+
});
|
|
303662
|
+
}
|
|
303663
|
+
/**
|
|
303664
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
303665
|
+
*
|
|
303666
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
303667
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
303668
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
303669
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
303670
|
+
* which is correct whenever there is exactly one.
|
|
303671
|
+
*
|
|
303672
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
303673
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
303674
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
303675
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
303676
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
303677
|
+
* see EscalationDecision.automatic.
|
|
303678
|
+
*/
|
|
303679
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
303680
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
303681
|
+
if (requestId) {
|
|
303682
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
303683
|
+
return;
|
|
303684
|
+
}
|
|
303685
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
303686
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
303687
|
+
}
|
|
303688
|
+
/**
|
|
303689
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
303690
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
303691
|
+
* same system-decided case as the early-return skips above, not a person
|
|
303692
|
+
* reviewing and accepting that section's output.
|
|
303693
|
+
*/
|
|
303694
|
+
releasePendingEscalations(action = "skip") {
|
|
303695
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
303696
|
+
}
|
|
303329
303697
|
/**
|
|
303330
303698
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
303331
303699
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -303459,6 +303827,20 @@ ${input}` }],
|
|
|
303459
303827
|
this.mediaSink = sink;
|
|
303460
303828
|
this.registerMediaTools(this.workspacePath);
|
|
303461
303829
|
}
|
|
303830
|
+
/**
|
|
303831
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
303832
|
+
*
|
|
303833
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
303834
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
303835
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
303836
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
303837
|
+
* touch the machine, and reading a page the user already opened touches
|
|
303838
|
+
* nothing. `disabledTools` still removes it.
|
|
303839
|
+
*/
|
|
303840
|
+
setCurrentPageProvider(provider) {
|
|
303841
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
303842
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
303843
|
+
}
|
|
303462
303844
|
registerMediaTools(workspacePath) {
|
|
303463
303845
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
303464
303846
|
const permitted = (name) => !denied.has(name);
|
|
@@ -303591,8 +303973,12 @@ ${last3.partialOutput}` : "");
|
|
|
303591
303973
|
for (const server of this.config.tools.mcpServers) {
|
|
303592
303974
|
try {
|
|
303593
303975
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
303594
|
-
|
|
303595
|
-
|
|
303976
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
303977
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
303978
|
+
this.toolRegistry.registerMcpTools(
|
|
303979
|
+
this.mcpClient,
|
|
303980
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
303981
|
+
);
|
|
303596
303982
|
} catch (err) {
|
|
303597
303983
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
303598
303984
|
}
|
|
@@ -304057,6 +304443,7 @@ ${prompt}` : prompt;
|
|
|
304057
304443
|
} else if (complexity === "Moderate") {
|
|
304058
304444
|
const t22 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
304059
304445
|
t22.setRunBreaker(runBreaker);
|
|
304446
|
+
t22.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
304060
304447
|
t22.setPresenter(true);
|
|
304061
304448
|
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
304449
|
if (identityPrompt) {
|
|
@@ -304101,11 +304488,17 @@ ${prompt}` : prompt;
|
|
|
304101
304488
|
t2Results = [t2Result];
|
|
304102
304489
|
const completed = t2Result.t3Results.filter((r4) => r4.status === "COMPLETED");
|
|
304103
304490
|
if (completed.length > 0) {
|
|
304104
|
-
finalOutput =
|
|
304491
|
+
finalOutput = composeRootSectionOutput(
|
|
304492
|
+
t2Result,
|
|
304493
|
+
completed.map((r4) => typeof r4.output === "string" ? r4.output : JSON.stringify(r4.output))
|
|
304494
|
+
);
|
|
304105
304495
|
} else {
|
|
304106
304496
|
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
304497
|
const reasons = Array.from(
|
|
304108
|
-
new Set(
|
|
304498
|
+
new Set([
|
|
304499
|
+
...t2Result.issues ?? [],
|
|
304500
|
+
...t2Result.t3Results.flatMap((r4) => r4.issues ?? [])
|
|
304501
|
+
].filter(Boolean))
|
|
304109
304502
|
);
|
|
304110
304503
|
if (partial2) {
|
|
304111
304504
|
finalOutput = reasons.length ? `${partial2}
|
|
@@ -304118,6 +304511,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
|
|
|
304118
304511
|
} else {
|
|
304119
304512
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
304120
304513
|
t1.setRunBreaker(runBreaker);
|
|
304514
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
304121
304515
|
t1.setPresenter(true);
|
|
304122
304516
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
304123
304517
|
if (identityPrompt) {
|
|
@@ -304142,6 +304536,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
|
|
|
304142
304536
|
finalOutput = result.output;
|
|
304143
304537
|
t2Results = result.t2Results;
|
|
304144
304538
|
}
|
|
304539
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
304540
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
304145
304541
|
const trip = runBreaker.reason();
|
|
304146
304542
|
if (trip) {
|
|
304147
304543
|
finalOutput = [
|
|
@@ -304182,6 +304578,10 @@ ${finalOutput}` : ""
|
|
|
304182
304578
|
escalator.cancelAllPending();
|
|
304183
304579
|
} catch {
|
|
304184
304580
|
}
|
|
304581
|
+
try {
|
|
304582
|
+
this.releasePendingEscalations();
|
|
304583
|
+
} catch {
|
|
304584
|
+
}
|
|
304185
304585
|
this.router.restoreTierModels();
|
|
304186
304586
|
this.router.setRunSignal(void 0);
|
|
304187
304587
|
if (this.taskAnalyzer) {
|
|
@@ -305498,6 +305898,13 @@ var ConfigManager = class {
|
|
|
305498
305898
|
}
|
|
305499
305899
|
async load() {
|
|
305500
305900
|
this.config = await this.loadConfig();
|
|
305901
|
+
const servers = this.config.tools?.mcpServers;
|
|
305902
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
305903
|
+
const mcpNamesChanged = renames.length > 0;
|
|
305904
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
305905
|
+
this.config.tools.mcpServers = disambiguated;
|
|
305906
|
+
this.renameMcpServerReferences(renames);
|
|
305907
|
+
}
|
|
305501
305908
|
this.ignore = new CascadeIgnore();
|
|
305502
305909
|
await this.ignore.load(this.workspacePath);
|
|
305503
305910
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -305506,6 +305913,7 @@ var ConfigManager = class {
|
|
|
305506
305913
|
await this.injectEnvKeys();
|
|
305507
305914
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
305508
305915
|
await this.ensureDefaultIdentity();
|
|
305916
|
+
if (mcpNamesChanged) await this.save();
|
|
305509
305917
|
}
|
|
305510
305918
|
getConfig() {
|
|
305511
305919
|
return this.config;
|
|
@@ -305589,6 +305997,66 @@ var ConfigManager = class {
|
|
|
305589
305997
|
this.config.providers.push({ type: "ollama" });
|
|
305590
305998
|
}
|
|
305591
305999
|
}
|
|
306000
|
+
/**
|
|
306001
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
306002
|
+
*
|
|
306003
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
306004
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
306005
|
+
*
|
|
306006
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
306007
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
306008
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
306009
|
+
* move with it. A plain rewrite is correct here.
|
|
306010
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
306011
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
306012
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
306013
|
+
* already been updated to the disambiguated list by the time this runs
|
|
306014
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
306015
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
306016
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
306017
|
+
* AND granted to the renamed identity, not moved.
|
|
306018
|
+
*
|
|
306019
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
306020
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
306021
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
306022
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
306023
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
306024
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
306025
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
306026
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
306027
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
306028
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
306029
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
306030
|
+
* together, in the one pass that still sees the original entry.
|
|
306031
|
+
*
|
|
306032
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
306033
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
306034
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
306035
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
306036
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
306037
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
306038
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
306039
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
306040
|
+
* would do the opposite — move a denial that already worked for the
|
|
306041
|
+
* survivor onto a server it may never have meant to cover.
|
|
306042
|
+
*/
|
|
306043
|
+
renameMcpServerReferences(renames) {
|
|
306044
|
+
const tools = this.config.tools;
|
|
306045
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
306046
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s3) => s3.name));
|
|
306047
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
306048
|
+
for (const { from, to } of renames) {
|
|
306049
|
+
const tos = byFrom.get(from);
|
|
306050
|
+
if (tos) tos.push(to);
|
|
306051
|
+
else byFrom.set(from, [to]);
|
|
306052
|
+
}
|
|
306053
|
+
let trusted = tools.mcpTrusted;
|
|
306054
|
+
for (const [from, tos] of byFrom) {
|
|
306055
|
+
if (!trusted.includes(from)) continue;
|
|
306056
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n8) => n8 !== from), ...tos];
|
|
306057
|
+
}
|
|
306058
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
306059
|
+
}
|
|
305592
306060
|
async ensureDefaultIdentity() {
|
|
305593
306061
|
const existing = this.store.getDefaultIdentity();
|
|
305594
306062
|
if (existing) return;
|
|
@@ -306492,6 +306960,14 @@ var DashboardSocket = class {
|
|
|
306492
306960
|
this.secret = options.secret;
|
|
306493
306961
|
this.setupHandlers();
|
|
306494
306962
|
}
|
|
306963
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
306964
|
+
roomSize(room) {
|
|
306965
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
306966
|
+
}
|
|
306967
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
306968
|
+
hasSocket(socketId) {
|
|
306969
|
+
return this.io.sockets.sockets.has(socketId);
|
|
306970
|
+
}
|
|
306495
306971
|
broadcastToRoom(room, event, data) {
|
|
306496
306972
|
this.io.to(room).emit(event, data);
|
|
306497
306973
|
}
|
|
@@ -306563,6 +307039,22 @@ var DashboardSocket = class {
|
|
|
306563
307039
|
});
|
|
306564
307040
|
});
|
|
306565
307041
|
}
|
|
307042
|
+
/**
|
|
307043
|
+
* A client subscribed to a session's room.
|
|
307044
|
+
*
|
|
307045
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
307046
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
307047
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
307048
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
307049
|
+
*/
|
|
307050
|
+
onJoinSession(callback) {
|
|
307051
|
+
this.io.on("connection", (socket) => {
|
|
307052
|
+
socket.on("join:session", (payload) => {
|
|
307053
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
307054
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
307055
|
+
});
|
|
307056
|
+
});
|
|
307057
|
+
}
|
|
306566
307058
|
onSessionRate(callback) {
|
|
306567
307059
|
this.io.on("connection", (socket) => {
|
|
306568
307060
|
socket.on("session:rate", (payload) => {
|
|
@@ -306600,6 +307092,31 @@ var DashboardSocket = class {
|
|
|
306600
307092
|
});
|
|
306601
307093
|
});
|
|
306602
307094
|
}
|
|
307095
|
+
/**
|
|
307096
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
307097
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
307098
|
+
* answers here.
|
|
307099
|
+
*
|
|
307100
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
307101
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
307102
|
+
*/
|
|
307103
|
+
onEscalationDecision(callback) {
|
|
307104
|
+
this.io.on("connection", (socket) => {
|
|
307105
|
+
socket.on("escalation:decide", (payload) => {
|
|
307106
|
+
const action = payload?.action;
|
|
307107
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
307108
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
307109
|
+
callback({
|
|
307110
|
+
sessionId: payload.sessionId,
|
|
307111
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
307112
|
+
// run concurrently, so more than one can be waiting.
|
|
307113
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
307114
|
+
action,
|
|
307115
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
307116
|
+
});
|
|
307117
|
+
});
|
|
307118
|
+
});
|
|
307119
|
+
}
|
|
306603
307120
|
onSessionSteer(callback) {
|
|
306604
307121
|
this.io.on("connection", (socket) => {
|
|
306605
307122
|
socket.on("session:steer", (payload) => {
|
|
@@ -306761,6 +307278,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
306761
307278
|
|
|
306762
307279
|
// src/dashboard/server.ts
|
|
306763
307280
|
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))));
|
|
307281
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
306764
307282
|
var DashboardServer = class {
|
|
306765
307283
|
app;
|
|
306766
307284
|
httpServer;
|
|
@@ -306771,6 +307289,13 @@ var DashboardServer = class {
|
|
|
306771
307289
|
globalStore = null;
|
|
306772
307290
|
broadcastTimer = null;
|
|
306773
307291
|
activeSessions = /* @__PURE__ */ new Map();
|
|
307292
|
+
/**
|
|
307293
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
307294
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
307295
|
+
* browser — so the tool is simply not registered rather than present and
|
|
307296
|
+
* always failing.
|
|
307297
|
+
*/
|
|
307298
|
+
currentPageProvider;
|
|
306774
307299
|
activeControllers = /* @__PURE__ */ new Map();
|
|
306775
307300
|
/**
|
|
306776
307301
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -306785,6 +307310,17 @@ var DashboardServer = class {
|
|
|
306785
307310
|
* map is how that answer reaches the run that's blocked on it.
|
|
306786
307311
|
*/
|
|
306787
307312
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
307313
|
+
/**
|
|
307314
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
307315
|
+
* delivered to a client that was not reachable when it was raised.
|
|
307316
|
+
*
|
|
307317
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
307318
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
307319
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
307320
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
307321
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
307322
|
+
*/
|
|
307323
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
306788
307324
|
/**
|
|
306789
307325
|
* The orchestration decision trail ("why") of each session's most recent
|
|
306790
307326
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -306862,20 +307398,29 @@ var DashboardServer = class {
|
|
|
306862
307398
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
306863
307399
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
306864
307400
|
this.activeSessions.set(sessionId, cascade);
|
|
307401
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
306865
307402
|
cascade.on("stream:token", (e3) => {
|
|
306866
|
-
this.
|
|
307403
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
306867
307404
|
});
|
|
306868
307405
|
cascade.on("tier:status", (e3) => {
|
|
306869
|
-
this.
|
|
307406
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e3 });
|
|
306870
307407
|
});
|
|
306871
307408
|
cascade.on("permission:user-required", (e3) => {
|
|
306872
|
-
this.
|
|
307409
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e3 });
|
|
306873
307410
|
});
|
|
306874
307411
|
cascade.on("peer:message", (e3) => {
|
|
306875
307412
|
this.socket.emitPeerMessage(e3);
|
|
306876
307413
|
});
|
|
306877
307414
|
cascade.on("plan:approval-required", (e3) => {
|
|
306878
|
-
this.
|
|
307415
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e3 });
|
|
307416
|
+
});
|
|
307417
|
+
cascade.on("escalation:decision-required", (e3) => {
|
|
307418
|
+
this.raiseEscalation(sessionId, socketId, e3);
|
|
307419
|
+
});
|
|
307420
|
+
cascade.on("escalation:timeout", (e3) => {
|
|
307421
|
+
const ev = e3;
|
|
307422
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
307423
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e3 });
|
|
306879
307424
|
});
|
|
306880
307425
|
try {
|
|
306881
307426
|
const result = await cascade.run({
|
|
@@ -306886,7 +307431,7 @@ var DashboardServer = class {
|
|
|
306886
307431
|
this.recordSessionTask(sessionId, result.taskId);
|
|
306887
307432
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
306888
307433
|
this.captureWhy(sessionId, cascade, result);
|
|
306889
|
-
this.
|
|
307434
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
306890
307435
|
this.socket.broadcast("cost:update", {
|
|
306891
307436
|
sessionId,
|
|
306892
307437
|
totalTokens: result.usage.totalTokens,
|
|
@@ -306899,7 +307444,7 @@ var DashboardServer = class {
|
|
|
306899
307444
|
} catch (err) {
|
|
306900
307445
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
306901
307446
|
this.captureWhy(sessionId, cascade);
|
|
306902
|
-
this.
|
|
307447
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
306903
307448
|
sessionId,
|
|
306904
307449
|
error: err instanceof Error ? err.message : String(err)
|
|
306905
307450
|
});
|
|
@@ -306907,6 +307452,7 @@ var DashboardServer = class {
|
|
|
306907
307452
|
this.activeSessions.delete(sessionId);
|
|
306908
307453
|
this.activeControllers.delete(sessionId);
|
|
306909
307454
|
this.denyPendingApprovals(sessionId);
|
|
307455
|
+
this.clearSessionEscalations(sessionId);
|
|
306910
307456
|
}
|
|
306911
307457
|
});
|
|
306912
307458
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -306916,10 +307462,20 @@ var DashboardServer = class {
|
|
|
306916
307462
|
editedPlan
|
|
306917
307463
|
);
|
|
306918
307464
|
});
|
|
307465
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
307466
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
307467
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
307468
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
307469
|
+
});
|
|
307470
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
307471
|
+
this.replayEscalations(sessionId, socketId);
|
|
307472
|
+
});
|
|
306919
307473
|
this.socket.onSessionHalt((sessionId) => {
|
|
306920
307474
|
this.activeControllers.get(sessionId)?.abort();
|
|
306921
307475
|
this.denyPendingApprovals(sessionId);
|
|
306922
307476
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
307477
|
+
this.clearSessionEscalations(sessionId);
|
|
307478
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
306923
307479
|
});
|
|
306924
307480
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
306925
307481
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -306934,6 +307490,10 @@ var DashboardServer = class {
|
|
|
306934
307490
|
}
|
|
306935
307491
|
});
|
|
306936
307492
|
}
|
|
307493
|
+
/** Wire the host's browser view in (desktop only). */
|
|
307494
|
+
setCurrentPageProvider(provider) {
|
|
307495
|
+
this.currentPageProvider = provider;
|
|
307496
|
+
}
|
|
306937
307497
|
async start() {
|
|
306938
307498
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
306939
307499
|
if (!isLoopback) {
|
|
@@ -307222,6 +307782,116 @@ var DashboardServer = class {
|
|
|
307222
307782
|
this.activeSessions.delete(sessionId);
|
|
307223
307783
|
}
|
|
307224
307784
|
}
|
|
307785
|
+
/**
|
|
307786
|
+
* Is anyone able to answer for this session right now?
|
|
307787
|
+
*
|
|
307788
|
+
* The originating socket first — that connection is definitely watching this
|
|
307789
|
+
* run — then the session room, which is where a client lands after a
|
|
307790
|
+
* reconnect issues it a new socket id.
|
|
307791
|
+
*/
|
|
307792
|
+
hasResponder(sessionId, socketId) {
|
|
307793
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
307794
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
307795
|
+
}
|
|
307796
|
+
/**
|
|
307797
|
+
* Send a run event to whoever is watching this session.
|
|
307798
|
+
*
|
|
307799
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
307800
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
307801
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
307802
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
307803
|
+
* client that is both the originator and a room member from seeing every
|
|
307804
|
+
* token twice.
|
|
307805
|
+
*/
|
|
307806
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
307807
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
307808
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
307809
|
+
}
|
|
307810
|
+
/**
|
|
307811
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
307812
|
+
*
|
|
307813
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
307814
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
307815
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
307816
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
307817
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
307818
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
307819
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
307820
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
307821
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
307822
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
307823
|
+
* mistaken for an absent user.
|
|
307824
|
+
*/
|
|
307825
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
307826
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto6.randomUUID();
|
|
307827
|
+
const payload = { sessionId, ...event };
|
|
307828
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
307829
|
+
this.pendingEscalations.set(requestId, entry);
|
|
307830
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
307831
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
307832
|
+
return;
|
|
307833
|
+
}
|
|
307834
|
+
entry.graceTimer = setTimeout(() => {
|
|
307835
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
307836
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
307837
|
+
this.pendingEscalations.delete(requestId);
|
|
307838
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
307839
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
307840
|
+
}
|
|
307841
|
+
/**
|
|
307842
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
307843
|
+
*
|
|
307844
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
307845
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
307846
|
+
* its gates and the clients queue theirs.
|
|
307847
|
+
*
|
|
307848
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
307849
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
307850
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
307851
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
307852
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
307853
|
+
* sides never have to agree on a wall clock.
|
|
307854
|
+
*/
|
|
307855
|
+
replayEscalations(sessionId, socketId) {
|
|
307856
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
307857
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307858
|
+
if (entry.graceTimer) {
|
|
307859
|
+
clearTimeout(entry.graceTimer);
|
|
307860
|
+
entry.graceTimer = void 0;
|
|
307861
|
+
}
|
|
307862
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
307863
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
307864
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
307865
|
+
}
|
|
307866
|
+
}
|
|
307867
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
307868
|
+
clearPendingEscalation(requestId) {
|
|
307869
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
307870
|
+
if (!entry) return;
|
|
307871
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307872
|
+
this.pendingEscalations.delete(requestId);
|
|
307873
|
+
}
|
|
307874
|
+
/**
|
|
307875
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
307876
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
307877
|
+
* Insertion order makes `values()` oldest-first.
|
|
307878
|
+
*/
|
|
307879
|
+
clearOldestSessionEscalation(sessionId) {
|
|
307880
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
307881
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307882
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307883
|
+
this.pendingEscalations.delete(id);
|
|
307884
|
+
return;
|
|
307885
|
+
}
|
|
307886
|
+
}
|
|
307887
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
307888
|
+
clearSessionEscalations(sessionId) {
|
|
307889
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
307890
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307891
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307892
|
+
this.pendingEscalations.delete(id);
|
|
307893
|
+
}
|
|
307894
|
+
}
|
|
307225
307895
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
307226
307896
|
denyPendingApprovals(sessionId) {
|
|
307227
307897
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -307722,6 +308392,7 @@ ${prompt}`;
|
|
|
307722
308392
|
void (async () => {
|
|
307723
308393
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
307724
308394
|
this.activeSessions.set(sessionId, cascade);
|
|
308395
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
307725
308396
|
cascade.on("stream:token", (e3) => {
|
|
307726
308397
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
307727
308398
|
});
|
|
@@ -307737,6 +308408,14 @@ ${prompt}`;
|
|
|
307737
308408
|
cascade.on("plan:approval-required", (e3) => {
|
|
307738
308409
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e3 });
|
|
307739
308410
|
});
|
|
308411
|
+
cascade.on("escalation:decision-required", (e3) => {
|
|
308412
|
+
this.raiseEscalation(sessionId, void 0, e3);
|
|
308413
|
+
});
|
|
308414
|
+
cascade.on("escalation:timeout", (e3) => {
|
|
308415
|
+
const ev = e3;
|
|
308416
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
308417
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e3 });
|
|
308418
|
+
});
|
|
307740
308419
|
try {
|
|
307741
308420
|
const result = await cascade.run({
|
|
307742
308421
|
prompt: runPrompt,
|
|
@@ -307764,6 +308443,7 @@ ${prompt}`;
|
|
|
307764
308443
|
} finally {
|
|
307765
308444
|
this.activeSessions.delete(sessionId);
|
|
307766
308445
|
this.denyPendingApprovals(sessionId);
|
|
308446
|
+
this.clearSessionEscalations(sessionId);
|
|
307767
308447
|
}
|
|
307768
308448
|
})();
|
|
307769
308449
|
});
|
|
@@ -308096,6 +308776,41 @@ var HooksRunner = class {
|
|
|
308096
308776
|
}
|
|
308097
308777
|
};
|
|
308098
308778
|
|
|
308779
|
+
// src/mcp/discover.ts
|
|
308780
|
+
async function discoverMcpTools(servers) {
|
|
308781
|
+
const results = [];
|
|
308782
|
+
for (const server of servers) {
|
|
308783
|
+
const client = new McpClient({ trustedServers: [server.name] });
|
|
308784
|
+
try {
|
|
308785
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
308786
|
+
const connect = () => client.connect(server, authProvider ? { authProvider } : {});
|
|
308787
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
308788
|
+
const parsed = client.getToolDefinitions().flatMap((def) => {
|
|
308789
|
+
const [, serverName, toolName] = def.name.split("::");
|
|
308790
|
+
return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
|
|
308791
|
+
});
|
|
308792
|
+
const names = assignMcpToolNames(parsed);
|
|
308793
|
+
const tools = parsed.map(({ def, server: s3, tool }, i4) => ({
|
|
308794
|
+
server: s3,
|
|
308795
|
+
tool,
|
|
308796
|
+
name: names[i4],
|
|
308797
|
+
description: def.description.replace(`[MCP:${s3}] `, "")
|
|
308798
|
+
}));
|
|
308799
|
+
results.push({ server: server.name, tools });
|
|
308800
|
+
} catch (err) {
|
|
308801
|
+
results.push({
|
|
308802
|
+
server: server.name,
|
|
308803
|
+
tools: [],
|
|
308804
|
+
error: err instanceof Error ? err.message : String(err)
|
|
308805
|
+
});
|
|
308806
|
+
} finally {
|
|
308807
|
+
await client.disconnectAll().catch(() => {
|
|
308808
|
+
});
|
|
308809
|
+
}
|
|
308810
|
+
}
|
|
308811
|
+
return results;
|
|
308812
|
+
}
|
|
308813
|
+
|
|
308099
308814
|
// src/retrieval/chunk.ts
|
|
308100
308815
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
308101
308816
|
function splitLongBlock(block, max) {
|
|
@@ -308634,11 +309349,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
308634
309349
|
for (const i4 of incoming) map.set(key(i4), i4);
|
|
308635
309350
|
return [...map.values()];
|
|
308636
309351
|
}
|
|
309352
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
309353
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
309354
|
+
let n8 = 2;
|
|
309355
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n8}`))) n8++;
|
|
309356
|
+
return `${desired} ${n8}`;
|
|
309357
|
+
}
|
|
309358
|
+
function mergeMcpServers(local, incoming) {
|
|
309359
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
309360
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
309361
|
+
const byName = /* @__PURE__ */ new Map();
|
|
309362
|
+
for (const l2 of local ?? []) byName.set(l2.name, l2);
|
|
309363
|
+
const prefixes = new Set([...byName.values()].map((m3) => mcpServerPrefix(m3.name)));
|
|
309364
|
+
const synced = [];
|
|
309365
|
+
for (const inc of incoming) {
|
|
309366
|
+
if (byName.has(inc.name)) {
|
|
309367
|
+
byName.set(inc.name, inc);
|
|
309368
|
+
synced.push(inc);
|
|
309369
|
+
continue;
|
|
309370
|
+
}
|
|
309371
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
309372
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
309373
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
309374
|
+
byName.set(name, server);
|
|
309375
|
+
synced.push(server);
|
|
309376
|
+
prefixes.add(mcpServerPrefix(name));
|
|
309377
|
+
}
|
|
309378
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
309379
|
+
}
|
|
309380
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
309381
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
309382
|
+
return tools.map((t4) => {
|
|
309383
|
+
for (const [from, to] of rewrite) if (t4.startsWith(from)) return to + t4.slice(from.length);
|
|
309384
|
+
return t4;
|
|
309385
|
+
});
|
|
309386
|
+
}
|
|
309387
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
309388
|
+
const prefixes = (syncedServers ?? []).map((m3) => mcpServerPrefix(m3.name));
|
|
309389
|
+
const covered = (tool) => prefixes.some((p3) => tool.startsWith(p3));
|
|
309390
|
+
const kept = (local ?? []).filter((t4) => !covered(t4));
|
|
309391
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
309392
|
+
return merged.length ? merged : void 0;
|
|
309393
|
+
}
|
|
308637
309394
|
function gatherSyncBundle(config2) {
|
|
308638
|
-
const bundle = { v:
|
|
309395
|
+
const bundle = { v: 2 };
|
|
308639
309396
|
if (config2.providers?.length) bundle.providers = config2.providers;
|
|
308640
309397
|
if (config2.tools?.webSearch) bundle.webSearch = config2.tools.webSearch;
|
|
308641
309398
|
if (config2.tools?.mcpServers?.length) bundle.mcpServers = config2.tools.mcpServers;
|
|
309399
|
+
const syncedDisabledTools = config2.tools?.disabledTools?.filter(isMcpToolName);
|
|
309400
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
308642
309401
|
if (config2.models) bundle.models = config2.models;
|
|
308643
309402
|
if (config2.budget) bundle.budget = config2.budget;
|
|
308644
309403
|
const prefs = {};
|
|
@@ -308653,7 +309412,16 @@ function applySyncBundle(bundle, config2) {
|
|
|
308653
309412
|
const next = { ...config2, tools: { ...config2.tools } };
|
|
308654
309413
|
if (bundle.providers) next.providers = mergeByKey(config2.providers, bundle.providers, providerSig) ?? [];
|
|
308655
309414
|
if (bundle.webSearch) next.tools.webSearch = { ...config2.tools?.webSearch, ...bundle.webSearch };
|
|
308656
|
-
|
|
309415
|
+
const mcp = mergeMcpServers(config2.tools?.mcpServers, bundle.mcpServers);
|
|
309416
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
|
|
309417
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
309418
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
309419
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
309420
|
+
config2.tools?.disabledTools,
|
|
309421
|
+
rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
|
|
309422
|
+
bundle.mcpServers ? mcp.synced : void 0
|
|
309423
|
+
);
|
|
309424
|
+
}
|
|
308657
309425
|
if (bundle.models) next.models = { ...config2.models, ...bundle.models };
|
|
308658
309426
|
if (bundle.budget) next.budget = { ...config2.budget, ...bundle.budget };
|
|
308659
309427
|
if (bundle.prefs) {
|
|
@@ -309088,6 +309856,7 @@ exports.CascadeToolError = CascadeToolError;
|
|
|
309088
309856
|
exports.CloudClient = CloudClient;
|
|
309089
309857
|
exports.CodeSearchTool = CodeSearchTool;
|
|
309090
309858
|
exports.ConfigManager = ConfigManager;
|
|
309859
|
+
exports.CurrentPageTool = CurrentPageTool;
|
|
309091
309860
|
exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
|
|
309092
309861
|
exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;
|
|
309093
309862
|
exports.DEFAULT_AUTO_SUMMARIZE_AT = DEFAULT_AUTO_SUMMARIZE_AT;
|
|
@@ -309110,6 +309879,8 @@ exports.HooksRunner = HooksRunner;
|
|
|
309110
309879
|
exports.Keystore = Keystore;
|
|
309111
309880
|
exports.LLMReranker = LLMReranker;
|
|
309112
309881
|
exports.LM_STUDIO_BASE_URL = LM_STUDIO_BASE_URL;
|
|
309882
|
+
exports.MAX_PAGE_TEXT_CHARS = MAX_PAGE_TEXT_CHARS;
|
|
309883
|
+
exports.MCP_TOOL_PREFIX = MCP_TOOL_PREFIX;
|
|
309113
309884
|
exports.MODELS = MODELS;
|
|
309114
309885
|
exports.McpClient = McpClient;
|
|
309115
309886
|
exports.McpOAuthProvider = McpOAuthProvider;
|
|
@@ -309146,7 +309917,9 @@ exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
|
|
|
309146
309917
|
exports.createCascade = createCascade;
|
|
309147
309918
|
exports.decryptSyncBlob = decryptJSON;
|
|
309148
309919
|
exports.diffManifest = diffManifest;
|
|
309920
|
+
exports.disambiguateMcpServerNames = disambiguateMcpServerNames;
|
|
309149
309921
|
exports.discoverMcpAuthServer = discoverMcpAuthServer;
|
|
309922
|
+
exports.discoverMcpTools = discoverMcpTools;
|
|
309150
309923
|
exports.distillSessionFacts = distillSessionFacts;
|
|
309151
309924
|
exports.embedderFromProviders = embedderFromProviders;
|
|
309152
309925
|
exports.encryptSyncBlob = encryptJSON;
|
|
@@ -309155,12 +309928,17 @@ exports.gatherSyncBundle = gatherSyncBundle;
|
|
|
309155
309928
|
exports.hashContent = hashContent;
|
|
309156
309929
|
exports.heuristicCodeChunker = heuristicCodeChunker;
|
|
309157
309930
|
exports.inferAzureBaseModel = inferAzureBaseModel;
|
|
309931
|
+
exports.isMcpToolName = isMcpToolName;
|
|
309932
|
+
exports.mcpServerPrefix = mcpServerPrefix;
|
|
309933
|
+
exports.mcpToolName = mcpToolName;
|
|
309158
309934
|
exports.nodeHttpFetch = nodeHttpFetch;
|
|
309159
309935
|
exports.parseRankOrder = parseRankOrder;
|
|
309160
309936
|
exports.planRetrieval = planRetrieval;
|
|
309161
309937
|
exports.preferIpv4Host = preferIpv4Host;
|
|
309162
309938
|
exports.reciprocalRankFusion = reciprocalRankFusion;
|
|
309163
309939
|
exports.refreshMcpToken = refreshMcpToken;
|
|
309940
|
+
exports.removeMcpServerDenials = removeMcpServerDenials;
|
|
309164
309941
|
exports.runCascade = runCascade;
|
|
309165
309942
|
exports.sessionWorthRemembering = sessionWorthRemembering;
|
|
309166
309943
|
exports.streamCascade = streamCascade;
|
|
309944
|
+
exports.uniqueMcpServerName = uniqueMcpServerName;
|