cascade-ai 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +958 -64
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +958 -64
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +992 -55
- package/dist/index.cjs +992 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +512 -4
- package/dist/index.d.ts +512 -4
- package/dist/index.js +983 -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.55.0";
|
|
224251
224251
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
224252
224252
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
224253
224253
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -251963,7 +251963,21 @@ var ModelSelector = class {
|
|
|
251963
251963
|
this.availableProviders = availableProviders;
|
|
251964
251964
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
251965
251965
|
}
|
|
251966
|
+
/**
|
|
251967
|
+
* Verdicts from earlier runs about ids that 404 on this account. Consulted
|
|
251968
|
+
* BEFORE a model enters the pool, which is the only placement that helps: a
|
|
251969
|
+
* T3 wave runs concurrently, so filtering after the first failure still lets
|
|
251970
|
+
* every other worker in the wave call the dead id.
|
|
251971
|
+
*/
|
|
251972
|
+
deadModels;
|
|
251973
|
+
setDeadModelStore(store) {
|
|
251974
|
+
this.deadModels = store;
|
|
251975
|
+
for (const [id, m3] of [...this.availableModels]) {
|
|
251976
|
+
if (this.deadModels.isDead(m3.provider, m3.id)) this.availableModels.delete(id);
|
|
251977
|
+
}
|
|
251978
|
+
}
|
|
251966
251979
|
addDynamicModel(model) {
|
|
251980
|
+
if (this.deadModels?.isDead(model.provider, model.id)) return;
|
|
251967
251981
|
this.availableModels.set(model.id, model);
|
|
251968
251982
|
if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
|
|
251969
251983
|
}
|
|
@@ -252370,6 +252384,121 @@ var LocalRequestQueue = class {
|
|
|
252370
252384
|
}
|
|
252371
252385
|
};
|
|
252372
252386
|
|
|
252387
|
+
// src/core/router/dead-models.ts
|
|
252388
|
+
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
252389
|
+
function deadModelKey(provider, modelId) {
|
|
252390
|
+
return `${provider}:${modelId}`;
|
|
252391
|
+
}
|
|
252392
|
+
var DeadModelStore = class {
|
|
252393
|
+
constructor(persistence, opts = {}) {
|
|
252394
|
+
this.persistence = persistence;
|
|
252395
|
+
this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
|
|
252396
|
+
this.now = opts.now ?? (() => Date.now());
|
|
252397
|
+
this.restore();
|
|
252398
|
+
}
|
|
252399
|
+
persistence;
|
|
252400
|
+
records = /* @__PURE__ */ new Map();
|
|
252401
|
+
ttlMs;
|
|
252402
|
+
now;
|
|
252403
|
+
restore() {
|
|
252404
|
+
if (!this.persistence) return;
|
|
252405
|
+
try {
|
|
252406
|
+
for (const r4 of this.persistence.load()) {
|
|
252407
|
+
if (!this.isExpired(r4)) this.records.set(r4.key, r4);
|
|
252408
|
+
}
|
|
252409
|
+
} catch {
|
|
252410
|
+
}
|
|
252411
|
+
}
|
|
252412
|
+
isExpired(r4) {
|
|
252413
|
+
return this.now() - r4.lastSeenAt > this.ttlMs;
|
|
252414
|
+
}
|
|
252415
|
+
flush() {
|
|
252416
|
+
if (!this.persistence) return;
|
|
252417
|
+
try {
|
|
252418
|
+
this.persistence.save([...this.records.values()]);
|
|
252419
|
+
} catch {
|
|
252420
|
+
}
|
|
252421
|
+
}
|
|
252422
|
+
/**
|
|
252423
|
+
* Record that a model was found dead. Returns true when this is the first
|
|
252424
|
+
* time (so the caller can log it once rather than once per concurrent
|
|
252425
|
+
* worker in the wave that discovered it).
|
|
252426
|
+
*/
|
|
252427
|
+
record(provider, modelId, reason) {
|
|
252428
|
+
const key = deadModelKey(provider, modelId);
|
|
252429
|
+
const now = this.now();
|
|
252430
|
+
const existing = this.records.get(key);
|
|
252431
|
+
if (existing && !this.isExpired(existing)) {
|
|
252432
|
+
existing.lastSeenAt = now;
|
|
252433
|
+
existing.hits += 1;
|
|
252434
|
+
this.flush();
|
|
252435
|
+
return false;
|
|
252436
|
+
}
|
|
252437
|
+
this.records.set(key, {
|
|
252438
|
+
key,
|
|
252439
|
+
provider,
|
|
252440
|
+
modelId,
|
|
252441
|
+
reason,
|
|
252442
|
+
firstSeenAt: now,
|
|
252443
|
+
lastSeenAt: now,
|
|
252444
|
+
hits: 1
|
|
252445
|
+
});
|
|
252446
|
+
this.flush();
|
|
252447
|
+
return true;
|
|
252448
|
+
}
|
|
252449
|
+
/** True when this model is known dead and the verdict has not expired. */
|
|
252450
|
+
isDead(provider, modelId) {
|
|
252451
|
+
const r4 = this.records.get(deadModelKey(provider, modelId));
|
|
252452
|
+
if (!r4) return false;
|
|
252453
|
+
if (this.isExpired(r4)) {
|
|
252454
|
+
this.records.delete(r4.key);
|
|
252455
|
+
this.flush();
|
|
252456
|
+
return false;
|
|
252457
|
+
}
|
|
252458
|
+
return true;
|
|
252459
|
+
}
|
|
252460
|
+
/** Why a model is considered dead, for /why and the models command. */
|
|
252461
|
+
reasonFor(provider, modelId) {
|
|
252462
|
+
const r4 = this.records.get(deadModelKey(provider, modelId));
|
|
252463
|
+
return r4 && !this.isExpired(r4) ? r4.reason : null;
|
|
252464
|
+
}
|
|
252465
|
+
/** Live records, expired ones filtered out. */
|
|
252466
|
+
list() {
|
|
252467
|
+
const live = [...this.records.values()].filter((r4) => !this.isExpired(r4));
|
|
252468
|
+
return live.sort((a2, b3) => b3.lastSeenAt - a2.lastSeenAt);
|
|
252469
|
+
}
|
|
252470
|
+
/**
|
|
252471
|
+
* Forget a verdict — used when the user fixes the underlying problem (adds
|
|
252472
|
+
* the model to their project, gets quota) and wants it retried now rather
|
|
252473
|
+
* than after the TTL.
|
|
252474
|
+
*/
|
|
252475
|
+
forget(provider, modelId) {
|
|
252476
|
+
const removed = this.records.delete(deadModelKey(provider, modelId));
|
|
252477
|
+
if (removed) this.flush();
|
|
252478
|
+
return removed;
|
|
252479
|
+
}
|
|
252480
|
+
clear() {
|
|
252481
|
+
this.records.clear();
|
|
252482
|
+
this.flush();
|
|
252483
|
+
}
|
|
252484
|
+
};
|
|
252485
|
+
function fileDeadModelPersistence(filePath) {
|
|
252486
|
+
return {
|
|
252487
|
+
load() {
|
|
252488
|
+
const fs31 = __require("fs");
|
|
252489
|
+
if (!fs31.existsSync(filePath)) return [];
|
|
252490
|
+
const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
|
|
252491
|
+
return Array.isArray(raw) ? raw : [];
|
|
252492
|
+
},
|
|
252493
|
+
save(records) {
|
|
252494
|
+
const fs31 = __require("fs");
|
|
252495
|
+
const path28 = __require("path");
|
|
252496
|
+
fs31.mkdirSync(path28.dirname(filePath), { recursive: true });
|
|
252497
|
+
fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
252498
|
+
}
|
|
252499
|
+
};
|
|
252500
|
+
}
|
|
252501
|
+
|
|
252373
252502
|
// src/utils/retry.ts
|
|
252374
252503
|
var CascadeCancelledError = class extends Error {
|
|
252375
252504
|
constructor(reason) {
|
|
@@ -252959,10 +253088,47 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
252959
253088
|
*/
|
|
252960
253089
|
budgetState = "ok";
|
|
252961
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
|
+
}
|
|
252962
253108
|
tpmLimiter;
|
|
252963
253109
|
localQueue;
|
|
252964
253110
|
taskAnalyzer;
|
|
252965
253111
|
pendingFeedbackSource;
|
|
253112
|
+
/** Models known dead for this account (404 / retired / not enabled). */
|
|
253113
|
+
deadModels = new DeadModelStore();
|
|
253114
|
+
/**
|
|
253115
|
+
* Back the dead-model memory with durable storage (a JSON file on desktop, a
|
|
253116
|
+
* per-user table in the cloud). Without one the verdicts still work, but only
|
|
253117
|
+
* for the life of the process — so tomorrow's run pays the same wave of 404s
|
|
253118
|
+
* to learn the same fact.
|
|
253119
|
+
*/
|
|
253120
|
+
setDeadModelStore(store) {
|
|
253121
|
+
this.deadModels = store;
|
|
253122
|
+
this.selector.setDeadModelStore(store);
|
|
253123
|
+
}
|
|
253124
|
+
/** Models known dead for this account, for `cascade models` and /why. */
|
|
253125
|
+
listDeadModels() {
|
|
253126
|
+
return this.deadModels.list();
|
|
253127
|
+
}
|
|
253128
|
+
/** Retry a model the user has since fixed, without waiting out the TTL. */
|
|
253129
|
+
forgetDeadModel(provider, modelId) {
|
|
253130
|
+
return this.deadModels.forget(provider, modelId);
|
|
253131
|
+
}
|
|
252966
253132
|
worldStateDB;
|
|
252967
253133
|
privacyPaths;
|
|
252968
253134
|
guidanceQueue;
|
|
@@ -253327,6 +253493,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
253327
253493
|
}
|
|
253328
253494
|
if (isModelNotFoundError(errMsg)) {
|
|
253329
253495
|
this.selector.removeModel(model.id);
|
|
253496
|
+
const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
|
|
253497
|
+
if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
|
|
253330
253498
|
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
253331
253499
|
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
253332
253500
|
if (next && next.id !== model.id) {
|
|
@@ -254594,6 +254762,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
254594
254762
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
254595
254763
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
254596
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
|
+
}
|
|
254597
254772
|
issues.push(`Execution error: ${errMsg}`);
|
|
254598
254773
|
const finalOutput = output || errMsg;
|
|
254599
254774
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -255615,6 +255790,26 @@ var RedactionLayer = class {
|
|
|
255615
255790
|
}
|
|
255616
255791
|
};
|
|
255617
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
|
+
|
|
255618
255813
|
// src/core/tiers/t2-manager.ts
|
|
255619
255814
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
255620
255815
|
return [
|
|
@@ -255640,6 +255835,19 @@ var T2Manager = class extends BaseTier {
|
|
|
255640
255835
|
t2PeerBus;
|
|
255641
255836
|
permissionEscalator;
|
|
255642
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;
|
|
255643
255851
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
255644
255852
|
planApprovalCallback;
|
|
255645
255853
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -255688,6 +255896,10 @@ var T2Manager = class extends BaseTier {
|
|
|
255688
255896
|
setToolCreator(creator) {
|
|
255689
255897
|
this.toolCreator = creator;
|
|
255690
255898
|
}
|
|
255899
|
+
/** Ask the user what to do when a section escalates. */
|
|
255900
|
+
setEscalationCallback(cb) {
|
|
255901
|
+
this.escalationCallback = cb;
|
|
255902
|
+
}
|
|
255691
255903
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
255692
255904
|
setPlanApprovalCallback(cb) {
|
|
255693
255905
|
this.planApprovalCallback = cb;
|
|
@@ -255792,9 +256004,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
255792
256004
|
currentAction: "Aggregating T3 results",
|
|
255793
256005
|
status: "IN_PROGRESS"
|
|
255794
256006
|
});
|
|
255795
|
-
|
|
256007
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
255796
256008
|
const issues = t3Results.filter((r4) => r4.status !== "COMPLETED").flatMap((r4) => r4.issues);
|
|
255797
|
-
|
|
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
|
+
}
|
|
255798
256058
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
255799
256059
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
255800
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";
|
|
@@ -255805,7 +256065,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
255805
256065
|
status: overallStatus,
|
|
255806
256066
|
t3Results,
|
|
255807
256067
|
sectionSummary: summary,
|
|
255808
|
-
issues
|
|
256068
|
+
issues,
|
|
256069
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
255809
256070
|
};
|
|
255810
256071
|
this.publishSectionOutput(result);
|
|
255811
256072
|
return result;
|
|
@@ -256239,8 +256500,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
256239
256500
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
256240
256501
|
);
|
|
256241
256502
|
}
|
|
256242
|
-
async aggregateResults(assignment, results) {
|
|
256243
|
-
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
|
+
);
|
|
256244
256507
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
256245
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");
|
|
256246
256509
|
const peerContext = peerOutputs ? `
|
|
@@ -256446,6 +256709,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
256446
256709
|
escalations = [];
|
|
256447
256710
|
store;
|
|
256448
256711
|
runBreaker;
|
|
256712
|
+
escalationCallback;
|
|
256449
256713
|
t2PeerBus = new PeerBus();
|
|
256450
256714
|
permissionEscalator;
|
|
256451
256715
|
toolCreator;
|
|
@@ -256460,6 +256724,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
256460
256724
|
this.toolRegistry = toolRegistry;
|
|
256461
256725
|
this.config = config2;
|
|
256462
256726
|
}
|
|
256727
|
+
/** Ask the user what to do when any section escalates. */
|
|
256728
|
+
setEscalationCallback(cb) {
|
|
256729
|
+
this.escalationCallback = cb;
|
|
256730
|
+
}
|
|
256463
256731
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
256464
256732
|
setRunBreaker(breaker) {
|
|
256465
256733
|
this.runBreaker = breaker;
|
|
@@ -256619,8 +256887,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
256619
256887
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s3) => s3.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s3) => s3.issues).join("; ")}`
|
|
256620
256888
|
};
|
|
256621
256889
|
}
|
|
256622
|
-
const sectionsText = t2Results.map((r4) =>
|
|
256623
|
-
|
|
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");
|
|
256624
256895
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
256625
256896
|
Review the following execution outputs against the original user prompt.
|
|
256626
256897
|
|
|
@@ -256630,6 +256901,7 @@ T2 Manager Summaries:
|
|
|
256630
256901
|
${sectionsText}
|
|
256631
256902
|
|
|
256632
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.
|
|
256633
256905
|
If yes, reply with exactly: "APPROVED".
|
|
256634
256906
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
256635
256907
|
try {
|
|
@@ -256827,6 +257099,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
256827
257099
|
const managers = sections.map((section) => {
|
|
256828
257100
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
256829
257101
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
257102
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
256830
257103
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
256831
257104
|
if (this.store) {
|
|
256832
257105
|
manager.setStore(this.store);
|
|
@@ -257036,7 +257309,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
257036
257309
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
257037
257310
|
const completedSections = t2Results.filter((r4) => r4.status !== "FAILED");
|
|
257038
257311
|
if (!completedSections.length) {
|
|
257039
|
-
const allIssues =
|
|
257312
|
+
const allIssues = [
|
|
257313
|
+
...t2Results.flatMap((r4) => r4.issues ?? []),
|
|
257314
|
+
...t2Results.flatMap((r4) => r4.t3Results.flatMap((t4) => t4.issues))
|
|
257315
|
+
].filter(Boolean);
|
|
257040
257316
|
const critical = allIssues.find((i4) => i4.includes("[CRITICAL_TOOL_ERROR]"));
|
|
257041
257317
|
const stalled = allIssues.find((i4) => /^Stalled:/.test(i4));
|
|
257042
257318
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -257122,9 +257398,16 @@ var CAPABILITIES = [
|
|
|
257122
257398
|
api: "openai-images"
|
|
257123
257399
|
},
|
|
257124
257400
|
{
|
|
257401
|
+
// Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
|
|
257402
|
+
// page but 404s on `:predict` ("not found for API version v1beta, or is not
|
|
257403
|
+
// supported for predict") — the Gemini image models are called through
|
|
257404
|
+
// generateContent with an image response modality, while `:predict` is the
|
|
257405
|
+
// Imagen shape this executor implements. A billing line item is not proof
|
|
257406
|
+
// of a callable endpoint, which is the assumption that put the wrong id
|
|
257407
|
+
// here in the first place.
|
|
257125
257408
|
modality: "image",
|
|
257126
257409
|
provider: "gemini",
|
|
257127
|
-
modelId: "
|
|
257410
|
+
modelId: "imagen-4.0-generate-001",
|
|
257128
257411
|
api: "gemini-predict"
|
|
257129
257412
|
},
|
|
257130
257413
|
// ── Speech (text → audio) ──
|
|
@@ -257841,6 +258124,102 @@ var RunBreaker = class {
|
|
|
257841
258124
|
|
|
257842
258125
|
// src/tools/registry.ts
|
|
257843
258126
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
258127
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
258128
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
258129
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
258130
|
+
function boundedSegment(raw) {
|
|
258131
|
+
const clean3 = sanitizeToolNameSegment(raw);
|
|
258132
|
+
if (clean3.length <= MAX_SEGMENT_LENGTH) return clean3;
|
|
258133
|
+
const hash = crypto6.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
258134
|
+
return `${clean3.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
258135
|
+
}
|
|
258136
|
+
function isStableSegment(raw) {
|
|
258137
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
258138
|
+
}
|
|
258139
|
+
function sanitizeToolNameSegment(segment) {
|
|
258140
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
258141
|
+
let start = 0;
|
|
258142
|
+
let end = cleaned.length;
|
|
258143
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
258144
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
258145
|
+
return cleaned.slice(start, end) || "_";
|
|
258146
|
+
}
|
|
258147
|
+
function mcpToolName(serverName, toolName) {
|
|
258148
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
258149
|
+
}
|
|
258150
|
+
function isMcpToolName(name) {
|
|
258151
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
258152
|
+
}
|
|
258153
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
258154
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
258155
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
258156
|
+
for (let n8 = 2; n8 < 1e3; n8++) {
|
|
258157
|
+
const candidate = `${desired} (${n8})`;
|
|
258158
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
258159
|
+
}
|
|
258160
|
+
return `${desired} (${crypto6.randomUUID().slice(0, 8)})`;
|
|
258161
|
+
}
|
|
258162
|
+
function disambiguateMcpServerNames(servers) {
|
|
258163
|
+
const seenNames = [];
|
|
258164
|
+
const renames = [];
|
|
258165
|
+
const result = servers.map((s3) => {
|
|
258166
|
+
const name = uniqueMcpServerName(s3.name, seenNames);
|
|
258167
|
+
seenNames.push(name);
|
|
258168
|
+
if (name === s3.name) return s3;
|
|
258169
|
+
renames.push({ from: s3.name, to: name });
|
|
258170
|
+
return { ...s3, name };
|
|
258171
|
+
});
|
|
258172
|
+
return { servers: renames.length ? result : servers, renames };
|
|
258173
|
+
}
|
|
258174
|
+
function assignMcpToolNames(tools) {
|
|
258175
|
+
const names = new Array(tools.length);
|
|
258176
|
+
const bases = tools.map((t4) => mcpToolName(t4.server, t4.tool));
|
|
258177
|
+
const groups = /* @__PURE__ */ new Map();
|
|
258178
|
+
bases.forEach((base, i4) => {
|
|
258179
|
+
const g2 = groups.get(base);
|
|
258180
|
+
if (g2) g2.push(i4);
|
|
258181
|
+
else groups.set(base, [i4]);
|
|
258182
|
+
});
|
|
258183
|
+
const used = new Set(bases);
|
|
258184
|
+
for (const [base, idx] of groups) {
|
|
258185
|
+
if (idx.length === 1) {
|
|
258186
|
+
names[idx[0]] = base;
|
|
258187
|
+
continue;
|
|
258188
|
+
}
|
|
258189
|
+
const raw = (i4) => `${tools[i4].server}::${tools[i4].tool}`;
|
|
258190
|
+
const isClean = (i4) => isStableSegment(tools[i4].server) && isStableSegment(tools[i4].tool);
|
|
258191
|
+
const ordered = [...idx].sort((a2, b3) => {
|
|
258192
|
+
const la = isClean(a2) ? 0 : 1;
|
|
258193
|
+
const lb = isClean(b3) ? 0 : 1;
|
|
258194
|
+
if (la !== lb) return la - lb;
|
|
258195
|
+
const ra = raw(a2);
|
|
258196
|
+
const rb = raw(b3);
|
|
258197
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
258198
|
+
});
|
|
258199
|
+
names[ordered[0]] = base;
|
|
258200
|
+
let n8 = 2;
|
|
258201
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
258202
|
+
let candidate = `${base}_${n8}`;
|
|
258203
|
+
while (used.has(candidate)) {
|
|
258204
|
+
n8++;
|
|
258205
|
+
candidate = `${base}_${n8}`;
|
|
258206
|
+
}
|
|
258207
|
+
names[ordered[rank3]] = candidate;
|
|
258208
|
+
used.add(candidate);
|
|
258209
|
+
n8++;
|
|
258210
|
+
}
|
|
258211
|
+
}
|
|
258212
|
+
return names;
|
|
258213
|
+
}
|
|
258214
|
+
function mcpServerPrefix(serverName) {
|
|
258215
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
258216
|
+
}
|
|
258217
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
258218
|
+
if (!disabledTools?.length) return disabledTools;
|
|
258219
|
+
const prefix = mcpServerPrefix(serverName);
|
|
258220
|
+
const kept = disabledTools.filter((t4) => !t4.startsWith(prefix));
|
|
258221
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
258222
|
+
}
|
|
257844
258223
|
var execAsync = util$1.promisify(child_process.exec);
|
|
257845
258224
|
var ShellTool = class extends BaseTool {
|
|
257846
258225
|
name = "shell";
|
|
@@ -286142,20 +286521,6 @@ ${text}`;
|
|
|
286142
286521
|
}
|
|
286143
286522
|
};
|
|
286144
286523
|
|
|
286145
|
-
// src/tools/tool-name.ts
|
|
286146
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
286147
|
-
function sanitizeToolNameSegment(segment) {
|
|
286148
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
286149
|
-
let start = 0;
|
|
286150
|
-
let end = cleaned.length;
|
|
286151
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
286152
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
286153
|
-
return cleaned.slice(start, end) || "_";
|
|
286154
|
-
}
|
|
286155
|
-
function mcpToolName(serverName, toolName) {
|
|
286156
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
286157
|
-
}
|
|
286158
|
-
|
|
286159
286524
|
// src/tools/mcp.ts
|
|
286160
286525
|
var McpToolWrapper = class extends BaseTool {
|
|
286161
286526
|
name;
|
|
@@ -286164,12 +286529,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
286164
286529
|
mcpClient;
|
|
286165
286530
|
serverName;
|
|
286166
286531
|
toolName;
|
|
286167
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
286532
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
286168
286533
|
super();
|
|
286169
286534
|
this.mcpClient = mcpClient;
|
|
286170
286535
|
this.serverName = serverName;
|
|
286171
286536
|
this.toolName = toolName;
|
|
286172
|
-
this.name = mcpToolName(serverName, toolName);
|
|
286537
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
286173
286538
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
286174
286539
|
this.inputSchema = inputSchema;
|
|
286175
286540
|
}
|
|
@@ -286240,22 +286605,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
286240
286605
|
getRegisteredPlugins() {
|
|
286241
286606
|
return Array.from(this.plugins.keys());
|
|
286242
286607
|
}
|
|
286243
|
-
/**
|
|
286244
|
-
|
|
286245
|
-
|
|
286246
|
-
|
|
286247
|
-
|
|
286248
|
-
|
|
286608
|
+
/**
|
|
286609
|
+
* Registers all tools from an MCP client.
|
|
286610
|
+
*
|
|
286611
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
286612
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
286613
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
286614
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
286615
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
286616
|
+
* never counted against the tool budget, and never has to be refused.
|
|
286617
|
+
*/
|
|
286618
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
286619
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
286620
|
+
const [, server, tool] = def.name.split("::");
|
|
286621
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
286622
|
+
});
|
|
286623
|
+
const names = assignMcpToolNames(parsed);
|
|
286624
|
+
parsed.forEach(({ def, server, tool }, i4) => {
|
|
286249
286625
|
const wrapper = new McpToolWrapper(
|
|
286250
286626
|
mcpClient,
|
|
286251
|
-
|
|
286252
|
-
|
|
286253
|
-
def.description.replace(`[MCP:${
|
|
286254
|
-
def.inputSchema
|
|
286627
|
+
server,
|
|
286628
|
+
tool,
|
|
286629
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
286630
|
+
def.inputSchema,
|
|
286631
|
+
names[i4]
|
|
286255
286632
|
);
|
|
286633
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
286256
286634
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
286257
286635
|
this.register(wrapper);
|
|
286258
|
-
}
|
|
286636
|
+
});
|
|
286259
286637
|
}
|
|
286260
286638
|
setIgnoredPaths(patterns) {
|
|
286261
286639
|
this.ignoreMatcher = ignore().add(patterns);
|
|
@@ -299995,6 +300373,17 @@ function fileOAuthProvider(storePath) {
|
|
|
299995
300373
|
}
|
|
299996
300374
|
});
|
|
299997
300375
|
}
|
|
300376
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
300377
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
300378
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
300379
|
+
const run = prev.then(fn, fn);
|
|
300380
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
300381
|
+
oauthStoreLocks.set(storePath, tail);
|
|
300382
|
+
void tail.then(() => {
|
|
300383
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
300384
|
+
});
|
|
300385
|
+
return run;
|
|
300386
|
+
}
|
|
299998
300387
|
function beginMcpOAuth(provider, serverUrl) {
|
|
299999
300388
|
return auth(provider, { serverUrl });
|
|
300000
300389
|
}
|
|
@@ -302945,7 +303334,50 @@ var GuidanceQueue = class {
|
|
|
302945
303334
|
}
|
|
302946
303335
|
};
|
|
302947
303336
|
|
|
303337
|
+
// src/tools/current-page.ts
|
|
303338
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
303339
|
+
var CurrentPageTool = class extends BaseTool {
|
|
303340
|
+
name = "read_current_page";
|
|
303341
|
+
description = 'Read the web page the user currently has open in the built-in browser \u2014 its URL, title, and visible text. Use this whenever the user refers to "this page", "what I am looking at", or asks about something on screen. Prefer it over web_fetch for the open page: it sees content behind a login and content rendered by JavaScript, which web_fetch cannot.';
|
|
303342
|
+
inputSchema = {
|
|
303343
|
+
type: "object",
|
|
303344
|
+
properties: {},
|
|
303345
|
+
required: []
|
|
303346
|
+
};
|
|
303347
|
+
provider;
|
|
303348
|
+
constructor(provider) {
|
|
303349
|
+
super();
|
|
303350
|
+
this.provider = provider;
|
|
303351
|
+
}
|
|
303352
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
303353
|
+
// nothing they have not already opened themselves.
|
|
303354
|
+
isDangerous() {
|
|
303355
|
+
return false;
|
|
303356
|
+
}
|
|
303357
|
+
async execute(_input, _options) {
|
|
303358
|
+
let page2;
|
|
303359
|
+
try {
|
|
303360
|
+
page2 = await this.provider();
|
|
303361
|
+
} catch (err) {
|
|
303362
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
303363
|
+
}
|
|
303364
|
+
if (!page2) {
|
|
303365
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
303366
|
+
}
|
|
303367
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
303368
|
+
|
|
303369
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
303370
|
+
return [
|
|
303371
|
+
`URL: ${page2.url}`,
|
|
303372
|
+
`Title: ${page2.title}`,
|
|
303373
|
+
"",
|
|
303374
|
+
text.trim() || "[the page rendered no readable text]"
|
|
303375
|
+
].join("\n");
|
|
303376
|
+
}
|
|
303377
|
+
};
|
|
303378
|
+
|
|
302948
303379
|
// src/core/cascade.ts
|
|
303380
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
302949
303381
|
function buildContextualPrompt(prompt, history = []) {
|
|
302950
303382
|
const recent = history.slice(-6);
|
|
302951
303383
|
if (recent.length === 0) return prompt;
|
|
@@ -303000,6 +303432,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
303000
303432
|
}
|
|
303001
303433
|
});
|
|
303002
303434
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
303435
|
+
try {
|
|
303436
|
+
const nodePath = __require("path");
|
|
303437
|
+
this.router.setDeadModelStore(new DeadModelStore(
|
|
303438
|
+
fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
|
|
303439
|
+
));
|
|
303440
|
+
} catch {
|
|
303441
|
+
}
|
|
303003
303442
|
this.multimodal = new MultimodalRegistry(
|
|
303004
303443
|
(this.config.providers ?? []).map((p3) => p3.type)
|
|
303005
303444
|
);
|
|
@@ -303161,6 +303600,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
303161
303600
|
});
|
|
303162
303601
|
});
|
|
303163
303602
|
}
|
|
303603
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
303604
|
+
//
|
|
303605
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
303606
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
303607
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
303608
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
303609
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
303610
|
+
//
|
|
303611
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
303612
|
+
// timeout because the plan is already the model's considered proposal;
|
|
303613
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
303614
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
303615
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
303616
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
303617
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
303618
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
303619
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
303620
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
303621
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
303622
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
303623
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
303624
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
303625
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
303626
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
303627
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
303628
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
303629
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
303630
|
+
const requestId = crypto6.randomUUID();
|
|
303631
|
+
return await new Promise((resolve2) => {
|
|
303632
|
+
let timeout;
|
|
303633
|
+
const settle = (decision) => {
|
|
303634
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
303635
|
+
if (timeout) clearTimeout(timeout);
|
|
303636
|
+
signal?.removeEventListener("abort", onAbort);
|
|
303637
|
+
resolve2(decision);
|
|
303638
|
+
};
|
|
303639
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
303640
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
303641
|
+
timeout = setTimeout(() => {
|
|
303642
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
303643
|
+
settle({ action: "timeout" });
|
|
303644
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
303645
|
+
this.pendingEscalations.set(requestId, settle);
|
|
303646
|
+
this.emit("escalation:decision-required", {
|
|
303647
|
+
taskId,
|
|
303648
|
+
requestId,
|
|
303649
|
+
sectionId: ctx.sectionId,
|
|
303650
|
+
sectionTitle: ctx.sectionTitle,
|
|
303651
|
+
issues: ctx.issues,
|
|
303652
|
+
summary: ctx.summary,
|
|
303653
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
303654
|
+
});
|
|
303655
|
+
});
|
|
303656
|
+
}
|
|
303657
|
+
/**
|
|
303658
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
303659
|
+
*
|
|
303660
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
303661
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
303662
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
303663
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
303664
|
+
* which is correct whenever there is exactly one.
|
|
303665
|
+
*
|
|
303666
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
303667
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
303668
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
303669
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
303670
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
303671
|
+
* see EscalationDecision.automatic.
|
|
303672
|
+
*/
|
|
303673
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
303674
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
303675
|
+
if (requestId) {
|
|
303676
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
303677
|
+
return;
|
|
303678
|
+
}
|
|
303679
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
303680
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
303681
|
+
}
|
|
303682
|
+
/**
|
|
303683
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
303684
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
303685
|
+
* same system-decided case as the early-return skips above, not a person
|
|
303686
|
+
* reviewing and accepting that section's output.
|
|
303687
|
+
*/
|
|
303688
|
+
releasePendingEscalations(action = "skip") {
|
|
303689
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
303690
|
+
}
|
|
303164
303691
|
/**
|
|
303165
303692
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
303166
303693
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -303294,6 +303821,20 @@ ${input}` }],
|
|
|
303294
303821
|
this.mediaSink = sink;
|
|
303295
303822
|
this.registerMediaTools(this.workspacePath);
|
|
303296
303823
|
}
|
|
303824
|
+
/**
|
|
303825
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
303826
|
+
*
|
|
303827
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
303828
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
303829
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
303830
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
303831
|
+
* touch the machine, and reading a page the user already opened touches
|
|
303832
|
+
* nothing. `disabledTools` still removes it.
|
|
303833
|
+
*/
|
|
303834
|
+
setCurrentPageProvider(provider) {
|
|
303835
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
303836
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
303837
|
+
}
|
|
303297
303838
|
registerMediaTools(workspacePath) {
|
|
303298
303839
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
303299
303840
|
const permitted = (name) => !denied.has(name);
|
|
@@ -303426,8 +303967,12 @@ ${last3.partialOutput}` : "");
|
|
|
303426
303967
|
for (const server of this.config.tools.mcpServers) {
|
|
303427
303968
|
try {
|
|
303428
303969
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
303429
|
-
|
|
303430
|
-
|
|
303970
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
303971
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
303972
|
+
this.toolRegistry.registerMcpTools(
|
|
303973
|
+
this.mcpClient,
|
|
303974
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
303975
|
+
);
|
|
303431
303976
|
} catch (err) {
|
|
303432
303977
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
303433
303978
|
}
|
|
@@ -303892,6 +304437,7 @@ ${prompt}` : prompt;
|
|
|
303892
304437
|
} else if (complexity === "Moderate") {
|
|
303893
304438
|
const t22 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
303894
304439
|
t22.setRunBreaker(runBreaker);
|
|
304440
|
+
t22.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
303895
304441
|
t22.setPresenter(true);
|
|
303896
304442
|
t22.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
303897
304443
|
if (identityPrompt) {
|
|
@@ -303936,11 +304482,17 @@ ${prompt}` : prompt;
|
|
|
303936
304482
|
t2Results = [t2Result];
|
|
303937
304483
|
const completed = t2Result.t3Results.filter((r4) => r4.status === "COMPLETED");
|
|
303938
304484
|
if (completed.length > 0) {
|
|
303939
|
-
finalOutput =
|
|
304485
|
+
finalOutput = composeRootSectionOutput(
|
|
304486
|
+
t2Result,
|
|
304487
|
+
completed.map((r4) => typeof r4.output === "string" ? r4.output : JSON.stringify(r4.output))
|
|
304488
|
+
);
|
|
303940
304489
|
} else {
|
|
303941
304490
|
const partial2 = t2Result.t3Results.map((r4) => typeof r4.output === "string" ? r4.output : "").filter((o3) => o3.trim()).sort((a2, b3) => b3.length - a2.length)[0];
|
|
303942
304491
|
const reasons = Array.from(
|
|
303943
|
-
new Set(
|
|
304492
|
+
new Set([
|
|
304493
|
+
...t2Result.issues ?? [],
|
|
304494
|
+
...t2Result.t3Results.flatMap((r4) => r4.issues ?? [])
|
|
304495
|
+
].filter(Boolean))
|
|
303944
304496
|
);
|
|
303945
304497
|
if (partial2) {
|
|
303946
304498
|
finalOutput = reasons.length ? `${partial2}
|
|
@@ -303953,6 +304505,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
|
|
|
303953
304505
|
} else {
|
|
303954
304506
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
303955
304507
|
t1.setRunBreaker(runBreaker);
|
|
304508
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
303956
304509
|
t1.setPresenter(true);
|
|
303957
304510
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
303958
304511
|
if (identityPrompt) {
|
|
@@ -303977,6 +304530,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
|
|
|
303977
304530
|
finalOutput = result.output;
|
|
303978
304531
|
t2Results = result.t2Results;
|
|
303979
304532
|
}
|
|
304533
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
304534
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
303980
304535
|
const trip = runBreaker.reason();
|
|
303981
304536
|
if (trip) {
|
|
303982
304537
|
finalOutput = [
|
|
@@ -304017,6 +304572,10 @@ ${finalOutput}` : ""
|
|
|
304017
304572
|
escalator.cancelAllPending();
|
|
304018
304573
|
} catch {
|
|
304019
304574
|
}
|
|
304575
|
+
try {
|
|
304576
|
+
this.releasePendingEscalations();
|
|
304577
|
+
} catch {
|
|
304578
|
+
}
|
|
304020
304579
|
this.router.restoreTierModels();
|
|
304021
304580
|
this.router.setRunSignal(void 0);
|
|
304022
304581
|
if (this.taskAnalyzer) {
|
|
@@ -305333,6 +305892,13 @@ var ConfigManager = class {
|
|
|
305333
305892
|
}
|
|
305334
305893
|
async load() {
|
|
305335
305894
|
this.config = await this.loadConfig();
|
|
305895
|
+
const servers = this.config.tools?.mcpServers;
|
|
305896
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
305897
|
+
const mcpNamesChanged = renames.length > 0;
|
|
305898
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
305899
|
+
this.config.tools.mcpServers = disambiguated;
|
|
305900
|
+
this.renameMcpServerReferences(renames);
|
|
305901
|
+
}
|
|
305336
305902
|
this.ignore = new CascadeIgnore();
|
|
305337
305903
|
await this.ignore.load(this.workspacePath);
|
|
305338
305904
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -305341,6 +305907,7 @@ var ConfigManager = class {
|
|
|
305341
305907
|
await this.injectEnvKeys();
|
|
305342
305908
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
305343
305909
|
await this.ensureDefaultIdentity();
|
|
305910
|
+
if (mcpNamesChanged) await this.save();
|
|
305344
305911
|
}
|
|
305345
305912
|
getConfig() {
|
|
305346
305913
|
return this.config;
|
|
@@ -305424,6 +305991,66 @@ var ConfigManager = class {
|
|
|
305424
305991
|
this.config.providers.push({ type: "ollama" });
|
|
305425
305992
|
}
|
|
305426
305993
|
}
|
|
305994
|
+
/**
|
|
305995
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
305996
|
+
*
|
|
305997
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
305998
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
305999
|
+
*
|
|
306000
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
306001
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
306002
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
306003
|
+
* move with it. A plain rewrite is correct here.
|
|
306004
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
306005
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
306006
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
306007
|
+
* already been updated to the disambiguated list by the time this runs
|
|
306008
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
306009
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
306010
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
306011
|
+
* AND granted to the renamed identity, not moved.
|
|
306012
|
+
*
|
|
306013
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
306014
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
306015
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
306016
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
306017
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
306018
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
306019
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
306020
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
306021
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
306022
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
306023
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
306024
|
+
* together, in the one pass that still sees the original entry.
|
|
306025
|
+
*
|
|
306026
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
306027
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
306028
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
306029
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
306030
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
306031
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
306032
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
306033
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
306034
|
+
* would do the opposite — move a denial that already worked for the
|
|
306035
|
+
* survivor onto a server it may never have meant to cover.
|
|
306036
|
+
*/
|
|
306037
|
+
renameMcpServerReferences(renames) {
|
|
306038
|
+
const tools = this.config.tools;
|
|
306039
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
306040
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s3) => s3.name));
|
|
306041
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
306042
|
+
for (const { from, to } of renames) {
|
|
306043
|
+
const tos = byFrom.get(from);
|
|
306044
|
+
if (tos) tos.push(to);
|
|
306045
|
+
else byFrom.set(from, [to]);
|
|
306046
|
+
}
|
|
306047
|
+
let trusted = tools.mcpTrusted;
|
|
306048
|
+
for (const [from, tos] of byFrom) {
|
|
306049
|
+
if (!trusted.includes(from)) continue;
|
|
306050
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n8) => n8 !== from), ...tos];
|
|
306051
|
+
}
|
|
306052
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
306053
|
+
}
|
|
305427
306054
|
async ensureDefaultIdentity() {
|
|
305428
306055
|
const existing = this.store.getDefaultIdentity();
|
|
305429
306056
|
if (existing) return;
|
|
@@ -306327,6 +306954,14 @@ var DashboardSocket = class {
|
|
|
306327
306954
|
this.secret = options.secret;
|
|
306328
306955
|
this.setupHandlers();
|
|
306329
306956
|
}
|
|
306957
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
306958
|
+
roomSize(room) {
|
|
306959
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
306960
|
+
}
|
|
306961
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
306962
|
+
hasSocket(socketId) {
|
|
306963
|
+
return this.io.sockets.sockets.has(socketId);
|
|
306964
|
+
}
|
|
306330
306965
|
broadcastToRoom(room, event, data) {
|
|
306331
306966
|
this.io.to(room).emit(event, data);
|
|
306332
306967
|
}
|
|
@@ -306398,6 +307033,22 @@ var DashboardSocket = class {
|
|
|
306398
307033
|
});
|
|
306399
307034
|
});
|
|
306400
307035
|
}
|
|
307036
|
+
/**
|
|
307037
|
+
* A client subscribed to a session's room.
|
|
307038
|
+
*
|
|
307039
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
307040
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
307041
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
307042
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
307043
|
+
*/
|
|
307044
|
+
onJoinSession(callback) {
|
|
307045
|
+
this.io.on("connection", (socket) => {
|
|
307046
|
+
socket.on("join:session", (payload) => {
|
|
307047
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
307048
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
307049
|
+
});
|
|
307050
|
+
});
|
|
307051
|
+
}
|
|
306401
307052
|
onSessionRate(callback) {
|
|
306402
307053
|
this.io.on("connection", (socket) => {
|
|
306403
307054
|
socket.on("session:rate", (payload) => {
|
|
@@ -306435,6 +307086,31 @@ var DashboardSocket = class {
|
|
|
306435
307086
|
});
|
|
306436
307087
|
});
|
|
306437
307088
|
}
|
|
307089
|
+
/**
|
|
307090
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
307091
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
307092
|
+
* answers here.
|
|
307093
|
+
*
|
|
307094
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
307095
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
307096
|
+
*/
|
|
307097
|
+
onEscalationDecision(callback) {
|
|
307098
|
+
this.io.on("connection", (socket) => {
|
|
307099
|
+
socket.on("escalation:decide", (payload) => {
|
|
307100
|
+
const action = payload?.action;
|
|
307101
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
307102
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
307103
|
+
callback({
|
|
307104
|
+
sessionId: payload.sessionId,
|
|
307105
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
307106
|
+
// run concurrently, so more than one can be waiting.
|
|
307107
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
307108
|
+
action,
|
|
307109
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
307110
|
+
});
|
|
307111
|
+
});
|
|
307112
|
+
});
|
|
307113
|
+
}
|
|
306438
307114
|
onSessionSteer(callback) {
|
|
306439
307115
|
this.io.on("connection", (socket) => {
|
|
306440
307116
|
socket.on("session:steer", (payload) => {
|
|
@@ -306596,6 +307272,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
306596
307272
|
|
|
306597
307273
|
// src/dashboard/server.ts
|
|
306598
307274
|
var __dirname2 = path26__namespace.default.dirname(Url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('desktop-core.cjs', document.baseURI).href))));
|
|
307275
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
306599
307276
|
var DashboardServer = class {
|
|
306600
307277
|
app;
|
|
306601
307278
|
httpServer;
|
|
@@ -306606,6 +307283,13 @@ var DashboardServer = class {
|
|
|
306606
307283
|
globalStore = null;
|
|
306607
307284
|
broadcastTimer = null;
|
|
306608
307285
|
activeSessions = /* @__PURE__ */ new Map();
|
|
307286
|
+
/**
|
|
307287
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
307288
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
307289
|
+
* browser — so the tool is simply not registered rather than present and
|
|
307290
|
+
* always failing.
|
|
307291
|
+
*/
|
|
307292
|
+
currentPageProvider;
|
|
306609
307293
|
activeControllers = /* @__PURE__ */ new Map();
|
|
306610
307294
|
/**
|
|
306611
307295
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -306620,6 +307304,17 @@ var DashboardServer = class {
|
|
|
306620
307304
|
* map is how that answer reaches the run that's blocked on it.
|
|
306621
307305
|
*/
|
|
306622
307306
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
307307
|
+
/**
|
|
307308
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
307309
|
+
* delivered to a client that was not reachable when it was raised.
|
|
307310
|
+
*
|
|
307311
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
307312
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
307313
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
307314
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
307315
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
307316
|
+
*/
|
|
307317
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
306623
307318
|
/**
|
|
306624
307319
|
* The orchestration decision trail ("why") of each session's most recent
|
|
306625
307320
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -306697,20 +307392,29 @@ var DashboardServer = class {
|
|
|
306697
307392
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
306698
307393
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
306699
307394
|
this.activeSessions.set(sessionId, cascade);
|
|
307395
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
306700
307396
|
cascade.on("stream:token", (e3) => {
|
|
306701
|
-
this.
|
|
307397
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
306702
307398
|
});
|
|
306703
307399
|
cascade.on("tier:status", (e3) => {
|
|
306704
|
-
this.
|
|
307400
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e3 });
|
|
306705
307401
|
});
|
|
306706
307402
|
cascade.on("permission:user-required", (e3) => {
|
|
306707
|
-
this.
|
|
307403
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e3 });
|
|
306708
307404
|
});
|
|
306709
307405
|
cascade.on("peer:message", (e3) => {
|
|
306710
307406
|
this.socket.emitPeerMessage(e3);
|
|
306711
307407
|
});
|
|
306712
307408
|
cascade.on("plan:approval-required", (e3) => {
|
|
306713
|
-
this.
|
|
307409
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e3 });
|
|
307410
|
+
});
|
|
307411
|
+
cascade.on("escalation:decision-required", (e3) => {
|
|
307412
|
+
this.raiseEscalation(sessionId, socketId, e3);
|
|
307413
|
+
});
|
|
307414
|
+
cascade.on("escalation:timeout", (e3) => {
|
|
307415
|
+
const ev = e3;
|
|
307416
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
307417
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e3 });
|
|
306714
307418
|
});
|
|
306715
307419
|
try {
|
|
306716
307420
|
const result = await cascade.run({
|
|
@@ -306721,7 +307425,7 @@ var DashboardServer = class {
|
|
|
306721
307425
|
this.recordSessionTask(sessionId, result.taskId);
|
|
306722
307426
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
306723
307427
|
this.captureWhy(sessionId, cascade, result);
|
|
306724
|
-
this.
|
|
307428
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
306725
307429
|
this.socket.broadcast("cost:update", {
|
|
306726
307430
|
sessionId,
|
|
306727
307431
|
totalTokens: result.usage.totalTokens,
|
|
@@ -306734,7 +307438,7 @@ var DashboardServer = class {
|
|
|
306734
307438
|
} catch (err) {
|
|
306735
307439
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
306736
307440
|
this.captureWhy(sessionId, cascade);
|
|
306737
|
-
this.
|
|
307441
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
306738
307442
|
sessionId,
|
|
306739
307443
|
error: err instanceof Error ? err.message : String(err)
|
|
306740
307444
|
});
|
|
@@ -306742,6 +307446,7 @@ var DashboardServer = class {
|
|
|
306742
307446
|
this.activeSessions.delete(sessionId);
|
|
306743
307447
|
this.activeControllers.delete(sessionId);
|
|
306744
307448
|
this.denyPendingApprovals(sessionId);
|
|
307449
|
+
this.clearSessionEscalations(sessionId);
|
|
306745
307450
|
}
|
|
306746
307451
|
});
|
|
306747
307452
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -306751,10 +307456,20 @@ var DashboardServer = class {
|
|
|
306751
307456
|
editedPlan
|
|
306752
307457
|
);
|
|
306753
307458
|
});
|
|
307459
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
307460
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
307461
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
307462
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
307463
|
+
});
|
|
307464
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
307465
|
+
this.replayEscalations(sessionId, socketId);
|
|
307466
|
+
});
|
|
306754
307467
|
this.socket.onSessionHalt((sessionId) => {
|
|
306755
307468
|
this.activeControllers.get(sessionId)?.abort();
|
|
306756
307469
|
this.denyPendingApprovals(sessionId);
|
|
306757
307470
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
307471
|
+
this.clearSessionEscalations(sessionId);
|
|
307472
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
306758
307473
|
});
|
|
306759
307474
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
306760
307475
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -306769,6 +307484,10 @@ var DashboardServer = class {
|
|
|
306769
307484
|
}
|
|
306770
307485
|
});
|
|
306771
307486
|
}
|
|
307487
|
+
/** Wire the host's browser view in (desktop only). */
|
|
307488
|
+
setCurrentPageProvider(provider) {
|
|
307489
|
+
this.currentPageProvider = provider;
|
|
307490
|
+
}
|
|
306772
307491
|
async start() {
|
|
306773
307492
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
306774
307493
|
if (!isLoopback) {
|
|
@@ -307057,6 +307776,116 @@ var DashboardServer = class {
|
|
|
307057
307776
|
this.activeSessions.delete(sessionId);
|
|
307058
307777
|
}
|
|
307059
307778
|
}
|
|
307779
|
+
/**
|
|
307780
|
+
* Is anyone able to answer for this session right now?
|
|
307781
|
+
*
|
|
307782
|
+
* The originating socket first — that connection is definitely watching this
|
|
307783
|
+
* run — then the session room, which is where a client lands after a
|
|
307784
|
+
* reconnect issues it a new socket id.
|
|
307785
|
+
*/
|
|
307786
|
+
hasResponder(sessionId, socketId) {
|
|
307787
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
307788
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
307789
|
+
}
|
|
307790
|
+
/**
|
|
307791
|
+
* Send a run event to whoever is watching this session.
|
|
307792
|
+
*
|
|
307793
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
307794
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
307795
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
307796
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
307797
|
+
* client that is both the originator and a room member from seeing every
|
|
307798
|
+
* token twice.
|
|
307799
|
+
*/
|
|
307800
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
307801
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
307802
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
307803
|
+
}
|
|
307804
|
+
/**
|
|
307805
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
307806
|
+
*
|
|
307807
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
307808
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
307809
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
307810
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
307811
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
307812
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
307813
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
307814
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
307815
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
307816
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
307817
|
+
* mistaken for an absent user.
|
|
307818
|
+
*/
|
|
307819
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
307820
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto6.randomUUID();
|
|
307821
|
+
const payload = { sessionId, ...event };
|
|
307822
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
307823
|
+
this.pendingEscalations.set(requestId, entry);
|
|
307824
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
307825
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
307826
|
+
return;
|
|
307827
|
+
}
|
|
307828
|
+
entry.graceTimer = setTimeout(() => {
|
|
307829
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
307830
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
307831
|
+
this.pendingEscalations.delete(requestId);
|
|
307832
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
307833
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
307834
|
+
}
|
|
307835
|
+
/**
|
|
307836
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
307837
|
+
*
|
|
307838
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
307839
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
307840
|
+
* its gates and the clients queue theirs.
|
|
307841
|
+
*
|
|
307842
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
307843
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
307844
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
307845
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
307846
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
307847
|
+
* sides never have to agree on a wall clock.
|
|
307848
|
+
*/
|
|
307849
|
+
replayEscalations(sessionId, socketId) {
|
|
307850
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
307851
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307852
|
+
if (entry.graceTimer) {
|
|
307853
|
+
clearTimeout(entry.graceTimer);
|
|
307854
|
+
entry.graceTimer = void 0;
|
|
307855
|
+
}
|
|
307856
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
307857
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
307858
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
307859
|
+
}
|
|
307860
|
+
}
|
|
307861
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
307862
|
+
clearPendingEscalation(requestId) {
|
|
307863
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
307864
|
+
if (!entry) return;
|
|
307865
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307866
|
+
this.pendingEscalations.delete(requestId);
|
|
307867
|
+
}
|
|
307868
|
+
/**
|
|
307869
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
307870
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
307871
|
+
* Insertion order makes `values()` oldest-first.
|
|
307872
|
+
*/
|
|
307873
|
+
clearOldestSessionEscalation(sessionId) {
|
|
307874
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
307875
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307876
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307877
|
+
this.pendingEscalations.delete(id);
|
|
307878
|
+
return;
|
|
307879
|
+
}
|
|
307880
|
+
}
|
|
307881
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
307882
|
+
clearSessionEscalations(sessionId) {
|
|
307883
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
307884
|
+
if (entry.sessionId !== sessionId) continue;
|
|
307885
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
307886
|
+
this.pendingEscalations.delete(id);
|
|
307887
|
+
}
|
|
307888
|
+
}
|
|
307060
307889
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
307061
307890
|
denyPendingApprovals(sessionId) {
|
|
307062
307891
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -307557,6 +308386,7 @@ ${prompt}`;
|
|
|
307557
308386
|
void (async () => {
|
|
307558
308387
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
307559
308388
|
this.activeSessions.set(sessionId, cascade);
|
|
308389
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
307560
308390
|
cascade.on("stream:token", (e3) => {
|
|
307561
308391
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e3.tierId, text: e3.text, primary: e3.primary });
|
|
307562
308392
|
});
|
|
@@ -307572,6 +308402,14 @@ ${prompt}`;
|
|
|
307572
308402
|
cascade.on("plan:approval-required", (e3) => {
|
|
307573
308403
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e3 });
|
|
307574
308404
|
});
|
|
308405
|
+
cascade.on("escalation:decision-required", (e3) => {
|
|
308406
|
+
this.raiseEscalation(sessionId, void 0, e3);
|
|
308407
|
+
});
|
|
308408
|
+
cascade.on("escalation:timeout", (e3) => {
|
|
308409
|
+
const ev = e3;
|
|
308410
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
308411
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e3 });
|
|
308412
|
+
});
|
|
307575
308413
|
try {
|
|
307576
308414
|
const result = await cascade.run({
|
|
307577
308415
|
prompt: runPrompt,
|
|
@@ -307599,6 +308437,7 @@ ${prompt}`;
|
|
|
307599
308437
|
} finally {
|
|
307600
308438
|
this.activeSessions.delete(sessionId);
|
|
307601
308439
|
this.denyPendingApprovals(sessionId);
|
|
308440
|
+
this.clearSessionEscalations(sessionId);
|
|
307602
308441
|
}
|
|
307603
308442
|
})();
|
|
307604
308443
|
});
|
|
@@ -307931,6 +308770,41 @@ var HooksRunner = class {
|
|
|
307931
308770
|
}
|
|
307932
308771
|
};
|
|
307933
308772
|
|
|
308773
|
+
// src/mcp/discover.ts
|
|
308774
|
+
async function discoverMcpTools(servers) {
|
|
308775
|
+
const results = [];
|
|
308776
|
+
for (const server of servers) {
|
|
308777
|
+
const client = new McpClient({ trustedServers: [server.name] });
|
|
308778
|
+
try {
|
|
308779
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
308780
|
+
const connect = () => client.connect(server, authProvider ? { authProvider } : {});
|
|
308781
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
308782
|
+
const parsed = client.getToolDefinitions().flatMap((def) => {
|
|
308783
|
+
const [, serverName, toolName] = def.name.split("::");
|
|
308784
|
+
return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
|
|
308785
|
+
});
|
|
308786
|
+
const names = assignMcpToolNames(parsed);
|
|
308787
|
+
const tools = parsed.map(({ def, server: s3, tool }, i4) => ({
|
|
308788
|
+
server: s3,
|
|
308789
|
+
tool,
|
|
308790
|
+
name: names[i4],
|
|
308791
|
+
description: def.description.replace(`[MCP:${s3}] `, "")
|
|
308792
|
+
}));
|
|
308793
|
+
results.push({ server: server.name, tools });
|
|
308794
|
+
} catch (err) {
|
|
308795
|
+
results.push({
|
|
308796
|
+
server: server.name,
|
|
308797
|
+
tools: [],
|
|
308798
|
+
error: err instanceof Error ? err.message : String(err)
|
|
308799
|
+
});
|
|
308800
|
+
} finally {
|
|
308801
|
+
await client.disconnectAll().catch(() => {
|
|
308802
|
+
});
|
|
308803
|
+
}
|
|
308804
|
+
}
|
|
308805
|
+
return results;
|
|
308806
|
+
}
|
|
308807
|
+
|
|
307934
308808
|
// src/retrieval/chunk.ts
|
|
307935
308809
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
307936
308810
|
function splitLongBlock(block, max) {
|
|
@@ -308469,11 +309343,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
308469
309343
|
for (const i4 of incoming) map.set(key(i4), i4);
|
|
308470
309344
|
return [...map.values()];
|
|
308471
309345
|
}
|
|
309346
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
309347
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
309348
|
+
let n8 = 2;
|
|
309349
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n8}`))) n8++;
|
|
309350
|
+
return `${desired} ${n8}`;
|
|
309351
|
+
}
|
|
309352
|
+
function mergeMcpServers(local, incoming) {
|
|
309353
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
309354
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
309355
|
+
const byName = /* @__PURE__ */ new Map();
|
|
309356
|
+
for (const l2 of local ?? []) byName.set(l2.name, l2);
|
|
309357
|
+
const prefixes = new Set([...byName.values()].map((m3) => mcpServerPrefix(m3.name)));
|
|
309358
|
+
const synced = [];
|
|
309359
|
+
for (const inc of incoming) {
|
|
309360
|
+
if (byName.has(inc.name)) {
|
|
309361
|
+
byName.set(inc.name, inc);
|
|
309362
|
+
synced.push(inc);
|
|
309363
|
+
continue;
|
|
309364
|
+
}
|
|
309365
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
309366
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
309367
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
309368
|
+
byName.set(name, server);
|
|
309369
|
+
synced.push(server);
|
|
309370
|
+
prefixes.add(mcpServerPrefix(name));
|
|
309371
|
+
}
|
|
309372
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
309373
|
+
}
|
|
309374
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
309375
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
309376
|
+
return tools.map((t4) => {
|
|
309377
|
+
for (const [from, to] of rewrite) if (t4.startsWith(from)) return to + t4.slice(from.length);
|
|
309378
|
+
return t4;
|
|
309379
|
+
});
|
|
309380
|
+
}
|
|
309381
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
309382
|
+
const prefixes = (syncedServers ?? []).map((m3) => mcpServerPrefix(m3.name));
|
|
309383
|
+
const covered = (tool) => prefixes.some((p3) => tool.startsWith(p3));
|
|
309384
|
+
const kept = (local ?? []).filter((t4) => !covered(t4));
|
|
309385
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
309386
|
+
return merged.length ? merged : void 0;
|
|
309387
|
+
}
|
|
308472
309388
|
function gatherSyncBundle(config2) {
|
|
308473
|
-
const bundle = { v:
|
|
309389
|
+
const bundle = { v: 2 };
|
|
308474
309390
|
if (config2.providers?.length) bundle.providers = config2.providers;
|
|
308475
309391
|
if (config2.tools?.webSearch) bundle.webSearch = config2.tools.webSearch;
|
|
308476
309392
|
if (config2.tools?.mcpServers?.length) bundle.mcpServers = config2.tools.mcpServers;
|
|
309393
|
+
const syncedDisabledTools = config2.tools?.disabledTools?.filter(isMcpToolName);
|
|
309394
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
308477
309395
|
if (config2.models) bundle.models = config2.models;
|
|
308478
309396
|
if (config2.budget) bundle.budget = config2.budget;
|
|
308479
309397
|
const prefs = {};
|
|
@@ -308488,7 +309406,16 @@ function applySyncBundle(bundle, config2) {
|
|
|
308488
309406
|
const next = { ...config2, tools: { ...config2.tools } };
|
|
308489
309407
|
if (bundle.providers) next.providers = mergeByKey(config2.providers, bundle.providers, providerSig) ?? [];
|
|
308490
309408
|
if (bundle.webSearch) next.tools.webSearch = { ...config2.tools?.webSearch, ...bundle.webSearch };
|
|
308491
|
-
|
|
309409
|
+
const mcp = mergeMcpServers(config2.tools?.mcpServers, bundle.mcpServers);
|
|
309410
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
|
|
309411
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
309412
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
309413
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
309414
|
+
config2.tools?.disabledTools,
|
|
309415
|
+
rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
|
|
309416
|
+
bundle.mcpServers ? mcp.synced : void 0
|
|
309417
|
+
);
|
|
309418
|
+
}
|
|
308492
309419
|
if (bundle.models) next.models = { ...config2.models, ...bundle.models };
|
|
308493
309420
|
if (bundle.budget) next.budget = { ...config2.budget, ...bundle.budget };
|
|
308494
309421
|
if (bundle.prefs) {
|
|
@@ -308923,6 +309850,7 @@ exports.CascadeToolError = CascadeToolError;
|
|
|
308923
309850
|
exports.CloudClient = CloudClient;
|
|
308924
309851
|
exports.CodeSearchTool = CodeSearchTool;
|
|
308925
309852
|
exports.ConfigManager = ConfigManager;
|
|
309853
|
+
exports.CurrentPageTool = CurrentPageTool;
|
|
308926
309854
|
exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
|
|
308927
309855
|
exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;
|
|
308928
309856
|
exports.DEFAULT_AUTO_SUMMARIZE_AT = DEFAULT_AUTO_SUMMARIZE_AT;
|
|
@@ -308945,6 +309873,8 @@ exports.HooksRunner = HooksRunner;
|
|
|
308945
309873
|
exports.Keystore = Keystore;
|
|
308946
309874
|
exports.LLMReranker = LLMReranker;
|
|
308947
309875
|
exports.LM_STUDIO_BASE_URL = LM_STUDIO_BASE_URL;
|
|
309876
|
+
exports.MAX_PAGE_TEXT_CHARS = MAX_PAGE_TEXT_CHARS;
|
|
309877
|
+
exports.MCP_TOOL_PREFIX = MCP_TOOL_PREFIX;
|
|
308948
309878
|
exports.MODELS = MODELS;
|
|
308949
309879
|
exports.McpClient = McpClient;
|
|
308950
309880
|
exports.McpOAuthProvider = McpOAuthProvider;
|
|
@@ -308981,7 +309911,9 @@ exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
|
|
|
308981
309911
|
exports.createCascade = createCascade;
|
|
308982
309912
|
exports.decryptSyncBlob = decryptJSON;
|
|
308983
309913
|
exports.diffManifest = diffManifest;
|
|
309914
|
+
exports.disambiguateMcpServerNames = disambiguateMcpServerNames;
|
|
308984
309915
|
exports.discoverMcpAuthServer = discoverMcpAuthServer;
|
|
309916
|
+
exports.discoverMcpTools = discoverMcpTools;
|
|
308985
309917
|
exports.distillSessionFacts = distillSessionFacts;
|
|
308986
309918
|
exports.embedderFromProviders = embedderFromProviders;
|
|
308987
309919
|
exports.encryptSyncBlob = encryptJSON;
|
|
@@ -308990,12 +309922,17 @@ exports.gatherSyncBundle = gatherSyncBundle;
|
|
|
308990
309922
|
exports.hashContent = hashContent;
|
|
308991
309923
|
exports.heuristicCodeChunker = heuristicCodeChunker;
|
|
308992
309924
|
exports.inferAzureBaseModel = inferAzureBaseModel;
|
|
309925
|
+
exports.isMcpToolName = isMcpToolName;
|
|
309926
|
+
exports.mcpServerPrefix = mcpServerPrefix;
|
|
309927
|
+
exports.mcpToolName = mcpToolName;
|
|
308993
309928
|
exports.nodeHttpFetch = nodeHttpFetch;
|
|
308994
309929
|
exports.parseRankOrder = parseRankOrder;
|
|
308995
309930
|
exports.planRetrieval = planRetrieval;
|
|
308996
309931
|
exports.preferIpv4Host = preferIpv4Host;
|
|
308997
309932
|
exports.reciprocalRankFusion = reciprocalRankFusion;
|
|
308998
309933
|
exports.refreshMcpToken = refreshMcpToken;
|
|
309934
|
+
exports.removeMcpServerDenials = removeMcpServerDenials;
|
|
308999
309935
|
exports.runCascade = runCascade;
|
|
309000
309936
|
exports.sessionWorthRemembering = sessionWorthRemembering;
|
|
309001
309937
|
exports.streamCascade = streamCascade;
|
|
309938
|
+
exports.uniqueMcpServerName = uniqueMcpServerName;
|