cascade-ai 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +958 -64
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +958 -64
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +992 -55
- package/dist/index.cjs +992 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +512 -4
- package/dist/index.d.ts +512 -4
- package/dist/index.js +983 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,6 +41,12 @@ import cron from 'node-cron';
|
|
|
41
41
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
42
42
|
var __defProp = Object.defineProperty;
|
|
43
43
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
44
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
45
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
46
|
+
}) : x)(function(x) {
|
|
47
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
48
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
49
|
+
});
|
|
44
50
|
var __esm = (fn, res) => function __init() {
|
|
45
51
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
46
52
|
};
|
|
@@ -1845,7 +1851,7 @@ function reconcilePrice(model, live) {
|
|
|
1845
1851
|
}
|
|
1846
1852
|
|
|
1847
1853
|
// src/constants.ts
|
|
1848
|
-
var CASCADE_VERSION = "0.
|
|
1854
|
+
var CASCADE_VERSION = "0.55.0";
|
|
1849
1855
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1850
1856
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1851
1857
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -3658,7 +3664,21 @@ var ModelSelector = class {
|
|
|
3658
3664
|
this.availableProviders = availableProviders;
|
|
3659
3665
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
3660
3666
|
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Verdicts from earlier runs about ids that 404 on this account. Consulted
|
|
3669
|
+
* BEFORE a model enters the pool, which is the only placement that helps: a
|
|
3670
|
+
* T3 wave runs concurrently, so filtering after the first failure still lets
|
|
3671
|
+
* every other worker in the wave call the dead id.
|
|
3672
|
+
*/
|
|
3673
|
+
deadModels;
|
|
3674
|
+
setDeadModelStore(store) {
|
|
3675
|
+
this.deadModels = store;
|
|
3676
|
+
for (const [id, m] of [...this.availableModels]) {
|
|
3677
|
+
if (this.deadModels.isDead(m.provider, m.id)) this.availableModels.delete(id);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3661
3680
|
addDynamicModel(model) {
|
|
3681
|
+
if (this.deadModels?.isDead(model.provider, model.id)) return;
|
|
3662
3682
|
this.availableModels.set(model.id, model);
|
|
3663
3683
|
if (!(model.id in MODELS)) this.discovered.add(normalizeModelId(model.id));
|
|
3664
3684
|
}
|
|
@@ -4065,6 +4085,121 @@ var LocalRequestQueue = class {
|
|
|
4065
4085
|
}
|
|
4066
4086
|
};
|
|
4067
4087
|
|
|
4088
|
+
// src/core/router/dead-models.ts
|
|
4089
|
+
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4090
|
+
function deadModelKey(provider, modelId) {
|
|
4091
|
+
return `${provider}:${modelId}`;
|
|
4092
|
+
}
|
|
4093
|
+
var DeadModelStore = class {
|
|
4094
|
+
constructor(persistence, opts = {}) {
|
|
4095
|
+
this.persistence = persistence;
|
|
4096
|
+
this.ttlMs = opts.ttlMs ?? DEAD_MODEL_TTL_MS;
|
|
4097
|
+
this.now = opts.now ?? (() => Date.now());
|
|
4098
|
+
this.restore();
|
|
4099
|
+
}
|
|
4100
|
+
persistence;
|
|
4101
|
+
records = /* @__PURE__ */ new Map();
|
|
4102
|
+
ttlMs;
|
|
4103
|
+
now;
|
|
4104
|
+
restore() {
|
|
4105
|
+
if (!this.persistence) return;
|
|
4106
|
+
try {
|
|
4107
|
+
for (const r of this.persistence.load()) {
|
|
4108
|
+
if (!this.isExpired(r)) this.records.set(r.key, r);
|
|
4109
|
+
}
|
|
4110
|
+
} catch {
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
isExpired(r) {
|
|
4114
|
+
return this.now() - r.lastSeenAt > this.ttlMs;
|
|
4115
|
+
}
|
|
4116
|
+
flush() {
|
|
4117
|
+
if (!this.persistence) return;
|
|
4118
|
+
try {
|
|
4119
|
+
this.persistence.save([...this.records.values()]);
|
|
4120
|
+
} catch {
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* Record that a model was found dead. Returns true when this is the first
|
|
4125
|
+
* time (so the caller can log it once rather than once per concurrent
|
|
4126
|
+
* worker in the wave that discovered it).
|
|
4127
|
+
*/
|
|
4128
|
+
record(provider, modelId, reason) {
|
|
4129
|
+
const key = deadModelKey(provider, modelId);
|
|
4130
|
+
const now = this.now();
|
|
4131
|
+
const existing = this.records.get(key);
|
|
4132
|
+
if (existing && !this.isExpired(existing)) {
|
|
4133
|
+
existing.lastSeenAt = now;
|
|
4134
|
+
existing.hits += 1;
|
|
4135
|
+
this.flush();
|
|
4136
|
+
return false;
|
|
4137
|
+
}
|
|
4138
|
+
this.records.set(key, {
|
|
4139
|
+
key,
|
|
4140
|
+
provider,
|
|
4141
|
+
modelId,
|
|
4142
|
+
reason,
|
|
4143
|
+
firstSeenAt: now,
|
|
4144
|
+
lastSeenAt: now,
|
|
4145
|
+
hits: 1
|
|
4146
|
+
});
|
|
4147
|
+
this.flush();
|
|
4148
|
+
return true;
|
|
4149
|
+
}
|
|
4150
|
+
/** True when this model is known dead and the verdict has not expired. */
|
|
4151
|
+
isDead(provider, modelId) {
|
|
4152
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
4153
|
+
if (!r) return false;
|
|
4154
|
+
if (this.isExpired(r)) {
|
|
4155
|
+
this.records.delete(r.key);
|
|
4156
|
+
this.flush();
|
|
4157
|
+
return false;
|
|
4158
|
+
}
|
|
4159
|
+
return true;
|
|
4160
|
+
}
|
|
4161
|
+
/** Why a model is considered dead, for /why and the models command. */
|
|
4162
|
+
reasonFor(provider, modelId) {
|
|
4163
|
+
const r = this.records.get(deadModelKey(provider, modelId));
|
|
4164
|
+
return r && !this.isExpired(r) ? r.reason : null;
|
|
4165
|
+
}
|
|
4166
|
+
/** Live records, expired ones filtered out. */
|
|
4167
|
+
list() {
|
|
4168
|
+
const live = [...this.records.values()].filter((r) => !this.isExpired(r));
|
|
4169
|
+
return live.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
4170
|
+
}
|
|
4171
|
+
/**
|
|
4172
|
+
* Forget a verdict — used when the user fixes the underlying problem (adds
|
|
4173
|
+
* the model to their project, gets quota) and wants it retried now rather
|
|
4174
|
+
* than after the TTL.
|
|
4175
|
+
*/
|
|
4176
|
+
forget(provider, modelId) {
|
|
4177
|
+
const removed = this.records.delete(deadModelKey(provider, modelId));
|
|
4178
|
+
if (removed) this.flush();
|
|
4179
|
+
return removed;
|
|
4180
|
+
}
|
|
4181
|
+
clear() {
|
|
4182
|
+
this.records.clear();
|
|
4183
|
+
this.flush();
|
|
4184
|
+
}
|
|
4185
|
+
};
|
|
4186
|
+
function fileDeadModelPersistence(filePath) {
|
|
4187
|
+
return {
|
|
4188
|
+
load() {
|
|
4189
|
+
const fs25 = __require("fs");
|
|
4190
|
+
if (!fs25.existsSync(filePath)) return [];
|
|
4191
|
+
const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
4192
|
+
return Array.isArray(raw) ? raw : [];
|
|
4193
|
+
},
|
|
4194
|
+
save(records) {
|
|
4195
|
+
const fs25 = __require("fs");
|
|
4196
|
+
const path26 = __require("path");
|
|
4197
|
+
fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
|
|
4198
|
+
fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4068
4203
|
// src/utils/retry.ts
|
|
4069
4204
|
var CascadeCancelledError = class extends Error {
|
|
4070
4205
|
constructor(reason) {
|
|
@@ -4654,10 +4789,47 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4654
4789
|
*/
|
|
4655
4790
|
budgetState = "ok";
|
|
4656
4791
|
budgetExceededReason;
|
|
4792
|
+
/**
|
|
4793
|
+
* Whether the run/session budget is permanently tripped, and why.
|
|
4794
|
+
*
|
|
4795
|
+
* A tier or worker that catches `BudgetExceededError` internally (T3Worker
|
|
4796
|
+
* does, so one dead-end subtask doesn't crash the whole run) absorbs the
|
|
4797
|
+
* exception into a normal FAILED/PARTIAL result — the tier returns as if
|
|
4798
|
+
* nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
|
|
4799
|
+
* budget-exceeded handling (the `run:budget-exceeded` event and
|
|
4800
|
+
* `lastInterruptedRun` for `/continue`). This lets a caller check the
|
|
4801
|
+
* router's own state directly after a tier returns, regardless of how that
|
|
4802
|
+
* tier chose to report the error internally.
|
|
4803
|
+
*/
|
|
4804
|
+
budgetExceededInfo() {
|
|
4805
|
+
if (this.runBudgetExceeded) return { reason: this.runBudgetExceededReason ?? "Per-task budget exceeded." };
|
|
4806
|
+
if (this.budgetState === "exceeded") return { reason: this.budgetExceededReason ?? "Session budget exceeded." };
|
|
4807
|
+
return null;
|
|
4808
|
+
}
|
|
4657
4809
|
tpmLimiter;
|
|
4658
4810
|
localQueue;
|
|
4659
4811
|
taskAnalyzer;
|
|
4660
4812
|
pendingFeedbackSource;
|
|
4813
|
+
/** Models known dead for this account (404 / retired / not enabled). */
|
|
4814
|
+
deadModels = new DeadModelStore();
|
|
4815
|
+
/**
|
|
4816
|
+
* Back the dead-model memory with durable storage (a JSON file on desktop, a
|
|
4817
|
+
* per-user table in the cloud). Without one the verdicts still work, but only
|
|
4818
|
+
* for the life of the process — so tomorrow's run pays the same wave of 404s
|
|
4819
|
+
* to learn the same fact.
|
|
4820
|
+
*/
|
|
4821
|
+
setDeadModelStore(store) {
|
|
4822
|
+
this.deadModels = store;
|
|
4823
|
+
this.selector.setDeadModelStore(store);
|
|
4824
|
+
}
|
|
4825
|
+
/** Models known dead for this account, for `cascade models` and /why. */
|
|
4826
|
+
listDeadModels() {
|
|
4827
|
+
return this.deadModels.list();
|
|
4828
|
+
}
|
|
4829
|
+
/** Retry a model the user has since fixed, without waiting out the TTL. */
|
|
4830
|
+
forgetDeadModel(provider, modelId) {
|
|
4831
|
+
return this.deadModels.forget(provider, modelId);
|
|
4832
|
+
}
|
|
4661
4833
|
worldStateDB;
|
|
4662
4834
|
privacyPaths;
|
|
4663
4835
|
guidanceQueue;
|
|
@@ -5022,6 +5194,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
5022
5194
|
}
|
|
5023
5195
|
if (isModelNotFoundError(errMsg)) {
|
|
5024
5196
|
this.selector.removeModel(model.id);
|
|
5197
|
+
const firstSighting = this.deadModels.record(model.provider, model.id, errMsg);
|
|
5198
|
+
if (firstSighting) this.emit("model:dead", { provider: model.provider, modelId: model.id, reason: errMsg });
|
|
5025
5199
|
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
5026
5200
|
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
5027
5201
|
if (next && next.id !== model.id) {
|
|
@@ -6289,6 +6463,13 @@ Now execute your subtask using this context where relevant.`
|
|
|
6289
6463
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput2, "FAILED");
|
|
6290
6464
|
return this.buildResult("ESCALATED", finalOutput2, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6291
6465
|
}
|
|
6466
|
+
if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
6467
|
+
issues.push(errMsg);
|
|
6468
|
+
const stopped = output || errMsg;
|
|
6469
|
+
this.setStatus("FAILED", stopped);
|
|
6470
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, stopped, "FAILED");
|
|
6471
|
+
return this.buildResult("FAILED", stopped, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
6472
|
+
}
|
|
6292
6473
|
issues.push(`Execution error: ${errMsg}`);
|
|
6293
6474
|
const finalOutput = output || errMsg;
|
|
6294
6475
|
this.setStatus("FAILED", finalOutput);
|
|
@@ -7310,6 +7491,26 @@ var RedactionLayer = class {
|
|
|
7310
7491
|
}
|
|
7311
7492
|
};
|
|
7312
7493
|
|
|
7494
|
+
// src/core/tiers/escalation-policy.ts
|
|
7495
|
+
function sectionNeedsDecision(results) {
|
|
7496
|
+
return results.some((r) => r.status === "ESCALATED");
|
|
7497
|
+
}
|
|
7498
|
+
function settledEscalationStatus(results) {
|
|
7499
|
+
const hasWork = results.some((r) => r.status === "COMPLETED" || !!r.output);
|
|
7500
|
+
return hasWork ? "PARTIAL" : "FAILED";
|
|
7501
|
+
}
|
|
7502
|
+
function composeRootSectionOutput(t2Result, completedOutputs) {
|
|
7503
|
+
let output = `${t2Result.sectionSummary}
|
|
7504
|
+
|
|
7505
|
+
${completedOutputs.join("\n\n")}`;
|
|
7506
|
+
if (t2Result.status === "FAILED" && t2Result.issues?.length) {
|
|
7507
|
+
output += `
|
|
7508
|
+
|
|
7509
|
+
_(incomplete: ${t2Result.issues.join("; ")})_`;
|
|
7510
|
+
}
|
|
7511
|
+
return output;
|
|
7512
|
+
}
|
|
7513
|
+
|
|
7313
7514
|
// src/core/tiers/t2-manager.ts
|
|
7314
7515
|
function buildT2SystemPrompt(hasPeerMessage) {
|
|
7315
7516
|
return [
|
|
@@ -7335,6 +7536,19 @@ var T2Manager = class extends BaseTier {
|
|
|
7335
7536
|
t2PeerBus;
|
|
7336
7537
|
permissionEscalator;
|
|
7337
7538
|
toolCreator;
|
|
7539
|
+
/**
|
|
7540
|
+
* Asked when a section ends ESCALATED — i.e. a worker hit something it could
|
|
7541
|
+
* not decide alone. Without this the escalation was a dead end: the status
|
|
7542
|
+
* said "needs a decision" and no decision was ever requested, so an MCP run
|
|
7543
|
+
* that legitimately needed input just stopped there.
|
|
7544
|
+
*/
|
|
7545
|
+
escalationCallback;
|
|
7546
|
+
/**
|
|
7547
|
+
* The one allowed retry has been spent. Retrying is bounded because an
|
|
7548
|
+
* escalation loop that can re-ask forever burns a budget on a question that
|
|
7549
|
+
* is not resolving; a second escalation is terminal instead.
|
|
7550
|
+
*/
|
|
7551
|
+
escalationRetryUsed = false;
|
|
7338
7552
|
/** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
|
|
7339
7553
|
planApprovalCallback;
|
|
7340
7554
|
/** AbortController for the current T3 wave — aborted on cancel-and-respawn */
|
|
@@ -7383,6 +7597,10 @@ var T2Manager = class extends BaseTier {
|
|
|
7383
7597
|
setToolCreator(creator) {
|
|
7384
7598
|
this.toolCreator = creator;
|
|
7385
7599
|
}
|
|
7600
|
+
/** Ask the user what to do when a section escalates. */
|
|
7601
|
+
setEscalationCallback(cb) {
|
|
7602
|
+
this.escalationCallback = cb;
|
|
7603
|
+
}
|
|
7386
7604
|
/** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
|
|
7387
7605
|
setPlanApprovalCallback(cb) {
|
|
7388
7606
|
this.planApprovalCallback = cb;
|
|
@@ -7487,9 +7705,57 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7487
7705
|
currentAction: "Aggregating T3 results",
|
|
7488
7706
|
status: "IN_PROGRESS"
|
|
7489
7707
|
});
|
|
7490
|
-
|
|
7708
|
+
let summary = await this.aggregateResults(assignment, t3Results);
|
|
7491
7709
|
const issues = t3Results.filter((r) => r.status !== "COMPLETED").flatMap((r) => r.issues);
|
|
7492
|
-
|
|
7710
|
+
let overallStatus = this.determineStatus(t3Results);
|
|
7711
|
+
let userSkipped = false;
|
|
7712
|
+
const hasEscalated = sectionNeedsDecision(t3Results);
|
|
7713
|
+
const settleEscalated = async (reason) => {
|
|
7714
|
+
if (reason) issues.push(reason);
|
|
7715
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7716
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7717
|
+
};
|
|
7718
|
+
if (hasEscalated && this.escalationCallback && this.escalationRetryUsed) {
|
|
7719
|
+
await settleEscalated("Escalated again after the retry \u2014 no further attempts were made.");
|
|
7720
|
+
} else if (hasEscalated && !this.escalationCallback) {
|
|
7721
|
+
await settleEscalated();
|
|
7722
|
+
} else if (hasEscalated && this.escalationCallback) {
|
|
7723
|
+
this.sendStatusUpdate({
|
|
7724
|
+
progressPct: 95,
|
|
7725
|
+
currentAction: "Escalated \u2014 waiting for your decision",
|
|
7726
|
+
status: "ESCALATING"
|
|
7727
|
+
});
|
|
7728
|
+
const decision = await this.escalationCallback({
|
|
7729
|
+
sectionId: assignment.sectionId,
|
|
7730
|
+
sectionTitle: assignment.sectionTitle,
|
|
7731
|
+
issues,
|
|
7732
|
+
summary
|
|
7733
|
+
});
|
|
7734
|
+
this.log(`Escalation decision for "${assignment.sectionTitle}": ${decision.action}`);
|
|
7735
|
+
if (decision.action === "skip") {
|
|
7736
|
+
summary = await this.aggregateResults(assignment, t3Results, { includeEscalated: true });
|
|
7737
|
+
overallStatus = settledEscalationStatus(t3Results);
|
|
7738
|
+
userSkipped = decision.automatic !== true;
|
|
7739
|
+
} else if (decision.action === "retry" || decision.action === "guidance") {
|
|
7740
|
+
this.escalationRetryUsed = true;
|
|
7741
|
+
const guided = decision.action === "guidance" && decision.note?.trim() ? {
|
|
7742
|
+
...assignment,
|
|
7743
|
+
description: `${assignment.description}
|
|
7744
|
+
|
|
7745
|
+
Guidance (must be followed): ${decision.note.trim()}`,
|
|
7746
|
+
// Drop T1's preplanned subtasks so the retry re-decomposes with
|
|
7747
|
+
// the guidance in hand. The workers execute from their subtask
|
|
7748
|
+
// slices, not from this description — leaving the slices in
|
|
7749
|
+
// place gave "Retry with guidance" byte-identical worker
|
|
7750
|
+
// prompts to "Retry as-is", making the note do nothing at all.
|
|
7751
|
+
t3Subtasks: []
|
|
7752
|
+
} : assignment;
|
|
7753
|
+
return await this.execute(guided, taskId, signal);
|
|
7754
|
+
} else {
|
|
7755
|
+
issues.push("Escalated, but no decision was received in time.");
|
|
7756
|
+
overallStatus = "FAILED";
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7493
7759
|
const isOk = overallStatus === "COMPLETED" || overallStatus === "PARTIAL";
|
|
7494
7760
|
this.setStatus(isOk ? "COMPLETED" : "FAILED", summary);
|
|
7495
7761
|
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";
|
|
@@ -7500,7 +7766,8 @@ Guidance (must be followed): ${decision.note}`
|
|
|
7500
7766
|
status: overallStatus,
|
|
7501
7767
|
t3Results,
|
|
7502
7768
|
sectionSummary: summary,
|
|
7503
|
-
issues
|
|
7769
|
+
issues,
|
|
7770
|
+
...userSkipped ? { userSkipped: true } : {}
|
|
7504
7771
|
};
|
|
7505
7772
|
this.publishSectionOutput(result);
|
|
7506
7773
|
return result;
|
|
@@ -7934,8 +8201,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7934
8201
|
result.status === "COMPLETED" ? "COMPLETED" : "FAILED"
|
|
7935
8202
|
);
|
|
7936
8203
|
}
|
|
7937
|
-
async aggregateResults(assignment, results) {
|
|
7938
|
-
const completed = results.filter(
|
|
8204
|
+
async aggregateResults(assignment, results, opts = {}) {
|
|
8205
|
+
const completed = results.filter(
|
|
8206
|
+
(r) => r.status === "COMPLETED" || opts.includeEscalated && r.status === "ESCALATED" && r.output
|
|
8207
|
+
);
|
|
7939
8208
|
if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
|
|
7940
8209
|
const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
|
|
7941
8210
|
const peerContext = peerOutputs ? `
|
|
@@ -8141,6 +8410,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
8141
8410
|
escalations = [];
|
|
8142
8411
|
store;
|
|
8143
8412
|
runBreaker;
|
|
8413
|
+
escalationCallback;
|
|
8144
8414
|
t2PeerBus = new PeerBus();
|
|
8145
8415
|
permissionEscalator;
|
|
8146
8416
|
toolCreator;
|
|
@@ -8155,6 +8425,10 @@ var T1Administrator = class extends BaseTier {
|
|
|
8155
8425
|
this.toolRegistry = toolRegistry;
|
|
8156
8426
|
this.config = config;
|
|
8157
8427
|
}
|
|
8428
|
+
/** Ask the user what to do when any section escalates. */
|
|
8429
|
+
setEscalationCallback(cb) {
|
|
8430
|
+
this.escalationCallback = cb;
|
|
8431
|
+
}
|
|
8158
8432
|
/** Share the run-wide circuit breaker with every section manager. */
|
|
8159
8433
|
setRunBreaker(breaker) {
|
|
8160
8434
|
this.runBreaker = breaker;
|
|
@@ -8314,8 +8588,11 @@ Create a CORRECTION PLAN that contains only the new sections needed to fix the i
|
|
|
8314
8588
|
reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
|
|
8315
8589
|
};
|
|
8316
8590
|
}
|
|
8317
|
-
const sectionsText = t2Results.map((r) =>
|
|
8318
|
-
|
|
8591
|
+
const sectionsText = t2Results.map((r) => {
|
|
8592
|
+
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.]" : "";
|
|
8593
|
+
return `**${r.sectionTitle}**
|
|
8594
|
+
${r.sectionSummary}${skipNote}`;
|
|
8595
|
+
}).join("\n\n");
|
|
8319
8596
|
const prompt = `You are a strict QA Reviewer for the Cascade AI system.
|
|
8320
8597
|
Review the following execution outputs against the original user prompt.
|
|
8321
8598
|
|
|
@@ -8325,6 +8602,7 @@ T2 Manager Summaries:
|
|
|
8325
8602
|
${sectionsText}
|
|
8326
8603
|
|
|
8327
8604
|
Does the current state of the workspace and the outputs fully satisfy the user's request?
|
|
8605
|
+
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.
|
|
8328
8606
|
If yes, reply with exactly: "APPROVED".
|
|
8329
8607
|
If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
|
|
8330
8608
|
try {
|
|
@@ -8522,6 +8800,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8522
8800
|
const managers = sections.map((section) => {
|
|
8523
8801
|
const manager = new T2Manager(this.router, this.toolRegistry, this.id);
|
|
8524
8802
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
8803
|
+
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
8525
8804
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
8526
8805
|
if (this.store) {
|
|
8527
8806
|
manager.setStore(this.store);
|
|
@@ -8731,7 +9010,10 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
8731
9010
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
8732
9011
|
const completedSections = t2Results.filter((r) => r.status !== "FAILED");
|
|
8733
9012
|
if (!completedSections.length) {
|
|
8734
|
-
const allIssues =
|
|
9013
|
+
const allIssues = [
|
|
9014
|
+
...t2Results.flatMap((r) => r.issues ?? []),
|
|
9015
|
+
...t2Results.flatMap((r) => r.t3Results.flatMap((t) => t.issues))
|
|
9016
|
+
].filter(Boolean);
|
|
8735
9017
|
const critical = allIssues.find((i) => i.includes("[CRITICAL_TOOL_ERROR]"));
|
|
8736
9018
|
const stalled = allIssues.find((i) => /^Stalled:/.test(i));
|
|
8737
9019
|
const topReason = critical ?? stalled ?? allIssues[0] ?? "no specific reason recorded";
|
|
@@ -8817,9 +9099,16 @@ var CAPABILITIES = [
|
|
|
8817
9099
|
api: "openai-images"
|
|
8818
9100
|
},
|
|
8819
9101
|
{
|
|
9102
|
+
// Imagen, not `gemini-3-pro-image`. That id appears on Google's PRICING
|
|
9103
|
+
// page but 404s on `:predict` ("not found for API version v1beta, or is not
|
|
9104
|
+
// supported for predict") — the Gemini image models are called through
|
|
9105
|
+
// generateContent with an image response modality, while `:predict` is the
|
|
9106
|
+
// Imagen shape this executor implements. A billing line item is not proof
|
|
9107
|
+
// of a callable endpoint, which is the assumption that put the wrong id
|
|
9108
|
+
// here in the first place.
|
|
8820
9109
|
modality: "image",
|
|
8821
9110
|
provider: "gemini",
|
|
8822
|
-
modelId: "
|
|
9111
|
+
modelId: "imagen-4.0-generate-001",
|
|
8823
9112
|
api: "gemini-predict"
|
|
8824
9113
|
},
|
|
8825
9114
|
// ── Speech (text → audio) ──
|
|
@@ -9533,6 +9822,102 @@ var RunBreaker = class {
|
|
|
9533
9822
|
return out;
|
|
9534
9823
|
}
|
|
9535
9824
|
};
|
|
9825
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
9826
|
+
var MAX_SEGMENT_LENGTH = 24;
|
|
9827
|
+
var SEGMENT_HASH_LENGTH = 8;
|
|
9828
|
+
function boundedSegment(raw) {
|
|
9829
|
+
const clean = sanitizeToolNameSegment(raw);
|
|
9830
|
+
if (clean.length <= MAX_SEGMENT_LENGTH) return clean;
|
|
9831
|
+
const hash = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, SEGMENT_HASH_LENGTH);
|
|
9832
|
+
return `${clean.slice(0, MAX_SEGMENT_LENGTH - SEGMENT_HASH_LENGTH - 1)}_${hash}`;
|
|
9833
|
+
}
|
|
9834
|
+
function isStableSegment(raw) {
|
|
9835
|
+
return sanitizeToolNameSegment(raw) === raw && raw.length <= MAX_SEGMENT_LENGTH;
|
|
9836
|
+
}
|
|
9837
|
+
function sanitizeToolNameSegment(segment) {
|
|
9838
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
9839
|
+
let start = 0;
|
|
9840
|
+
let end = cleaned.length;
|
|
9841
|
+
while (start < end && cleaned[start] === "_") start++;
|
|
9842
|
+
while (end > start && cleaned[end - 1] === "_") end--;
|
|
9843
|
+
return cleaned.slice(start, end) || "_";
|
|
9844
|
+
}
|
|
9845
|
+
function mcpToolName(serverName, toolName) {
|
|
9846
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__${boundedSegment(toolName)}`;
|
|
9847
|
+
}
|
|
9848
|
+
function isMcpToolName(name) {
|
|
9849
|
+
return name.startsWith(MCP_TOOL_PREFIX);
|
|
9850
|
+
}
|
|
9851
|
+
function uniqueMcpServerName(desired, existingNames) {
|
|
9852
|
+
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
9853
|
+
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
9854
|
+
for (let n = 2; n < 1e3; n++) {
|
|
9855
|
+
const candidate = `${desired} (${n})`;
|
|
9856
|
+
if (!taken.has(mcpServerPrefix(candidate))) return candidate;
|
|
9857
|
+
}
|
|
9858
|
+
return `${desired} (${randomUUID().slice(0, 8)})`;
|
|
9859
|
+
}
|
|
9860
|
+
function disambiguateMcpServerNames(servers) {
|
|
9861
|
+
const seenNames = [];
|
|
9862
|
+
const renames = [];
|
|
9863
|
+
const result = servers.map((s) => {
|
|
9864
|
+
const name = uniqueMcpServerName(s.name, seenNames);
|
|
9865
|
+
seenNames.push(name);
|
|
9866
|
+
if (name === s.name) return s;
|
|
9867
|
+
renames.push({ from: s.name, to: name });
|
|
9868
|
+
return { ...s, name };
|
|
9869
|
+
});
|
|
9870
|
+
return { servers: renames.length ? result : servers, renames };
|
|
9871
|
+
}
|
|
9872
|
+
function assignMcpToolNames(tools) {
|
|
9873
|
+
const names = new Array(tools.length);
|
|
9874
|
+
const bases = tools.map((t) => mcpToolName(t.server, t.tool));
|
|
9875
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9876
|
+
bases.forEach((base, i) => {
|
|
9877
|
+
const g = groups.get(base);
|
|
9878
|
+
if (g) g.push(i);
|
|
9879
|
+
else groups.set(base, [i]);
|
|
9880
|
+
});
|
|
9881
|
+
const used = new Set(bases);
|
|
9882
|
+
for (const [base, idx] of groups) {
|
|
9883
|
+
if (idx.length === 1) {
|
|
9884
|
+
names[idx[0]] = base;
|
|
9885
|
+
continue;
|
|
9886
|
+
}
|
|
9887
|
+
const raw = (i) => `${tools[i].server}::${tools[i].tool}`;
|
|
9888
|
+
const isClean = (i) => isStableSegment(tools[i].server) && isStableSegment(tools[i].tool);
|
|
9889
|
+
const ordered = [...idx].sort((a, b) => {
|
|
9890
|
+
const la = isClean(a) ? 0 : 1;
|
|
9891
|
+
const lb = isClean(b) ? 0 : 1;
|
|
9892
|
+
if (la !== lb) return la - lb;
|
|
9893
|
+
const ra = raw(a);
|
|
9894
|
+
const rb = raw(b);
|
|
9895
|
+
return ra < rb ? -1 : ra > rb ? 1 : 0;
|
|
9896
|
+
});
|
|
9897
|
+
names[ordered[0]] = base;
|
|
9898
|
+
let n = 2;
|
|
9899
|
+
for (let rank3 = 1; rank3 < ordered.length; rank3++) {
|
|
9900
|
+
let candidate = `${base}_${n}`;
|
|
9901
|
+
while (used.has(candidate)) {
|
|
9902
|
+
n++;
|
|
9903
|
+
candidate = `${base}_${n}`;
|
|
9904
|
+
}
|
|
9905
|
+
names[ordered[rank3]] = candidate;
|
|
9906
|
+
used.add(candidate);
|
|
9907
|
+
n++;
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
return names;
|
|
9911
|
+
}
|
|
9912
|
+
function mcpServerPrefix(serverName) {
|
|
9913
|
+
return `${MCP_TOOL_PREFIX}${boundedSegment(serverName)}__`;
|
|
9914
|
+
}
|
|
9915
|
+
function removeMcpServerDenials(disabledTools, serverName) {
|
|
9916
|
+
if (!disabledTools?.length) return disabledTools;
|
|
9917
|
+
const prefix = mcpServerPrefix(serverName);
|
|
9918
|
+
const kept = disabledTools.filter((t) => !t.startsWith(prefix));
|
|
9919
|
+
return kept.length === disabledTools.length ? disabledTools : kept;
|
|
9920
|
+
}
|
|
9536
9921
|
var execAsync = promisify(exec);
|
|
9537
9922
|
var ShellTool = class extends BaseTool {
|
|
9538
9923
|
name = "shell";
|
|
@@ -10948,20 +11333,6 @@ ${text}`;
|
|
|
10948
11333
|
}
|
|
10949
11334
|
};
|
|
10950
11335
|
|
|
10951
|
-
// src/tools/tool-name.ts
|
|
10952
|
-
var MCP_TOOL_PREFIX = "mcp__";
|
|
10953
|
-
function sanitizeToolNameSegment(segment) {
|
|
10954
|
-
const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
10955
|
-
let start = 0;
|
|
10956
|
-
let end = cleaned.length;
|
|
10957
|
-
while (start < end && cleaned[start] === "_") start++;
|
|
10958
|
-
while (end > start && cleaned[end - 1] === "_") end--;
|
|
10959
|
-
return cleaned.slice(start, end) || "_";
|
|
10960
|
-
}
|
|
10961
|
-
function mcpToolName(serverName, toolName) {
|
|
10962
|
-
return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
|
|
10963
|
-
}
|
|
10964
|
-
|
|
10965
11336
|
// src/tools/mcp.ts
|
|
10966
11337
|
var McpToolWrapper = class extends BaseTool {
|
|
10967
11338
|
name;
|
|
@@ -10970,12 +11341,12 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
10970
11341
|
mcpClient;
|
|
10971
11342
|
serverName;
|
|
10972
11343
|
toolName;
|
|
10973
|
-
constructor(mcpClient, serverName, toolName, description, inputSchema) {
|
|
11344
|
+
constructor(mcpClient, serverName, toolName, description, inputSchema, registeredName) {
|
|
10974
11345
|
super();
|
|
10975
11346
|
this.mcpClient = mcpClient;
|
|
10976
11347
|
this.serverName = serverName;
|
|
10977
11348
|
this.toolName = toolName;
|
|
10978
|
-
this.name = mcpToolName(serverName, toolName);
|
|
11349
|
+
this.name = registeredName ?? mcpToolName(serverName, toolName);
|
|
10979
11350
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
10980
11351
|
this.inputSchema = inputSchema;
|
|
10981
11352
|
}
|
|
@@ -11046,22 +11417,35 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
11046
11417
|
getRegisteredPlugins() {
|
|
11047
11418
|
return Array.from(this.plugins.keys());
|
|
11048
11419
|
}
|
|
11049
|
-
/**
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11420
|
+
/**
|
|
11421
|
+
* Registers all tools from an MCP client.
|
|
11422
|
+
*
|
|
11423
|
+
* `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
|
|
11424
|
+
* same string the user sees and the same one that reaches the provider — so
|
|
11425
|
+
* a per-tool opt-out in Settings can be expressed as a plain name list.
|
|
11426
|
+
* Filtering here rather than at call time means a deselected tool is genuinely
|
|
11427
|
+
* absent from the definitions handed to the model, so it is never proposed,
|
|
11428
|
+
* never counted against the tool budget, and never has to be refused.
|
|
11429
|
+
*/
|
|
11430
|
+
registerMcpTools(mcpClient, isAllowed) {
|
|
11431
|
+
const parsed = mcpClient.getToolDefinitions().flatMap((def) => {
|
|
11432
|
+
const [, server, tool] = def.name.split("::");
|
|
11433
|
+
return server && tool ? [{ def, server, tool }] : [];
|
|
11434
|
+
});
|
|
11435
|
+
const names = assignMcpToolNames(parsed);
|
|
11436
|
+
parsed.forEach(({ def, server, tool }, i) => {
|
|
11055
11437
|
const wrapper = new McpToolWrapper(
|
|
11056
11438
|
mcpClient,
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
def.description.replace(`[MCP:${
|
|
11060
|
-
def.inputSchema
|
|
11439
|
+
server,
|
|
11440
|
+
tool,
|
|
11441
|
+
def.description.replace(`[MCP:${server}] `, ""),
|
|
11442
|
+
def.inputSchema,
|
|
11443
|
+
names[i]
|
|
11061
11444
|
);
|
|
11445
|
+
if (isAllowed && !isAllowed(wrapper.name)) return;
|
|
11062
11446
|
wrapper.setWorkspaceRoot(this.workspaceRoot);
|
|
11063
11447
|
this.register(wrapper);
|
|
11064
|
-
}
|
|
11448
|
+
});
|
|
11065
11449
|
}
|
|
11066
11450
|
setIgnoredPaths(patterns) {
|
|
11067
11451
|
this.ignoreMatcher = ignore().add(patterns);
|
|
@@ -11398,6 +11782,17 @@ function fileOAuthProvider(storePath) {
|
|
|
11398
11782
|
}
|
|
11399
11783
|
});
|
|
11400
11784
|
}
|
|
11785
|
+
var oauthStoreLocks = /* @__PURE__ */ new Map();
|
|
11786
|
+
function withOAuthStoreLock(storePath, fn) {
|
|
11787
|
+
const prev = oauthStoreLocks.get(storePath) ?? Promise.resolve();
|
|
11788
|
+
const run = prev.then(fn, fn);
|
|
11789
|
+
const tail = run.then(() => void 0, () => void 0);
|
|
11790
|
+
oauthStoreLocks.set(storePath, tail);
|
|
11791
|
+
void tail.then(() => {
|
|
11792
|
+
if (oauthStoreLocks.get(storePath) === tail) oauthStoreLocks.delete(storePath);
|
|
11793
|
+
});
|
|
11794
|
+
return run;
|
|
11795
|
+
}
|
|
11401
11796
|
function beginMcpOAuth(provider, serverUrl) {
|
|
11402
11797
|
return auth(provider, { serverUrl });
|
|
11403
11798
|
}
|
|
@@ -14336,7 +14731,50 @@ var GuidanceQueue = class {
|
|
|
14336
14731
|
}
|
|
14337
14732
|
};
|
|
14338
14733
|
|
|
14734
|
+
// src/tools/current-page.ts
|
|
14735
|
+
var MAX_PAGE_TEXT_CHARS = 4e4;
|
|
14736
|
+
var CurrentPageTool = class extends BaseTool {
|
|
14737
|
+
name = "read_current_page";
|
|
14738
|
+
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.';
|
|
14739
|
+
inputSchema = {
|
|
14740
|
+
type: "object",
|
|
14741
|
+
properties: {},
|
|
14742
|
+
required: []
|
|
14743
|
+
};
|
|
14744
|
+
provider;
|
|
14745
|
+
constructor(provider) {
|
|
14746
|
+
super();
|
|
14747
|
+
this.provider = provider;
|
|
14748
|
+
}
|
|
14749
|
+
// Reading what is already on the user's screen changes nothing and reaches
|
|
14750
|
+
// nothing they have not already opened themselves.
|
|
14751
|
+
isDangerous() {
|
|
14752
|
+
return false;
|
|
14753
|
+
}
|
|
14754
|
+
async execute(_input, _options) {
|
|
14755
|
+
let page2;
|
|
14756
|
+
try {
|
|
14757
|
+
page2 = await this.provider();
|
|
14758
|
+
} catch (err) {
|
|
14759
|
+
return `Error: could not read the open page \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
14760
|
+
}
|
|
14761
|
+
if (!page2) {
|
|
14762
|
+
return "No page is open in the built-in browser. Ask the user to open one, or use web_fetch for a public URL.";
|
|
14763
|
+
}
|
|
14764
|
+
const text = page2.text.length > MAX_PAGE_TEXT_CHARS ? `${page2.text.slice(0, MAX_PAGE_TEXT_CHARS)}
|
|
14765
|
+
|
|
14766
|
+
[truncated \u2014 the page is longer than ${MAX_PAGE_TEXT_CHARS} characters]` : page2.text;
|
|
14767
|
+
return [
|
|
14768
|
+
`URL: ${page2.url}`,
|
|
14769
|
+
`Title: ${page2.title}`,
|
|
14770
|
+
"",
|
|
14771
|
+
text.trim() || "[the page rendered no readable text]"
|
|
14772
|
+
].join("\n");
|
|
14773
|
+
}
|
|
14774
|
+
};
|
|
14775
|
+
|
|
14339
14776
|
// src/core/cascade.ts
|
|
14777
|
+
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
14340
14778
|
function buildContextualPrompt(prompt, history = []) {
|
|
14341
14779
|
const recent = history.slice(-6);
|
|
14342
14780
|
if (recent.length === 0) return prompt;
|
|
@@ -14391,6 +14829,13 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
14391
14829
|
}
|
|
14392
14830
|
});
|
|
14393
14831
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14832
|
+
try {
|
|
14833
|
+
const nodePath = __require("path");
|
|
14834
|
+
this.router.setDeadModelStore(new DeadModelStore(
|
|
14835
|
+
fileDeadModelPersistence(nodePath.join(workspacePath, ".cascade", "dead-models.json"))
|
|
14836
|
+
));
|
|
14837
|
+
} catch {
|
|
14838
|
+
}
|
|
14394
14839
|
this.multimodal = new MultimodalRegistry(
|
|
14395
14840
|
(this.config.providers ?? []).map((p) => p.type)
|
|
14396
14841
|
);
|
|
@@ -14552,6 +14997,94 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
14552
14997
|
});
|
|
14553
14998
|
});
|
|
14554
14999
|
}
|
|
15000
|
+
// ── Escalation decisions ────────────────────────────────────────────
|
|
15001
|
+
//
|
|
15002
|
+
// A section that ends ESCALATED used to be a dead end: the Cockpit showed
|
|
15003
|
+
// "Section escalated — needs a decision" and no decision was ever requested,
|
|
15004
|
+
// so the run stopped there having spent a full orchestration. This is the
|
|
15005
|
+
// same round-trip shape as plan approval, with one deliberate difference —
|
|
15006
|
+
// an unanswered escalation FAILS the section instead of proceeding.
|
|
15007
|
+
//
|
|
15008
|
+
// That direction is the safe one here. Plan approval defaults to "yes" on
|
|
15009
|
+
// timeout because the plan is already the model's considered proposal;
|
|
15010
|
+
// an escalation exists precisely BECAUSE the worker was not confident, so
|
|
15011
|
+
// acting unattended is the option most likely to be wrong. And in cloud a
|
|
15012
|
+
// waiting run holds server resources, so hanging is not free either.
|
|
15013
|
+
// Keyed by request id, NOT a single slot. A Complex run dispatches each wave
|
|
15014
|
+
// of ready sections with `Promise.all` (t1-administrator.ts), so two sections
|
|
15015
|
+
// can be waiting on a decision at the same moment. A single field made that
|
|
15016
|
+
// case fail hard rather than merely confusingly: the second escalation
|
|
15017
|
+
// overwrote the first's resolver, the user's answer resolved only the second,
|
|
15018
|
+
// and then the FIRST section's timer found the slot already empty and
|
|
15019
|
+
// returned without resolving anything — parking the run forever, past its own
|
|
15020
|
+
// timeout. Same class of bug as the twelve-failover dead-model race: a wave
|
|
15021
|
+
// is concurrent, so per-run singletons are never safe inside it.
|
|
15022
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
15023
|
+
async requestEscalationDecision(ctx, taskId, signal) {
|
|
15024
|
+
if (this.config.autonomy === "auto") return { action: "skip", automatic: true };
|
|
15025
|
+
if (this.listenerCount("escalation:decision-required") === 0) return { action: "skip", automatic: true };
|
|
15026
|
+
if (signal?.aborted) return { action: "skip", automatic: true };
|
|
15027
|
+
const requestId = randomUUID();
|
|
15028
|
+
return await new Promise((resolve2) => {
|
|
15029
|
+
let timeout;
|
|
15030
|
+
const settle = (decision) => {
|
|
15031
|
+
if (!this.pendingEscalations.delete(requestId)) return;
|
|
15032
|
+
if (timeout) clearTimeout(timeout);
|
|
15033
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15034
|
+
resolve2(decision);
|
|
15035
|
+
};
|
|
15036
|
+
const onAbort = () => settle({ action: "skip", automatic: true });
|
|
15037
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15038
|
+
timeout = setTimeout(() => {
|
|
15039
|
+
this.emit("escalation:timeout", { taskId, requestId, sectionId: ctx.sectionId });
|
|
15040
|
+
settle({ action: "timeout" });
|
|
15041
|
+
}, ESCALATION_DECISION_TIMEOUT_MS);
|
|
15042
|
+
this.pendingEscalations.set(requestId, settle);
|
|
15043
|
+
this.emit("escalation:decision-required", {
|
|
15044
|
+
taskId,
|
|
15045
|
+
requestId,
|
|
15046
|
+
sectionId: ctx.sectionId,
|
|
15047
|
+
sectionTitle: ctx.sectionTitle,
|
|
15048
|
+
issues: ctx.issues,
|
|
15049
|
+
summary: ctx.summary,
|
|
15050
|
+
timeoutMs: ESCALATION_DECISION_TIMEOUT_MS
|
|
15051
|
+
});
|
|
15052
|
+
});
|
|
15053
|
+
}
|
|
15054
|
+
/**
|
|
15055
|
+
* Resolve a pending escalation from a REPL / dashboard / cloud listener.
|
|
15056
|
+
*
|
|
15057
|
+
* `requestId` comes back from the `escalation:decision-required` event and is
|
|
15058
|
+
* what makes an answer land on the section that asked. It is optional so a
|
|
15059
|
+
* host with only one escalation in flight (and any caller that predates the
|
|
15060
|
+
* id) still works: without it the oldest outstanding request is answered,
|
|
15061
|
+
* which is correct whenever there is exactly one.
|
|
15062
|
+
*
|
|
15063
|
+
* `automatic` defaults to false — a call into this method is normally a real
|
|
15064
|
+
* person answering the prompt. Pass `true` from a host that is itself
|
|
15065
|
+
* settling the gate on nobody's behalf (a REST caller who never had a
|
|
15066
|
+
* chance to connect, a session halt with no per-section review behind it),
|
|
15067
|
+
* so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
|
|
15068
|
+
* see EscalationDecision.automatic.
|
|
15069
|
+
*/
|
|
15070
|
+
resolveEscalation(action, note, requestId, automatic) {
|
|
15071
|
+
const decision = { action, ...note ? { note } : {}, ...automatic ? { automatic: true } : {} };
|
|
15072
|
+
if (requestId) {
|
|
15073
|
+
this.pendingEscalations.get(requestId)?.(decision);
|
|
15074
|
+
return;
|
|
15075
|
+
}
|
|
15076
|
+
const oldest = this.pendingEscalations.keys().next();
|
|
15077
|
+
if (!oldest.done) this.pendingEscalations.get(oldest.value)?.(decision);
|
|
15078
|
+
}
|
|
15079
|
+
/**
|
|
15080
|
+
* Release every parked escalation — run teardown, so none outlive the run.
|
|
15081
|
+
* `automatic: true`: teardown resolving a section nobody answered is the
|
|
15082
|
+
* same system-decided case as the early-return skips above, not a person
|
|
15083
|
+
* reviewing and accepting that section's output.
|
|
15084
|
+
*/
|
|
15085
|
+
releasePendingEscalations(action = "skip") {
|
|
15086
|
+
for (const settle of Array.from(this.pendingEscalations.values())) settle({ action, automatic: true });
|
|
15087
|
+
}
|
|
14555
15088
|
/**
|
|
14556
15089
|
* Resolve a pending boardroom plan approval from a REPL / dashboard listener.
|
|
14557
15090
|
* An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
|
|
@@ -14685,6 +15218,20 @@ ${input}` }],
|
|
|
14685
15218
|
this.mediaSink = sink;
|
|
14686
15219
|
this.registerMediaTools(this.workspacePath);
|
|
14687
15220
|
}
|
|
15221
|
+
/**
|
|
15222
|
+
* Let the agent read the page the user has open in the host's browser.
|
|
15223
|
+
*
|
|
15224
|
+
* Host-supplied because only a host WITH a browser can answer it — so the
|
|
15225
|
+
* tool is simply absent in the CLI and in hosted runs, rather than present
|
|
15226
|
+
* and always failing. Registered outside `enabledTools` for the same reason
|
|
15227
|
+
* media tools are: that allowlist is a blast-radius control for tools that
|
|
15228
|
+
* touch the machine, and reading a page the user already opened touches
|
|
15229
|
+
* nothing. `disabledTools` still removes it.
|
|
15230
|
+
*/
|
|
15231
|
+
setCurrentPageProvider(provider) {
|
|
15232
|
+
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
15233
|
+
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
15234
|
+
}
|
|
14688
15235
|
registerMediaTools(workspacePath) {
|
|
14689
15236
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14690
15237
|
const permitted = (name) => !denied.has(name);
|
|
@@ -14817,8 +15364,12 @@ ${last.partialOutput}` : "");
|
|
|
14817
15364
|
for (const server of this.config.tools.mcpServers) {
|
|
14818
15365
|
try {
|
|
14819
15366
|
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
14820
|
-
|
|
14821
|
-
|
|
15367
|
+
const connect = () => this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
15368
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
15369
|
+
this.toolRegistry.registerMcpTools(
|
|
15370
|
+
this.mcpClient,
|
|
15371
|
+
(name) => !(this.config.tools?.disabledTools ?? []).includes(name)
|
|
15372
|
+
);
|
|
14822
15373
|
} catch (err) {
|
|
14823
15374
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
14824
15375
|
}
|
|
@@ -15283,6 +15834,7 @@ ${prompt}` : prompt;
|
|
|
15283
15834
|
} else if (complexity === "Moderate") {
|
|
15284
15835
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
15285
15836
|
t2.setRunBreaker(runBreaker);
|
|
15837
|
+
t2.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15286
15838
|
t2.setPresenter(true);
|
|
15287
15839
|
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.");
|
|
15288
15840
|
if (identityPrompt) {
|
|
@@ -15327,11 +15879,17 @@ ${prompt}` : prompt;
|
|
|
15327
15879
|
t2Results = [t2Result];
|
|
15328
15880
|
const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
|
|
15329
15881
|
if (completed.length > 0) {
|
|
15330
|
-
finalOutput =
|
|
15882
|
+
finalOutput = composeRootSectionOutput(
|
|
15883
|
+
t2Result,
|
|
15884
|
+
completed.map((r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output))
|
|
15885
|
+
);
|
|
15331
15886
|
} else {
|
|
15332
15887
|
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
15333
15888
|
const reasons = Array.from(
|
|
15334
|
-
new Set(
|
|
15889
|
+
new Set([
|
|
15890
|
+
...t2Result.issues ?? [],
|
|
15891
|
+
...t2Result.t3Results.flatMap((r) => r.issues ?? [])
|
|
15892
|
+
].filter(Boolean))
|
|
15335
15893
|
);
|
|
15336
15894
|
if (partial) {
|
|
15337
15895
|
finalOutput = reasons.length ? `${partial}
|
|
@@ -15344,6 +15902,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15344
15902
|
} else {
|
|
15345
15903
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
15346
15904
|
t1.setRunBreaker(runBreaker);
|
|
15905
|
+
t1.setEscalationCallback((ctx) => this.requestEscalationDecision(ctx, taskId, options.signal));
|
|
15347
15906
|
t1.setPresenter(true);
|
|
15348
15907
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
15349
15908
|
if (identityPrompt) {
|
|
@@ -15368,6 +15927,8 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
15368
15927
|
finalOutput = result.output;
|
|
15369
15928
|
t2Results = result.t2Results;
|
|
15370
15929
|
}
|
|
15930
|
+
const budgetInfo = this.router.budgetExceededInfo();
|
|
15931
|
+
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
15371
15932
|
const trip = runBreaker.reason();
|
|
15372
15933
|
if (trip) {
|
|
15373
15934
|
finalOutput = [
|
|
@@ -15408,6 +15969,10 @@ ${finalOutput}` : ""
|
|
|
15408
15969
|
escalator.cancelAllPending();
|
|
15409
15970
|
} catch {
|
|
15410
15971
|
}
|
|
15972
|
+
try {
|
|
15973
|
+
this.releasePendingEscalations();
|
|
15974
|
+
} catch {
|
|
15975
|
+
}
|
|
15411
15976
|
this.router.restoreTierModels();
|
|
15412
15977
|
this.router.setRunSignal(void 0);
|
|
15413
15978
|
if (this.taskAnalyzer) {
|
|
@@ -16724,6 +17289,13 @@ var ConfigManager = class {
|
|
|
16724
17289
|
}
|
|
16725
17290
|
async load() {
|
|
16726
17291
|
this.config = await this.loadConfig();
|
|
17292
|
+
const servers = this.config.tools?.mcpServers;
|
|
17293
|
+
const { servers: disambiguated, renames } = servers?.length ? disambiguateMcpServerNames(servers) : { servers, renames: [] };
|
|
17294
|
+
const mcpNamesChanged = renames.length > 0;
|
|
17295
|
+
if (mcpNamesChanged && this.config.tools) {
|
|
17296
|
+
this.config.tools.mcpServers = disambiguated;
|
|
17297
|
+
this.renameMcpServerReferences(renames);
|
|
17298
|
+
}
|
|
16727
17299
|
this.ignore = new CascadeIgnore();
|
|
16728
17300
|
await this.ignore.load(this.workspacePath);
|
|
16729
17301
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
@@ -16732,6 +17304,7 @@ var ConfigManager = class {
|
|
|
16732
17304
|
await this.injectEnvKeys();
|
|
16733
17305
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
16734
17306
|
await this.ensureDefaultIdentity();
|
|
17307
|
+
if (mcpNamesChanged) await this.save();
|
|
16735
17308
|
}
|
|
16736
17309
|
getConfig() {
|
|
16737
17310
|
return this.config;
|
|
@@ -16815,6 +17388,66 @@ var ConfigManager = class {
|
|
|
16815
17388
|
this.config.providers.push({ type: "ollama" });
|
|
16816
17389
|
}
|
|
16817
17390
|
}
|
|
17391
|
+
/**
|
|
17392
|
+
* Follow a disambiguation rename into `tools.mcpTrusted`.
|
|
17393
|
+
*
|
|
17394
|
+
* `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
|
|
17395
|
+
* of rename reach here, and they need OPPOSITE treatment:
|
|
17396
|
+
*
|
|
17397
|
+
* - Distinct raw names that only collide via sanitizing (`foo bar` /
|
|
17398
|
+
* `foo@bar`): each had its OWN trust entry, and the old string now belongs
|
|
17399
|
+
* to nothing — the server that used it moved away, so the entry has to
|
|
17400
|
+
* move with it. A plain rewrite is correct here.
|
|
17401
|
+
* - Literally identical names (a hand-edited or duplicated config: two rows
|
|
17402
|
+
* both named `foo`): there is only ONE trust entry for both, since
|
|
17403
|
+
* `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
|
|
17404
|
+
* already been updated to the disambiguated list by the time this runs
|
|
17405
|
+
* (see `load()`), so the untouched survivor is STILL named `foo` — a plain
|
|
17406
|
+
* rewrite would move the one entry entirely onto the renamed row and
|
|
17407
|
+
* leave the survivor untrusted. The entry must be kept for the survivor
|
|
17408
|
+
* AND granted to the renamed identity, not moved.
|
|
17409
|
+
*
|
|
17410
|
+
* Renames are grouped by `from` before either treatment is applied, and
|
|
17411
|
+
* each group is settled in one step. Two DIFFERENT rows can start out with
|
|
17412
|
+
* the exact same raw name and BOTH get renamed away (e.g. three servers
|
|
17413
|
+
* named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
|
|
17414
|
+
* with `foo bar` and with each other, so both are renamed to distinct
|
|
17415
|
+
* identities while `foo bar` survives untouched). Processing renames one at
|
|
17416
|
+
* a time in that shape drops coverage: the first rename's REPLACE branch
|
|
17417
|
+
* would erase `from` from `trusted` entirely, so the guard on the second
|
|
17418
|
+
* rename with the same `from` sees it already gone and silently skips —
|
|
17419
|
+
* granting trust to only one of the two renamed identities. Grouping first
|
|
17420
|
+
* means every renamed identity that shares a `from` is added or substituted
|
|
17421
|
+
* together, in the one pass that still sees the original entry.
|
|
17422
|
+
*
|
|
17423
|
+
* `tools.disabledTools` deliberately gets no equivalent treatment. It's
|
|
17424
|
+
* matched by sanitized PREFIX, and a prefix collision is exactly what made
|
|
17425
|
+
* an existing entry ambiguous between the two servers in the first place —
|
|
17426
|
+
* there is no way to tell, from the stored string alone, which of the two
|
|
17427
|
+
* a denial was meant for. Leaving those entries untouched resolves that
|
|
17428
|
+
* ambiguity for free: the survivor keeps the original (now unique) prefix,
|
|
17429
|
+
* so an old entry keeps applying to it, while the renamed server starts
|
|
17430
|
+
* clean under its new prefix. Rewriting the entry to follow the rename
|
|
17431
|
+
* would do the opposite — move a denial that already worked for the
|
|
17432
|
+
* survivor onto a server it may never have meant to cover.
|
|
17433
|
+
*/
|
|
17434
|
+
renameMcpServerReferences(renames) {
|
|
17435
|
+
const tools = this.config.tools;
|
|
17436
|
+
if (!tools?.mcpTrusted?.length || !renames.length) return;
|
|
17437
|
+
const currentNames = new Set((tools.mcpServers ?? []).map((s) => s.name));
|
|
17438
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
17439
|
+
for (const { from, to } of renames) {
|
|
17440
|
+
const tos = byFrom.get(from);
|
|
17441
|
+
if (tos) tos.push(to);
|
|
17442
|
+
else byFrom.set(from, [to]);
|
|
17443
|
+
}
|
|
17444
|
+
let trusted = tools.mcpTrusted;
|
|
17445
|
+
for (const [from, tos] of byFrom) {
|
|
17446
|
+
if (!trusted.includes(from)) continue;
|
|
17447
|
+
trusted = currentNames.has(from) ? [...trusted, ...tos] : [...trusted.filter((n) => n !== from), ...tos];
|
|
17448
|
+
}
|
|
17449
|
+
tools.mcpTrusted = Array.from(new Set(trusted));
|
|
17450
|
+
}
|
|
16818
17451
|
async ensureDefaultIdentity() {
|
|
16819
17452
|
const existing = this.store.getDefaultIdentity();
|
|
16820
17453
|
if (existing) return;
|
|
@@ -16937,6 +17570,14 @@ var DashboardSocket = class {
|
|
|
16937
17570
|
this.secret = options.secret;
|
|
16938
17571
|
this.setupHandlers();
|
|
16939
17572
|
}
|
|
17573
|
+
/** How many sockets are currently in a room — 0 means nobody can answer. */
|
|
17574
|
+
roomSize(room) {
|
|
17575
|
+
return this.io.sockets.adapter.rooms.get(room)?.size ?? 0;
|
|
17576
|
+
}
|
|
17577
|
+
/** Is this exact connection still live? A reconnect issues a NEW socket id. */
|
|
17578
|
+
hasSocket(socketId) {
|
|
17579
|
+
return this.io.sockets.sockets.has(socketId);
|
|
17580
|
+
}
|
|
16940
17581
|
broadcastToRoom(room, event, data) {
|
|
16941
17582
|
this.io.to(room).emit(event, data);
|
|
16942
17583
|
}
|
|
@@ -17008,6 +17649,22 @@ var DashboardSocket = class {
|
|
|
17008
17649
|
});
|
|
17009
17650
|
});
|
|
17010
17651
|
}
|
|
17652
|
+
/**
|
|
17653
|
+
* A client subscribed to a session's room.
|
|
17654
|
+
*
|
|
17655
|
+
* The host uses this to replay a gate the run is already parked on. Socket.IO
|
|
17656
|
+
* does not buffer room emissions, so a prompt broadcast while the renderer was
|
|
17657
|
+
* reconnecting is simply gone; without a replay the section waits out its full
|
|
17658
|
+
* timeout with the user sitting in front of a screen that never asked.
|
|
17659
|
+
*/
|
|
17660
|
+
onJoinSession(callback) {
|
|
17661
|
+
this.io.on("connection", (socket) => {
|
|
17662
|
+
socket.on("join:session", (payload) => {
|
|
17663
|
+
const { sessionId } = normalizeSessionSubscriptionPayload(payload);
|
|
17664
|
+
if (sessionId) callback(sessionId, socket.id);
|
|
17665
|
+
});
|
|
17666
|
+
});
|
|
17667
|
+
}
|
|
17011
17668
|
onSessionRate(callback) {
|
|
17012
17669
|
this.io.on("connection", (socket) => {
|
|
17013
17670
|
socket.on("session:rate", (payload) => {
|
|
@@ -17045,6 +17702,31 @@ var DashboardSocket = class {
|
|
|
17045
17702
|
});
|
|
17046
17703
|
});
|
|
17047
17704
|
}
|
|
17705
|
+
/**
|
|
17706
|
+
* A section escalated and the run is parked waiting for an answer. The
|
|
17707
|
+
* desktop shows the escalation modal on `escalation:decision-required` and
|
|
17708
|
+
* answers here.
|
|
17709
|
+
*
|
|
17710
|
+
* Unlike a plan decision, silence is NOT taken as consent — the SDK fails
|
|
17711
|
+
* the section on timeout — so this handler only ever forwards a real choice.
|
|
17712
|
+
*/
|
|
17713
|
+
onEscalationDecision(callback) {
|
|
17714
|
+
this.io.on("connection", (socket) => {
|
|
17715
|
+
socket.on("escalation:decide", (payload) => {
|
|
17716
|
+
const action = payload?.action;
|
|
17717
|
+
if (typeof payload?.sessionId !== "string") return;
|
|
17718
|
+
if (action !== "retry" && action !== "skip" && action !== "guidance") return;
|
|
17719
|
+
callback({
|
|
17720
|
+
sessionId: payload.sessionId,
|
|
17721
|
+
// Identifies WHICH parked section this answers — sections in a wave
|
|
17722
|
+
// run concurrently, so more than one can be waiting.
|
|
17723
|
+
requestId: typeof payload.requestId === "string" ? payload.requestId : void 0,
|
|
17724
|
+
action,
|
|
17725
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0
|
|
17726
|
+
});
|
|
17727
|
+
});
|
|
17728
|
+
});
|
|
17729
|
+
}
|
|
17048
17730
|
onSessionSteer(callback) {
|
|
17049
17731
|
this.io.on("connection", (socket) => {
|
|
17050
17732
|
socket.on("session:steer", (payload) => {
|
|
@@ -17203,6 +17885,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
17203
17885
|
|
|
17204
17886
|
// src/dashboard/server.ts
|
|
17205
17887
|
var __dirname$1 = path24.dirname(fileURLToPath(import.meta.url));
|
|
17888
|
+
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
17206
17889
|
var DashboardServer = class {
|
|
17207
17890
|
app;
|
|
17208
17891
|
httpServer;
|
|
@@ -17213,6 +17896,13 @@ var DashboardServer = class {
|
|
|
17213
17896
|
globalStore = null;
|
|
17214
17897
|
broadcastTimer = null;
|
|
17215
17898
|
activeSessions = /* @__PURE__ */ new Map();
|
|
17899
|
+
/**
|
|
17900
|
+
* Supplied by the desktop shell so a run can read the page the user has
|
|
17901
|
+
* open in the built-in browser. Absent in the CLI, where there is no
|
|
17902
|
+
* browser — so the tool is simply not registered rather than present and
|
|
17903
|
+
* always failing.
|
|
17904
|
+
*/
|
|
17905
|
+
currentPageProvider;
|
|
17216
17906
|
activeControllers = /* @__PURE__ */ new Map();
|
|
17217
17907
|
/**
|
|
17218
17908
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -17227,6 +17917,17 @@ var DashboardServer = class {
|
|
|
17227
17917
|
* map is how that answer reaches the run that's blocked on it.
|
|
17228
17918
|
*/
|
|
17229
17919
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
17920
|
+
/**
|
|
17921
|
+
* The escalation prompt a session is currently parked on, kept so it can be
|
|
17922
|
+
* delivered to a client that was not reachable when it was raised.
|
|
17923
|
+
*
|
|
17924
|
+
* Socket.IO buffers nothing: a prompt emitted while the renderer is mid
|
|
17925
|
+
* reconnect is gone, and the section then waits out its full five minutes
|
|
17926
|
+
* with the user in front of a screen that never asked them anything. Holding
|
|
17927
|
+
* the payload lets `join:session` replay it, and lets a run with no reachable
|
|
17928
|
+
* client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
|
|
17929
|
+
*/
|
|
17930
|
+
pendingEscalations = /* @__PURE__ */ new Map();
|
|
17230
17931
|
/**
|
|
17231
17932
|
* The orchestration decision trail ("why") of each session's most recent
|
|
17232
17933
|
* run — captured when the run ends so the desktop's Why panel can show it
|
|
@@ -17304,20 +18005,29 @@ var DashboardServer = class {
|
|
|
17304
18005
|
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
17305
18006
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
17306
18007
|
this.activeSessions.set(sessionId, cascade);
|
|
18008
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
17307
18009
|
cascade.on("stream:token", (e) => {
|
|
17308
|
-
this.
|
|
18010
|
+
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
17309
18011
|
});
|
|
17310
18012
|
cascade.on("tier:status", (e) => {
|
|
17311
|
-
this.
|
|
18013
|
+
this.emitToSessionClients(socketId, sessionId, "tier:status", { sessionId, ...e });
|
|
17312
18014
|
});
|
|
17313
18015
|
cascade.on("permission:user-required", (e) => {
|
|
17314
|
-
this.
|
|
18016
|
+
this.emitToSessionClients(socketId, sessionId, "permission:user-required", { sessionId, ...e });
|
|
17315
18017
|
});
|
|
17316
18018
|
cascade.on("peer:message", (e) => {
|
|
17317
18019
|
this.socket.emitPeerMessage(e);
|
|
17318
18020
|
});
|
|
17319
18021
|
cascade.on("plan:approval-required", (e) => {
|
|
17320
|
-
this.
|
|
18022
|
+
this.emitToSessionClients(socketId, sessionId, "plan:approval-required", { sessionId, ...e });
|
|
18023
|
+
});
|
|
18024
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
18025
|
+
this.raiseEscalation(sessionId, socketId, e);
|
|
18026
|
+
});
|
|
18027
|
+
cascade.on("escalation:timeout", (e) => {
|
|
18028
|
+
const ev = e;
|
|
18029
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
18030
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
17321
18031
|
});
|
|
17322
18032
|
try {
|
|
17323
18033
|
const result = await cascade.run({
|
|
@@ -17328,7 +18038,7 @@ var DashboardServer = class {
|
|
|
17328
18038
|
this.recordSessionTask(sessionId, result.taskId);
|
|
17329
18039
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17330
18040
|
this.captureWhy(sessionId, cascade, result);
|
|
17331
|
-
this.
|
|
18041
|
+
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
17332
18042
|
this.socket.broadcast("cost:update", {
|
|
17333
18043
|
sessionId,
|
|
17334
18044
|
totalTokens: result.usage.totalTokens,
|
|
@@ -17341,7 +18051,7 @@ var DashboardServer = class {
|
|
|
17341
18051
|
} catch (err) {
|
|
17342
18052
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17343
18053
|
this.captureWhy(sessionId, cascade);
|
|
17344
|
-
this.
|
|
18054
|
+
this.emitToSessionClients(socketId, sessionId, "session:error", {
|
|
17345
18055
|
sessionId,
|
|
17346
18056
|
error: err instanceof Error ? err.message : String(err)
|
|
17347
18057
|
});
|
|
@@ -17349,6 +18059,7 @@ var DashboardServer = class {
|
|
|
17349
18059
|
this.activeSessions.delete(sessionId);
|
|
17350
18060
|
this.activeControllers.delete(sessionId);
|
|
17351
18061
|
this.denyPendingApprovals(sessionId);
|
|
18062
|
+
this.clearSessionEscalations(sessionId);
|
|
17352
18063
|
}
|
|
17353
18064
|
});
|
|
17354
18065
|
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
@@ -17358,10 +18069,20 @@ var DashboardServer = class {
|
|
|
17358
18069
|
editedPlan
|
|
17359
18070
|
);
|
|
17360
18071
|
});
|
|
18072
|
+
this.socket.onEscalationDecision(({ sessionId, requestId, action, note }) => {
|
|
18073
|
+
if (requestId) this.clearPendingEscalation(requestId);
|
|
18074
|
+
else this.clearOldestSessionEscalation(sessionId);
|
|
18075
|
+
this.activeSessions.get(sessionId)?.resolveEscalation(action, note, requestId);
|
|
18076
|
+
});
|
|
18077
|
+
this.socket.onJoinSession((sessionId, socketId) => {
|
|
18078
|
+
this.replayEscalations(sessionId, socketId);
|
|
18079
|
+
});
|
|
17361
18080
|
this.socket.onSessionHalt((sessionId) => {
|
|
17362
18081
|
this.activeControllers.get(sessionId)?.abort();
|
|
17363
18082
|
this.denyPendingApprovals(sessionId);
|
|
17364
18083
|
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
18084
|
+
this.clearSessionEscalations(sessionId);
|
|
18085
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", void 0, void 0, true);
|
|
17365
18086
|
});
|
|
17366
18087
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
17367
18088
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -17376,6 +18097,10 @@ var DashboardServer = class {
|
|
|
17376
18097
|
}
|
|
17377
18098
|
});
|
|
17378
18099
|
}
|
|
18100
|
+
/** Wire the host's browser view in (desktop only). */
|
|
18101
|
+
setCurrentPageProvider(provider) {
|
|
18102
|
+
this.currentPageProvider = provider;
|
|
18103
|
+
}
|
|
17379
18104
|
async start() {
|
|
17380
18105
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
17381
18106
|
if (!isLoopback) {
|
|
@@ -17664,6 +18389,116 @@ var DashboardServer = class {
|
|
|
17664
18389
|
this.activeSessions.delete(sessionId);
|
|
17665
18390
|
}
|
|
17666
18391
|
}
|
|
18392
|
+
/**
|
|
18393
|
+
* Is anyone able to answer for this session right now?
|
|
18394
|
+
*
|
|
18395
|
+
* The originating socket first — that connection is definitely watching this
|
|
18396
|
+
* run — then the session room, which is where a client lands after a
|
|
18397
|
+
* reconnect issues it a new socket id.
|
|
18398
|
+
*/
|
|
18399
|
+
hasResponder(sessionId, socketId) {
|
|
18400
|
+
if (socketId && this.socket.hasSocket(socketId)) return true;
|
|
18401
|
+
return this.socket.roomSize(`session:${sessionId}`) > 0;
|
|
18402
|
+
}
|
|
18403
|
+
/**
|
|
18404
|
+
* Send a run event to whoever is watching this session.
|
|
18405
|
+
*
|
|
18406
|
+
* A captured socket id goes stale the moment Socket.IO reconnects — the
|
|
18407
|
+
* client is back, but under a new id, and `emitToSocket` on the old one is a
|
|
18408
|
+
* silent no-op. Falling back to the session room keeps the run addressable
|
|
18409
|
+
* across a reconnect; emitting to one OR the other (never both) keeps a
|
|
18410
|
+
* client that is both the originator and a room member from seeing every
|
|
18411
|
+
* token twice.
|
|
18412
|
+
*/
|
|
18413
|
+
emitToSessionClients(socketId, sessionId, event, data) {
|
|
18414
|
+
if (socketId && this.socket.hasSocket(socketId)) this.socket.emitToSocket(socketId, event, data);
|
|
18415
|
+
else this.socket.broadcastToRoom(`session:${sessionId}`, event, data);
|
|
18416
|
+
}
|
|
18417
|
+
/**
|
|
18418
|
+
* Raise an escalation prompt, and make sure it can still be answered.
|
|
18419
|
+
*
|
|
18420
|
+
* Held in `pendingEscalations` — keyed by requestId, because a Complex wave
|
|
18421
|
+
* dispatches sections concurrently and two can be parked at once — so a
|
|
18422
|
+
* client that joins late (or rejoins after a reconnect) gets every waiting
|
|
18423
|
+
* prompt replayed. Keying by session would drop the first of a pair: its
|
|
18424
|
+
* grace timer would be cleared by the second, leaving it parked for the full
|
|
18425
|
+
* timeout with nobody able to answer it. If nobody is reachable at all, the gate is
|
|
18426
|
+
* settled as `skip` after a short grace period rather than parked for the
|
|
18427
|
+
* full timeout: that matches the SDK's policy when no listener exists, and
|
|
18428
|
+
* five minutes of a held run is a real cost for a REST caller who has already
|
|
18429
|
+
* hung up. The grace period exists so an ordinary two-second reconnect is not
|
|
18430
|
+
* mistaken for an absent user.
|
|
18431
|
+
*/
|
|
18432
|
+
raiseEscalation(sessionId, socketId, event) {
|
|
18433
|
+
const requestId = typeof event.requestId === "string" && event.requestId ? event.requestId : randomUUID();
|
|
18434
|
+
const payload = { sessionId, ...event };
|
|
18435
|
+
const entry = { payload, sessionId, requestId, raisedAt: Date.now() };
|
|
18436
|
+
this.pendingEscalations.set(requestId, entry);
|
|
18437
|
+
if (this.hasResponder(sessionId, socketId)) {
|
|
18438
|
+
this.emitToSessionClients(socketId, sessionId, "escalation:decision-required", payload);
|
|
18439
|
+
return;
|
|
18440
|
+
}
|
|
18441
|
+
entry.graceTimer = setTimeout(() => {
|
|
18442
|
+
if (this.pendingEscalations.get(requestId) !== entry) return;
|
|
18443
|
+
if (this.hasResponder(sessionId, socketId)) return;
|
|
18444
|
+
this.pendingEscalations.delete(requestId);
|
|
18445
|
+
this.activeSessions.get(sessionId)?.resolveEscalation("skip", "No client was connected to answer.", requestId, true);
|
|
18446
|
+
}, ORPHANED_ESCALATION_GRACE_MS);
|
|
18447
|
+
}
|
|
18448
|
+
/**
|
|
18449
|
+
* Replay EVERY parked prompt to a client that just subscribed to the session.
|
|
18450
|
+
*
|
|
18451
|
+
* Plural because a Complex run dispatches sections concurrently, so a wave can
|
|
18452
|
+
* leave more than one waiting at the same time — the same reason the SDK keys
|
|
18453
|
+
* its gates and the clients queue theirs.
|
|
18454
|
+
*
|
|
18455
|
+
* `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
|
|
18456
|
+
* has been running since the prompt was raised. Sent verbatim, a client
|
|
18457
|
+
* reconnecting four minutes in would show a fresh five-minute countdown and
|
|
18458
|
+
* then watch the section fail with four of those minutes still on the clock.
|
|
18459
|
+
* A remaining duration is used rather than an absolute deadline so the two
|
|
18460
|
+
* sides never have to agree on a wall clock.
|
|
18461
|
+
*/
|
|
18462
|
+
replayEscalations(sessionId, socketId) {
|
|
18463
|
+
for (const entry of this.pendingEscalations.values()) {
|
|
18464
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18465
|
+
if (entry.graceTimer) {
|
|
18466
|
+
clearTimeout(entry.graceTimer);
|
|
18467
|
+
entry.graceTimer = void 0;
|
|
18468
|
+
}
|
|
18469
|
+
const originalTimeout = typeof entry.payload.timeoutMs === "number" ? entry.payload.timeoutMs : 0;
|
|
18470
|
+
const remaining = Math.max(0, originalTimeout - (Date.now() - entry.raisedAt));
|
|
18471
|
+
this.socket.emitToSocket(socketId, "escalation:decision-required", { ...entry.payload, timeoutMs: remaining });
|
|
18472
|
+
}
|
|
18473
|
+
}
|
|
18474
|
+
/** Forget ONE parked prompt — the one that was answered or timed out. */
|
|
18475
|
+
clearPendingEscalation(requestId) {
|
|
18476
|
+
const entry = this.pendingEscalations.get(requestId);
|
|
18477
|
+
if (!entry) return;
|
|
18478
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18479
|
+
this.pendingEscalations.delete(requestId);
|
|
18480
|
+
}
|
|
18481
|
+
/**
|
|
18482
|
+
* Forget the session's OLDEST prompt — for a client that answered without a
|
|
18483
|
+
* requestId, which is exactly the gate `resolveEscalation` will settle.
|
|
18484
|
+
* Insertion order makes `values()` oldest-first.
|
|
18485
|
+
*/
|
|
18486
|
+
clearOldestSessionEscalation(sessionId) {
|
|
18487
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18488
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18489
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18490
|
+
this.pendingEscalations.delete(id);
|
|
18491
|
+
return;
|
|
18492
|
+
}
|
|
18493
|
+
}
|
|
18494
|
+
/** Forget every prompt a session was parked on (run ended, or halted). */
|
|
18495
|
+
clearSessionEscalations(sessionId) {
|
|
18496
|
+
for (const [id, entry] of this.pendingEscalations) {
|
|
18497
|
+
if (entry.sessionId !== sessionId) continue;
|
|
18498
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
18499
|
+
this.pendingEscalations.delete(id);
|
|
18500
|
+
}
|
|
18501
|
+
}
|
|
17667
18502
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
17668
18503
|
denyPendingApprovals(sessionId) {
|
|
17669
18504
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -18164,6 +18999,7 @@ ${prompt}`;
|
|
|
18164
18999
|
void (async () => {
|
|
18165
19000
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
18166
19001
|
this.activeSessions.set(sessionId, cascade);
|
|
19002
|
+
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
18167
19003
|
cascade.on("stream:token", (e) => {
|
|
18168
19004
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
18169
19005
|
});
|
|
@@ -18179,6 +19015,14 @@ ${prompt}`;
|
|
|
18179
19015
|
cascade.on("plan:approval-required", (e) => {
|
|
18180
19016
|
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
18181
19017
|
});
|
|
19018
|
+
cascade.on("escalation:decision-required", (e) => {
|
|
19019
|
+
this.raiseEscalation(sessionId, void 0, e);
|
|
19020
|
+
});
|
|
19021
|
+
cascade.on("escalation:timeout", (e) => {
|
|
19022
|
+
const ev = e;
|
|
19023
|
+
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
19024
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
19025
|
+
});
|
|
18182
19026
|
try {
|
|
18183
19027
|
const result = await cascade.run({
|
|
18184
19028
|
prompt: runPrompt,
|
|
@@ -18206,6 +19050,7 @@ ${prompt}`;
|
|
|
18206
19050
|
} finally {
|
|
18207
19051
|
this.activeSessions.delete(sessionId);
|
|
18208
19052
|
this.denyPendingApprovals(sessionId);
|
|
19053
|
+
this.clearSessionEscalations(sessionId);
|
|
18209
19054
|
}
|
|
18210
19055
|
})();
|
|
18211
19056
|
});
|
|
@@ -18538,6 +19383,41 @@ var HooksRunner = class {
|
|
|
18538
19383
|
}
|
|
18539
19384
|
};
|
|
18540
19385
|
|
|
19386
|
+
// src/mcp/discover.ts
|
|
19387
|
+
async function discoverMcpTools(servers) {
|
|
19388
|
+
const results = [];
|
|
19389
|
+
for (const server of servers) {
|
|
19390
|
+
const client = new McpClient({ trustedServers: [server.name] });
|
|
19391
|
+
try {
|
|
19392
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
19393
|
+
const connect = () => client.connect(server, authProvider ? { authProvider } : {});
|
|
19394
|
+
await (server.oauthStore ? withOAuthStoreLock(server.oauthStore, connect) : connect());
|
|
19395
|
+
const parsed = client.getToolDefinitions().flatMap((def) => {
|
|
19396
|
+
const [, serverName, toolName] = def.name.split("::");
|
|
19397
|
+
return serverName && toolName ? [{ def, server: serverName, tool: toolName }] : [];
|
|
19398
|
+
});
|
|
19399
|
+
const names = assignMcpToolNames(parsed);
|
|
19400
|
+
const tools = parsed.map(({ def, server: s, tool }, i) => ({
|
|
19401
|
+
server: s,
|
|
19402
|
+
tool,
|
|
19403
|
+
name: names[i],
|
|
19404
|
+
description: def.description.replace(`[MCP:${s}] `, "")
|
|
19405
|
+
}));
|
|
19406
|
+
results.push({ server: server.name, tools });
|
|
19407
|
+
} catch (err) {
|
|
19408
|
+
results.push({
|
|
19409
|
+
server: server.name,
|
|
19410
|
+
tools: [],
|
|
19411
|
+
error: err instanceof Error ? err.message : String(err)
|
|
19412
|
+
});
|
|
19413
|
+
} finally {
|
|
19414
|
+
await client.disconnectAll().catch(() => {
|
|
19415
|
+
});
|
|
19416
|
+
}
|
|
19417
|
+
}
|
|
19418
|
+
return results;
|
|
19419
|
+
}
|
|
19420
|
+
|
|
18541
19421
|
// src/retrieval/chunk.ts
|
|
18542
19422
|
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
18543
19423
|
function splitLongBlock(block, max) {
|
|
@@ -19076,11 +19956,55 @@ function mergeByKey(local, incoming, key) {
|
|
|
19076
19956
|
for (const i of incoming) map.set(key(i), i);
|
|
19077
19957
|
return [...map.values()];
|
|
19078
19958
|
}
|
|
19959
|
+
function uniqueServerName(desired, takenPrefixes) {
|
|
19960
|
+
if (!takenPrefixes.has(mcpServerPrefix(desired))) return desired;
|
|
19961
|
+
let n = 2;
|
|
19962
|
+
while (takenPrefixes.has(mcpServerPrefix(`${desired} ${n}`))) n++;
|
|
19963
|
+
return `${desired} ${n}`;
|
|
19964
|
+
}
|
|
19965
|
+
function mergeMcpServers(local, incoming) {
|
|
19966
|
+
const rewrite = /* @__PURE__ */ new Map();
|
|
19967
|
+
if (!incoming?.length) return { servers: local, synced: [], rewrite };
|
|
19968
|
+
const byName = /* @__PURE__ */ new Map();
|
|
19969
|
+
for (const l of local ?? []) byName.set(l.name, l);
|
|
19970
|
+
const prefixes = new Set([...byName.values()].map((m) => mcpServerPrefix(m.name)));
|
|
19971
|
+
const synced = [];
|
|
19972
|
+
for (const inc of incoming) {
|
|
19973
|
+
if (byName.has(inc.name)) {
|
|
19974
|
+
byName.set(inc.name, inc);
|
|
19975
|
+
synced.push(inc);
|
|
19976
|
+
continue;
|
|
19977
|
+
}
|
|
19978
|
+
const name = uniqueServerName(inc.name, prefixes);
|
|
19979
|
+
if (name !== inc.name) rewrite.set(mcpServerPrefix(inc.name), mcpServerPrefix(name));
|
|
19980
|
+
const server = name === inc.name ? inc : { ...inc, name };
|
|
19981
|
+
byName.set(name, server);
|
|
19982
|
+
synced.push(server);
|
|
19983
|
+
prefixes.add(mcpServerPrefix(name));
|
|
19984
|
+
}
|
|
19985
|
+
return { servers: [...byName.values()], synced, rewrite };
|
|
19986
|
+
}
|
|
19987
|
+
function rewriteDisabledTools(tools, rewrite) {
|
|
19988
|
+
if (!tools?.length || rewrite.size === 0) return tools;
|
|
19989
|
+
return tools.map((t) => {
|
|
19990
|
+
for (const [from, to] of rewrite) if (t.startsWith(from)) return to + t.slice(from.length);
|
|
19991
|
+
return t;
|
|
19992
|
+
});
|
|
19993
|
+
}
|
|
19994
|
+
function mergeDisabledTools(local, incoming, syncedServers) {
|
|
19995
|
+
const prefixes = (syncedServers ?? []).map((m) => mcpServerPrefix(m.name));
|
|
19996
|
+
const covered = (tool) => prefixes.some((p) => tool.startsWith(p));
|
|
19997
|
+
const kept = (local ?? []).filter((t) => !covered(t));
|
|
19998
|
+
const merged = [.../* @__PURE__ */ new Set([...kept, ...incoming ?? []])];
|
|
19999
|
+
return merged.length ? merged : void 0;
|
|
20000
|
+
}
|
|
19079
20001
|
function gatherSyncBundle(config) {
|
|
19080
|
-
const bundle = { v:
|
|
20002
|
+
const bundle = { v: 2 };
|
|
19081
20003
|
if (config.providers?.length) bundle.providers = config.providers;
|
|
19082
20004
|
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
19083
20005
|
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
20006
|
+
const syncedDisabledTools = config.tools?.disabledTools?.filter(isMcpToolName);
|
|
20007
|
+
if (syncedDisabledTools?.length) bundle.disabledTools = syncedDisabledTools;
|
|
19084
20008
|
if (config.models) bundle.models = config.models;
|
|
19085
20009
|
if (config.budget) bundle.budget = config.budget;
|
|
19086
20010
|
const prefs = {};
|
|
@@ -19095,7 +20019,16 @@ function applySyncBundle(bundle, config) {
|
|
|
19095
20019
|
const next = { ...config, tools: { ...config.tools } };
|
|
19096
20020
|
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
19097
20021
|
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
19098
|
-
|
|
20022
|
+
const mcp = mergeMcpServers(config.tools?.mcpServers, bundle.mcpServers);
|
|
20023
|
+
if (bundle.mcpServers) next.tools.mcpServers = mcp.servers;
|
|
20024
|
+
const carriesSelections = bundle.v >= 2 || bundle.disabledTools !== void 0;
|
|
20025
|
+
if (carriesSelections && (bundle.mcpServers || bundle.disabledTools)) {
|
|
20026
|
+
next.tools.disabledTools = mergeDisabledTools(
|
|
20027
|
+
config.tools?.disabledTools,
|
|
20028
|
+
rewriteDisabledTools(bundle.disabledTools, mcp.rewrite),
|
|
20029
|
+
bundle.mcpServers ? mcp.synced : void 0
|
|
20030
|
+
);
|
|
20031
|
+
}
|
|
19099
20032
|
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
19100
20033
|
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
19101
20034
|
if (bundle.prefs) {
|
|
@@ -19108,6 +20041,6 @@ function applySyncBundle(bundle, config) {
|
|
|
19108
20041
|
return next;
|
|
19109
20042
|
}
|
|
19110
20043
|
|
|
19111
|
-
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, discoverMcpAuthServer, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, runCascade, sessionWorthRemembering, streamCascade };
|
|
20044
|
+
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, isMcpToolName, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, runCascade, sessionWorthRemembering, streamCascade, uniqueMcpServerName };
|
|
19112
20045
|
//# sourceMappingURL=index.js.map
|
|
19113
20046
|
//# sourceMappingURL=index.js.map
|