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/index.cjs
CHANGED
|
@@ -87,6 +87,12 @@ var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
|
87
87
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
88
88
|
var __defProp = Object.defineProperty;
|
|
89
89
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
90
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
91
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
92
|
+
}) : x)(function(x) {
|
|
93
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
94
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
95
|
+
});
|
|
90
96
|
var __esm = (fn, res) => function __init() {
|
|
91
97
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
92
98
|
};
|
|
@@ -1891,7 +1897,7 @@ function reconcilePrice(model, live) {
|
|
|
1891
1897
|
}
|
|
1892
1898
|
|
|
1893
1899
|
// src/constants.ts
|
|
1894
|
-
var CASCADE_VERSION = "0.
|
|
1900
|
+
var CASCADE_VERSION = "0.55.0";
|
|
1895
1901
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1896
1902
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1897
1903
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -3704,7 +3710,21 @@ var ModelSelector = class {
|
|
|
3704
3710
|
this.availableProviders = availableProviders;
|
|
3705
3711
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
3706
3712
|
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Verdicts from earlier runs about ids that 404 on this account. Consulted
|
|
3715
|
+
* BEFORE a model enters the pool, which is the only placement that helps: a
|
|
3716
|
+
* T3 wave runs concurrently, so filtering after the first failure still lets
|
|
3717
|
+
* every other worker in the wave call the dead id.
|
|
3718
|
+
*/
|
|
3719
|
+
deadModels;
|
|
3720
|
+
setDeadModelStore(store) {
|
|
3721
|
+
this.deadModels = store;
|
|
3722
|
+
for (const [id, m] of [...this.availableModels]) {
|
|
3723
|
+
if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3707
3726
|
addDynamicModel(model) {
|
|
3727
|
+
if (this.deadModels?.isDead(model.provider, model.id)) return;
|
|
3708
3728
|
this.availableModels.set(model.id, model);
|
|
3709
3729
|
if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
|
|
3710
3730
|
}
|
|
@@ -4111,6 +4131,121 @@ var LocalRequestQueue = class {
|
|
|
4111
4131
|
}
|
|
4112
4132
|
};
|
|
4113
4133
|
|
|
4134
|
+
// src/core/router/dead-models.ts
|
|
4135
|
+
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4136
|
+
function deadModelKey(provider, modelId) {
|
|
4137
|
+
return `${provider}:${modelId}`;
|
|
4138
|
+
}
|
|
4139
|
+
var DeadModelStore = class {
|
|
4140
|
+
constructor(persistence, opts = {}) {
|
|
4141
|
+
this.persistence = persistence;
|
|
4142
|
+
this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
|
|
4143
|
+
this.now = opts.now ?? (() => Date.now());
|
|
4144
|
+
this.restore();
|
|
4145
|
+
}
|
|
4146
|
+
persistence;
|
|
4147
|
+
records = /* @__PURE__ */ new Map();
|
|
4148
|
+
ttlMs;
|
|
4149
|
+
now;
|
|
4150
|
+
restore() {
|
|
4151
|
+
if (!this.persistence) return;
|
|
4152
|
+
try {
|
|
4153
|
+
for (const r of this.persistence.load()) {
|
|
4154
|
+
if (!this.isExpired(r)) this.records.set(r.key, r);
|
|
4155
|
+
}
|
|
4156
|
+
} catch {
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
isExpired(r) {
|
|
4160
|
+
return this.now() - r.lastSeenAt > this.ttlMs;
|
|
4161
|
+
}
|
|
4162
|
+
flush() {
|
|
4163
|
+
if (!this.persistence) return;
|
|
4164
|
+
try {
|
|
4165
|
+
this.persistence.save([...this.records.values()]);
|
|
4166
|
+
} catch {
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Record that a model was found dead. Returns true when this is the first
|
|
4171
|
+
* time (so the caller can log it once rather than once per concurrent
|
|
4172
|
+
* worker in the wave that discovered it).
|
|
4173
|
+
*/
|
|
4174
|
+
record(provider, modelId, reason) {
|
|
4175
|
+
const key = deadModelKey(provider, modelId);
|
|
4176
|
+
const now = this.now();
|
|
4177
|
+
const existing = this.records.get(key);
|
|
4178
|
+
if (existing && !this.isExpired(existing)) {
|
|
4179
|
+
existing.lastSeenAt = now;
|
|
4180
|
+
existing.hits += 1;
|
|
4181
|
+
this.flush();
|
|
4182
|
+
return false;
|
|
4183
|
+
}
|
|
4184
|
+
this.records.set(key, {
|
|
4185
|
+
key,
|
|
4186
|
+
provider,
|
|
4187
|
+
modelId,
|
|
4188
|
+
reason,
|
|
4189
|
+
firstSeenAt: now,
|
|
4190
|
+
lastSeenAt: now,
|
|
4191
|
+
hits: 1
|
|
4192
|
+
});
|
|
4193
|
+
this.flush();
|
|
4194
|
+
return true;
|
|
4195
|
+
}
|
|
4196
|
+
/** True when this model is known dead and the verdict has not expired. */
|
|
4197
|
+
isDead(provider, modelId) {
|
|
4198
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
4199
|
+
if (!r) return false;
|
|
4200
|
+
if (this.isExpired(r)) {
|
|
4201
|
+
this.records.delete(r.key);
|
|
4202
|
+
this.flush();
|
|
4203
|
+
return false;
|
|
4204
|
+
}
|
|
4205
|
+
return true;
|
|
4206
|
+
}
|
|
4207
|
+
/** Why a model is considered dead, for /why and the models command. */
|
|
4208
|
+
reasonFor(provider, modelId) {
|
|
4209
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
4210
|
+
return r && !this.isExpired(r) ? r.reason : null;
|
|
4211
|
+
}
|
|
4212
|
+
/** Live records, expired ones filtered out. */
|
|
4213
|
+
list() {
|
|
4214
|
+
const live = [...this.records.values()].filter((r) => !this.isExpired(r));
|
|
4215
|
+
return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Forget a verdict — used when the user fixes the underlying problem (adds
|
|
4219
|
+
* the model to their project, gets quota) and wants it retried now rather
|
|
4220
|
+
* than after the TTL.
|
|
4221
|
+
*/
|
|
4222
|
+
forget(provider, modelId) {
|
|
4223
|
+
const removed = this.records.delete(deadModelKey(provider, modelId));
|
|
4224
|
+
if (removed) this.flush();
|
|
4225
|
+
return removed;
|
|
4226
|
+
}
|
|
4227
|
+
clear() {
|
|
4228
|
+
this.records.clear();
|
|
4229
|
+
this.flush();
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
function fileDeadModelPersistence(filePath) {
|
|
4233
|
+
return {
|
|
4234
|
+
load() {
|
|
4235
|
+
const fs25 = __require("fs");
|
|
4236
|
+
if (!fs25.existsSync(filePath)) return [];
|
|
4237
|
+
const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
4238
|
+
return Array.isArray(raw) ? raw : [];
|
|
4239
|
+
},
|
|
4240
|
+
save(records) {
|
|
4241
|
+
const fs25 = __require("fs");
|
|
4242
|
+
const path26 = __require("path");
|
|
4243
|
+
fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
|
|
4244
|
+
fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4245
|
+
}
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4114
4249
|
// src/utils/retry.ts
|
|
4115
4250
|
var CascadeCancelledError = class extends Error {
|
|
4116
4251
|
constructor(reason) {
|
|
@@ -4700,10 +4835,47 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4700
4835
|
*/
|
|
4701
4836
|
budgetState = "ok";
|
|
4702
4837
|
budgetExceededReason;
|
|
4838
|
+
/**
|
|
4839
|
+
* Whether the run/session budget is permanently tripped, and why.
|
|
4840
|
+
*
|
|
4841
|
+
* A tier or worker that catches `BudgetExceededError` internally (T3Worker
|
|
4842
|
+
* does, so one dead-end subtask doesn't crash the whole run) absorbs the
|
|
4843
|
+
* exception into a normal FAILED/PARTIAL result — the tier returns as if
|
|
4844
|
+
* nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
|
|
4845
|
+
* budget-exceeded handling (the `run:budget-exceeded` event and
|
|
4846
|
+
* `lastInterruptedRun` for `/continue`). This lets a caller check the
|
|
4847
|
+
* router's own state directly after a tier returns, regardless of how that
|
|
4848
|
+
* tier chose to report the error internally.
|
|
4849
|
+
*/
|
|
4850
|
+
budgetExceededInfo() {
|
|
4851
|
+
if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
|
|
4852
|
+
if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
|
|
4853
|
+
return null;
|
|
4854
|
+
}
|
|
4703
4855
|
tpmLimiter;
|
|
4704
4856
|
localQueue;
|
|
4705
4857
|
taskAnalyzer;
|
|
4706
4858
|
pendingFeedbackSource;
|
|
4859
|
+
/** Models known dead for this account (404 / retired / not enabled). */
|
|
4860
|
+
deadModels = new DeadModelStore();
|
|
4861
|
+
/**
|
|
4862
|
+
* Back the dead-model memory with durable storage (a JSON file on desktop, a
|
|
4863
|
+
* per-user table in the cloud). Without one the verdicts still work, but only
|
|
4864
|
+
* for the life of the process — so tomorrow's run pays the same wave of 404s
|
|
4865
|
+
* to learn the same fact.
|
|
4866
|
+
*/
|
|
4867
|
+
setDeadModelStore(store) {
|
|
4868
|
+
this.deadModels = store;
|
|
4869
|
+
this.selector.setDeadModelStore(store);
|
|
4870
|
+
}
|
|
4871
|
+
/** Models known dead for this account, for `cascade models` and /why. */
|
|
4872
|
+
listDeadModels() {
|
|
4873
|
+
return this.deadModels.list();
|
|
4874
|
+
}
|
|
4875
|
+
/** Retry a model the user has since fixed, without waiting out the TTL. */
|
|
4876
|
+
forgetDeadModel(provider, modelId) {
|
|
4877
|
+
return this.deadModels.forget(provider, modelId);
|
|
4878
|
+
}
|
|
4707
4879
|
worldStateDB;
|
|
4708
4880
|
privacyPaths;
|
|
4709
4881
|
guidanceQueue;
|
|
@@ -5068,6 +5240,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5068
5240
|
}
|
|
5069
5241
|
if (isModelNotFoundError(errMsg)) {
|
|
5070
5242
|
this.selector.removeModel(model.id);
|
|
5243
|
+
const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
|
|
5244
|
+
if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
|
|
5071
5245
|
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
5072
5246
|
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
5073
5247
|
if (next && next.id !== model.id) {
|
|
@@ -6335,6 +6509,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
6335
6509
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
6336
6510
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6337
6511
|
}
|
|
6512
|
+
if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
6513
|
+
issues.push(errMsg);
|
|
6514
|
+
const stopped = output || errMsg;
|
|
6515
|
+
this.setStatus("FAILED", stopped);
|
|
6516
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
|
|
6517
|
+
return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6518
|
+
}
|
|
6338
6519
|
issues.push(`Execution error: ${errMsg}`);
|
|
6339
6520
|
const finalOutput = output || errMsg;
|
|
6340
6521
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -7356,6 +7537,26 @@ var RedactionLayer = class {
|
|
|
7356
7537
|
}
|
|
7357
7538
|
};
|
|
7358
7539
|
|
|
7540
|
+
// src/core/tiers/escalation-policy.ts
|
|
7541
|
+
function sectionNeedsDecision(results) {
|
|
7542
|
+
return results.some((r) => r.status === "ESCALATED");
|
|
7543
|
+
}
|
|
7544
|
+
function settledEscalationStatus(results) {
|
|
7545
|
+
const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
|
|
7546
|
+
return hasWork ? "PARTIAL" : "FAILED";
|
|
7547
|
+
}
|
|
7548
|
+
function composeRootSectionOutput(t2Result, completedOutputs) {
|
|
7549
|
+
let output = `${t2Result.sectionSummary}
|
|
7550
|
+
|
|
7551
|
+
${completedOutputs.join("\n\n")}`;
|
|
7552
|
+
if (t2Result.status === "FAILED" && t2Result.issues?.length) {
|
|
7553
|
+
output += `
|
|
7554
|
+
|
|
7555
|
+
_(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
7556
|
+
}
|
|
7557
|
+
return output;
|
|
7558
|
+
}
|
|
7559
|
+
|
|
7359
7560
|
// src/core/tiers/t2-manager.ts
|
|
7360
7561
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
7361
7562
|
return [
|
|
@@ -7381,6 +7582,19 @@ var T2Manager = class extends BaseTier {
|
|
|
7381
7582
|
t2PeerBus;
|
|
7382
7583
|
permissionEscalator;
|
|
7383
7584
|
toolCreator;
|
|
7585
|
+
/**
|
|
7586
|
+
* Asked when a section ends ESCALATED — i.e. a worker hit something it could
|
|
7587
|
+
* not decide alone. Without this the escalation was a dead end: the status
|
|
7588
|
+
* said "needs a decision" and no decision was ever requested, so an MCP run
|
|
7589
|
+
* that legitimately needed input just stopped there.
|
|
7590
|
+
*/
|
|
7591
|
+
escalationCallback;
|
|
7592
|
+
/**
|
|
7593
|
+
* The one allowed retry has been spent. Retrying is bounded because an
|
|
7594
|
+
* escalation loop that can re-ask forever burns a budget on a question that
|
|
7595
|
+
* is not resolving; a second escalation is terminal instead.
|
|
7596
|
+
*/
|
|
7597
|
+
escalationRetryUsed = false;
|
|
7384
7598
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
7385
7599
|
planApprovalCallback;
|
|
7386
7600
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -7429,6 +7643,10 @@ var T2Manager = class extends BaseTier {
|
|
|
7429
7643
|
setToolCreator(creator) {
|
|
7430
7644
|
this.toolCreator = creator;
|
|
7431
7645
|
}
|
|
7646
|
+
/** Ask the user what to do when a section escalates. */
|
|
7647
|
+
setEscalationCallback(cb) {
|
|
7648
|
+
this.escalationCallback = cb;
|
|
7649
|
+
}
|
|
7432
7650
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
7433
7651
|
setPlanApprovalCallback(cb) {
|
|
7434
7652
|
this.planApprovalCallback = cb;
|
|
@@ -7533,9 +7751,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7533
7751
|
currentAction: "Aggregating T3 results",
|
|
7534
7752
|
status: "IN_PROGRESS"
|
|
7535
7753
|
});
|
|
7536
|
-
|
|
7754
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
7537
7755
|
const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
|
|
7538
|
-
|
|
7756
|
+
let overallStatus = this.determineStatus(t3Results);
|
|
7757
|
+
let userSkipped = false;
|
|
7758
|
+
const hasEscalated = sectionNeedsDecision(t3Results);
|
|
7759
|
+
const settleEscalated = async (reason) => {
|
|
7760
|
+
if (reason) issues.push(reason);
|
|
7761
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7762
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7763
|
+
};
|
|
7764
|
+
if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
|
|
7765
|
+
await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
|
|
7766
|
+
} else if (hasEscalated && !this.escalationCallback) {
|
|
7767
|
+
await settleEscalated();
|
|
7768
|
+
} else if (hasEscalated && this.escalationCallback) {
|
|
7769
|
+
this.sendStatusUpdate({
|
|
7770
|
+
progressPct: 95,
|
|
7771
|
+
currentAction: "Escalated \u2014 waiting for your decision",
|
|
7772
|
+
status: "ESCALATING"
|
|
7773
|
+
});
|
|
7774
|
+
const decision = await this.escalationCallback({
|
|
7775
|
+
sectionId: assignment.sectionId,
|
|
7776
|
+
sectionTitle: assignment.sectionTitle,
|
|
7777
|
+
issues,
|
|
7778
|
+
summary
|
|
7779
|
+
});
|
|
7780
|
+
this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
|
|
7781
|
+
if (decision.action === "skip") {
|
|
7782
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7783
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7784
|
+
userSkipped = decision.automatic !== true;
|
|
7785
|
+
} else if (decision.action === "retry" || decision.action === "guidance") {
|
|
7786
|
+
this.escalationRetryUsed = true;
|
|
7787
|
+
const guided = decision.action === "guidance" && decision.note?.trim() ? {
|
|
7788
|
+
...assignment,
|
|
7789
|
+
description: `${assignment.description}
|
|
7790
|
+
|
|
7791
|
+
Guidance (must be followed): ${decision.note.trim()}`,
|
|
7792
|
+
// Drop T1's preplanned subtasks so the retry re-decomposes with
|
|
7793
|
+
// the guidance in hand. The workers execute from their subtask
|
|
7794
|
+
// slices, not from this description — leaving the slices in
|
|
7795
|
+
// place gave "Retry with guidance" byte-identical worker
|
|
7796
|
+
// prompts to "Retry as-is", making the note do nothing at all.
|
|
7797
|
+
t3Subtasks: []
|
|
7798
|
+
} : assignment;
|
|
7799
|
+
return await this.execute(guided, taskId, signal);
|
|
7800
|
+
} else {
|
|
7801
|
+
issues.push("Escalated, but no decision was received in time.");
|
|
7802
|
+
overallStatus = "FAILED";
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7539
7805
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7540
7806
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7541
7807
|
const sectionAction = overallStatus === "COMPLETED" ? "Section complete" : overallStatus === "PARTIAL" ? "Section partially complete \u2014 some subtasks failed" : overallStatus === "ESCALATED" ? "Section escalated \u2014 needs a decision" : "Section failed \u2014 no subtask completed";
|
|
@@ -7546,7 +7812,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7546
7812
|
status: overallStatus,
|
|
7547
7813
|
t3Results,
|
|
7548
7814
|
sectionSummary: summary,
|
|
7549
|
-
issues
|
|
7815
|
+
issues,
|
|
7816
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
7550
7817
|
};
|
|
7551
7818
|
this.publishSectionOutput(result);
|
|
7552
7819
|
return result;
|
|
@@ -7980,8 +8247,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7980
8247
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
7981
8248
|
);
|
|
7982
8249
|
}
|
|
7983
|
-
async aggregateResults(assignment, results) {
|
|
7984
|
-
const completed = results.filter(
|
|
8250
|
+
async aggregateResults(assignment, results, opts = {}) {
|
|
8251
|
+
const completed = results.filter(
|
|
8252
|
+
(r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
|
|
8253
|
+
);
|
|
7985
8254
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
7986
8255
|
const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
|
|
7987
8256
|
const peerContext = peerOutputs ? `
|
|
@@ -8187,6 +8456,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
8187
8456
|
escalations = [];
|
|
8188
8457
|
store;
|
|
8189
8458
|
runBreaker;
|
|
8459
|
+
escalationCallback;
|
|
8190
8460
|
t2PeerBus = new PeerBus();
|
|
8191
8461
|
permissionEscalator;
|
|
8192
8462
|
toolCreator;
|
|
@@ -8201,6 +8471,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
8201
8471
|
this.toolRegistry = toolRegistry;
|
|
8202
8472
|
this.config = config;
|
|
8203
8473
|
}
|
|
8474
|
+
/** Ask the user what to do when any section escalates. */
|
|
8475
|
+
setEscalationCallback(cb) {
|
|
8476
|
+
this.escalationCallback = cb;
|
|
8477
|
+
}
|
|
8204
8478
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
8205
8479
|
setRunBreaker(breaker) {
|
|
8206
8480
|
this.runBreaker = breaker;
|
|
@@ -8360,8 +8634,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
8360
8634
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
|
|
8361
8635
|
};
|
|
8362
8636
|
}
|
|
8363
|
-
const sectionsText = t2Results.map((r) =>
|
|
8364
|
-
|
|
8637
|
+
const sectionsText = t2Results.map((r) => {
|
|
8638
|
+
const skipNote = r.userSkipped ? "\n[The user was asked to retry or skip this section and explicitly chose to SKIP it. This is an intentional decision, not a failure \u2014 do not cite it as a reason to reject.]" : "";
|
|
8639
|
+
return `**${r.sectionTitle}**
|
|
8640
|
+
${r.sectionSummary}${skipNote}`;
|
|
8641
|
+
}).join("\n\n");
|
|
8365
8642
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
8366
8643
|
Review the following execution outputs against the original user prompt.
|
|
8367
8644
|
|
|
@@ -8371,6 +8648,7 @@ T2 Manager Summaries:
|
|
|
8371
8648
|
${sectionsText}
|
|
8372
8649
|
|
|
8373
8650
|
Does the current state of the workspace and the outputs fully satisfy the user's request?
|
|
8651
|
+
A section marked as user-skipped above is accepted as-is by the user's own choice \u2014 treat it as satisfied, not as a gap.
|
|
8374
8652
|
If yes, reply with exactly: "APPROVED".
|
|
8375
8653
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
8376
8654
|
try {
|
|
@@ -8568,6 +8846,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8568
8846
|
const managers = sections.map((section) => {
|
|
8569
8847
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8570
8848
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8849
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
8571
8850
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8572
8851
|
if (this.store) {
|
|
8573
8852
|
manager.setStore(this.store);
|
|
@@ -8777,7 +9056,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8777
9056
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
8778
9057
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
8779
9058
|
if (!completedSections.length) {
|
|
8780
|
-
const allIssues =
|
|
9059
|
+
const allIssues = [
|
|
9060
|
+
...t2Results.flatMap((r) => r.issues ?? []),
|
|
9061
|
+
...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
|
|
9062
|
+
].filter(Boolean);
|
|
8781
9063
|
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
8782
9064
|
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
8783
9065
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -8863,9 +9145,16 @@ var CAPABILITIES = [
|
|
|
8863
9145
|
api: "openai-images"
|
|
8864
9146
|
},
|
|
8865
9147
|
{
|
|
9148
|
+
// Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
|
|
9149
|
+
// page but 404s on `:predict` ("not found for API version v1beta, or is not
|
|
9150
|
+
// supported for predict") — the Gemini image models are called through
|
|
9151
|
+
// generateContent with an image response modality, while `:predict` is the
|
|
9152
|
+
// Imagen shape this executor implements. A billing line item is not proof
|
|
9153
|
+
// of a callable endpoint, which is the assumption that put the wrong id
|
|
9154
|
+
// here in the first place.
|
|
8866
9155
|
modality: "image",
|
|
8867
9156
|
provider: "gemini",
|
|
8868
|
-
modelId: "
|
|
9157
|
+
modelId: "imagen-4.0-generate-001",
|
|
8869
9158
|
api: "gemini-predict"
|
|
8870
9159
|
},
|
|
8871
9160
|
// ── Speech (text → audio) ──
|
|
@@ -9579,6 +9868,102 @@ var RunBreaker = class {
|
|
|
9579
9868
|
return out;
|
|
9580
9869
|
}
|
|
9581
9870
|
};
|
|
9871
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
9872
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
9873
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
9874
|
+
function boundedSegment(raw) {
|
|
9875
|
+
const clean = sanitizeToolNameSegment(raw);
|
|
9876
|
+
if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
|
|
9877
|
+
const hash = crypto4.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
9878
|
+
return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
9879
|
+
}
|
|
9880
|
+
function isStableSegment(raw) {
|
|
9881
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
9882
|
+
}
|
|
9883
|
+
function sanitizeToolNameSegment(segment) {
|
|
9884
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
9885
|
+
let start = 0;
|
|
9886
|
+
let end = cleaned.length;
|
|
9887
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
9888
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
9889
|
+
return cleaned.slice(start, end) || "_";
|
|
9890
|
+
}
|
|
9891
|
+
function mcpToolName(serverName, toolName) {
|
|
9892
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
9893
|
+
}
|
|
9894
|
+
function isMcpToolName(name) {
|
|
9895
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
9896
|
+
}
|
|
9897
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
9898
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
9899
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
9900
|
+
for (let n = 2; n < 1e3; n++) {
|
|
9901
|
+
const candidate = `${desired} (${n})`;
|
|
9902
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
9903
|
+
}
|
|
9904
|
+
return `${desired} (${crypto4.randomUUID().slice(0, 8)})`;
|
|
9905
|
+
}
|
|
9906
|
+
function disambiguateMcpServerNames(servers) {
|
|
9907
|
+
const seenNames = [];
|
|
9908
|
+
const renames = [];
|
|
9909
|
+
const result = servers.map((s) => {
|
|
9910
|
+
const name = uniqueMcpServerName(s.name, seenNames);
|
|
9911
|
+
seenNames.push(name);
|
|
9912
|
+
if (name === s.name) return s;
|
|
9913
|
+
renames.push({ from: s.name, to: name });
|
|
9914
|
+
return { ...s, name };
|
|
9915
|
+
});
|
|
9916
|
+
return { servers: renames.length ? result : servers, renames };
|
|
9917
|
+
}
|
|
9918
|
+
function assignMcpToolNames(tools) {
|
|
9919
|
+
const names = new Array(tools.length);
|
|
9920
|
+
const bases = tools.map((t) => mcpToolName(t.server, t.tool));
|
|
9921
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9922
|
+
bases.forEach((base, i) => {
|
|
9923
|
+
const g = groups.get(base);
|
|
9924
|
+
if (g) g.push(i);
|
|
9925
|
+
else groups.set(base, [i]);
|
|
9926
|
+
});
|
|
9927
|
+
const used = new Set(bases);
|
|
9928
|
+
for (const [base, idx] of groups) {
|
|
9929
|
+
if (idx.length === 1) {
|
|
9930
|
+
names[idx[0]] = base;
|
|
9931
|
+
continue;
|
|
9932
|
+
}
|
|
9933
|
+
const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
|
|
9934
|
+
const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
|
|
9935
|
+
const ordered = [...idx].sort((a, b) => {
|
|
9936
|
+
const la = isClean(a) ? 0 : 1;
|
|
9937
|
+
const lb = isClean(b) ? 0 : 1;
|
|
9938
|
+
if (la !== lb) return la - lb;
|
|
9939
|
+
const ra = raw(a);
|
|
9940
|
+
const rb = raw(b);
|
|
9941
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
9942
|
+
});
|
|
9943
|
+
names[ordered[0]] = base;
|
|
9944
|
+
let n = 2;
|
|
9945
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
9946
|
+
let candidate = `${base}_${n}`;
|
|
9947
|
+
while (used.has(candidate)) {
|
|
9948
|
+
n++;
|
|
9949
|
+
candidate = `${base}_${n}`;
|
|
9950
|
+
}
|
|
9951
|
+
names[ordered[rank3]] = candidate;
|
|
9952
|
+
used.add(candidate);
|
|
9953
|
+
n++;
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
return names;
|
|
9957
|
+
}
|
|
9958
|
+
function mcpServerPrefix(serverName) {
|
|
9959
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
9960
|
+
}
|
|
9961
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
9962
|
+
if (!disabledTools?.length) return disabledTools;
|
|
9963
|
+
const prefix = mcpServerPrefix(serverName);
|
|
9964
|
+
const kept = disabledTools.filter((t) => !t.startsWith(prefix));
|
|
9965
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
9966
|
+
}
|
|
9582
9967
|
var execAsync = util.promisify(child_process.exec);
|
|
9583
9968
|
var ShellTool = class extends BaseTool {
|
|
9584
9969
|
name = "shell";
|
|
@@ -10994,20 +11379,6 @@ ${text}`;
|
|
|
10994
11379
|
}
|
|
10995
11380
|
};
|
|
10996
11381
|
|
|
10997
|
-
// src/tools/tool-name.ts
|
|
10998
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
10999
|
-
function sanitizeToolNameSegment(segment) {
|
|
11000
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
11001
|
-
let start = 0;
|
|
11002
|
-
let end = cleaned.length;
|
|
11003
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
11004
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
11005
|
-
return cleaned.slice(start, end) || "_";
|
|
11006
|
-
}
|
|
11007
|
-
function mcpToolName(serverName, toolName) {
|
|
11008
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
11009
|
-
}
|
|
11010
|
-
|
|
11011
11382
|
// src/tools/mcp.ts
|
|
11012
11383
|
var McpToolWrapper = class extends BaseTool {
|
|
11013
11384
|
name;
|
|
@@ -11016,12 +11387,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
11016
11387
|
mcpClient;
|
|
11017
11388
|
serverName;
|
|
11018
11389
|
toolName;
|
|
11019
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
11390
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
11020
11391
|
super();
|
|
11021
11392
|
this.mcpClient = mcpClient;
|
|
11022
11393
|
this.serverName = serverName;
|
|
11023
11394
|
this.toolName = toolName;
|
|
11024
|
-
this.name = mcpToolName(serverName, toolName);
|
|
11395
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
11025
11396
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
11026
11397
|
this.inputSchema = inputSchema;
|
|
11027
11398
|
}
|
|
@@ -11092,22 +11463,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
11092
11463
|
getRegisteredPlugins() {
|
|
11093
11464
|
return Array.from(this.plugins.keys());
|
|
11094
11465
|
}
|
|
11095
|
-
/**
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11466
|
+
/**
|
|
11467
|
+
* Registers all tools from an MCP client.
|
|
11468
|
+
*
|
|
11469
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
11470
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
11471
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
11472
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
11473
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
11474
|
+
* never counted against the tool budget, and never has to be refused.
|
|
11475
|
+
*/
|
|
11476
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
11477
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
11478
|
+
const [, server, tool] = def.name.split("::");
|
|
11479
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
11480
|
+
});
|
|
11481
|
+
const names = assignMcpToolNames(parsed);
|
|
11482
|
+
parsed.forEach(({ def, server, tool }, i) => {
|
|
11101
11483
|
const wrapper = new McpToolWrapper(
|
|
11102
11484
|
mcpClient,
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
def.description.replace(`[MCP:${
|
|
11106
|
-
def.inputSchema
|
|
11485
|
+
server,
|
|
11486
|
+
tool,
|
|
11487
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
11488
|
+
def.inputSchema,
|
|
11489
|
+
names[i]
|
|
11107
11490
|
);
|
|
11491
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
11108
11492
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
11109
11493
|
this.register(wrapper);
|
|
11110
|
-
}
|
|
11494
|
+
});
|
|
11111
11495
|
}
|
|
11112
11496
|
setIgnoredPaths(patterns) {
|
|
11113
11497
|
this.ignoreMatcher = ignore().add(patterns);
|
|
@@ -11444,6 +11828,17 @@ function fileOAuthProvider(storePath) {
|
|
|
11444
11828
|
}
|
|
11445
11829
|
});
|
|
11446
11830
|
}
|
|
11831
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
11832
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
11833
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
11834
|
+
const run = prev.then(fn, fn);
|
|
11835
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
11836
|
+
oauthStoreLocks.set(storePath, tail);
|
|
11837
|
+
void tail.then(() => {
|
|
11838
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
11839
|
+
});
|
|
11840
|
+
return run;
|
|
11841
|
+
}
|
|
11447
11842
|
function beginMcpOAuth(provider, serverUrl) {
|
|
11448
11843
|
return auth_js.auth(provider, { serverUrl });
|
|
11449
11844
|
}
|
|
@@ -14382,7 +14777,50 @@ var GuidanceQueue = class {
|
|
|
14382
14777
|
}
|
|
14383
14778
|
};
|
|
14384
14779
|
|
|
14780
|
+
// src/tools/current-page.ts
|
|
14781
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
14782
|
+
var CurrentPageTool = class extends BaseTool {
|
|
14783
|
+
name = "read_current_page";
|
|
14784
|
+
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.';
|
|
14785
|
+
inputSchema = {
|
|
14786
|
+
type: "object",
|
|
14787
|
+
properties: {},
|
|
14788
|
+
required: []
|
|
14789
|
+
};
|
|
14790
|
+
provider;
|
|
14791
|
+
constructor(provider) {
|
|
14792
|
+
super();
|
|
14793
|
+
this.provider = provider;
|
|
14794
|
+
}
|
|
14795
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
14796
|
+
// nothing they have not already opened themselves.
|
|
14797
|
+
isDangerous() {
|
|
14798
|
+
return false;
|
|
14799
|
+
}
|
|
14800
|
+
async execute(_input, _options) {
|
|
14801
|
+
let page2;
|
|
14802
|
+
try {
|
|
14803
|
+
page2 = await this.provider();
|
|
14804
|
+
} catch (err) {
|
|
14805
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
14806
|
+
}
|
|
14807
|
+
if (!page2) {
|
|
14808
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
14809
|
+
}
|
|
14810
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
14811
|
+
|
|
14812
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
14813
|
+
return [
|
|
14814
|
+
`URL: ${page2.url}`,
|
|
14815
|
+
`Title: ${page2.title}`,
|
|
14816
|
+
"",
|
|
14817
|
+
text.trim() || "[the page rendered no readable text]"
|
|
14818
|
+
].join("\n");
|
|
14819
|
+
}
|
|
14820
|
+
};
|
|
14821
|
+
|
|
14385
14822
|
// src/core/cascade.ts
|
|
14823
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
14386
14824
|
function buildContextualPrompt(prompt, history = []) {
|
|
14387
14825
|
const recent = history.slice(-6);
|
|
14388
14826
|
if (recent.length === 0) return prompt;
|
|
@@ -14437,6 +14875,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
14437
14875
|
}
|
|
14438
14876
|
});
|
|
14439
14877
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14878
|
+
try {
|
|
14879
|
+
const nodePath = __require("path");
|
|
14880
|
+
this.router.setDeadModelStore(new DeadModelStore(
|
|
14881
|
+
fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
|
|
14882
|
+
));
|
|
14883
|
+
} catch {
|
|
14884
|
+
}
|
|
14440
14885
|
this.multimodal = new MultimodalRegistry(
|
|
14441
14886
|
(this.config.providers ?? []).map((p) => p.type)
|
|
14442
14887
|
);
|
|
@@ -14598,6 +15043,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
14598
15043
|
});
|
|
14599
15044
|
});
|
|
14600
15045
|
}
|
|
15046
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
15047
|
+
//
|
|
15048
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
15049
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
15050
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
15051
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
15052
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
15053
|
+
//
|
|
15054
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
15055
|
+
// timeout because the plan is already the model's considered proposal;
|
|
15056
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
15057
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
15058
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
15059
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
15060
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
15061
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
15062
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
15063
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
15064
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
15065
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
15066
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
15067
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
15068
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
15069
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
15070
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
15071
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
15072
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
15073
|
+
const requestId = crypto4.randomUUID();
|
|
15074
|
+
return await new Promise((resolve2) => {
|
|
15075
|
+
let timeout;
|
|
15076
|
+
const settle = (decision) => {
|
|
15077
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
15078
|
+
if (timeout) clearTimeout(timeout);
|
|
15079
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15080
|
+
resolve2(decision);
|
|
15081
|
+
};
|
|
15082
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
15083
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15084
|
+
timeout = setTimeout(() => {
|
|
15085
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
15086
|
+
settle({ action: "timeout" });
|
|
15087
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
15088
|
+
this.pendingEscalations.set(requestId, settle);
|
|
15089
|
+
this.emit("escalation:decision-required", {
|
|
15090
|
+
taskId,
|
|
15091
|
+
requestId,
|
|
15092
|
+
sectionId: ctx.sectionId,
|
|
15093
|
+
sectionTitle: ctx.sectionTitle,
|
|
15094
|
+
issues: ctx.issues,
|
|
15095
|
+
summary: ctx.summary,
|
|
15096
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
15097
|
+
});
|
|
15098
|
+
});
|
|
15099
|
+
}
|
|
15100
|
+
/**
|
|
15101
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
15102
|
+
*
|
|
15103
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
15104
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
15105
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
15106
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
15107
|
+
* which is correct whenever there is exactly one.
|
|
15108
|
+
*
|
|
15109
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
15110
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
15111
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
15112
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
15113
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
15114
|
+
* see EscalationDecision.automatic.
|
|
15115
|
+
*/
|
|
15116
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
15117
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
15118
|
+
if (requestId) {
|
|
15119
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
15120
|
+
return;
|
|
15121
|
+
}
|
|
15122
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
15123
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
15124
|
+
}
|
|
15125
|
+
/**
|
|
15126
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
15127
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
15128
|
+
* same system-decided case as the early-return skips above, not a person
|
|
15129
|
+
* reviewing and accepting that section's output.
|
|
15130
|
+
*/
|
|
15131
|
+
releasePendingEscalations(action = "skip") {
|
|
15132
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
15133
|
+
}
|
|
14601
15134
|
/**
|
|
14602
15135
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
14603
15136
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -14731,6 +15264,20 @@ ${input}` }],
|
|
|
14731
15264
|
this.mediaSink = sink;
|
|
14732
15265
|
this.registerMediaTools(this.workspacePath);
|
|
14733
15266
|
}
|
|
15267
|
+
/**
|
|
15268
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
15269
|
+
*
|
|
15270
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
15271
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
15272
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
15273
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
15274
|
+
* touch the machine, and reading a page the user already opened touches
|
|
15275
|
+
* nothing. `disabledTools` still removes it.
|
|
15276
|
+
*/
|
|
15277
|
+
setCurrentPageProvider(provider) {
|
|
15278
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
15279
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
15280
|
+
}
|
|
14734
15281
|
registerMediaTools(workspacePath) {
|
|
14735
15282
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14736
15283
|
const permitted = (name) => !denied.has(name);
|
|
@@ -14863,8 +15410,12 @@ ${last.partialOutput}` : "");
|
|
|
14863
15410
|
for (const server of this.config.tools.mcpServers) {
|
|
14864
15411
|
try {
|
|
14865
15412
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
14866
|
-
|
|
14867
|
-
|
|
15413
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
15414
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
15415
|
+
this.toolRegistry.registerMcpTools(
|
|
15416
|
+
this.mcpClient,
|
|
15417
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
15418
|
+
);
|
|
14868
15419
|
} catch (err) {
|
|
14869
15420
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
14870
15421
|
}
|
|
@@ -15329,6 +15880,7 @@ ${prompt}` : prompt;
|
|
|
15329
15880
|
} else if (complexity === "Moderate") {
|
|
15330
15881
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
15331
15882
|
t2.setRunBreaker(runBreaker);
|
|
15883
|
+
t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15332
15884
|
t2.setPresenter(true);
|
|
15333
15885
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
15334
15886
|
if (identityPrompt) {
|
|
@@ -15373,11 +15925,17 @@ ${prompt}` : prompt;
|
|
|
15373
15925
|
t2Results = [t2Result];
|
|
15374
15926
|
const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
|
|
15375
15927
|
if (completed.length > 0) {
|
|
15376
|
-
finalOutput =
|
|
15928
|
+
finalOutput = composeRootSectionOutput(
|
|
15929
|
+
t2Result,
|
|
15930
|
+
completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
|
|
15931
|
+
);
|
|
15377
15932
|
} else {
|
|
15378
15933
|
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
15379
15934
|
const reasons = Array.from(
|
|
15380
|
-
new Set(
|
|
15935
|
+
new Set([
|
|
15936
|
+
...t2Result.issues ?? [],
|
|
15937
|
+
...t2Result.t3Results.flatMap((r) => r.issues ?? [])
|
|
15938
|
+
].filter(Boolean))
|
|
15381
15939
|
);
|
|
15382
15940
|
if (partial) {
|
|
15383
15941
|
finalOutput = reasons.length ? `${partial}
|
|
@@ -15390,6 +15948,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15390
15948
|
} else {
|
|
15391
15949
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
15392
15950
|
t1.setRunBreaker(runBreaker);
|
|
15951
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15393
15952
|
t1.setPresenter(true);
|
|
15394
15953
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
15395
15954
|
if (identityPrompt) {
|
|
@@ -15414,6 +15973,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15414
15973
|
finalOutput = result.output;
|
|
15415
15974
|
t2Results = result.t2Results;
|
|
15416
15975
|
}
|
|
15976
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
15977
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
15417
15978
|
const trip = runBreaker.reason();
|
|
15418
15979
|
if (trip) {
|
|
15419
15980
|
finalOutput = [
|
|
@@ -15454,6 +16015,10 @@ ${finalOutput}` : ""
|
|
|
15454
16015
|
escalator.cancelAllPending();
|
|
15455
16016
|
} catch {
|
|
15456
16017
|
}
|
|
16018
|
+
try {
|
|
16019
|
+
this.releasePendingEscalations();
|
|
16020
|
+
} catch {
|
|
16021
|
+
}
|
|
15457
16022
|
this.router.restoreTierModels();
|
|
15458
16023
|
this.router.setRunSignal(void 0);
|
|
15459
16024
|
if (this.taskAnalyzer) {
|
|
@@ -16770,6 +17335,13 @@ var ConfigManager = class {
|
|
|
16770
17335
|
}
|
|
16771
17336
|
async load() {
|
|
16772
17337
|
this.config = await this.loadConfig();
|
|
17338
|
+
const servers = this.config.tools?.mcpServers;
|
|
17339
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
17340
|
+
const mcpNamesChanged = renames.length > 0;
|
|
17341
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
17342
|
+
this.config.tools.mcpServers = disambiguated;
|
|
17343
|
+
this.renameMcpServerReferences(renames);
|
|
17344
|
+
}
|
|
16773
17345
|
this.ignore = new CascadeIgnore();
|
|
16774
17346
|
await this.ignore.load(this.workspacePath);
|
|
16775
17347
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -16778,6 +17350,7 @@ var ConfigManager = class {
|
|
|
16778
17350
|
await this.injectEnvKeys();
|
|
16779
17351
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
16780
17352
|
await this.ensureDefaultIdentity();
|
|
17353
|
+
if (mcpNamesChanged) await this.save();
|
|
16781
17354
|
}
|
|
16782
17355
|
getConfig() {
|
|
16783
17356
|
return this.config;
|
|
@@ -16861,6 +17434,66 @@ var ConfigManager = class {
|
|
|
16861
17434
|
this.config.providers.push({ type: "ollama" });
|
|
16862
17435
|
}
|
|
16863
17436
|
}
|
|
17437
|
+
/**
|
|
17438
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
17439
|
+
*
|
|
17440
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
17441
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
17442
|
+
*
|
|
17443
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
17444
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
17445
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
17446
|
+
* move with it. A plain rewrite is correct here.
|
|
17447
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
17448
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
17449
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
17450
|
+
* already been updated to the disambiguated list by the time this runs
|
|
17451
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
17452
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
17453
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
17454
|
+
* AND granted to the renamed identity, not moved.
|
|
17455
|
+
*
|
|
17456
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
17457
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
17458
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
17459
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
17460
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
17461
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
17462
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
17463
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
17464
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
17465
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
17466
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
17467
|
+
* together, in the one pass that still sees the original entry.
|
|
17468
|
+
*
|
|
17469
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
17470
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
17471
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
17472
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
17473
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
17474
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
17475
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
17476
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
17477
|
+
* would do the opposite — move a denial that already worked for the
|
|
17478
|
+
* survivor onto a server it may never have meant to cover.
|
|
17479
|
+
*/
|
|
17480
|
+
renameMcpServerReferences(renames) {
|
|
17481
|
+
const tools = this.config.tools;
|
|
17482
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
17483
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
|
|
17484
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
17485
|
+
for (const { from, to } of renames) {
|
|
17486
|
+
const tos = byFrom.get(from);
|
|
17487
|
+
if (tos) tos.push(to);
|
|
17488
|
+
else byFrom.set(from, [to]);
|
|
17489
|
+
}
|
|
17490
|
+
let trusted = tools.mcpTrusted;
|
|
17491
|
+
for (const [from, tos] of byFrom) {
|
|
17492
|
+
if (!trusted.includes(from)) continue;
|
|
17493
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
|
|
17494
|
+
}
|
|
17495
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
17496
|
+
}
|
|
16864
17497
|
async ensureDefaultIdentity() {
|
|
16865
17498
|
const existing = this.store.getDefaultIdentity();
|
|
16866
17499
|
if (existing) return;
|
|
@@ -16983,6 +17616,14 @@ var DashboardSocket = class {
|
|
|
16983
17616
|
this.secret = options.secret;
|
|
16984
17617
|
this.setupHandlers();
|
|
16985
17618
|
}
|
|
17619
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
17620
|
+
roomSize(room) {
|
|
17621
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
17622
|
+
}
|
|
17623
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
17624
|
+
hasSocket(socketId) {
|
|
17625
|
+
return this.io.sockets.sockets.has(socketId);
|
|
17626
|
+
}
|
|
16986
17627
|
broadcastToRoom(room, event, data) {
|
|
16987
17628
|
this.io.to(room).emit(event, data);
|
|
16988
17629
|
}
|
|
@@ -17054,6 +17695,22 @@ var DashboardSocket = class {
|
|
|
17054
17695
|
});
|
|
17055
17696
|
});
|
|
17056
17697
|
}
|
|
17698
|
+
/**
|
|
17699
|
+
* A client subscribed to a session's room.
|
|
17700
|
+
*
|
|
17701
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
17702
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
17703
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
17704
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
17705
|
+
*/
|
|
17706
|
+
onJoinSession(callback) {
|
|
17707
|
+
this.io.on("connection", (socket) => {
|
|
17708
|
+
socket.on("join:session", (payload) => {
|
|
17709
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
17710
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
17711
|
+
});
|
|
17712
|
+
});
|
|
17713
|
+
}
|
|
17057
17714
|
onSessionRate(callback) {
|
|
17058
17715
|
this.io.on("connection", (socket) => {
|
|
17059
17716
|
socket.on("session:rate", (payload) => {
|
|
@@ -17091,6 +17748,31 @@ var DashboardSocket = class {
|
|
|
17091
17748
|
});
|
|
17092
17749
|
});
|
|
17093
17750
|
}
|
|
17751
|
+
/**
|
|
17752
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
17753
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
17754
|
+
* answers here.
|
|
17755
|
+
*
|
|
17756
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
17757
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
17758
|
+
*/
|
|
17759
|
+
onEscalationDecision(callback) {
|
|
17760
|
+
this.io.on("connection", (socket) => {
|
|
17761
|
+
socket.on("escalation:decide", (payload) => {
|
|
17762
|
+
const action = payload?.action;
|
|
17763
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
17764
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
17765
|
+
callback({
|
|
17766
|
+
sessionId: payload.sessionId,
|
|
17767
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
17768
|
+
// run concurrently, so more than one can be waiting.
|
|
17769
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
17770
|
+
action,
|
|
17771
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
17772
|
+
});
|
|
17773
|
+
});
|
|
17774
|
+
});
|
|
17775
|
+
}
|
|
17094
17776
|
onSessionSteer(callback) {
|
|
17095
17777
|
this.io.on("connection", (socket) => {
|
|
17096
17778
|
socket.on("session:steer", (payload) => {
|
|
@@ -17249,6 +17931,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
17249
17931
|
|
|
17250
17932
|
// src/dashboard/server.ts
|
|
17251
17933
|
var __dirname$1 = path24__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
17934
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
17252
17935
|
var DashboardServer = class {
|
|
17253
17936
|
app;
|
|
17254
17937
|
httpServer;
|
|
@@ -17259,6 +17942,13 @@ var DashboardServer = class {
|
|
|
17259
17942
|
globalStore = null;
|
|
17260
17943
|
broadcastTimer = null;
|
|
17261
17944
|
activeSessions = /* @__PURE__ */ new Map();
|
|
17945
|
+
/**
|
|
17946
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
17947
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
17948
|
+
* browser — so the tool is simply not registered rather than present and
|
|
17949
|
+
* always failing.
|
|
17950
|
+
*/
|
|
17951
|
+
currentPageProvider;
|
|
17262
17952
|
activeControllers = /* @__PURE__ */ new Map();
|
|
17263
17953
|
/**
|
|
17264
17954
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -17273,6 +17963,17 @@ var DashboardServer = class {
|
|
|
17273
17963
|
* map is how that answer reaches the run that's blocked on it.
|
|
17274
17964
|
*/
|
|
17275
17965
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
17966
|
+
/**
|
|
17967
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
17968
|
+
* delivered to a client that was not reachable when it was raised.
|
|
17969
|
+
*
|
|
17970
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
17971
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
17972
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
17973
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
17974
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
17975
|
+
*/
|
|
17976
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
17276
17977
|
/**
|
|
17277
17978
|
* The orchestration decision trail ("why") of each session's most recent
|
|
17278
17979
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -17350,20 +18051,29 @@ var DashboardServer = class {
|
|
|
17350
18051
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
17351
18052
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
17352
18053
|
this.activeSessions.set(sessionId, cascade);
|
|
18054
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
17353
18055
|
cascade.on("stream:token", (e) => {
|
|
17354
|
-
this.
|
|
18056
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
17355
18057
|
});
|
|
17356
18058
|
cascade.on("tier:status", (e) => {
|
|
17357
|
-
this.
|
|
18059
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
|
|
17358
18060
|
});
|
|
17359
18061
|
cascade.on("permission:user-required", (e) => {
|
|
17360
|
-
this.
|
|
18062
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
|
|
17361
18063
|
});
|
|
17362
18064
|
cascade.on("peer:message", (e) => {
|
|
17363
18065
|
this.socket.emitPeerMessage(e);
|
|
17364
18066
|
});
|
|
17365
18067
|
cascade.on("plan:approval-required", (e) => {
|
|
17366
|
-
this.
|
|
18068
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
|
|
18069
|
+
});
|
|
18070
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
18071
|
+
this.raiseEscalation(sessionId, socketId, e);
|
|
18072
|
+
});
|
|
18073
|
+
cascade.on("escalation:timeout", (e) => {
|
|
18074
|
+
const ev = e;
|
|
18075
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
18076
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
17367
18077
|
});
|
|
17368
18078
|
try {
|
|
17369
18079
|
const result = await cascade.run({
|
|
@@ -17374,7 +18084,7 @@ var DashboardServer = class {
|
|
|
17374
18084
|
this.recordSessionTask(sessionId, result.taskId);
|
|
17375
18085
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17376
18086
|
this.captureWhy(sessionId, cascade, result);
|
|
17377
|
-
this.
|
|
18087
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
17378
18088
|
this.socket.broadcast("cost:update", {
|
|
17379
18089
|
sessionId,
|
|
17380
18090
|
totalTokens: result.usage.totalTokens,
|
|
@@ -17387,7 +18097,7 @@ var DashboardServer = class {
|
|
|
17387
18097
|
} catch (err) {
|
|
17388
18098
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17389
18099
|
this.captureWhy(sessionId, cascade);
|
|
17390
|
-
this.
|
|
18100
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
17391
18101
|
sessionId,
|
|
17392
18102
|
error: err instanceof Error ? err.message : String(err)
|
|
17393
18103
|
});
|
|
@@ -17395,6 +18105,7 @@ var DashboardServer = class {
|
|
|
17395
18105
|
this.activeSessions.delete(sessionId);
|
|
17396
18106
|
this.activeControllers.delete(sessionId);
|
|
17397
18107
|
this.denyPendingApprovals(sessionId);
|
|
18108
|
+
this.clearSessionEscalations(sessionId);
|
|
17398
18109
|
}
|
|
17399
18110
|
});
|
|
17400
18111
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -17404,10 +18115,20 @@ var DashboardServer = class {
|
|
|
17404
18115
|
editedPlan
|
|
17405
18116
|
);
|
|
17406
18117
|
});
|
|
18118
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
18119
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
18120
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
18121
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
18122
|
+
});
|
|
18123
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
18124
|
+
this.replayEscalations(sessionId, socketId);
|
|
18125
|
+
});
|
|
17407
18126
|
this.socket.onSessionHalt((sessionId) => {
|
|
17408
18127
|
this.activeControllers.get(sessionId)?.abort();
|
|
17409
18128
|
this.denyPendingApprovals(sessionId);
|
|
17410
18129
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
18130
|
+
this.clearSessionEscalations(sessionId);
|
|
18131
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
17411
18132
|
});
|
|
17412
18133
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
17413
18134
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -17422,6 +18143,10 @@ var DashboardServer = class {
|
|
|
17422
18143
|
}
|
|
17423
18144
|
});
|
|
17424
18145
|
}
|
|
18146
|
+
/** Wire the host's browser view in (desktop only). */
|
|
18147
|
+
setCurrentPageProvider(provider) {
|
|
18148
|
+
this.currentPageProvider = provider;
|
|
18149
|
+
}
|
|
17425
18150
|
async start() {
|
|
17426
18151
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
17427
18152
|
if (!isLoopback) {
|
|
@@ -17710,6 +18435,116 @@ var DashboardServer = class {
|
|
|
17710
18435
|
this.activeSessions.delete(sessionId);
|
|
17711
18436
|
}
|
|
17712
18437
|
}
|
|
18438
|
+
/**
|
|
18439
|
+
* Is anyone able to answer for this session right now?
|
|
18440
|
+
*
|
|
18441
|
+
* The originating socket first — that connection is definitely watching this
|
|
18442
|
+
* run — then the session room, which is where a client lands after a
|
|
18443
|
+
* reconnect issues it a new socket id.
|
|
18444
|
+
*/
|
|
18445
|
+
hasResponder(sessionId, socketId) {
|
|
18446
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
18447
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
18448
|
+
}
|
|
18449
|
+
/**
|
|
18450
|
+
* Send a run event to whoever is watching this session.
|
|
18451
|
+
*
|
|
18452
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
18453
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
18454
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
18455
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
18456
|
+
* client that is both the originator and a room member from seeing every
|
|
18457
|
+
* token twice.
|
|
18458
|
+
*/
|
|
18459
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
18460
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
18461
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
18462
|
+
}
|
|
18463
|
+
/**
|
|
18464
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
18465
|
+
*
|
|
18466
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
18467
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
18468
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
18469
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
18470
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
18471
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
18472
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
18473
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
18474
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
18475
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
18476
|
+
* mistaken for an absent user.
|
|
18477
|
+
*/
|
|
18478
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
18479
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto4.randomUUID();
|
|
18480
|
+
const payload = { sessionId, ...event };
|
|
18481
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
18482
|
+
this.pendingEscalations.set(requestId, entry);
|
|
18483
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
18484
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
18485
|
+
return;
|
|
18486
|
+
}
|
|
18487
|
+
entry.graceTimer = setTimeout(() => {
|
|
18488
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
18489
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
18490
|
+
this.pendingEscalations.delete(requestId);
|
|
18491
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
18492
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
18493
|
+
}
|
|
18494
|
+
/**
|
|
18495
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
18496
|
+
*
|
|
18497
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
18498
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
18499
|
+
* its gates and the clients queue theirs.
|
|
18500
|
+
*
|
|
18501
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
18502
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
18503
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
18504
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
18505
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
18506
|
+
* sides never have to agree on a wall clock.
|
|
18507
|
+
*/
|
|
18508
|
+
replayEscalations(sessionId, socketId) {
|
|
18509
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
18510
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18511
|
+
if (entry.graceTimer) {
|
|
18512
|
+
clearTimeout(entry.graceTimer);
|
|
18513
|
+
entry.graceTimer = void 0;
|
|
18514
|
+
}
|
|
18515
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
18516
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
18517
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
18518
|
+
}
|
|
18519
|
+
}
|
|
18520
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
18521
|
+
clearPendingEscalation(requestId) {
|
|
18522
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
18523
|
+
if (!entry) return;
|
|
18524
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18525
|
+
this.pendingEscalations.delete(requestId);
|
|
18526
|
+
}
|
|
18527
|
+
/**
|
|
18528
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
18529
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
18530
|
+
* Insertion order makes `values()` oldest-first.
|
|
18531
|
+
*/
|
|
18532
|
+
clearOldestSessionEscalation(sessionId) {
|
|
18533
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18534
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18535
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18536
|
+
this.pendingEscalations.delete(id);
|
|
18537
|
+
return;
|
|
18538
|
+
}
|
|
18539
|
+
}
|
|
18540
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
18541
|
+
clearSessionEscalations(sessionId) {
|
|
18542
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18543
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18544
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18545
|
+
this.pendingEscalations.delete(id);
|
|
18546
|
+
}
|
|
18547
|
+
}
|
|
17713
18548
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
17714
18549
|
denyPendingApprovals(sessionId) {
|
|
17715
18550
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -18210,6 +19045,7 @@ ${prompt}`;
|
|
|
18210
19045
|
void (async () => {
|
|
18211
19046
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
18212
19047
|
this.activeSessions.set(sessionId, cascade);
|
|
19048
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
18213
19049
|
cascade.on("stream:token", (e) => {
|
|
18214
19050
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
18215
19051
|
});
|
|
@@ -18225,6 +19061,14 @@ ${prompt}`;
|
|
|
18225
19061
|
cascade.on("plan:approval-required", (e) => {
|
|
18226
19062
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
18227
19063
|
});
|
|
19064
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
19065
|
+
this.raiseEscalation(sessionId, void 0, e);
|
|
19066
|
+
});
|
|
19067
|
+
cascade.on("escalation:timeout", (e) => {
|
|
19068
|
+
const ev = e;
|
|
19069
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
19070
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
19071
|
+
});
|
|
18228
19072
|
try {
|
|
18229
19073
|
const result = await cascade.run({
|
|
18230
19074
|
prompt: runPrompt,
|
|
@@ -18252,6 +19096,7 @@ ${prompt}`;
|
|
|
18252
19096
|
} finally {
|
|
18253
19097
|
this.activeSessions.delete(sessionId);
|
|
18254
19098
|
this.denyPendingApprovals(sessionId);
|
|
19099
|
+
this.clearSessionEscalations(sessionId);
|
|
18255
19100
|
}
|
|
18256
19101
|
})();
|
|
18257
19102
|
});
|
|
@@ -18584,6 +19429,41 @@ var HooksRunner = class {
|
|
|
18584
19429
|
}
|
|
18585
19430
|
};
|
|
18586
19431
|
|
|
19432
|
+
// src/mcp/discover.ts
|
|
19433
|
+
async function discoverMcpTools(servers) {
|
|
19434
|
+
const results = [];
|
|
19435
|
+
for (const server of servers) {
|
|
19436
|
+
const client = new McpClient({ trustedServers: [server.name] });
|
|
19437
|
+
try {
|
|
19438
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
19439
|
+
const connect = () => client.connect(server, authProvider ? { authProvider } : {});
|
|
19440
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
19441
|
+
const parsed = client.getToolDefinitions().flatMap((def) => {
|
|
19442
|
+
const [, serverName, toolName] = def.name.split("::");
|
|
19443
|
+
return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
|
|
19444
|
+
});
|
|
19445
|
+
const names = assignMcpToolNames(parsed);
|
|
19446
|
+
const tools = parsed.map(({ def, server: s, tool }, i) => ({
|
|
19447
|
+
server: s,
|
|
19448
|
+
tool,
|
|
19449
|
+
name: names[i],
|
|
19450
|
+
description: def.description.replace(`[MCP:${s}] `, "")
|
|
19451
|
+
}));
|
|
19452
|
+
results.push({ server: server.name, tools });
|
|
19453
|
+
} catch (err) {
|
|
19454
|
+
results.push({
|
|
19455
|
+
server: server.name,
|
|
19456
|
+
tools: [],
|
|
19457
|
+
error: err instanceof Error ? err.message : String(err)
|
|
19458
|
+
});
|
|
19459
|
+
} finally {
|
|
19460
|
+
await client.disconnectAll().catch(() => {
|
|
19461
|
+
});
|
|
19462
|
+
}
|
|
19463
|
+
}
|
|
19464
|
+
return results;
|
|
19465
|
+
}
|
|
19466
|
+
|
|
18587
19467
|
// src/retrieval/chunk.ts
|
|
18588
19468
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
18589
19469
|
function splitLongBlock(block, max) {
|
|
@@ -19122,11 +20002,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
19122
20002
|
for (const i of incoming) map.set(key(i), i);
|
|
19123
20003
|
return [...map.values()];
|
|
19124
20004
|
}
|
|
20005
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
20006
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
20007
|
+
let n = 2;
|
|
20008
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
|
|
20009
|
+
return `${desired} ${n}`;
|
|
20010
|
+
}
|
|
20011
|
+
function mergeMcpServers(local, incoming) {
|
|
20012
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
20013
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
20014
|
+
const byName = /* @__PURE__ */ new Map();
|
|
20015
|
+
for (const l of local ?? []) byName.set(l.name, l);
|
|
20016
|
+
const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
|
|
20017
|
+
const synced = [];
|
|
20018
|
+
for (const inc of incoming) {
|
|
20019
|
+
if (byName.has(inc.name)) {
|
|
20020
|
+
byName.set(inc.name, inc);
|
|
20021
|
+
synced.push(inc);
|
|
20022
|
+
continue;
|
|
20023
|
+
}
|
|
20024
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
20025
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
20026
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
20027
|
+
byName.set(name, server);
|
|
20028
|
+
synced.push(server);
|
|
20029
|
+
prefixes.add(mcpServerPrefix(name));
|
|
20030
|
+
}
|
|
20031
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
20032
|
+
}
|
|
20033
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
20034
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
20035
|
+
return tools.map((t) => {
|
|
20036
|
+
for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
|
|
20037
|
+
return t;
|
|
20038
|
+
});
|
|
20039
|
+
}
|
|
20040
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
20041
|
+
const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
|
|
20042
|
+
const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
|
|
20043
|
+
const kept = (local ?? []).filter((t) => !covered(t));
|
|
20044
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
20045
|
+
return merged.length ? merged : void 0;
|
|
20046
|
+
}
|
|
19125
20047
|
function gatherSyncBundle(config) {
|
|
19126
|
-
const bundle = { v:
|
|
20048
|
+
const bundle = { v: 2 };
|
|
19127
20049
|
if (config.providers?.length) bundle.providers = config.providers;
|
|
19128
20050
|
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
19129
20051
|
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
20052
|
+
const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
|
|
20053
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
19130
20054
|
if (config.models) bundle.models = config.models;
|
|
19131
20055
|
if (config.budget) bundle.budget = config.budget;
|
|
19132
20056
|
const prefs = {};
|
|
@@ -19141,7 +20065,16 @@ function applySyncBundle(bundle, config) {
|
|
|
19141
20065
|
const next = { ...config, tools: { ...config.tools } };
|
|
19142
20066
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
19143
20067
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
19144
|
-
|
|
20068
|
+
const mcp = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
|
|
20069
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
|
|
20070
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
20071
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
20072
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
20073
|
+
config.tools?.disabledTools,
|
|
20074
|
+
rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
|
|
20075
|
+
bundle.mcpServers ? mcp.synced : void 0
|
|
20076
|
+
);
|
|
20077
|
+
}
|
|
19145
20078
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
19146
20079
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
19147
20080
|
if (bundle.prefs) {
|
|
@@ -19176,6 +20109,7 @@ exports.CascadeToolError = CascadeToolError;
|
|
|
19176
20109
|
exports.CloudClient = CloudClient;
|
|
19177
20110
|
exports.CodeSearchTool = CodeSearchTool;
|
|
19178
20111
|
exports.ConfigManager = ConfigManager;
|
|
20112
|
+
exports.CurrentPageTool = CurrentPageTool;
|
|
19179
20113
|
exports.DEFAULT_API_PORT = DEFAULT_API_PORT;
|
|
19180
20114
|
exports.DEFAULT_APPROVAL_REQUIRED = DEFAULT_APPROVAL_REQUIRED;
|
|
19181
20115
|
exports.DEFAULT_AUTO_SUMMARIZE_AT = DEFAULT_AUTO_SUMMARIZE_AT;
|
|
@@ -19198,6 +20132,8 @@ exports.HooksRunner = HooksRunner;
|
|
|
19198
20132
|
exports.Keystore = Keystore;
|
|
19199
20133
|
exports.LLMReranker = LLMReranker;
|
|
19200
20134
|
exports.LM_STUDIO_BASE_URL = LM_STUDIO_BASE_URL;
|
|
20135
|
+
exports.MAX_PAGE_TEXT_CHARS = MAX_PAGE_TEXT_CHARS;
|
|
20136
|
+
exports.MCP_TOOL_PREFIX = MCP_TOOL_PREFIX;
|
|
19201
20137
|
exports.MODELS = MODELS;
|
|
19202
20138
|
exports.McpClient = McpClient;
|
|
19203
20139
|
exports.McpOAuthProvider = McpOAuthProvider;
|
|
@@ -19234,7 +20170,9 @@ exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
|
|
|
19234
20170
|
exports.createCascade = createCascade;
|
|
19235
20171
|
exports.decryptSyncBlob = decryptJSON;
|
|
19236
20172
|
exports.diffManifest = diffManifest;
|
|
20173
|
+
exports.disambiguateMcpServerNames = disambiguateMcpServerNames;
|
|
19237
20174
|
exports.discoverMcpAuthServer = discoverMcpAuthServer;
|
|
20175
|
+
exports.discoverMcpTools = discoverMcpTools;
|
|
19238
20176
|
exports.distillSessionFacts = distillSessionFacts;
|
|
19239
20177
|
exports.embedderFromProviders = embedderFromProviders;
|
|
19240
20178
|
exports.encryptSyncBlob = encryptJSON;
|
|
@@ -19243,14 +20181,19 @@ exports.gatherSyncBundle = gatherSyncBundle;
|
|
|
19243
20181
|
exports.hashContent = hashContent;
|
|
19244
20182
|
exports.heuristicCodeChunker = heuristicCodeChunker;
|
|
19245
20183
|
exports.inferAzureBaseModel = inferAzureBaseModel;
|
|
20184
|
+
exports.isMcpToolName = isMcpToolName;
|
|
20185
|
+
exports.mcpServerPrefix = mcpServerPrefix;
|
|
20186
|
+
exports.mcpToolName = mcpToolName;
|
|
19246
20187
|
exports.nodeHttpFetch = nodeHttpFetch;
|
|
19247
20188
|
exports.parseRankOrder = parseRankOrder;
|
|
19248
20189
|
exports.planRetrieval = planRetrieval;
|
|
19249
20190
|
exports.preferIpv4Host = preferIpv4Host;
|
|
19250
20191
|
exports.reciprocalRankFusion = reciprocalRankFusion;
|
|
19251
20192
|
exports.refreshMcpToken = refreshMcpToken;
|
|
20193
|
+
exports.removeMcpServerDenials = removeMcpServerDenials;
|
|
19252
20194
|
exports.runCascade = runCascade;
|
|
19253
20195
|
exports.sessionWorthRemembering = sessionWorthRemembering;
|
|
19254
20196
|
exports.streamCascade = streamCascade;
|
|
20197
|
+
exports.uniqueMcpServerName = uniqueMcpServerName;
|
|
19255
20198
|
//# sourceMappingURL=index.cjs.map
|
|
19256
20199
|
//# sourceMappingURL=index.cjs.map
|