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/cli.cjs
CHANGED
|
@@ -104,6 +104,12 @@ var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
|
104
104
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
105
105
|
var __defProp = Object.defineProperty;
|
|
106
106
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
107
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
108
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
109
|
+
}) : x)(function(x) {
|
|
110
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
111
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
112
|
+
});
|
|
107
113
|
var __esm = (fn, res) => function __init() {
|
|
108
114
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
109
115
|
};
|
|
@@ -1793,7 +1799,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1793
1799
|
var init_constants = __esm({
|
|
1794
1800
|
"src/constants.ts"() {
|
|
1795
1801
|
init_pricing();
|
|
1796
|
-
CASCADE_VERSION = "0.
|
|
1802
|
+
CASCADE_VERSION = "0.55.0";
|
|
1797
1803
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1798
1804
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1799
1805
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -5436,6 +5442,102 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
|
5436
5442
|
}
|
|
5437
5443
|
return merged;
|
|
5438
5444
|
}
|
|
5445
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
5446
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
5447
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
5448
|
+
function boundedSegment(raw) {
|
|
5449
|
+
const clean = sanitizeToolNameSegment(raw);
|
|
5450
|
+
if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
|
|
5451
|
+
const hash = crypto.createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
5452
|
+
return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
5453
|
+
}
|
|
5454
|
+
function isStableSegment(raw) {
|
|
5455
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
5456
|
+
}
|
|
5457
|
+
function sanitizeToolNameSegment(segment) {
|
|
5458
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
5459
|
+
let start = 0;
|
|
5460
|
+
let end = cleaned.length;
|
|
5461
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
5462
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
5463
|
+
return cleaned.slice(start, end) || "_";
|
|
5464
|
+
}
|
|
5465
|
+
function mcpToolName(serverName, toolName) {
|
|
5466
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
5467
|
+
}
|
|
5468
|
+
function isMcpToolName(name) {
|
|
5469
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
5470
|
+
}
|
|
5471
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
5472
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
5473
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
5474
|
+
for (let n = 2; n < 1e3; n++) {
|
|
5475
|
+
const candidate = `${desired} (${n})`;
|
|
5476
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
5477
|
+
}
|
|
5478
|
+
return `${desired} (${crypto.randomUUID().slice(0, 8)})`;
|
|
5479
|
+
}
|
|
5480
|
+
function disambiguateMcpServerNames(servers) {
|
|
5481
|
+
const seenNames = [];
|
|
5482
|
+
const renames = [];
|
|
5483
|
+
const result = servers.map((s) => {
|
|
5484
|
+
const name = uniqueMcpServerName(s.name, seenNames);
|
|
5485
|
+
seenNames.push(name);
|
|
5486
|
+
if (name === s.name) return s;
|
|
5487
|
+
renames.push({ from: s.name, to: name });
|
|
5488
|
+
return { ...s, name };
|
|
5489
|
+
});
|
|
5490
|
+
return { servers: renames.length ? result : servers, renames };
|
|
5491
|
+
}
|
|
5492
|
+
function assignMcpToolNames(tools) {
|
|
5493
|
+
const names = new Array(tools.length);
|
|
5494
|
+
const bases = tools.map((t) => mcpToolName(t.server, t.tool));
|
|
5495
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5496
|
+
bases.forEach((base, i) => {
|
|
5497
|
+
const g = groups.get(base);
|
|
5498
|
+
if (g) g.push(i);
|
|
5499
|
+
else groups.set(base, [i]);
|
|
5500
|
+
});
|
|
5501
|
+
const used = new Set(bases);
|
|
5502
|
+
for (const [base, idx] of groups) {
|
|
5503
|
+
if (idx.length === 1) {
|
|
5504
|
+
names[idx[0]] = base;
|
|
5505
|
+
continue;
|
|
5506
|
+
}
|
|
5507
|
+
const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
|
|
5508
|
+
const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
|
|
5509
|
+
const ordered = [...idx].sort((a, b) => {
|
|
5510
|
+
const la = isClean(a) ? 0 : 1;
|
|
5511
|
+
const lb = isClean(b) ? 0 : 1;
|
|
5512
|
+
if (la !== lb) return la - lb;
|
|
5513
|
+
const ra = raw(a);
|
|
5514
|
+
const rb = raw(b);
|
|
5515
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
5516
|
+
});
|
|
5517
|
+
names[ordered[0]] = base;
|
|
5518
|
+
let n = 2;
|
|
5519
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
5520
|
+
let candidate = `${base}_${n}`;
|
|
5521
|
+
while (used.has(candidate)) {
|
|
5522
|
+
n++;
|
|
5523
|
+
candidate = `${base}_${n}`;
|
|
5524
|
+
}
|
|
5525
|
+
names[ordered[rank3]] = candidate;
|
|
5526
|
+
used.add(candidate);
|
|
5527
|
+
n++;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
return names;
|
|
5531
|
+
}
|
|
5532
|
+
function mcpServerPrefix(serverName) {
|
|
5533
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
5534
|
+
}
|
|
5535
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
5536
|
+
if (!disabledTools?.length) return disabledTools;
|
|
5537
|
+
const prefix = mcpServerPrefix(serverName);
|
|
5538
|
+
const kept = disabledTools.filter((t) => !t.startsWith(prefix));
|
|
5539
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
5540
|
+
}
|
|
5439
5541
|
|
|
5440
5542
|
// src/config/index.ts
|
|
5441
5543
|
init_constants();
|
|
@@ -5459,6 +5561,13 @@ var ConfigManager = class {
|
|
|
5459
5561
|
}
|
|
5460
5562
|
async load() {
|
|
5461
5563
|
this.config = await this.loadConfig();
|
|
5564
|
+
const servers = this.config.tools?.mcpServers;
|
|
5565
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
5566
|
+
const mcpNamesChanged = renames.length > 0;
|
|
5567
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
5568
|
+
this.config.tools.mcpServers = disambiguated;
|
|
5569
|
+
this.renameMcpServerReferences(renames);
|
|
5570
|
+
}
|
|
5462
5571
|
this.ignore = new CascadeIgnore();
|
|
5463
5572
|
await this.ignore.load(this.workspacePath);
|
|
5464
5573
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -5467,6 +5576,7 @@ var ConfigManager = class {
|
|
|
5467
5576
|
await this.injectEnvKeys();
|
|
5468
5577
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5469
5578
|
await this.ensureDefaultIdentity();
|
|
5579
|
+
if (mcpNamesChanged) await this.save();
|
|
5470
5580
|
}
|
|
5471
5581
|
getConfig() {
|
|
5472
5582
|
return this.config;
|
|
@@ -5550,6 +5660,66 @@ var ConfigManager = class {
|
|
|
5550
5660
|
this.config.providers.push({ type: "ollama" });
|
|
5551
5661
|
}
|
|
5552
5662
|
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
5665
|
+
*
|
|
5666
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
5667
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
5668
|
+
*
|
|
5669
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
5670
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
5671
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
5672
|
+
* move with it. A plain rewrite is correct here.
|
|
5673
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
5674
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
5675
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
5676
|
+
* already been updated to the disambiguated list by the time this runs
|
|
5677
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
5678
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
5679
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
5680
|
+
* AND granted to the renamed identity, not moved.
|
|
5681
|
+
*
|
|
5682
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
5683
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
5684
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
5685
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
5686
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
5687
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
5688
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
5689
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
5690
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
5691
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
5692
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
5693
|
+
* together, in the one pass that still sees the original entry.
|
|
5694
|
+
*
|
|
5695
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
5696
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
5697
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
5698
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
5699
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
5700
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
5701
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
5702
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
5703
|
+
* would do the opposite — move a denial that already worked for the
|
|
5704
|
+
* survivor onto a server it may never have meant to cover.
|
|
5705
|
+
*/
|
|
5706
|
+
renameMcpServerReferences(renames) {
|
|
5707
|
+
const tools = this.config.tools;
|
|
5708
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
5709
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
|
|
5710
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
5711
|
+
for (const { from, to } of renames) {
|
|
5712
|
+
const tos = byFrom.get(from);
|
|
5713
|
+
if (tos) tos.push(to);
|
|
5714
|
+
else byFrom.set(from, [to]);
|
|
5715
|
+
}
|
|
5716
|
+
let trusted = tools.mcpTrusted;
|
|
5717
|
+
for (const [from, tos] of byFrom) {
|
|
5718
|
+
if (!trusted.includes(from)) continue;
|
|
5719
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
|
|
5720
|
+
}
|
|
5721
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
5722
|
+
}
|
|
5553
5723
|
async ensureDefaultIdentity() {
|
|
5554
5724
|
const existing = this.store.getDefaultIdentity();
|
|
5555
5725
|
if (existing) return;
|
|
@@ -5565,26 +5735,6 @@ var ConfigManager = class {
|
|
|
5565
5735
|
}
|
|
5566
5736
|
};
|
|
5567
5737
|
|
|
5568
|
-
// src/tools/tool-name.ts
|
|
5569
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
5570
|
-
function sanitizeToolNameSegment(segment) {
|
|
5571
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
5572
|
-
let start = 0;
|
|
5573
|
-
let end = cleaned.length;
|
|
5574
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
5575
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
5576
|
-
return cleaned.slice(start, end) || "_";
|
|
5577
|
-
}
|
|
5578
|
-
function mcpToolName(serverName, toolName) {
|
|
5579
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
5580
|
-
}
|
|
5581
|
-
function isMcpToolName(name) {
|
|
5582
|
-
return name.startsWith(MCP_TOOL_PREFIX);
|
|
5583
|
-
}
|
|
5584
|
-
function mcpServerPrefix(serverName) {
|
|
5585
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5588
5738
|
// src/cli/utils/terminal-input.ts
|
|
5589
5739
|
var PASTE_START = "\x1B[200~";
|
|
5590
5740
|
var PASTE_END = "\x1B[201~";
|
|
@@ -5909,7 +6059,21 @@ var ModelSelector = class {
|
|
|
5909
6059
|
this.availableProviders = availableProviders;
|
|
5910
6060
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
5911
6061
|
}
|
|
6062
|
+
/**
|
|
6063
|
+
* Verdicts from earlier runs about ids that 404 on this account. Consulted
|
|
6064
|
+
* BEFORE a model enters the pool, which is the only placement that helps: a
|
|
6065
|
+
* T3 wave runs concurrently, so filtering after the first failure still lets
|
|
6066
|
+
* every other worker in the wave call the dead id.
|
|
6067
|
+
*/
|
|
6068
|
+
deadModels;
|
|
6069
|
+
setDeadModelStore(store) {
|
|
6070
|
+
this.deadModels = store;
|
|
6071
|
+
for (const [id, m] of [...this.availableModels]) {
|
|
6072
|
+
if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
5912
6075
|
addDynamicModel(model) {
|
|
6076
|
+
if (this.deadModels?.isDead(model.provider, model.id)) return;
|
|
5913
6077
|
this.availableModels.set(model.id, model);
|
|
5914
6078
|
if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
|
|
5915
6079
|
}
|
|
@@ -6316,6 +6480,121 @@ var LocalRequestQueue = class {
|
|
|
6316
6480
|
}
|
|
6317
6481
|
};
|
|
6318
6482
|
|
|
6483
|
+
// src/core/router/dead-models.ts
|
|
6484
|
+
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6485
|
+
function deadModelKey(provider, modelId) {
|
|
6486
|
+
return `${provider}:${modelId}`;
|
|
6487
|
+
}
|
|
6488
|
+
var DeadModelStore = class {
|
|
6489
|
+
constructor(persistence, opts = {}) {
|
|
6490
|
+
this.persistence = persistence;
|
|
6491
|
+
this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
|
|
6492
|
+
this.now = opts.now ?? (() => Date.now());
|
|
6493
|
+
this.restore();
|
|
6494
|
+
}
|
|
6495
|
+
persistence;
|
|
6496
|
+
records = /* @__PURE__ */ new Map();
|
|
6497
|
+
ttlMs;
|
|
6498
|
+
now;
|
|
6499
|
+
restore() {
|
|
6500
|
+
if (!this.persistence) return;
|
|
6501
|
+
try {
|
|
6502
|
+
for (const r of this.persistence.load()) {
|
|
6503
|
+
if (!this.isExpired(r)) this.records.set(r.key, r);
|
|
6504
|
+
}
|
|
6505
|
+
} catch {
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
isExpired(r) {
|
|
6509
|
+
return this.now() - r.lastSeenAt > this.ttlMs;
|
|
6510
|
+
}
|
|
6511
|
+
flush() {
|
|
6512
|
+
if (!this.persistence) return;
|
|
6513
|
+
try {
|
|
6514
|
+
this.persistence.save([...this.records.values()]);
|
|
6515
|
+
} catch {
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
/**
|
|
6519
|
+
* Record that a model was found dead. Returns true when this is the first
|
|
6520
|
+
* time (so the caller can log it once rather than once per concurrent
|
|
6521
|
+
* worker in the wave that discovered it).
|
|
6522
|
+
*/
|
|
6523
|
+
record(provider, modelId, reason) {
|
|
6524
|
+
const key = deadModelKey(provider, modelId);
|
|
6525
|
+
const now = this.now();
|
|
6526
|
+
const existing = this.records.get(key);
|
|
6527
|
+
if (existing && !this.isExpired(existing)) {
|
|
6528
|
+
existing.lastSeenAt = now;
|
|
6529
|
+
existing.hits += 1;
|
|
6530
|
+
this.flush();
|
|
6531
|
+
return false;
|
|
6532
|
+
}
|
|
6533
|
+
this.records.set(key, {
|
|
6534
|
+
key,
|
|
6535
|
+
provider,
|
|
6536
|
+
modelId,
|
|
6537
|
+
reason,
|
|
6538
|
+
firstSeenAt: now,
|
|
6539
|
+
lastSeenAt: now,
|
|
6540
|
+
hits: 1
|
|
6541
|
+
});
|
|
6542
|
+
this.flush();
|
|
6543
|
+
return true;
|
|
6544
|
+
}
|
|
6545
|
+
/** True when this model is known dead and the verdict has not expired. */
|
|
6546
|
+
isDead(provider, modelId) {
|
|
6547
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
6548
|
+
if (!r) return false;
|
|
6549
|
+
if (this.isExpired(r)) {
|
|
6550
|
+
this.records.delete(r.key);
|
|
6551
|
+
this.flush();
|
|
6552
|
+
return false;
|
|
6553
|
+
}
|
|
6554
|
+
return true;
|
|
6555
|
+
}
|
|
6556
|
+
/** Why a model is considered dead, for /why and the models command. */
|
|
6557
|
+
reasonFor(provider, modelId) {
|
|
6558
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
6559
|
+
return r && !this.isExpired(r) ? r.reason : null;
|
|
6560
|
+
}
|
|
6561
|
+
/** Live records, expired ones filtered out. */
|
|
6562
|
+
list() {
|
|
6563
|
+
const live = [...this.records.values()].filter((r) => !this.isExpired(r));
|
|
6564
|
+
return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
6565
|
+
}
|
|
6566
|
+
/**
|
|
6567
|
+
* Forget a verdict — used when the user fixes the underlying problem (adds
|
|
6568
|
+
* the model to their project, gets quota) and wants it retried now rather
|
|
6569
|
+
* than after the TTL.
|
|
6570
|
+
*/
|
|
6571
|
+
forget(provider, modelId) {
|
|
6572
|
+
const removed = this.records.delete(deadModelKey(provider, modelId));
|
|
6573
|
+
if (removed) this.flush();
|
|
6574
|
+
return removed;
|
|
6575
|
+
}
|
|
6576
|
+
clear() {
|
|
6577
|
+
this.records.clear();
|
|
6578
|
+
this.flush();
|
|
6579
|
+
}
|
|
6580
|
+
};
|
|
6581
|
+
function fileDeadModelPersistence(filePath) {
|
|
6582
|
+
return {
|
|
6583
|
+
load() {
|
|
6584
|
+
const fs32 = __require("fs");
|
|
6585
|
+
if (!fs32.existsSync(filePath)) return [];
|
|
6586
|
+
const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
|
|
6587
|
+
return Array.isArray(raw) ? raw : [];
|
|
6588
|
+
},
|
|
6589
|
+
save(records) {
|
|
6590
|
+
const fs32 = __require("fs");
|
|
6591
|
+
const path37 = __require("path");
|
|
6592
|
+
fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
|
|
6593
|
+
fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6594
|
+
}
|
|
6595
|
+
};
|
|
6596
|
+
}
|
|
6597
|
+
|
|
6319
6598
|
// src/core/router/index.ts
|
|
6320
6599
|
init_constants();
|
|
6321
6600
|
init_cost();
|
|
@@ -6901,10 +7180,47 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
6901
7180
|
*/
|
|
6902
7181
|
budgetState = "ok";
|
|
6903
7182
|
budgetExceededReason;
|
|
7183
|
+
/**
|
|
7184
|
+
* Whether the run/session budget is permanently tripped, and why.
|
|
7185
|
+
*
|
|
7186
|
+
* A tier or worker that catches `BudgetExceededError` internally (T3Worker
|
|
7187
|
+
* does, so one dead-end subtask doesn't crash the whole run) absorbs the
|
|
7188
|
+
* exception into a normal FAILED/PARTIAL result — the tier returns as if
|
|
7189
|
+
* nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
|
|
7190
|
+
* budget-exceeded handling (the `run:budget-exceeded` event and
|
|
7191
|
+
* `lastInterruptedRun` for `/continue`). This lets a caller check the
|
|
7192
|
+
* router's own state directly after a tier returns, regardless of how that
|
|
7193
|
+
* tier chose to report the error internally.
|
|
7194
|
+
*/
|
|
7195
|
+
budgetExceededInfo() {
|
|
7196
|
+
if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
|
|
7197
|
+
if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
|
|
7198
|
+
return null;
|
|
7199
|
+
}
|
|
6904
7200
|
tpmLimiter;
|
|
6905
7201
|
localQueue;
|
|
6906
7202
|
taskAnalyzer;
|
|
6907
7203
|
pendingFeedbackSource;
|
|
7204
|
+
/** Models known dead for this account (404 / retired / not enabled). */
|
|
7205
|
+
deadModels = new DeadModelStore();
|
|
7206
|
+
/**
|
|
7207
|
+
* Back the dead-model memory with durable storage (a JSON file on desktop, a
|
|
7208
|
+
* per-user table in the cloud). Without one the verdicts still work, but only
|
|
7209
|
+
* for the life of the process — so tomorrow's run pays the same wave of 404s
|
|
7210
|
+
* to learn the same fact.
|
|
7211
|
+
*/
|
|
7212
|
+
setDeadModelStore(store) {
|
|
7213
|
+
this.deadModels = store;
|
|
7214
|
+
this.selector.setDeadModelStore(store);
|
|
7215
|
+
}
|
|
7216
|
+
/** Models known dead for this account, for `cascade models` and /why. */
|
|
7217
|
+
listDeadModels() {
|
|
7218
|
+
return this.deadModels.list();
|
|
7219
|
+
}
|
|
7220
|
+
/** Retry a model the user has since fixed, without waiting out the TTL. */
|
|
7221
|
+
forgetDeadModel(provider, modelId) {
|
|
7222
|
+
return this.deadModels.forget(provider, modelId);
|
|
7223
|
+
}
|
|
6908
7224
|
worldStateDB;
|
|
6909
7225
|
privacyPaths;
|
|
6910
7226
|
guidanceQueue;
|
|
@@ -7269,6 +7585,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7269
7585
|
}
|
|
7270
7586
|
if (isModelNotFoundError(errMsg)) {
|
|
7271
7587
|
this.selector.removeModel(model.id);
|
|
7588
|
+
const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
|
|
7589
|
+
if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
|
|
7272
7590
|
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
7273
7591
|
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
7274
7592
|
if (next && next.id !== model.id) {
|
|
@@ -8537,6 +8855,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
8537
8855
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
8538
8856
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
8539
8857
|
}
|
|
8858
|
+
if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
8859
|
+
issues.push(errMsg);
|
|
8860
|
+
const stopped = output || errMsg;
|
|
8861
|
+
this.setStatus("FAILED", stopped);
|
|
8862
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
|
|
8863
|
+
return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
8864
|
+
}
|
|
8540
8865
|
issues.push(`Execution error: ${errMsg}`);
|
|
8541
8866
|
const finalOutput = output || errMsg;
|
|
8542
8867
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -9558,6 +9883,26 @@ var RedactionLayer = class {
|
|
|
9558
9883
|
}
|
|
9559
9884
|
};
|
|
9560
9885
|
|
|
9886
|
+
// src/core/tiers/escalation-policy.ts
|
|
9887
|
+
function sectionNeedsDecision(results) {
|
|
9888
|
+
return results.some((r) => r.status === "ESCALATED");
|
|
9889
|
+
}
|
|
9890
|
+
function settledEscalationStatus(results) {
|
|
9891
|
+
const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
|
|
9892
|
+
return hasWork ? "PARTIAL" : "FAILED";
|
|
9893
|
+
}
|
|
9894
|
+
function composeRootSectionOutput(t2Result, completedOutputs) {
|
|
9895
|
+
let output = `${t2Result.sectionSummary}
|
|
9896
|
+
|
|
9897
|
+
${completedOutputs.join("\n\n")}`;
|
|
9898
|
+
if (t2Result.status === "FAILED" && t2Result.issues?.length) {
|
|
9899
|
+
output += `
|
|
9900
|
+
|
|
9901
|
+
_(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
9902
|
+
}
|
|
9903
|
+
return output;
|
|
9904
|
+
}
|
|
9905
|
+
|
|
9561
9906
|
// src/core/tiers/t2-manager.ts
|
|
9562
9907
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
9563
9908
|
return [
|
|
@@ -9583,6 +9928,19 @@ var T2Manager = class extends BaseTier {
|
|
|
9583
9928
|
t2PeerBus;
|
|
9584
9929
|
permissionEscalator;
|
|
9585
9930
|
toolCreator;
|
|
9931
|
+
/**
|
|
9932
|
+
* Asked when a section ends ESCALATED — i.e. a worker hit something it could
|
|
9933
|
+
* not decide alone. Without this the escalation was a dead end: the status
|
|
9934
|
+
* said "needs a decision" and no decision was ever requested, so an MCP run
|
|
9935
|
+
* that legitimately needed input just stopped there.
|
|
9936
|
+
*/
|
|
9937
|
+
escalationCallback;
|
|
9938
|
+
/**
|
|
9939
|
+
* The one allowed retry has been spent. Retrying is bounded because an
|
|
9940
|
+
* escalation loop that can re-ask forever burns a budget on a question that
|
|
9941
|
+
* is not resolving; a second escalation is terminal instead.
|
|
9942
|
+
*/
|
|
9943
|
+
escalationRetryUsed = false;
|
|
9586
9944
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
9587
9945
|
planApprovalCallback;
|
|
9588
9946
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -9631,6 +9989,10 @@ var T2Manager = class extends BaseTier {
|
|
|
9631
9989
|
setToolCreator(creator) {
|
|
9632
9990
|
this.toolCreator = creator;
|
|
9633
9991
|
}
|
|
9992
|
+
/** Ask the user what to do when a section escalates. */
|
|
9993
|
+
setEscalationCallback(cb) {
|
|
9994
|
+
this.escalationCallback = cb;
|
|
9995
|
+
}
|
|
9634
9996
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
9635
9997
|
setPlanApprovalCallback(cb) {
|
|
9636
9998
|
this.planApprovalCallback = cb;
|
|
@@ -9735,9 +10097,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9735
10097
|
currentAction: "Aggregating T3 results",
|
|
9736
10098
|
status: "IN_PROGRESS"
|
|
9737
10099
|
});
|
|
9738
|
-
|
|
10100
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
9739
10101
|
const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
|
|
9740
|
-
|
|
10102
|
+
let overallStatus = this.determineStatus(t3Results);
|
|
10103
|
+
let userSkipped = false;
|
|
10104
|
+
const hasEscalated = sectionNeedsDecision(t3Results);
|
|
10105
|
+
const settleEscalated = async (reason) => {
|
|
10106
|
+
if (reason) issues.push(reason);
|
|
10107
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
10108
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
10109
|
+
};
|
|
10110
|
+
if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
|
|
10111
|
+
await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
|
|
10112
|
+
} else if (hasEscalated && !this.escalationCallback) {
|
|
10113
|
+
await settleEscalated();
|
|
10114
|
+
} else if (hasEscalated && this.escalationCallback) {
|
|
10115
|
+
this.sendStatusUpdate({
|
|
10116
|
+
progressPct: 95,
|
|
10117
|
+
currentAction: "Escalated \u2014 waiting for your decision",
|
|
10118
|
+
status: "ESCALATING"
|
|
10119
|
+
});
|
|
10120
|
+
const decision = await this.escalationCallback({
|
|
10121
|
+
sectionId: assignment.sectionId,
|
|
10122
|
+
sectionTitle: assignment.sectionTitle,
|
|
10123
|
+
issues,
|
|
10124
|
+
summary
|
|
10125
|
+
});
|
|
10126
|
+
this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
|
|
10127
|
+
if (decision.action === "skip") {
|
|
10128
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
10129
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
10130
|
+
userSkipped = decision.automatic !== true;
|
|
10131
|
+
} else if (decision.action === "retry" || decision.action === "guidance") {
|
|
10132
|
+
this.escalationRetryUsed = true;
|
|
10133
|
+
const guided = decision.action === "guidance" && decision.note?.trim() ? {
|
|
10134
|
+
...assignment,
|
|
10135
|
+
description: `${assignment.description}
|
|
10136
|
+
|
|
10137
|
+
Guidance (must be followed): ${decision.note.trim()}`,
|
|
10138
|
+
// Drop T1's preplanned subtasks so the retry re-decomposes with
|
|
10139
|
+
// the guidance in hand. The workers execute from their subtask
|
|
10140
|
+
// slices, not from this description — leaving the slices in
|
|
10141
|
+
// place gave "Retry with guidance" byte-identical worker
|
|
10142
|
+
// prompts to "Retry as-is", making the note do nothing at all.
|
|
10143
|
+
t3Subtasks: []
|
|
10144
|
+
} : assignment;
|
|
10145
|
+
return await this.execute(guided, taskId, signal);
|
|
10146
|
+
} else {
|
|
10147
|
+
issues.push("Escalated, but no decision was received in time.");
|
|
10148
|
+
overallStatus = "FAILED";
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
9741
10151
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9742
10152
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9743
10153
|
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";
|
|
@@ -9748,7 +10158,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9748
10158
|
status: overallStatus,
|
|
9749
10159
|
t3Results,
|
|
9750
10160
|
sectionSummary: summary,
|
|
9751
|
-
issues
|
|
10161
|
+
issues,
|
|
10162
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
9752
10163
|
};
|
|
9753
10164
|
this.publishSectionOutput(result);
|
|
9754
10165
|
return result;
|
|
@@ -10182,8 +10593,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
10182
10593
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
10183
10594
|
);
|
|
10184
10595
|
}
|
|
10185
|
-
async aggregateResults(assignment, results) {
|
|
10186
|
-
const completed = results.filter(
|
|
10596
|
+
async aggregateResults(assignment, results, opts = {}) {
|
|
10597
|
+
const completed = results.filter(
|
|
10598
|
+
(r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
|
|
10599
|
+
);
|
|
10187
10600
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
10188
10601
|
const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
|
|
10189
10602
|
const peerContext = peerOutputs ? `
|
|
@@ -10392,6 +10805,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10392
10805
|
escalations = [];
|
|
10393
10806
|
store;
|
|
10394
10807
|
runBreaker;
|
|
10808
|
+
escalationCallback;
|
|
10395
10809
|
t2PeerBus = new PeerBus();
|
|
10396
10810
|
permissionEscalator;
|
|
10397
10811
|
toolCreator;
|
|
@@ -10406,6 +10820,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10406
10820
|
this.toolRegistry = toolRegistry;
|
|
10407
10821
|
this.config = config;
|
|
10408
10822
|
}
|
|
10823
|
+
/** Ask the user what to do when any section escalates. */
|
|
10824
|
+
setEscalationCallback(cb) {
|
|
10825
|
+
this.escalationCallback = cb;
|
|
10826
|
+
}
|
|
10409
10827
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
10410
10828
|
setRunBreaker(breaker) {
|
|
10411
10829
|
this.runBreaker = breaker;
|
|
@@ -10565,8 +10983,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
10565
10983
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
|
|
10566
10984
|
};
|
|
10567
10985
|
}
|
|
10568
|
-
const sectionsText = t2Results.map((r) =>
|
|
10569
|
-
|
|
10986
|
+
const sectionsText = t2Results.map((r) => {
|
|
10987
|
+
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.]" : "";
|
|
10988
|
+
return `**${r.sectionTitle}**
|
|
10989
|
+
${r.sectionSummary}${skipNote}`;
|
|
10990
|
+
}).join("\n\n");
|
|
10570
10991
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
10571
10992
|
Review the following execution outputs against the original user prompt.
|
|
10572
10993
|
|
|
@@ -10576,6 +10997,7 @@ T2 Manager Summaries:
|
|
|
10576
10997
|
${sectionsText}
|
|
10577
10998
|
|
|
10578
10999
|
Does the current state of the workspace and the outputs fully satisfy the user's request?
|
|
11000
|
+
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.
|
|
10579
11001
|
If yes, reply with exactly: "APPROVED".
|
|
10580
11002
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
10581
11003
|
try {
|
|
@@ -10773,6 +11195,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10773
11195
|
const managers = sections.map((section) => {
|
|
10774
11196
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10775
11197
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
11198
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
10776
11199
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10777
11200
|
if (this.store) {
|
|
10778
11201
|
manager.setStore(this.store);
|
|
@@ -10982,7 +11405,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10982
11405
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
10983
11406
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
10984
11407
|
if (!completedSections.length) {
|
|
10985
|
-
const allIssues =
|
|
11408
|
+
const allIssues = [
|
|
11409
|
+
...t2Results.flatMap((r) => r.issues ?? []),
|
|
11410
|
+
...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
|
|
11411
|
+
].filter(Boolean);
|
|
10986
11412
|
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
10987
11413
|
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
10988
11414
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -11072,9 +11498,16 @@ var CAPABILITIES = [
|
|
|
11072
11498
|
api: "openai-images"
|
|
11073
11499
|
},
|
|
11074
11500
|
{
|
|
11501
|
+
// Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
|
|
11502
|
+
// page but 404s on `:predict` ("not found for API version v1beta, or is not
|
|
11503
|
+
// supported for predict") — the Gemini image models are called through
|
|
11504
|
+
// generateContent with an image response modality, while `:predict` is the
|
|
11505
|
+
// Imagen shape this executor implements. A billing line item is not proof
|
|
11506
|
+
// of a callable endpoint, which is the assumption that put the wrong id
|
|
11507
|
+
// here in the first place.
|
|
11075
11508
|
modality: "image",
|
|
11076
11509
|
provider: "gemini",
|
|
11077
|
-
modelId: "
|
|
11510
|
+
modelId: "imagen-4.0-generate-001",
|
|
11078
11511
|
api: "gemini-predict"
|
|
11079
11512
|
},
|
|
11080
11513
|
// ── Speech (text → audio) ──
|
|
@@ -13214,12 +13647,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
13214
13647
|
mcpClient;
|
|
13215
13648
|
serverName;
|
|
13216
13649
|
toolName;
|
|
13217
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
13650
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
13218
13651
|
super();
|
|
13219
13652
|
this.mcpClient = mcpClient;
|
|
13220
13653
|
this.serverName = serverName;
|
|
13221
13654
|
this.toolName = toolName;
|
|
13222
|
-
this.name = mcpToolName(serverName, toolName);
|
|
13655
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
13223
13656
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
13224
13657
|
this.inputSchema = inputSchema;
|
|
13225
13658
|
}
|
|
@@ -13290,22 +13723,35 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
13290
13723
|
getRegisteredPlugins() {
|
|
13291
13724
|
return Array.from(this.plugins.keys());
|
|
13292
13725
|
}
|
|
13293
|
-
/**
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13726
|
+
/**
|
|
13727
|
+
* Registers all tools from an MCP client.
|
|
13728
|
+
*
|
|
13729
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
13730
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
13731
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
13732
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
13733
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
13734
|
+
* never counted against the tool budget, and never has to be refused.
|
|
13735
|
+
*/
|
|
13736
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
13737
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
13738
|
+
const [, server, tool] = def.name.split("::");
|
|
13739
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
13740
|
+
});
|
|
13741
|
+
const names = assignMcpToolNames(parsed);
|
|
13742
|
+
parsed.forEach(({ def, server, tool }, i) => {
|
|
13299
13743
|
const wrapper = new McpToolWrapper(
|
|
13300
13744
|
mcpClient,
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
def.description.replace(`[MCP:${
|
|
13304
|
-
def.inputSchema
|
|
13745
|
+
server,
|
|
13746
|
+
tool,
|
|
13747
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
13748
|
+
def.inputSchema,
|
|
13749
|
+
names[i]
|
|
13305
13750
|
);
|
|
13751
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
13306
13752
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
13307
13753
|
this.register(wrapper);
|
|
13308
|
-
}
|
|
13754
|
+
});
|
|
13309
13755
|
}
|
|
13310
13756
|
setIgnoredPaths(patterns) {
|
|
13311
13757
|
this.ignoreMatcher = ignore2().add(patterns);
|
|
@@ -13642,6 +14088,17 @@ function fileOAuthProvider(storePath) {
|
|
|
13642
14088
|
}
|
|
13643
14089
|
});
|
|
13644
14090
|
}
|
|
14091
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
14092
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
14093
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
14094
|
+
const run = prev.then(fn, fn);
|
|
14095
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
14096
|
+
oauthStoreLocks.set(storePath, tail);
|
|
14097
|
+
void tail.then(() => {
|
|
14098
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
14099
|
+
});
|
|
14100
|
+
return run;
|
|
14101
|
+
}
|
|
13645
14102
|
async function connectMcpWithLoopbackOAuth(opts) {
|
|
13646
14103
|
const listener = await startCallbackListener(opts);
|
|
13647
14104
|
const transport = new streamableHttp_js.StreamableHTTPClientTransport(new URL(opts.serverUrl), {
|
|
@@ -16122,7 +16579,50 @@ var GuidanceQueue = class {
|
|
|
16122
16579
|
}
|
|
16123
16580
|
};
|
|
16124
16581
|
|
|
16582
|
+
// src/tools/current-page.ts
|
|
16583
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
16584
|
+
var CurrentPageTool = class extends BaseTool {
|
|
16585
|
+
name = "read_current_page";
|
|
16586
|
+
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.';
|
|
16587
|
+
inputSchema = {
|
|
16588
|
+
type: "object",
|
|
16589
|
+
properties: {},
|
|
16590
|
+
required: []
|
|
16591
|
+
};
|
|
16592
|
+
provider;
|
|
16593
|
+
constructor(provider) {
|
|
16594
|
+
super();
|
|
16595
|
+
this.provider = provider;
|
|
16596
|
+
}
|
|
16597
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
16598
|
+
// nothing they have not already opened themselves.
|
|
16599
|
+
isDangerous() {
|
|
16600
|
+
return false;
|
|
16601
|
+
}
|
|
16602
|
+
async execute(_input, _options) {
|
|
16603
|
+
let page2;
|
|
16604
|
+
try {
|
|
16605
|
+
page2 = await this.provider();
|
|
16606
|
+
} catch (err) {
|
|
16607
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
16608
|
+
}
|
|
16609
|
+
if (!page2) {
|
|
16610
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
16611
|
+
}
|
|
16612
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
16613
|
+
|
|
16614
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
16615
|
+
return [
|
|
16616
|
+
`URL: ${page2.url}`,
|
|
16617
|
+
`Title: ${page2.title}`,
|
|
16618
|
+
"",
|
|
16619
|
+
text.trim() || "[the page rendered no readable text]"
|
|
16620
|
+
].join("\n");
|
|
16621
|
+
}
|
|
16622
|
+
};
|
|
16623
|
+
|
|
16125
16624
|
// src/core/cascade.ts
|
|
16625
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
16126
16626
|
function buildContextualPrompt(prompt, history = []) {
|
|
16127
16627
|
const recent = history.slice(-6);
|
|
16128
16628
|
if (recent.length === 0) return prompt;
|
|
@@ -16177,6 +16677,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16177
16677
|
}
|
|
16178
16678
|
});
|
|
16179
16679
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16680
|
+
try {
|
|
16681
|
+
const nodePath = __require("path");
|
|
16682
|
+
this.router.setDeadModelStore(new DeadModelStore(
|
|
16683
|
+
fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
|
|
16684
|
+
));
|
|
16685
|
+
} catch {
|
|
16686
|
+
}
|
|
16180
16687
|
this.multimodal = new MultimodalRegistry(
|
|
16181
16688
|
(this.config.providers ?? []).map((p) => p.type)
|
|
16182
16689
|
);
|
|
@@ -16338,6 +16845,94 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16338
16845
|
});
|
|
16339
16846
|
});
|
|
16340
16847
|
}
|
|
16848
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
16849
|
+
//
|
|
16850
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
16851
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
16852
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
16853
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
16854
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
16855
|
+
//
|
|
16856
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
16857
|
+
// timeout because the plan is already the model's considered proposal;
|
|
16858
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
16859
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
16860
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
16861
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
16862
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
16863
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
16864
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
16865
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
16866
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
16867
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
16868
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
16869
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
16870
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
16871
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
16872
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
16873
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
16874
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
16875
|
+
const requestId = crypto.randomUUID();
|
|
16876
|
+
return await new Promise((resolve2) => {
|
|
16877
|
+
let timeout;
|
|
16878
|
+
const settle = (decision) => {
|
|
16879
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
16880
|
+
if (timeout) clearTimeout(timeout);
|
|
16881
|
+
signal?.removeEventListener("abort", onAbort);
|
|
16882
|
+
resolve2(decision);
|
|
16883
|
+
};
|
|
16884
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
16885
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
16886
|
+
timeout = setTimeout(() => {
|
|
16887
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
16888
|
+
settle({ action: "timeout" });
|
|
16889
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
16890
|
+
this.pendingEscalations.set(requestId, settle);
|
|
16891
|
+
this.emit("escalation:decision-required", {
|
|
16892
|
+
taskId,
|
|
16893
|
+
requestId,
|
|
16894
|
+
sectionId: ctx.sectionId,
|
|
16895
|
+
sectionTitle: ctx.sectionTitle,
|
|
16896
|
+
issues: ctx.issues,
|
|
16897
|
+
summary: ctx.summary,
|
|
16898
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
16899
|
+
});
|
|
16900
|
+
});
|
|
16901
|
+
}
|
|
16902
|
+
/**
|
|
16903
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
16904
|
+
*
|
|
16905
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
16906
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
16907
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
16908
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
16909
|
+
* which is correct whenever there is exactly one.
|
|
16910
|
+
*
|
|
16911
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
16912
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
16913
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
16914
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
16915
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
16916
|
+
* see EscalationDecision.automatic.
|
|
16917
|
+
*/
|
|
16918
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
16919
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
16920
|
+
if (requestId) {
|
|
16921
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
16922
|
+
return;
|
|
16923
|
+
}
|
|
16924
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
16925
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
16926
|
+
}
|
|
16927
|
+
/**
|
|
16928
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
16929
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
16930
|
+
* same system-decided case as the early-return skips above, not a person
|
|
16931
|
+
* reviewing and accepting that section's output.
|
|
16932
|
+
*/
|
|
16933
|
+
releasePendingEscalations(action = "skip") {
|
|
16934
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
16935
|
+
}
|
|
16341
16936
|
/**
|
|
16342
16937
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
16343
16938
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -16471,6 +17066,20 @@ ${input}` }],
|
|
|
16471
17066
|
this.mediaSink = sink;
|
|
16472
17067
|
this.registerMediaTools(this.workspacePath);
|
|
16473
17068
|
}
|
|
17069
|
+
/**
|
|
17070
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
17071
|
+
*
|
|
17072
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
17073
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
17074
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
17075
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
17076
|
+
* touch the machine, and reading a page the user already opened touches
|
|
17077
|
+
* nothing. `disabledTools` still removes it.
|
|
17078
|
+
*/
|
|
17079
|
+
setCurrentPageProvider(provider) {
|
|
17080
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
17081
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
17082
|
+
}
|
|
16474
17083
|
registerMediaTools(workspacePath) {
|
|
16475
17084
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
16476
17085
|
const permitted = (name) => !denied.has(name);
|
|
@@ -16603,8 +17212,12 @@ ${last.partialOutput}` : "");
|
|
|
16603
17212
|
for (const server of this.config.tools.mcpServers) {
|
|
16604
17213
|
try {
|
|
16605
17214
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
16606
|
-
|
|
16607
|
-
|
|
17215
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
17216
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
17217
|
+
this.toolRegistry.registerMcpTools(
|
|
17218
|
+
this.mcpClient,
|
|
17219
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
17220
|
+
);
|
|
16608
17221
|
} catch (err) {
|
|
16609
17222
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
16610
17223
|
}
|
|
@@ -17069,6 +17682,7 @@ ${prompt}` : prompt;
|
|
|
17069
17682
|
} else if (complexity === "Moderate") {
|
|
17070
17683
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
17071
17684
|
t2.setRunBreaker(runBreaker);
|
|
17685
|
+
t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
17072
17686
|
t2.setPresenter(true);
|
|
17073
17687
|
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.");
|
|
17074
17688
|
if (identityPrompt) {
|
|
@@ -17113,11 +17727,17 @@ ${prompt}` : prompt;
|
|
|
17113
17727
|
t2Results = [t2Result];
|
|
17114
17728
|
const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
|
|
17115
17729
|
if (completed.length > 0) {
|
|
17116
|
-
finalOutput =
|
|
17730
|
+
finalOutput = composeRootSectionOutput(
|
|
17731
|
+
t2Result,
|
|
17732
|
+
completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
|
|
17733
|
+
);
|
|
17117
17734
|
} else {
|
|
17118
17735
|
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
17119
17736
|
const reasons = Array.from(
|
|
17120
|
-
new Set(
|
|
17737
|
+
new Set([
|
|
17738
|
+
...t2Result.issues ?? [],
|
|
17739
|
+
...t2Result.t3Results.flatMap((r) => r.issues ?? [])
|
|
17740
|
+
].filter(Boolean))
|
|
17121
17741
|
);
|
|
17122
17742
|
if (partial) {
|
|
17123
17743
|
finalOutput = reasons.length ? `${partial}
|
|
@@ -17130,6 +17750,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17130
17750
|
} else {
|
|
17131
17751
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
17132
17752
|
t1.setRunBreaker(runBreaker);
|
|
17753
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
17133
17754
|
t1.setPresenter(true);
|
|
17134
17755
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
17135
17756
|
if (identityPrompt) {
|
|
@@ -17154,6 +17775,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17154
17775
|
finalOutput = result.output;
|
|
17155
17776
|
t2Results = result.t2Results;
|
|
17156
17777
|
}
|
|
17778
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
17779
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
17157
17780
|
const trip = runBreaker.reason();
|
|
17158
17781
|
if (trip) {
|
|
17159
17782
|
finalOutput = [
|
|
@@ -17194,6 +17817,10 @@ ${finalOutput}` : ""
|
|
|
17194
17817
|
escalator.cancelAllPending();
|
|
17195
17818
|
} catch {
|
|
17196
17819
|
}
|
|
17820
|
+
try {
|
|
17821
|
+
this.releasePendingEscalations();
|
|
17822
|
+
} catch {
|
|
17823
|
+
}
|
|
17197
17824
|
this.router.restoreTierModels();
|
|
17198
17825
|
this.router.setRunSignal(void 0);
|
|
17199
17826
|
if (this.taskAnalyzer) {
|
|
@@ -21620,11 +22247,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
21620
22247
|
for (const i of incoming) map.set(key(i), i);
|
|
21621
22248
|
return [...map.values()];
|
|
21622
22249
|
}
|
|
22250
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
22251
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
22252
|
+
let n = 2;
|
|
22253
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
|
|
22254
|
+
return `${desired} ${n}`;
|
|
22255
|
+
}
|
|
22256
|
+
function mergeMcpServers(local, incoming) {
|
|
22257
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
22258
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
22259
|
+
const byName = /* @__PURE__ */ new Map();
|
|
22260
|
+
for (const l of local ?? []) byName.set(l.name, l);
|
|
22261
|
+
const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
|
|
22262
|
+
const synced = [];
|
|
22263
|
+
for (const inc of incoming) {
|
|
22264
|
+
if (byName.has(inc.name)) {
|
|
22265
|
+
byName.set(inc.name, inc);
|
|
22266
|
+
synced.push(inc);
|
|
22267
|
+
continue;
|
|
22268
|
+
}
|
|
22269
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
22270
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
22271
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
22272
|
+
byName.set(name, server);
|
|
22273
|
+
synced.push(server);
|
|
22274
|
+
prefixes.add(mcpServerPrefix(name));
|
|
22275
|
+
}
|
|
22276
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
22277
|
+
}
|
|
22278
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
22279
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
22280
|
+
return tools.map((t) => {
|
|
22281
|
+
for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
|
|
22282
|
+
return t;
|
|
22283
|
+
});
|
|
22284
|
+
}
|
|
22285
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
22286
|
+
const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
|
|
22287
|
+
const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
|
|
22288
|
+
const kept = (local ?? []).filter((t) => !covered(t));
|
|
22289
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
22290
|
+
return merged.length ? merged : void 0;
|
|
22291
|
+
}
|
|
21623
22292
|
function gatherSyncBundle(config) {
|
|
21624
|
-
const bundle = { v:
|
|
22293
|
+
const bundle = { v: 2 };
|
|
21625
22294
|
if (config.providers?.length) bundle.providers = config.providers;
|
|
21626
22295
|
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
21627
22296
|
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
22297
|
+
const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
|
|
22298
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
21628
22299
|
if (config.models) bundle.models = config.models;
|
|
21629
22300
|
if (config.budget) bundle.budget = config.budget;
|
|
21630
22301
|
const prefs = {};
|
|
@@ -21639,7 +22310,16 @@ function applySyncBundle(bundle, config) {
|
|
|
21639
22310
|
const next = { ...config, tools: { ...config.tools } };
|
|
21640
22311
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
21641
22312
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
21642
|
-
|
|
22313
|
+
const mcp2 = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
|
|
22314
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp2.servers;
|
|
22315
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
22316
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
22317
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
22318
|
+
config.tools?.disabledTools,
|
|
22319
|
+
rewriteDisabledTools(bundle.disabledTools, mcp2.rewrite),
|
|
22320
|
+
bundle.mcpServers ? mcp2.synced : void 0
|
|
22321
|
+
);
|
|
22322
|
+
}
|
|
21643
22323
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
21644
22324
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
21645
22325
|
if (bundle.prefs) {
|
|
@@ -21980,12 +22660,19 @@ function defaultName(url) {
|
|
|
21980
22660
|
}
|
|
21981
22661
|
}
|
|
21982
22662
|
async function mcpConnectCommand(url, opts = {}) {
|
|
21983
|
-
const
|
|
21984
|
-
const store = new FileMcpOAuthStore(storePathFor(name));
|
|
22663
|
+
const desiredName = opts.name?.trim() || defaultName(url);
|
|
21985
22664
|
console.log(chalk7__default.default.magenta("\n \u25C8 Connect an MCP server via OAuth"));
|
|
21986
22665
|
console.log(chalk7__default.default.dim(` ${url}
|
|
21987
22666
|
`));
|
|
21988
22667
|
try {
|
|
22668
|
+
const cm = new ConfigManager(process.cwd());
|
|
22669
|
+
await cm.load();
|
|
22670
|
+
const config = cm.getConfig();
|
|
22671
|
+
config.tools = config.tools ?? {};
|
|
22672
|
+
const servers = config.tools.mcpServers ?? [];
|
|
22673
|
+
const existingIdx = servers.findIndex((s) => s.name === desiredName);
|
|
22674
|
+
const name = existingIdx >= 0 ? desiredName : uniqueMcpServerName(desiredName, servers.map((s) => s.name));
|
|
22675
|
+
const store = new FileMcpOAuthStore(storePathFor(name));
|
|
21989
22676
|
await connectMcpWithLoopbackOAuth({
|
|
21990
22677
|
serverUrl: url,
|
|
21991
22678
|
store,
|
|
@@ -21995,14 +22682,8 @@ async function mcpConnectCommand(url, opts = {}) {
|
|
|
21995
22682
|
openBrowser2(u);
|
|
21996
22683
|
}
|
|
21997
22684
|
});
|
|
21998
|
-
const cm = new ConfigManager(process.cwd());
|
|
21999
|
-
await cm.load();
|
|
22000
|
-
const config = cm.getConfig();
|
|
22001
|
-
config.tools = config.tools ?? {};
|
|
22002
|
-
const servers = config.tools.mcpServers ?? [];
|
|
22003
22685
|
const entry = { name, url, oauthStore: storePathFor(name) };
|
|
22004
|
-
|
|
22005
|
-
if (idx >= 0) servers[idx] = entry;
|
|
22686
|
+
if (existingIdx >= 0) servers[existingIdx] = entry;
|
|
22006
22687
|
else servers.push(entry);
|
|
22007
22688
|
config.tools.mcpServers = servers;
|
|
22008
22689
|
config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
|
|
@@ -22047,6 +22728,7 @@ async function mcpRemoveCommand(name) {
|
|
|
22047
22728
|
}
|
|
22048
22729
|
config.tools.mcpServers = servers.filter((s) => s.name !== name);
|
|
22049
22730
|
config.tools.mcpTrusted = (config.tools.mcpTrusted ?? []).filter((n) => n !== name);
|
|
22731
|
+
config.tools.disabledTools = removeMcpServerDenials(config.tools.disabledTools, name);
|
|
22050
22732
|
if (match.oauthStore) {
|
|
22051
22733
|
try {
|
|
22052
22734
|
new FileMcpOAuthStore(match.oauthStore).clear();
|
|
@@ -22153,6 +22835,14 @@ var DashboardSocket = class {
|
|
|
22153
22835
|
this.secret = options.secret;
|
|
22154
22836
|
this.setupHandlers();
|
|
22155
22837
|
}
|
|
22838
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
22839
|
+
roomSize(room) {
|
|
22840
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
22841
|
+
}
|
|
22842
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
22843
|
+
hasSocket(socketId) {
|
|
22844
|
+
return this.io.sockets.sockets.has(socketId);
|
|
22845
|
+
}
|
|
22156
22846
|
broadcastToRoom(room, event, data) {
|
|
22157
22847
|
this.io.to(room).emit(event, data);
|
|
22158
22848
|
}
|
|
@@ -22224,6 +22914,22 @@ var DashboardSocket = class {
|
|
|
22224
22914
|
});
|
|
22225
22915
|
});
|
|
22226
22916
|
}
|
|
22917
|
+
/**
|
|
22918
|
+
* A client subscribed to a session's room.
|
|
22919
|
+
*
|
|
22920
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
22921
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
22922
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
22923
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
22924
|
+
*/
|
|
22925
|
+
onJoinSession(callback) {
|
|
22926
|
+
this.io.on("connection", (socket) => {
|
|
22927
|
+
socket.on("join:session", (payload) => {
|
|
22928
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
22929
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
22930
|
+
});
|
|
22931
|
+
});
|
|
22932
|
+
}
|
|
22227
22933
|
onSessionRate(callback) {
|
|
22228
22934
|
this.io.on("connection", (socket) => {
|
|
22229
22935
|
socket.on("session:rate", (payload) => {
|
|
@@ -22261,6 +22967,31 @@ var DashboardSocket = class {
|
|
|
22261
22967
|
});
|
|
22262
22968
|
});
|
|
22263
22969
|
}
|
|
22970
|
+
/**
|
|
22971
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
22972
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
22973
|
+
* answers here.
|
|
22974
|
+
*
|
|
22975
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
22976
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
22977
|
+
*/
|
|
22978
|
+
onEscalationDecision(callback) {
|
|
22979
|
+
this.io.on("connection", (socket) => {
|
|
22980
|
+
socket.on("escalation:decide", (payload) => {
|
|
22981
|
+
const action = payload?.action;
|
|
22982
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
22983
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
22984
|
+
callback({
|
|
22985
|
+
sessionId: payload.sessionId,
|
|
22986
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
22987
|
+
// run concurrently, so more than one can be waiting.
|
|
22988
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
22989
|
+
action,
|
|
22990
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
22991
|
+
});
|
|
22992
|
+
});
|
|
22993
|
+
});
|
|
22994
|
+
}
|
|
22264
22995
|
onSessionSteer(callback) {
|
|
22265
22996
|
this.io.on("connection", (socket) => {
|
|
22266
22997
|
socket.on("session:steer", (payload) => {
|
|
@@ -22422,6 +23153,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
22422
23153
|
|
|
22423
23154
|
// src/dashboard/server.ts
|
|
22424
23155
|
var __dirname$1 = path32__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
23156
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
22425
23157
|
var DashboardServer = class {
|
|
22426
23158
|
app;
|
|
22427
23159
|
httpServer;
|
|
@@ -22432,6 +23164,13 @@ var DashboardServer = class {
|
|
|
22432
23164
|
globalStore = null;
|
|
22433
23165
|
broadcastTimer = null;
|
|
22434
23166
|
activeSessions = /* @__PURE__ */ new Map();
|
|
23167
|
+
/**
|
|
23168
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
23169
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
23170
|
+
* browser — so the tool is simply not registered rather than present and
|
|
23171
|
+
* always failing.
|
|
23172
|
+
*/
|
|
23173
|
+
currentPageProvider;
|
|
22435
23174
|
activeControllers = /* @__PURE__ */ new Map();
|
|
22436
23175
|
/**
|
|
22437
23176
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -22446,6 +23185,17 @@ var DashboardServer = class {
|
|
|
22446
23185
|
* map is how that answer reaches the run that's blocked on it.
|
|
22447
23186
|
*/
|
|
22448
23187
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
23188
|
+
/**
|
|
23189
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
23190
|
+
* delivered to a client that was not reachable when it was raised.
|
|
23191
|
+
*
|
|
23192
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
23193
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
23194
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
23195
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
23196
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
23197
|
+
*/
|
|
23198
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
22449
23199
|
/**
|
|
22450
23200
|
* The orchestration decision trail ("why") of each session's most recent
|
|
22451
23201
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -22523,20 +23273,29 @@ var DashboardServer = class {
|
|
|
22523
23273
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
22524
23274
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
22525
23275
|
this.activeSessions.set(sessionId, cascade);
|
|
23276
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
22526
23277
|
cascade.on("stream:token", (e) => {
|
|
22527
|
-
this.
|
|
23278
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
22528
23279
|
});
|
|
22529
23280
|
cascade.on("tier:status", (e) => {
|
|
22530
|
-
this.
|
|
23281
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
|
|
22531
23282
|
});
|
|
22532
23283
|
cascade.on("permission:user-required", (e) => {
|
|
22533
|
-
this.
|
|
23284
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
|
|
22534
23285
|
});
|
|
22535
23286
|
cascade.on("peer:message", (e) => {
|
|
22536
23287
|
this.socket.emitPeerMessage(e);
|
|
22537
23288
|
});
|
|
22538
23289
|
cascade.on("plan:approval-required", (e) => {
|
|
22539
|
-
this.
|
|
23290
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
|
|
23291
|
+
});
|
|
23292
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
23293
|
+
this.raiseEscalation(sessionId, socketId, e);
|
|
23294
|
+
});
|
|
23295
|
+
cascade.on("escalation:timeout", (e) => {
|
|
23296
|
+
const ev = e;
|
|
23297
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
23298
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
22540
23299
|
});
|
|
22541
23300
|
try {
|
|
22542
23301
|
const result = await cascade.run({
|
|
@@ -22547,7 +23306,7 @@ var DashboardServer = class {
|
|
|
22547
23306
|
this.recordSessionTask(sessionId, result.taskId);
|
|
22548
23307
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
22549
23308
|
this.captureWhy(sessionId, cascade, result);
|
|
22550
|
-
this.
|
|
23309
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
22551
23310
|
this.socket.broadcast("cost:update", {
|
|
22552
23311
|
sessionId,
|
|
22553
23312
|
totalTokens: result.usage.totalTokens,
|
|
@@ -22560,7 +23319,7 @@ var DashboardServer = class {
|
|
|
22560
23319
|
} catch (err) {
|
|
22561
23320
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
22562
23321
|
this.captureWhy(sessionId, cascade);
|
|
22563
|
-
this.
|
|
23322
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
22564
23323
|
sessionId,
|
|
22565
23324
|
error: err instanceof Error ? err.message : String(err)
|
|
22566
23325
|
});
|
|
@@ -22568,6 +23327,7 @@ var DashboardServer = class {
|
|
|
22568
23327
|
this.activeSessions.delete(sessionId);
|
|
22569
23328
|
this.activeControllers.delete(sessionId);
|
|
22570
23329
|
this.denyPendingApprovals(sessionId);
|
|
23330
|
+
this.clearSessionEscalations(sessionId);
|
|
22571
23331
|
}
|
|
22572
23332
|
});
|
|
22573
23333
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -22577,10 +23337,20 @@ var DashboardServer = class {
|
|
|
22577
23337
|
editedPlan
|
|
22578
23338
|
);
|
|
22579
23339
|
});
|
|
23340
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
23341
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
23342
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
23343
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
23344
|
+
});
|
|
23345
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
23346
|
+
this.replayEscalations(sessionId, socketId);
|
|
23347
|
+
});
|
|
22580
23348
|
this.socket.onSessionHalt((sessionId) => {
|
|
22581
23349
|
this.activeControllers.get(sessionId)?.abort();
|
|
22582
23350
|
this.denyPendingApprovals(sessionId);
|
|
22583
23351
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
23352
|
+
this.clearSessionEscalations(sessionId);
|
|
23353
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
22584
23354
|
});
|
|
22585
23355
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
22586
23356
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -22595,6 +23365,10 @@ var DashboardServer = class {
|
|
|
22595
23365
|
}
|
|
22596
23366
|
});
|
|
22597
23367
|
}
|
|
23368
|
+
/** Wire the host's browser view in (desktop only). */
|
|
23369
|
+
setCurrentPageProvider(provider) {
|
|
23370
|
+
this.currentPageProvider = provider;
|
|
23371
|
+
}
|
|
22598
23372
|
async start() {
|
|
22599
23373
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
22600
23374
|
if (!isLoopback) {
|
|
@@ -22883,6 +23657,116 @@ var DashboardServer = class {
|
|
|
22883
23657
|
this.activeSessions.delete(sessionId);
|
|
22884
23658
|
}
|
|
22885
23659
|
}
|
|
23660
|
+
/**
|
|
23661
|
+
* Is anyone able to answer for this session right now?
|
|
23662
|
+
*
|
|
23663
|
+
* The originating socket first — that connection is definitely watching this
|
|
23664
|
+
* run — then the session room, which is where a client lands after a
|
|
23665
|
+
* reconnect issues it a new socket id.
|
|
23666
|
+
*/
|
|
23667
|
+
hasResponder(sessionId, socketId) {
|
|
23668
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
23669
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
23670
|
+
}
|
|
23671
|
+
/**
|
|
23672
|
+
* Send a run event to whoever is watching this session.
|
|
23673
|
+
*
|
|
23674
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
23675
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
23676
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
23677
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
23678
|
+
* client that is both the originator and a room member from seeing every
|
|
23679
|
+
* token twice.
|
|
23680
|
+
*/
|
|
23681
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
23682
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
23683
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
23684
|
+
}
|
|
23685
|
+
/**
|
|
23686
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
23687
|
+
*
|
|
23688
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
23689
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
23690
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
23691
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
23692
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
23693
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
23694
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
23695
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
23696
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
23697
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
23698
|
+
* mistaken for an absent user.
|
|
23699
|
+
*/
|
|
23700
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
23701
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : crypto.randomUUID();
|
|
23702
|
+
const payload = { sessionId, ...event };
|
|
23703
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
23704
|
+
this.pendingEscalations.set(requestId, entry);
|
|
23705
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
23706
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
23707
|
+
return;
|
|
23708
|
+
}
|
|
23709
|
+
entry.graceTimer = setTimeout(() => {
|
|
23710
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
23711
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
23712
|
+
this.pendingEscalations.delete(requestId);
|
|
23713
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
23714
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
23715
|
+
}
|
|
23716
|
+
/**
|
|
23717
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
23718
|
+
*
|
|
23719
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
23720
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
23721
|
+
* its gates and the clients queue theirs.
|
|
23722
|
+
*
|
|
23723
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
23724
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
23725
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
23726
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
23727
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
23728
|
+
* sides never have to agree on a wall clock.
|
|
23729
|
+
*/
|
|
23730
|
+
replayEscalations(sessionId, socketId) {
|
|
23731
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
23732
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23733
|
+
if (entry.graceTimer) {
|
|
23734
|
+
clearTimeout(entry.graceTimer);
|
|
23735
|
+
entry.graceTimer = void 0;
|
|
23736
|
+
}
|
|
23737
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
23738
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
23739
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
23740
|
+
}
|
|
23741
|
+
}
|
|
23742
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
23743
|
+
clearPendingEscalation(requestId) {
|
|
23744
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
23745
|
+
if (!entry) return;
|
|
23746
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23747
|
+
this.pendingEscalations.delete(requestId);
|
|
23748
|
+
}
|
|
23749
|
+
/**
|
|
23750
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
23751
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
23752
|
+
* Insertion order makes `values()` oldest-first.
|
|
23753
|
+
*/
|
|
23754
|
+
clearOldestSessionEscalation(sessionId) {
|
|
23755
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
23756
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23757
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23758
|
+
this.pendingEscalations.delete(id);
|
|
23759
|
+
return;
|
|
23760
|
+
}
|
|
23761
|
+
}
|
|
23762
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
23763
|
+
clearSessionEscalations(sessionId) {
|
|
23764
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
23765
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23766
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23767
|
+
this.pendingEscalations.delete(id);
|
|
23768
|
+
}
|
|
23769
|
+
}
|
|
22886
23770
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
22887
23771
|
denyPendingApprovals(sessionId) {
|
|
22888
23772
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -23383,6 +24267,7 @@ ${prompt}`;
|
|
|
23383
24267
|
void (async () => {
|
|
23384
24268
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
23385
24269
|
this.activeSessions.set(sessionId, cascade);
|
|
24270
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
23386
24271
|
cascade.on("stream:token", (e) => {
|
|
23387
24272
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
23388
24273
|
});
|
|
@@ -23398,6 +24283,14 @@ ${prompt}`;
|
|
|
23398
24283
|
cascade.on("plan:approval-required", (e) => {
|
|
23399
24284
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
23400
24285
|
});
|
|
24286
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
24287
|
+
this.raiseEscalation(sessionId, void 0, e);
|
|
24288
|
+
});
|
|
24289
|
+
cascade.on("escalation:timeout", (e) => {
|
|
24290
|
+
const ev = e;
|
|
24291
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
24292
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
24293
|
+
});
|
|
23401
24294
|
try {
|
|
23402
24295
|
const result = await cascade.run({
|
|
23403
24296
|
prompt: runPrompt,
|
|
@@ -23425,6 +24318,7 @@ ${prompt}`;
|
|
|
23425
24318
|
} finally {
|
|
23426
24319
|
this.activeSessions.delete(sessionId);
|
|
23427
24320
|
this.denyPendingApprovals(sessionId);
|
|
24321
|
+
this.clearSessionEscalations(sessionId);
|
|
23428
24322
|
}
|
|
23429
24323
|
})();
|
|
23430
24324
|
});
|