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.js
CHANGED
|
@@ -51,6 +51,12 @@ import cron from 'node-cron';
|
|
|
51
51
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
52
52
|
var __defProp = Object.defineProperty;
|
|
53
53
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
54
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
55
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
56
|
+
}) : x)(function(x) {
|
|
57
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
58
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
59
|
+
});
|
|
54
60
|
var __esm = (fn, res) => function __init() {
|
|
55
61
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
56
62
|
};
|
|
@@ -1740,7 +1746,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1740
1746
|
var init_constants = __esm({
|
|
1741
1747
|
"src/constants.ts"() {
|
|
1742
1748
|
init_pricing();
|
|
1743
|
-
CASCADE_VERSION = "0.
|
|
1749
|
+
CASCADE_VERSION = "0.55.0";
|
|
1744
1750
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1745
1751
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1746
1752
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -5383,6 +5389,102 @@ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
|
5383
5389
|
}
|
|
5384
5390
|
return merged;
|
|
5385
5391
|
}
|
|
5392
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
5393
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
5394
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
5395
|
+
function boundedSegment(raw) {
|
|
5396
|
+
const clean = sanitizeToolNameSegment(raw);
|
|
5397
|
+
if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
|
|
5398
|
+
const hash = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
5399
|
+
return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
5400
|
+
}
|
|
5401
|
+
function isStableSegment(raw) {
|
|
5402
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
5403
|
+
}
|
|
5404
|
+
function sanitizeToolNameSegment(segment) {
|
|
5405
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
5406
|
+
let start = 0;
|
|
5407
|
+
let end = cleaned.length;
|
|
5408
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
5409
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
5410
|
+
return cleaned.slice(start, end) || "_";
|
|
5411
|
+
}
|
|
5412
|
+
function mcpToolName(serverName, toolName) {
|
|
5413
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
5414
|
+
}
|
|
5415
|
+
function isMcpToolName(name) {
|
|
5416
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
5417
|
+
}
|
|
5418
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
5419
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
5420
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
5421
|
+
for (let n = 2; n < 1e3; n++) {
|
|
5422
|
+
const candidate = `${desired} (${n})`;
|
|
5423
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
5424
|
+
}
|
|
5425
|
+
return `${desired} (${randomUUID().slice(0, 8)})`;
|
|
5426
|
+
}
|
|
5427
|
+
function disambiguateMcpServerNames(servers) {
|
|
5428
|
+
const seenNames = [];
|
|
5429
|
+
const renames = [];
|
|
5430
|
+
const result = servers.map((s) => {
|
|
5431
|
+
const name = uniqueMcpServerName(s.name, seenNames);
|
|
5432
|
+
seenNames.push(name);
|
|
5433
|
+
if (name === s.name) return s;
|
|
5434
|
+
renames.push({ from: s.name, to: name });
|
|
5435
|
+
return { ...s, name };
|
|
5436
|
+
});
|
|
5437
|
+
return { servers: renames.length ? result : servers, renames };
|
|
5438
|
+
}
|
|
5439
|
+
function assignMcpToolNames(tools) {
|
|
5440
|
+
const names = new Array(tools.length);
|
|
5441
|
+
const bases = tools.map((t) => mcpToolName(t.server, t.tool));
|
|
5442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5443
|
+
bases.forEach((base, i) => {
|
|
5444
|
+
const g = groups.get(base);
|
|
5445
|
+
if (g) g.push(i);
|
|
5446
|
+
else groups.set(base, [i]);
|
|
5447
|
+
});
|
|
5448
|
+
const used = new Set(bases);
|
|
5449
|
+
for (const [base, idx] of groups) {
|
|
5450
|
+
if (idx.length === 1) {
|
|
5451
|
+
names[idx[0]] = base;
|
|
5452
|
+
continue;
|
|
5453
|
+
}
|
|
5454
|
+
const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
|
|
5455
|
+
const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
|
|
5456
|
+
const ordered = [...idx].sort((a, b) => {
|
|
5457
|
+
const la = isClean(a) ? 0 : 1;
|
|
5458
|
+
const lb = isClean(b) ? 0 : 1;
|
|
5459
|
+
if (la !== lb) return la - lb;
|
|
5460
|
+
const ra = raw(a);
|
|
5461
|
+
const rb = raw(b);
|
|
5462
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
5463
|
+
});
|
|
5464
|
+
names[ordered[0]] = base;
|
|
5465
|
+
let n = 2;
|
|
5466
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
5467
|
+
let candidate = `${base}_${n}`;
|
|
5468
|
+
while (used.has(candidate)) {
|
|
5469
|
+
n++;
|
|
5470
|
+
candidate = `${base}_${n}`;
|
|
5471
|
+
}
|
|
5472
|
+
names[ordered[rank3]] = candidate;
|
|
5473
|
+
used.add(candidate);
|
|
5474
|
+
n++;
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
return names;
|
|
5478
|
+
}
|
|
5479
|
+
function mcpServerPrefix(serverName) {
|
|
5480
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
5481
|
+
}
|
|
5482
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
5483
|
+
if (!disabledTools?.length) return disabledTools;
|
|
5484
|
+
const prefix = mcpServerPrefix(serverName);
|
|
5485
|
+
const kept = disabledTools.filter((t) => !t.startsWith(prefix));
|
|
5486
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
5487
|
+
}
|
|
5386
5488
|
|
|
5387
5489
|
// src/config/index.ts
|
|
5388
5490
|
init_constants();
|
|
@@ -5406,6 +5508,13 @@ var ConfigManager = class {
|
|
|
5406
5508
|
}
|
|
5407
5509
|
async load() {
|
|
5408
5510
|
this.config = await this.loadConfig();
|
|
5511
|
+
const servers = this.config.tools?.mcpServers;
|
|
5512
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
5513
|
+
const mcpNamesChanged = renames.length > 0;
|
|
5514
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
5515
|
+
this.config.tools.mcpServers = disambiguated;
|
|
5516
|
+
this.renameMcpServerReferences(renames);
|
|
5517
|
+
}
|
|
5409
5518
|
this.ignore = new CascadeIgnore();
|
|
5410
5519
|
await this.ignore.load(this.workspacePath);
|
|
5411
5520
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -5414,6 +5523,7 @@ var ConfigManager = class {
|
|
|
5414
5523
|
await this.injectEnvKeys();
|
|
5415
5524
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5416
5525
|
await this.ensureDefaultIdentity();
|
|
5526
|
+
if (mcpNamesChanged) await this.save();
|
|
5417
5527
|
}
|
|
5418
5528
|
getConfig() {
|
|
5419
5529
|
return this.config;
|
|
@@ -5497,6 +5607,66 @@ var ConfigManager = class {
|
|
|
5497
5607
|
this.config.providers.push({ type: "ollama" });
|
|
5498
5608
|
}
|
|
5499
5609
|
}
|
|
5610
|
+
/**
|
|
5611
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
5612
|
+
*
|
|
5613
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
5614
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
5615
|
+
*
|
|
5616
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
5617
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
5618
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
5619
|
+
* move with it. A plain rewrite is correct here.
|
|
5620
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
5621
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
5622
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
5623
|
+
* already been updated to the disambiguated list by the time this runs
|
|
5624
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
5625
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
5626
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
5627
|
+
* AND granted to the renamed identity, not moved.
|
|
5628
|
+
*
|
|
5629
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
5630
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
5631
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
5632
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
5633
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
5634
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
5635
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
5636
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
5637
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
5638
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
5639
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
5640
|
+
* together, in the one pass that still sees the original entry.
|
|
5641
|
+
*
|
|
5642
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
5643
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
5644
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
5645
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
5646
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
5647
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
5648
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
5649
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
5650
|
+
* would do the opposite — move a denial that already worked for the
|
|
5651
|
+
* survivor onto a server it may never have meant to cover.
|
|
5652
|
+
*/
|
|
5653
|
+
renameMcpServerReferences(renames) {
|
|
5654
|
+
const tools = this.config.tools;
|
|
5655
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
5656
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
|
|
5657
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
5658
|
+
for (const { from, to } of renames) {
|
|
5659
|
+
const tos = byFrom.get(from);
|
|
5660
|
+
if (tos) tos.push(to);
|
|
5661
|
+
else byFrom.set(from, [to]);
|
|
5662
|
+
}
|
|
5663
|
+
let trusted = tools.mcpTrusted;
|
|
5664
|
+
for (const [from, tos] of byFrom) {
|
|
5665
|
+
if (!trusted.includes(from)) continue;
|
|
5666
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
|
|
5667
|
+
}
|
|
5668
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
5669
|
+
}
|
|
5500
5670
|
async ensureDefaultIdentity() {
|
|
5501
5671
|
const existing = this.store.getDefaultIdentity();
|
|
5502
5672
|
if (existing) return;
|
|
@@ -5512,26 +5682,6 @@ var ConfigManager = class {
|
|
|
5512
5682
|
}
|
|
5513
5683
|
};
|
|
5514
5684
|
|
|
5515
|
-
// src/tools/tool-name.ts
|
|
5516
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
5517
|
-
function sanitizeToolNameSegment(segment) {
|
|
5518
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
5519
|
-
let start = 0;
|
|
5520
|
-
let end = cleaned.length;
|
|
5521
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
5522
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
5523
|
-
return cleaned.slice(start, end) || "_";
|
|
5524
|
-
}
|
|
5525
|
-
function mcpToolName(serverName, toolName) {
|
|
5526
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
5527
|
-
}
|
|
5528
|
-
function isMcpToolName(name) {
|
|
5529
|
-
return name.startsWith(MCP_TOOL_PREFIX);
|
|
5530
|
-
}
|
|
5531
|
-
function mcpServerPrefix(serverName) {
|
|
5532
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
|
|
5533
|
-
}
|
|
5534
|
-
|
|
5535
5685
|
// src/cli/utils/terminal-input.ts
|
|
5536
5686
|
var PASTE_START = "\x1B[200~";
|
|
5537
5687
|
var PASTE_END = "\x1B[201~";
|
|
@@ -5856,7 +6006,21 @@ var ModelSelector = class {
|
|
|
5856
6006
|
this.availableProviders = availableProviders;
|
|
5857
6007
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
5858
6008
|
}
|
|
6009
|
+
/**
|
|
6010
|
+
* Verdicts from earlier runs about ids that 404 on this account. Consulted
|
|
6011
|
+
* BEFORE a model enters the pool, which is the only placement that helps: a
|
|
6012
|
+
* T3 wave runs concurrently, so filtering after the first failure still lets
|
|
6013
|
+
* every other worker in the wave call the dead id.
|
|
6014
|
+
*/
|
|
6015
|
+
deadModels;
|
|
6016
|
+
setDeadModelStore(store) {
|
|
6017
|
+
this.deadModels = store;
|
|
6018
|
+
for (const [id, m] of [...this.availableModels]) {
|
|
6019
|
+
if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
5859
6022
|
addDynamicModel(model) {
|
|
6023
|
+
if (this.deadModels?.isDead(model.provider, model.id)) return;
|
|
5860
6024
|
this.availableModels.set(model.id, model);
|
|
5861
6025
|
if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
|
|
5862
6026
|
}
|
|
@@ -6263,6 +6427,121 @@ var LocalRequestQueue = class {
|
|
|
6263
6427
|
}
|
|
6264
6428
|
};
|
|
6265
6429
|
|
|
6430
|
+
// src/core/router/dead-models.ts
|
|
6431
|
+
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6432
|
+
function deadModelKey(provider, modelId) {
|
|
6433
|
+
return `${provider}:${modelId}`;
|
|
6434
|
+
}
|
|
6435
|
+
var DeadModelStore = class {
|
|
6436
|
+
constructor(persistence, opts = {}) {
|
|
6437
|
+
this.persistence = persistence;
|
|
6438
|
+
this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
|
|
6439
|
+
this.now = opts.now ?? (() => Date.now());
|
|
6440
|
+
this.restore();
|
|
6441
|
+
}
|
|
6442
|
+
persistence;
|
|
6443
|
+
records = /* @__PURE__ */ new Map();
|
|
6444
|
+
ttlMs;
|
|
6445
|
+
now;
|
|
6446
|
+
restore() {
|
|
6447
|
+
if (!this.persistence) return;
|
|
6448
|
+
try {
|
|
6449
|
+
for (const r of this.persistence.load()) {
|
|
6450
|
+
if (!this.isExpired(r)) this.records.set(r.key, r);
|
|
6451
|
+
}
|
|
6452
|
+
} catch {
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
isExpired(r) {
|
|
6456
|
+
return this.now() - r.lastSeenAt > this.ttlMs;
|
|
6457
|
+
}
|
|
6458
|
+
flush() {
|
|
6459
|
+
if (!this.persistence) return;
|
|
6460
|
+
try {
|
|
6461
|
+
this.persistence.save([...this.records.values()]);
|
|
6462
|
+
} catch {
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
/**
|
|
6466
|
+
* Record that a model was found dead. Returns true when this is the first
|
|
6467
|
+
* time (so the caller can log it once rather than once per concurrent
|
|
6468
|
+
* worker in the wave that discovered it).
|
|
6469
|
+
*/
|
|
6470
|
+
record(provider, modelId, reason) {
|
|
6471
|
+
const key = deadModelKey(provider, modelId);
|
|
6472
|
+
const now = this.now();
|
|
6473
|
+
const existing = this.records.get(key);
|
|
6474
|
+
if (existing && !this.isExpired(existing)) {
|
|
6475
|
+
existing.lastSeenAt = now;
|
|
6476
|
+
existing.hits += 1;
|
|
6477
|
+
this.flush();
|
|
6478
|
+
return false;
|
|
6479
|
+
}
|
|
6480
|
+
this.records.set(key, {
|
|
6481
|
+
key,
|
|
6482
|
+
provider,
|
|
6483
|
+
modelId,
|
|
6484
|
+
reason,
|
|
6485
|
+
firstSeenAt: now,
|
|
6486
|
+
lastSeenAt: now,
|
|
6487
|
+
hits: 1
|
|
6488
|
+
});
|
|
6489
|
+
this.flush();
|
|
6490
|
+
return true;
|
|
6491
|
+
}
|
|
6492
|
+
/** True when this model is known dead and the verdict has not expired. */
|
|
6493
|
+
isDead(provider, modelId) {
|
|
6494
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
6495
|
+
if (!r) return false;
|
|
6496
|
+
if (this.isExpired(r)) {
|
|
6497
|
+
this.records.delete(r.key);
|
|
6498
|
+
this.flush();
|
|
6499
|
+
return false;
|
|
6500
|
+
}
|
|
6501
|
+
return true;
|
|
6502
|
+
}
|
|
6503
|
+
/** Why a model is considered dead, for /why and the models command. */
|
|
6504
|
+
reasonFor(provider, modelId) {
|
|
6505
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
6506
|
+
return r && !this.isExpired(r) ? r.reason : null;
|
|
6507
|
+
}
|
|
6508
|
+
/** Live records, expired ones filtered out. */
|
|
6509
|
+
list() {
|
|
6510
|
+
const live = [...this.records.values()].filter((r) => !this.isExpired(r));
|
|
6511
|
+
return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
6512
|
+
}
|
|
6513
|
+
/**
|
|
6514
|
+
* Forget a verdict — used when the user fixes the underlying problem (adds
|
|
6515
|
+
* the model to their project, gets quota) and wants it retried now rather
|
|
6516
|
+
* than after the TTL.
|
|
6517
|
+
*/
|
|
6518
|
+
forget(provider, modelId) {
|
|
6519
|
+
const removed = this.records.delete(deadModelKey(provider, modelId));
|
|
6520
|
+
if (removed) this.flush();
|
|
6521
|
+
return removed;
|
|
6522
|
+
}
|
|
6523
|
+
clear() {
|
|
6524
|
+
this.records.clear();
|
|
6525
|
+
this.flush();
|
|
6526
|
+
}
|
|
6527
|
+
};
|
|
6528
|
+
function fileDeadModelPersistence(filePath) {
|
|
6529
|
+
return {
|
|
6530
|
+
load() {
|
|
6531
|
+
const fs32 = __require("fs");
|
|
6532
|
+
if (!fs32.existsSync(filePath)) return [];
|
|
6533
|
+
const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
|
|
6534
|
+
return Array.isArray(raw) ? raw : [];
|
|
6535
|
+
},
|
|
6536
|
+
save(records) {
|
|
6537
|
+
const fs32 = __require("fs");
|
|
6538
|
+
const path37 = __require("path");
|
|
6539
|
+
fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
|
|
6540
|
+
fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6541
|
+
}
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
|
|
6266
6545
|
// src/core/router/index.ts
|
|
6267
6546
|
init_constants();
|
|
6268
6547
|
init_cost();
|
|
@@ -6848,10 +7127,47 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
6848
7127
|
*/
|
|
6849
7128
|
budgetState = "ok";
|
|
6850
7129
|
budgetExceededReason;
|
|
7130
|
+
/**
|
|
7131
|
+
* Whether the run/session budget is permanently tripped, and why.
|
|
7132
|
+
*
|
|
7133
|
+
* A tier or worker that catches `BudgetExceededError` internally (T3Worker
|
|
7134
|
+
* does, so one dead-end subtask doesn't crash the whole run) absorbs the
|
|
7135
|
+
* exception into a normal FAILED/PARTIAL result — the tier returns as if
|
|
7136
|
+
* nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
|
|
7137
|
+
* budget-exceeded handling (the `run:budget-exceeded` event and
|
|
7138
|
+
* `lastInterruptedRun` for `/continue`). This lets a caller check the
|
|
7139
|
+
* router's own state directly after a tier returns, regardless of how that
|
|
7140
|
+
* tier chose to report the error internally.
|
|
7141
|
+
*/
|
|
7142
|
+
budgetExceededInfo() {
|
|
7143
|
+
if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
|
|
7144
|
+
if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
|
|
7145
|
+
return null;
|
|
7146
|
+
}
|
|
6851
7147
|
tpmLimiter;
|
|
6852
7148
|
localQueue;
|
|
6853
7149
|
taskAnalyzer;
|
|
6854
7150
|
pendingFeedbackSource;
|
|
7151
|
+
/** Models known dead for this account (404 / retired / not enabled). */
|
|
7152
|
+
deadModels = new DeadModelStore();
|
|
7153
|
+
/**
|
|
7154
|
+
* Back the dead-model memory with durable storage (a JSON file on desktop, a
|
|
7155
|
+
* per-user table in the cloud). Without one the verdicts still work, but only
|
|
7156
|
+
* for the life of the process — so tomorrow's run pays the same wave of 404s
|
|
7157
|
+
* to learn the same fact.
|
|
7158
|
+
*/
|
|
7159
|
+
setDeadModelStore(store) {
|
|
7160
|
+
this.deadModels = store;
|
|
7161
|
+
this.selector.setDeadModelStore(store);
|
|
7162
|
+
}
|
|
7163
|
+
/** Models known dead for this account, for `cascade models` and /why. */
|
|
7164
|
+
listDeadModels() {
|
|
7165
|
+
return this.deadModels.list();
|
|
7166
|
+
}
|
|
7167
|
+
/** Retry a model the user has since fixed, without waiting out the TTL. */
|
|
7168
|
+
forgetDeadModel(provider, modelId) {
|
|
7169
|
+
return this.deadModels.forget(provider, modelId);
|
|
7170
|
+
}
|
|
6855
7171
|
worldStateDB;
|
|
6856
7172
|
privacyPaths;
|
|
6857
7173
|
guidanceQueue;
|
|
@@ -7216,6 +7532,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7216
7532
|
}
|
|
7217
7533
|
if (isModelNotFoundError(errMsg)) {
|
|
7218
7534
|
this.selector.removeModel(model.id);
|
|
7535
|
+
const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
|
|
7536
|
+
if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
|
|
7219
7537
|
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
7220
7538
|
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
7221
7539
|
if (next && next.id !== model.id) {
|
|
@@ -8484,6 +8802,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
8484
8802
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
8485
8803
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
8486
8804
|
}
|
|
8805
|
+
if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
8806
|
+
issues.push(errMsg);
|
|
8807
|
+
const stopped = output || errMsg;
|
|
8808
|
+
this.setStatus("FAILED", stopped);
|
|
8809
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
|
|
8810
|
+
return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
8811
|
+
}
|
|
8487
8812
|
issues.push(`Execution error: ${errMsg}`);
|
|
8488
8813
|
const finalOutput = output || errMsg;
|
|
8489
8814
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -9505,6 +9830,26 @@ var RedactionLayer = class {
|
|
|
9505
9830
|
}
|
|
9506
9831
|
};
|
|
9507
9832
|
|
|
9833
|
+
// src/core/tiers/escalation-policy.ts
|
|
9834
|
+
function sectionNeedsDecision(results) {
|
|
9835
|
+
return results.some((r) => r.status === "ESCALATED");
|
|
9836
|
+
}
|
|
9837
|
+
function settledEscalationStatus(results) {
|
|
9838
|
+
const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
|
|
9839
|
+
return hasWork ? "PARTIAL" : "FAILED";
|
|
9840
|
+
}
|
|
9841
|
+
function composeRootSectionOutput(t2Result, completedOutputs) {
|
|
9842
|
+
let output = `${t2Result.sectionSummary}
|
|
9843
|
+
|
|
9844
|
+
${completedOutputs.join("\n\n")}`;
|
|
9845
|
+
if (t2Result.status === "FAILED" && t2Result.issues?.length) {
|
|
9846
|
+
output += `
|
|
9847
|
+
|
|
9848
|
+
_(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
9849
|
+
}
|
|
9850
|
+
return output;
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9508
9853
|
// src/core/tiers/t2-manager.ts
|
|
9509
9854
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
9510
9855
|
return [
|
|
@@ -9530,6 +9875,19 @@ var T2Manager = class extends BaseTier {
|
|
|
9530
9875
|
t2PeerBus;
|
|
9531
9876
|
permissionEscalator;
|
|
9532
9877
|
toolCreator;
|
|
9878
|
+
/**
|
|
9879
|
+
* Asked when a section ends ESCALATED — i.e. a worker hit something it could
|
|
9880
|
+
* not decide alone. Without this the escalation was a dead end: the status
|
|
9881
|
+
* said "needs a decision" and no decision was ever requested, so an MCP run
|
|
9882
|
+
* that legitimately needed input just stopped there.
|
|
9883
|
+
*/
|
|
9884
|
+
escalationCallback;
|
|
9885
|
+
/**
|
|
9886
|
+
* The one allowed retry has been spent. Retrying is bounded because an
|
|
9887
|
+
* escalation loop that can re-ask forever burns a budget on a question that
|
|
9888
|
+
* is not resolving; a second escalation is terminal instead.
|
|
9889
|
+
*/
|
|
9890
|
+
escalationRetryUsed = false;
|
|
9533
9891
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
9534
9892
|
planApprovalCallback;
|
|
9535
9893
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -9578,6 +9936,10 @@ var T2Manager = class extends BaseTier {
|
|
|
9578
9936
|
setToolCreator(creator) {
|
|
9579
9937
|
this.toolCreator = creator;
|
|
9580
9938
|
}
|
|
9939
|
+
/** Ask the user what to do when a section escalates. */
|
|
9940
|
+
setEscalationCallback(cb) {
|
|
9941
|
+
this.escalationCallback = cb;
|
|
9942
|
+
}
|
|
9581
9943
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
9582
9944
|
setPlanApprovalCallback(cb) {
|
|
9583
9945
|
this.planApprovalCallback = cb;
|
|
@@ -9682,9 +10044,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9682
10044
|
currentAction: "Aggregating T3 results",
|
|
9683
10045
|
status: "IN_PROGRESS"
|
|
9684
10046
|
});
|
|
9685
|
-
|
|
10047
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
9686
10048
|
const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
|
|
9687
|
-
|
|
10049
|
+
let overallStatus = this.determineStatus(t3Results);
|
|
10050
|
+
let userSkipped = false;
|
|
10051
|
+
const hasEscalated = sectionNeedsDecision(t3Results);
|
|
10052
|
+
const settleEscalated = async (reason) => {
|
|
10053
|
+
if (reason) issues.push(reason);
|
|
10054
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
10055
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
10056
|
+
};
|
|
10057
|
+
if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
|
|
10058
|
+
await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
|
|
10059
|
+
} else if (hasEscalated && !this.escalationCallback) {
|
|
10060
|
+
await settleEscalated();
|
|
10061
|
+
} else if (hasEscalated && this.escalationCallback) {
|
|
10062
|
+
this.sendStatusUpdate({
|
|
10063
|
+
progressPct: 95,
|
|
10064
|
+
currentAction: "Escalated \u2014 waiting for your decision",
|
|
10065
|
+
status: "ESCALATING"
|
|
10066
|
+
});
|
|
10067
|
+
const decision = await this.escalationCallback({
|
|
10068
|
+
sectionId: assignment.sectionId,
|
|
10069
|
+
sectionTitle: assignment.sectionTitle,
|
|
10070
|
+
issues,
|
|
10071
|
+
summary
|
|
10072
|
+
});
|
|
10073
|
+
this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
|
|
10074
|
+
if (decision.action === "skip") {
|
|
10075
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
10076
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
10077
|
+
userSkipped = decision.automatic !== true;
|
|
10078
|
+
} else if (decision.action === "retry" || decision.action === "guidance") {
|
|
10079
|
+
this.escalationRetryUsed = true;
|
|
10080
|
+
const guided = decision.action === "guidance" && decision.note?.trim() ? {
|
|
10081
|
+
...assignment,
|
|
10082
|
+
description: `${assignment.description}
|
|
10083
|
+
|
|
10084
|
+
Guidance (must be followed): ${decision.note.trim()}`,
|
|
10085
|
+
// Drop T1's preplanned subtasks so the retry re-decomposes with
|
|
10086
|
+
// the guidance in hand. The workers execute from their subtask
|
|
10087
|
+
// slices, not from this description — leaving the slices in
|
|
10088
|
+
// place gave "Retry with guidance" byte-identical worker
|
|
10089
|
+
// prompts to "Retry as-is", making the note do nothing at all.
|
|
10090
|
+
t3Subtasks: []
|
|
10091
|
+
} : assignment;
|
|
10092
|
+
return await this.execute(guided, taskId, signal);
|
|
10093
|
+
} else {
|
|
10094
|
+
issues.push("Escalated, but no decision was received in time.");
|
|
10095
|
+
overallStatus = "FAILED";
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
9688
10098
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
9689
10099
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
9690
10100
|
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";
|
|
@@ -9695,7 +10105,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
9695
10105
|
status: overallStatus,
|
|
9696
10106
|
t3Results,
|
|
9697
10107
|
sectionSummary: summary,
|
|
9698
|
-
issues
|
|
10108
|
+
issues,
|
|
10109
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
9699
10110
|
};
|
|
9700
10111
|
this.publishSectionOutput(result);
|
|
9701
10112
|
return result;
|
|
@@ -10129,8 +10540,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
10129
10540
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
10130
10541
|
);
|
|
10131
10542
|
}
|
|
10132
|
-
async aggregateResults(assignment, results) {
|
|
10133
|
-
const completed = results.filter(
|
|
10543
|
+
async aggregateResults(assignment, results, opts = {}) {
|
|
10544
|
+
const completed = results.filter(
|
|
10545
|
+
(r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
|
|
10546
|
+
);
|
|
10134
10547
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
10135
10548
|
const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
|
|
10136
10549
|
const peerContext = peerOutputs ? `
|
|
@@ -10339,6 +10752,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
10339
10752
|
escalations = [];
|
|
10340
10753
|
store;
|
|
10341
10754
|
runBreaker;
|
|
10755
|
+
escalationCallback;
|
|
10342
10756
|
t2PeerBus = new PeerBus();
|
|
10343
10757
|
permissionEscalator;
|
|
10344
10758
|
toolCreator;
|
|
@@ -10353,6 +10767,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
10353
10767
|
this.toolRegistry = toolRegistry;
|
|
10354
10768
|
this.config = config;
|
|
10355
10769
|
}
|
|
10770
|
+
/** Ask the user what to do when any section escalates. */
|
|
10771
|
+
setEscalationCallback(cb) {
|
|
10772
|
+
this.escalationCallback = cb;
|
|
10773
|
+
}
|
|
10356
10774
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
10357
10775
|
setRunBreaker(breaker) {
|
|
10358
10776
|
this.runBreaker = breaker;
|
|
@@ -10512,8 +10930,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
10512
10930
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
|
|
10513
10931
|
};
|
|
10514
10932
|
}
|
|
10515
|
-
const sectionsText = t2Results.map((r) =>
|
|
10516
|
-
|
|
10933
|
+
const sectionsText = t2Results.map((r) => {
|
|
10934
|
+
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.]" : "";
|
|
10935
|
+
return `**${r.sectionTitle}**
|
|
10936
|
+
${r.sectionSummary}${skipNote}`;
|
|
10937
|
+
}).join("\n\n");
|
|
10517
10938
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
10518
10939
|
Review the following execution outputs against the original user prompt.
|
|
10519
10940
|
|
|
@@ -10523,6 +10944,7 @@ T2 Manager Summaries:
|
|
|
10523
10944
|
${sectionsText}
|
|
10524
10945
|
|
|
10525
10946
|
Does the current state of the workspace and the outputs fully satisfy the user's request?
|
|
10947
|
+
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.
|
|
10526
10948
|
If yes, reply with exactly: "APPROVED".
|
|
10527
10949
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
10528
10950
|
try {
|
|
@@ -10720,6 +11142,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10720
11142
|
const managers = sections.map((section) => {
|
|
10721
11143
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
10722
11144
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
11145
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
10723
11146
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10724
11147
|
if (this.store) {
|
|
10725
11148
|
manager.setStore(this.store);
|
|
@@ -10929,7 +11352,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10929
11352
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
10930
11353
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
10931
11354
|
if (!completedSections.length) {
|
|
10932
|
-
const allIssues =
|
|
11355
|
+
const allIssues = [
|
|
11356
|
+
...t2Results.flatMap((r) => r.issues ?? []),
|
|
11357
|
+
...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
|
|
11358
|
+
].filter(Boolean);
|
|
10933
11359
|
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
10934
11360
|
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
10935
11361
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -11019,9 +11445,16 @@ var CAPABILITIES = [
|
|
|
11019
11445
|
api: "openai-images"
|
|
11020
11446
|
},
|
|
11021
11447
|
{
|
|
11448
|
+
// Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
|
|
11449
|
+
// page but 404s on `:predict` ("not found for API version v1beta, or is not
|
|
11450
|
+
// supported for predict") — the Gemini image models are called through
|
|
11451
|
+
// generateContent with an image response modality, while `:predict` is the
|
|
11452
|
+
// Imagen shape this executor implements. A billing line item is not proof
|
|
11453
|
+
// of a callable endpoint, which is the assumption that put the wrong id
|
|
11454
|
+
// here in the first place.
|
|
11022
11455
|
modality: "image",
|
|
11023
11456
|
provider: "gemini",
|
|
11024
|
-
modelId: "
|
|
11457
|
+
modelId: "imagen-4.0-generate-001",
|
|
11025
11458
|
api: "gemini-predict"
|
|
11026
11459
|
},
|
|
11027
11460
|
// ── Speech (text → audio) ──
|
|
@@ -13161,12 +13594,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
13161
13594
|
mcpClient;
|
|
13162
13595
|
serverName;
|
|
13163
13596
|
toolName;
|
|
13164
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
13597
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
13165
13598
|
super();
|
|
13166
13599
|
this.mcpClient = mcpClient;
|
|
13167
13600
|
this.serverName = serverName;
|
|
13168
13601
|
this.toolName = toolName;
|
|
13169
|
-
this.name = mcpToolName(serverName, toolName);
|
|
13602
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
13170
13603
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
13171
13604
|
this.inputSchema = inputSchema;
|
|
13172
13605
|
}
|
|
@@ -13237,22 +13670,35 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
13237
13670
|
getRegisteredPlugins() {
|
|
13238
13671
|
return Array.from(this.plugins.keys());
|
|
13239
13672
|
}
|
|
13240
|
-
/**
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
13673
|
+
/**
|
|
13674
|
+
* Registers all tools from an MCP client.
|
|
13675
|
+
*
|
|
13676
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
13677
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
13678
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
13679
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
13680
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
13681
|
+
* never counted against the tool budget, and never has to be refused.
|
|
13682
|
+
*/
|
|
13683
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
13684
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
13685
|
+
const [, server, tool] = def.name.split("::");
|
|
13686
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
13687
|
+
});
|
|
13688
|
+
const names = assignMcpToolNames(parsed);
|
|
13689
|
+
parsed.forEach(({ def, server, tool }, i) => {
|
|
13246
13690
|
const wrapper = new McpToolWrapper(
|
|
13247
13691
|
mcpClient,
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
def.description.replace(`[MCP:${
|
|
13251
|
-
def.inputSchema
|
|
13692
|
+
server,
|
|
13693
|
+
tool,
|
|
13694
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
13695
|
+
def.inputSchema,
|
|
13696
|
+
names[i]
|
|
13252
13697
|
);
|
|
13698
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
13253
13699
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
13254
13700
|
this.register(wrapper);
|
|
13255
|
-
}
|
|
13701
|
+
});
|
|
13256
13702
|
}
|
|
13257
13703
|
setIgnoredPaths(patterns) {
|
|
13258
13704
|
this.ignoreMatcher = ignore2().add(patterns);
|
|
@@ -13589,6 +14035,17 @@ function fileOAuthProvider(storePath) {
|
|
|
13589
14035
|
}
|
|
13590
14036
|
});
|
|
13591
14037
|
}
|
|
14038
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
14039
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
14040
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
14041
|
+
const run = prev.then(fn, fn);
|
|
14042
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
14043
|
+
oauthStoreLocks.set(storePath, tail);
|
|
14044
|
+
void tail.then(() => {
|
|
14045
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
14046
|
+
});
|
|
14047
|
+
return run;
|
|
14048
|
+
}
|
|
13592
14049
|
async function connectMcpWithLoopbackOAuth(opts) {
|
|
13593
14050
|
const listener = await startCallbackListener(opts);
|
|
13594
14051
|
const transport = new StreamableHTTPClientTransport(new URL(opts.serverUrl), {
|
|
@@ -16069,7 +16526,50 @@ var GuidanceQueue = class {
|
|
|
16069
16526
|
}
|
|
16070
16527
|
};
|
|
16071
16528
|
|
|
16529
|
+
// src/tools/current-page.ts
|
|
16530
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
16531
|
+
var CurrentPageTool = class extends BaseTool {
|
|
16532
|
+
name = "read_current_page";
|
|
16533
|
+
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.';
|
|
16534
|
+
inputSchema = {
|
|
16535
|
+
type: "object",
|
|
16536
|
+
properties: {},
|
|
16537
|
+
required: []
|
|
16538
|
+
};
|
|
16539
|
+
provider;
|
|
16540
|
+
constructor(provider) {
|
|
16541
|
+
super();
|
|
16542
|
+
this.provider = provider;
|
|
16543
|
+
}
|
|
16544
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
16545
|
+
// nothing they have not already opened themselves.
|
|
16546
|
+
isDangerous() {
|
|
16547
|
+
return false;
|
|
16548
|
+
}
|
|
16549
|
+
async execute(_input, _options) {
|
|
16550
|
+
let page2;
|
|
16551
|
+
try {
|
|
16552
|
+
page2 = await this.provider();
|
|
16553
|
+
} catch (err) {
|
|
16554
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
16555
|
+
}
|
|
16556
|
+
if (!page2) {
|
|
16557
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
16558
|
+
}
|
|
16559
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
16560
|
+
|
|
16561
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
16562
|
+
return [
|
|
16563
|
+
`URL: ${page2.url}`,
|
|
16564
|
+
`Title: ${page2.title}`,
|
|
16565
|
+
"",
|
|
16566
|
+
text.trim() || "[the page rendered no readable text]"
|
|
16567
|
+
].join("\n");
|
|
16568
|
+
}
|
|
16569
|
+
};
|
|
16570
|
+
|
|
16072
16571
|
// src/core/cascade.ts
|
|
16572
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
16073
16573
|
function buildContextualPrompt(prompt, history = []) {
|
|
16074
16574
|
const recent = history.slice(-6);
|
|
16075
16575
|
if (recent.length === 0) return prompt;
|
|
@@ -16124,6 +16624,13 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16124
16624
|
}
|
|
16125
16625
|
});
|
|
16126
16626
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16627
|
+
try {
|
|
16628
|
+
const nodePath = __require("path");
|
|
16629
|
+
this.router.setDeadModelStore(new DeadModelStore(
|
|
16630
|
+
fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
|
|
16631
|
+
));
|
|
16632
|
+
} catch {
|
|
16633
|
+
}
|
|
16127
16634
|
this.multimodal = new MultimodalRegistry(
|
|
16128
16635
|
(this.config.providers ?? []).map((p) => p.type)
|
|
16129
16636
|
);
|
|
@@ -16285,6 +16792,94 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16285
16792
|
});
|
|
16286
16793
|
});
|
|
16287
16794
|
}
|
|
16795
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
16796
|
+
//
|
|
16797
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
16798
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
16799
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
16800
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
16801
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
16802
|
+
//
|
|
16803
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
16804
|
+
// timeout because the plan is already the model's considered proposal;
|
|
16805
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
16806
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
16807
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
16808
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
16809
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
16810
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
16811
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
16812
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
16813
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
16814
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
16815
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
16816
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
16817
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
16818
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
16819
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
16820
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
16821
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
16822
|
+
const requestId = randomUUID();
|
|
16823
|
+
return await new Promise((resolve2) => {
|
|
16824
|
+
let timeout;
|
|
16825
|
+
const settle = (decision) => {
|
|
16826
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
16827
|
+
if (timeout) clearTimeout(timeout);
|
|
16828
|
+
signal?.removeEventListener("abort", onAbort);
|
|
16829
|
+
resolve2(decision);
|
|
16830
|
+
};
|
|
16831
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
16832
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
16833
|
+
timeout = setTimeout(() => {
|
|
16834
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
16835
|
+
settle({ action: "timeout" });
|
|
16836
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
16837
|
+
this.pendingEscalations.set(requestId, settle);
|
|
16838
|
+
this.emit("escalation:decision-required", {
|
|
16839
|
+
taskId,
|
|
16840
|
+
requestId,
|
|
16841
|
+
sectionId: ctx.sectionId,
|
|
16842
|
+
sectionTitle: ctx.sectionTitle,
|
|
16843
|
+
issues: ctx.issues,
|
|
16844
|
+
summary: ctx.summary,
|
|
16845
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
16846
|
+
});
|
|
16847
|
+
});
|
|
16848
|
+
}
|
|
16849
|
+
/**
|
|
16850
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
16851
|
+
*
|
|
16852
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
16853
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
16854
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
16855
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
16856
|
+
* which is correct whenever there is exactly one.
|
|
16857
|
+
*
|
|
16858
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
16859
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
16860
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
16861
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
16862
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
16863
|
+
* see EscalationDecision.automatic.
|
|
16864
|
+
*/
|
|
16865
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
16866
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
16867
|
+
if (requestId) {
|
|
16868
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
16869
|
+
return;
|
|
16870
|
+
}
|
|
16871
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
16872
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
16873
|
+
}
|
|
16874
|
+
/**
|
|
16875
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
16876
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
16877
|
+
* same system-decided case as the early-return skips above, not a person
|
|
16878
|
+
* reviewing and accepting that section's output.
|
|
16879
|
+
*/
|
|
16880
|
+
releasePendingEscalations(action = "skip") {
|
|
16881
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
16882
|
+
}
|
|
16288
16883
|
/**
|
|
16289
16884
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
16290
16885
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -16418,6 +17013,20 @@ ${input}` }],
|
|
|
16418
17013
|
this.mediaSink = sink;
|
|
16419
17014
|
this.registerMediaTools(this.workspacePath);
|
|
16420
17015
|
}
|
|
17016
|
+
/**
|
|
17017
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
17018
|
+
*
|
|
17019
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
17020
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
17021
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
17022
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
17023
|
+
* touch the machine, and reading a page the user already opened touches
|
|
17024
|
+
* nothing. `disabledTools` still removes it.
|
|
17025
|
+
*/
|
|
17026
|
+
setCurrentPageProvider(provider) {
|
|
17027
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
17028
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
17029
|
+
}
|
|
16421
17030
|
registerMediaTools(workspacePath) {
|
|
16422
17031
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
16423
17032
|
const permitted = (name) => !denied.has(name);
|
|
@@ -16550,8 +17159,12 @@ ${last.partialOutput}` : "");
|
|
|
16550
17159
|
for (const server of this.config.tools.mcpServers) {
|
|
16551
17160
|
try {
|
|
16552
17161
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
16553
|
-
|
|
16554
|
-
|
|
17162
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
17163
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
17164
|
+
this.toolRegistry.registerMcpTools(
|
|
17165
|
+
this.mcpClient,
|
|
17166
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
17167
|
+
);
|
|
16555
17168
|
} catch (err) {
|
|
16556
17169
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
16557
17170
|
}
|
|
@@ -17016,6 +17629,7 @@ ${prompt}` : prompt;
|
|
|
17016
17629
|
} else if (complexity === "Moderate") {
|
|
17017
17630
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
17018
17631
|
t2.setRunBreaker(runBreaker);
|
|
17632
|
+
t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
17019
17633
|
t2.setPresenter(true);
|
|
17020
17634
|
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.");
|
|
17021
17635
|
if (identityPrompt) {
|
|
@@ -17060,11 +17674,17 @@ ${prompt}` : prompt;
|
|
|
17060
17674
|
t2Results = [t2Result];
|
|
17061
17675
|
const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
|
|
17062
17676
|
if (completed.length > 0) {
|
|
17063
|
-
finalOutput =
|
|
17677
|
+
finalOutput = composeRootSectionOutput(
|
|
17678
|
+
t2Result,
|
|
17679
|
+
completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
|
|
17680
|
+
);
|
|
17064
17681
|
} else {
|
|
17065
17682
|
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
17066
17683
|
const reasons = Array.from(
|
|
17067
|
-
new Set(
|
|
17684
|
+
new Set([
|
|
17685
|
+
...t2Result.issues ?? [],
|
|
17686
|
+
...t2Result.t3Results.flatMap((r) => r.issues ?? [])
|
|
17687
|
+
].filter(Boolean))
|
|
17068
17688
|
);
|
|
17069
17689
|
if (partial) {
|
|
17070
17690
|
finalOutput = reasons.length ? `${partial}
|
|
@@ -17077,6 +17697,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17077
17697
|
} else {
|
|
17078
17698
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
17079
17699
|
t1.setRunBreaker(runBreaker);
|
|
17700
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
17080
17701
|
t1.setPresenter(true);
|
|
17081
17702
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
17082
17703
|
if (identityPrompt) {
|
|
@@ -17101,6 +17722,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17101
17722
|
finalOutput = result.output;
|
|
17102
17723
|
t2Results = result.t2Results;
|
|
17103
17724
|
}
|
|
17725
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
17726
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
17104
17727
|
const trip = runBreaker.reason();
|
|
17105
17728
|
if (trip) {
|
|
17106
17729
|
finalOutput = [
|
|
@@ -17141,6 +17764,10 @@ ${finalOutput}` : ""
|
|
|
17141
17764
|
escalator.cancelAllPending();
|
|
17142
17765
|
} catch {
|
|
17143
17766
|
}
|
|
17767
|
+
try {
|
|
17768
|
+
this.releasePendingEscalations();
|
|
17769
|
+
} catch {
|
|
17770
|
+
}
|
|
17144
17771
|
this.router.restoreTierModels();
|
|
17145
17772
|
this.router.setRunSignal(void 0);
|
|
17146
17773
|
if (this.taskAnalyzer) {
|
|
@@ -21567,11 +22194,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
21567
22194
|
for (const i of incoming) map.set(key(i), i);
|
|
21568
22195
|
return [...map.values()];
|
|
21569
22196
|
}
|
|
22197
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
22198
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
22199
|
+
let n = 2;
|
|
22200
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
|
|
22201
|
+
return `${desired} ${n}`;
|
|
22202
|
+
}
|
|
22203
|
+
function mergeMcpServers(local, incoming) {
|
|
22204
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
22205
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
22206
|
+
const byName = /* @__PURE__ */ new Map();
|
|
22207
|
+
for (const l of local ?? []) byName.set(l.name, l);
|
|
22208
|
+
const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
|
|
22209
|
+
const synced = [];
|
|
22210
|
+
for (const inc of incoming) {
|
|
22211
|
+
if (byName.has(inc.name)) {
|
|
22212
|
+
byName.set(inc.name, inc);
|
|
22213
|
+
synced.push(inc);
|
|
22214
|
+
continue;
|
|
22215
|
+
}
|
|
22216
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
22217
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
22218
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
22219
|
+
byName.set(name, server);
|
|
22220
|
+
synced.push(server);
|
|
22221
|
+
prefixes.add(mcpServerPrefix(name));
|
|
22222
|
+
}
|
|
22223
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
22224
|
+
}
|
|
22225
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
22226
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
22227
|
+
return tools.map((t) => {
|
|
22228
|
+
for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
|
|
22229
|
+
return t;
|
|
22230
|
+
});
|
|
22231
|
+
}
|
|
22232
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
22233
|
+
const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
|
|
22234
|
+
const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
|
|
22235
|
+
const kept = (local ?? []).filter((t) => !covered(t));
|
|
22236
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
22237
|
+
return merged.length ? merged : void 0;
|
|
22238
|
+
}
|
|
21570
22239
|
function gatherSyncBundle(config) {
|
|
21571
|
-
const bundle = { v:
|
|
22240
|
+
const bundle = { v: 2 };
|
|
21572
22241
|
if (config.providers?.length) bundle.providers = config.providers;
|
|
21573
22242
|
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
21574
22243
|
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
22244
|
+
const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
|
|
22245
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
21575
22246
|
if (config.models) bundle.models = config.models;
|
|
21576
22247
|
if (config.budget) bundle.budget = config.budget;
|
|
21577
22248
|
const prefs = {};
|
|
@@ -21586,7 +22257,16 @@ function applySyncBundle(bundle, config) {
|
|
|
21586
22257
|
const next = { ...config, tools: { ...config.tools } };
|
|
21587
22258
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
21588
22259
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
21589
|
-
|
|
22260
|
+
const mcp2 = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
|
|
22261
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp2.servers;
|
|
22262
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
22263
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
22264
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
22265
|
+
config.tools?.disabledTools,
|
|
22266
|
+
rewriteDisabledTools(bundle.disabledTools, mcp2.rewrite),
|
|
22267
|
+
bundle.mcpServers ? mcp2.synced : void 0
|
|
22268
|
+
);
|
|
22269
|
+
}
|
|
21590
22270
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
21591
22271
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
21592
22272
|
if (bundle.prefs) {
|
|
@@ -21927,12 +22607,19 @@ function defaultName(url) {
|
|
|
21927
22607
|
}
|
|
21928
22608
|
}
|
|
21929
22609
|
async function mcpConnectCommand(url, opts = {}) {
|
|
21930
|
-
const
|
|
21931
|
-
const store = new FileMcpOAuthStore(storePathFor(name));
|
|
22610
|
+
const desiredName = opts.name?.trim() || defaultName(url);
|
|
21932
22611
|
console.log(chalk7.magenta("\n \u25C8 Connect an MCP server via OAuth"));
|
|
21933
22612
|
console.log(chalk7.dim(` ${url}
|
|
21934
22613
|
`));
|
|
21935
22614
|
try {
|
|
22615
|
+
const cm = new ConfigManager(process.cwd());
|
|
22616
|
+
await cm.load();
|
|
22617
|
+
const config = cm.getConfig();
|
|
22618
|
+
config.tools = config.tools ?? {};
|
|
22619
|
+
const servers = config.tools.mcpServers ?? [];
|
|
22620
|
+
const existingIdx = servers.findIndex((s) => s.name === desiredName);
|
|
22621
|
+
const name = existingIdx >= 0 ? desiredName : uniqueMcpServerName(desiredName, servers.map((s) => s.name));
|
|
22622
|
+
const store = new FileMcpOAuthStore(storePathFor(name));
|
|
21936
22623
|
await connectMcpWithLoopbackOAuth({
|
|
21937
22624
|
serverUrl: url,
|
|
21938
22625
|
store,
|
|
@@ -21942,14 +22629,8 @@ async function mcpConnectCommand(url, opts = {}) {
|
|
|
21942
22629
|
openBrowser2(u);
|
|
21943
22630
|
}
|
|
21944
22631
|
});
|
|
21945
|
-
const cm = new ConfigManager(process.cwd());
|
|
21946
|
-
await cm.load();
|
|
21947
|
-
const config = cm.getConfig();
|
|
21948
|
-
config.tools = config.tools ?? {};
|
|
21949
|
-
const servers = config.tools.mcpServers ?? [];
|
|
21950
22632
|
const entry = { name, url, oauthStore: storePathFor(name) };
|
|
21951
|
-
|
|
21952
|
-
if (idx >= 0) servers[idx] = entry;
|
|
22633
|
+
if (existingIdx >= 0) servers[existingIdx] = entry;
|
|
21953
22634
|
else servers.push(entry);
|
|
21954
22635
|
config.tools.mcpServers = servers;
|
|
21955
22636
|
config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
|
|
@@ -21994,6 +22675,7 @@ async function mcpRemoveCommand(name) {
|
|
|
21994
22675
|
}
|
|
21995
22676
|
config.tools.mcpServers = servers.filter((s) => s.name !== name);
|
|
21996
22677
|
config.tools.mcpTrusted = (config.tools.mcpTrusted ?? []).filter((n) => n !== name);
|
|
22678
|
+
config.tools.disabledTools = removeMcpServerDenials(config.tools.disabledTools, name);
|
|
21997
22679
|
if (match.oauthStore) {
|
|
21998
22680
|
try {
|
|
21999
22681
|
new FileMcpOAuthStore(match.oauthStore).clear();
|
|
@@ -22100,6 +22782,14 @@ var DashboardSocket = class {
|
|
|
22100
22782
|
this.secret = options.secret;
|
|
22101
22783
|
this.setupHandlers();
|
|
22102
22784
|
}
|
|
22785
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
22786
|
+
roomSize(room) {
|
|
22787
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
22788
|
+
}
|
|
22789
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
22790
|
+
hasSocket(socketId) {
|
|
22791
|
+
return this.io.sockets.sockets.has(socketId);
|
|
22792
|
+
}
|
|
22103
22793
|
broadcastToRoom(room, event, data) {
|
|
22104
22794
|
this.io.to(room).emit(event, data);
|
|
22105
22795
|
}
|
|
@@ -22171,6 +22861,22 @@ var DashboardSocket = class {
|
|
|
22171
22861
|
});
|
|
22172
22862
|
});
|
|
22173
22863
|
}
|
|
22864
|
+
/**
|
|
22865
|
+
* A client subscribed to a session's room.
|
|
22866
|
+
*
|
|
22867
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
22868
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
22869
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
22870
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
22871
|
+
*/
|
|
22872
|
+
onJoinSession(callback) {
|
|
22873
|
+
this.io.on("connection", (socket) => {
|
|
22874
|
+
socket.on("join:session", (payload) => {
|
|
22875
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
22876
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
22877
|
+
});
|
|
22878
|
+
});
|
|
22879
|
+
}
|
|
22174
22880
|
onSessionRate(callback) {
|
|
22175
22881
|
this.io.on("connection", (socket) => {
|
|
22176
22882
|
socket.on("session:rate", (payload) => {
|
|
@@ -22208,6 +22914,31 @@ var DashboardSocket = class {
|
|
|
22208
22914
|
});
|
|
22209
22915
|
});
|
|
22210
22916
|
}
|
|
22917
|
+
/**
|
|
22918
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
22919
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
22920
|
+
* answers here.
|
|
22921
|
+
*
|
|
22922
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
22923
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
22924
|
+
*/
|
|
22925
|
+
onEscalationDecision(callback) {
|
|
22926
|
+
this.io.on("connection", (socket) => {
|
|
22927
|
+
socket.on("escalation:decide", (payload) => {
|
|
22928
|
+
const action = payload?.action;
|
|
22929
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
22930
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
22931
|
+
callback({
|
|
22932
|
+
sessionId: payload.sessionId,
|
|
22933
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
22934
|
+
// run concurrently, so more than one can be waiting.
|
|
22935
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
22936
|
+
action,
|
|
22937
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
22938
|
+
});
|
|
22939
|
+
});
|
|
22940
|
+
});
|
|
22941
|
+
}
|
|
22211
22942
|
onSessionSteer(callback) {
|
|
22212
22943
|
this.io.on("connection", (socket) => {
|
|
22213
22944
|
socket.on("session:steer", (payload) => {
|
|
@@ -22369,6 +23100,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
22369
23100
|
|
|
22370
23101
|
// src/dashboard/server.ts
|
|
22371
23102
|
var __dirname$1 = path32.dirname(fileURLToPath(import.meta.url));
|
|
23103
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
22372
23104
|
var DashboardServer = class {
|
|
22373
23105
|
app;
|
|
22374
23106
|
httpServer;
|
|
@@ -22379,6 +23111,13 @@ var DashboardServer = class {
|
|
|
22379
23111
|
globalStore = null;
|
|
22380
23112
|
broadcastTimer = null;
|
|
22381
23113
|
activeSessions = /* @__PURE__ */ new Map();
|
|
23114
|
+
/**
|
|
23115
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
23116
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
23117
|
+
* browser — so the tool is simply not registered rather than present and
|
|
23118
|
+
* always failing.
|
|
23119
|
+
*/
|
|
23120
|
+
currentPageProvider;
|
|
22382
23121
|
activeControllers = /* @__PURE__ */ new Map();
|
|
22383
23122
|
/**
|
|
22384
23123
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -22393,6 +23132,17 @@ var DashboardServer = class {
|
|
|
22393
23132
|
* map is how that answer reaches the run that's blocked on it.
|
|
22394
23133
|
*/
|
|
22395
23134
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
23135
|
+
/**
|
|
23136
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
23137
|
+
* delivered to a client that was not reachable when it was raised.
|
|
23138
|
+
*
|
|
23139
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
23140
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
23141
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
23142
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
23143
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
23144
|
+
*/
|
|
23145
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
22396
23146
|
/**
|
|
22397
23147
|
* The orchestration decision trail ("why") of each session's most recent
|
|
22398
23148
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -22470,20 +23220,29 @@ var DashboardServer = class {
|
|
|
22470
23220
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
22471
23221
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
22472
23222
|
this.activeSessions.set(sessionId, cascade);
|
|
23223
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
22473
23224
|
cascade.on("stream:token", (e) => {
|
|
22474
|
-
this.
|
|
23225
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
22475
23226
|
});
|
|
22476
23227
|
cascade.on("tier:status", (e) => {
|
|
22477
|
-
this.
|
|
23228
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
|
|
22478
23229
|
});
|
|
22479
23230
|
cascade.on("permission:user-required", (e) => {
|
|
22480
|
-
this.
|
|
23231
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
|
|
22481
23232
|
});
|
|
22482
23233
|
cascade.on("peer:message", (e) => {
|
|
22483
23234
|
this.socket.emitPeerMessage(e);
|
|
22484
23235
|
});
|
|
22485
23236
|
cascade.on("plan:approval-required", (e) => {
|
|
22486
|
-
this.
|
|
23237
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
|
|
23238
|
+
});
|
|
23239
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
23240
|
+
this.raiseEscalation(sessionId, socketId, e);
|
|
23241
|
+
});
|
|
23242
|
+
cascade.on("escalation:timeout", (e) => {
|
|
23243
|
+
const ev = e;
|
|
23244
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
23245
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
22487
23246
|
});
|
|
22488
23247
|
try {
|
|
22489
23248
|
const result = await cascade.run({
|
|
@@ -22494,7 +23253,7 @@ var DashboardServer = class {
|
|
|
22494
23253
|
this.recordSessionTask(sessionId, result.taskId);
|
|
22495
23254
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
22496
23255
|
this.captureWhy(sessionId, cascade, result);
|
|
22497
|
-
this.
|
|
23256
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
22498
23257
|
this.socket.broadcast("cost:update", {
|
|
22499
23258
|
sessionId,
|
|
22500
23259
|
totalTokens: result.usage.totalTokens,
|
|
@@ -22507,7 +23266,7 @@ var DashboardServer = class {
|
|
|
22507
23266
|
} catch (err) {
|
|
22508
23267
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
22509
23268
|
this.captureWhy(sessionId, cascade);
|
|
22510
|
-
this.
|
|
23269
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
22511
23270
|
sessionId,
|
|
22512
23271
|
error: err instanceof Error ? err.message : String(err)
|
|
22513
23272
|
});
|
|
@@ -22515,6 +23274,7 @@ var DashboardServer = class {
|
|
|
22515
23274
|
this.activeSessions.delete(sessionId);
|
|
22516
23275
|
this.activeControllers.delete(sessionId);
|
|
22517
23276
|
this.denyPendingApprovals(sessionId);
|
|
23277
|
+
this.clearSessionEscalations(sessionId);
|
|
22518
23278
|
}
|
|
22519
23279
|
});
|
|
22520
23280
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -22524,10 +23284,20 @@ var DashboardServer = class {
|
|
|
22524
23284
|
editedPlan
|
|
22525
23285
|
);
|
|
22526
23286
|
});
|
|
23287
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
23288
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
23289
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
23290
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
23291
|
+
});
|
|
23292
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
23293
|
+
this.replayEscalations(sessionId, socketId);
|
|
23294
|
+
});
|
|
22527
23295
|
this.socket.onSessionHalt((sessionId) => {
|
|
22528
23296
|
this.activeControllers.get(sessionId)?.abort();
|
|
22529
23297
|
this.denyPendingApprovals(sessionId);
|
|
22530
23298
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
23299
|
+
this.clearSessionEscalations(sessionId);
|
|
23300
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
22531
23301
|
});
|
|
22532
23302
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
22533
23303
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -22542,6 +23312,10 @@ var DashboardServer = class {
|
|
|
22542
23312
|
}
|
|
22543
23313
|
});
|
|
22544
23314
|
}
|
|
23315
|
+
/** Wire the host's browser view in (desktop only). */
|
|
23316
|
+
setCurrentPageProvider(provider) {
|
|
23317
|
+
this.currentPageProvider = provider;
|
|
23318
|
+
}
|
|
22545
23319
|
async start() {
|
|
22546
23320
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
22547
23321
|
if (!isLoopback) {
|
|
@@ -22830,6 +23604,116 @@ var DashboardServer = class {
|
|
|
22830
23604
|
this.activeSessions.delete(sessionId);
|
|
22831
23605
|
}
|
|
22832
23606
|
}
|
|
23607
|
+
/**
|
|
23608
|
+
* Is anyone able to answer for this session right now?
|
|
23609
|
+
*
|
|
23610
|
+
* The originating socket first — that connection is definitely watching this
|
|
23611
|
+
* run — then the session room, which is where a client lands after a
|
|
23612
|
+
* reconnect issues it a new socket id.
|
|
23613
|
+
*/
|
|
23614
|
+
hasResponder(sessionId, socketId) {
|
|
23615
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
23616
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
23617
|
+
}
|
|
23618
|
+
/**
|
|
23619
|
+
* Send a run event to whoever is watching this session.
|
|
23620
|
+
*
|
|
23621
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
23622
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
23623
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
23624
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
23625
|
+
* client that is both the originator and a room member from seeing every
|
|
23626
|
+
* token twice.
|
|
23627
|
+
*/
|
|
23628
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
23629
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
23630
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
23631
|
+
}
|
|
23632
|
+
/**
|
|
23633
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
23634
|
+
*
|
|
23635
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
23636
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
23637
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
23638
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
23639
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
23640
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
23641
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
23642
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
23643
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
23644
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
23645
|
+
* mistaken for an absent user.
|
|
23646
|
+
*/
|
|
23647
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
23648
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : randomUUID();
|
|
23649
|
+
const payload = { sessionId, ...event };
|
|
23650
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
23651
|
+
this.pendingEscalations.set(requestId, entry);
|
|
23652
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
23653
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
23654
|
+
return;
|
|
23655
|
+
}
|
|
23656
|
+
entry.graceTimer = setTimeout(() => {
|
|
23657
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
23658
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
23659
|
+
this.pendingEscalations.delete(requestId);
|
|
23660
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
23661
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
23662
|
+
}
|
|
23663
|
+
/**
|
|
23664
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
23665
|
+
*
|
|
23666
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
23667
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
23668
|
+
* its gates and the clients queue theirs.
|
|
23669
|
+
*
|
|
23670
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
23671
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
23672
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
23673
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
23674
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
23675
|
+
* sides never have to agree on a wall clock.
|
|
23676
|
+
*/
|
|
23677
|
+
replayEscalations(sessionId, socketId) {
|
|
23678
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
23679
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23680
|
+
if (entry.graceTimer) {
|
|
23681
|
+
clearTimeout(entry.graceTimer);
|
|
23682
|
+
entry.graceTimer = void 0;
|
|
23683
|
+
}
|
|
23684
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
23685
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
23686
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
23687
|
+
}
|
|
23688
|
+
}
|
|
23689
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
23690
|
+
clearPendingEscalation(requestId) {
|
|
23691
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
23692
|
+
if (!entry) return;
|
|
23693
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23694
|
+
this.pendingEscalations.delete(requestId);
|
|
23695
|
+
}
|
|
23696
|
+
/**
|
|
23697
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
23698
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
23699
|
+
* Insertion order makes `values()` oldest-first.
|
|
23700
|
+
*/
|
|
23701
|
+
clearOldestSessionEscalation(sessionId) {
|
|
23702
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
23703
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23704
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23705
|
+
this.pendingEscalations.delete(id);
|
|
23706
|
+
return;
|
|
23707
|
+
}
|
|
23708
|
+
}
|
|
23709
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
23710
|
+
clearSessionEscalations(sessionId) {
|
|
23711
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
23712
|
+
if (entry.sessionId !== sessionId) continue;
|
|
23713
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
23714
|
+
this.pendingEscalations.delete(id);
|
|
23715
|
+
}
|
|
23716
|
+
}
|
|
22833
23717
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
22834
23718
|
denyPendingApprovals(sessionId) {
|
|
22835
23719
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -23330,6 +24214,7 @@ ${prompt}`;
|
|
|
23330
24214
|
void (async () => {
|
|
23331
24215
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
23332
24216
|
this.activeSessions.set(sessionId, cascade);
|
|
24217
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
23333
24218
|
cascade.on("stream:token", (e) => {
|
|
23334
24219
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
23335
24220
|
});
|
|
@@ -23345,6 +24230,14 @@ ${prompt}`;
|
|
|
23345
24230
|
cascade.on("plan:approval-required", (e) => {
|
|
23346
24231
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
23347
24232
|
});
|
|
24233
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
24234
|
+
this.raiseEscalation(sessionId, void 0, e);
|
|
24235
|
+
});
|
|
24236
|
+
cascade.on("escalation:timeout", (e) => {
|
|
24237
|
+
const ev = e;
|
|
24238
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
24239
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
24240
|
+
});
|
|
23348
24241
|
try {
|
|
23349
24242
|
const result = await cascade.run({
|
|
23350
24243
|
prompt: runPrompt,
|
|
@@ -23372,6 +24265,7 @@ ${prompt}`;
|
|
|
23372
24265
|
} finally {
|
|
23373
24266
|
this.activeSessions.delete(sessionId);
|
|
23374
24267
|
this.denyPendingApprovals(sessionId);
|
|
24268
|
+
this.clearSessionEscalations(sessionId);
|
|
23375
24269
|
}
|
|
23376
24270
|
})();
|
|
23377
24271
|
});
|