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/index.d.cts CHANGED
@@ -192,6 +192,33 @@ interface ToolResult {
192
192
  output: string;
193
193
  isError: boolean;
194
194
  }
195
+ /**
196
+ * What to do about a section that escalated — a worker hit something it could
197
+ * not decide alone.
198
+ *
199
+ * `timeout` is a real outcome, not an error: a cloud run holds server resources
200
+ * while it waits, so an unanswered escalation fails the section rather than
201
+ * hanging indefinitely. The section reports WHY it failed, so an unattended run
202
+ * is diagnosable rather than merely dead.
203
+ */
204
+ interface EscalationDecision {
205
+ action: 'retry' | 'skip' | 'guidance' | 'timeout';
206
+ /** Required for 'guidance' — the instruction the retry must follow. */
207
+ note?: string;
208
+ /**
209
+ * Set when the SDK produced `action: 'skip'` itself — no listener wired,
210
+ * `autonomy: 'auto'`, or the run was aborted — rather than a person
211
+ * answering the prompt. `resolveEscalation()`, the one path a human
212
+ * response reaches, never sets this.
213
+ *
214
+ * A skip decided by the system is not the same fact as a skip a user
215
+ * chose: T2's `userSkipped` marker (which tells T1's reviewer "accept this
216
+ * as satisfied, not a gap") must NOT be set for the automatic case — a
217
+ * section nobody ever looked at is exactly the case a corrective pass
218
+ * exists to catch.
219
+ */
220
+ automatic?: boolean;
221
+ }
195
222
  interface ToolExecuteOptions {
196
223
  tierId: string;
197
224
  sessionId: string;
@@ -294,6 +321,19 @@ interface T2Result {
294
321
  t3Results: T3Result[];
295
322
  sectionSummary: string;
296
323
  issues: string[];
324
+ /**
325
+ * Set when this section reached PARTIAL because the user was asked about an
326
+ * escalation and chose "skip" — not because a T2/T3 pipeline shortfall left
327
+ * it incomplete. T1's reviewer pass sees only the section summary, so
328
+ * without this flag it cannot tell "the model gave up" from "the user
329
+ * decided this was good enough" and would generate a correction plan that
330
+ * redoes exactly the work the user just chose to keep as-is.
331
+ *
332
+ * NOT set when the 'skip' came from `EscalationDecision.automatic` — no
333
+ * listener wired, `autonomy: 'auto'`, or an aborted run. Nobody reviewed
334
+ * that section, so it must stay eligible for the corrective pass.
335
+ */
336
+ userSkipped?: boolean;
297
337
  }
298
338
  interface T3ResultPayload {
299
339
  subtaskId: string;
@@ -904,6 +944,16 @@ declare class ModelSelector {
904
944
  */
905
945
  private discovered;
906
946
  constructor(availableProviders: Set<ProviderType>);
947
+ /**
948
+ * Verdicts from earlier runs about ids that 404 on this account. Consulted
949
+ * BEFORE a model enters the pool, which is the only placement that helps: a
950
+ * T3 wave runs concurrently, so filtering after the first failure still lets
951
+ * every other worker in the wave call the dead id.
952
+ */
953
+ private deadModels?;
954
+ setDeadModelStore(store: {
955
+ isDead(provider: string, modelId: string): boolean;
956
+ }): void;
907
957
  addDynamicModel(model: ModelInfo): void;
908
958
  /** Record the ids a provider's API actually serves (discovery). Empty ⇒ ignored. */
909
959
  setValidatedModels(provider: ProviderType, ids: string[]): void;
@@ -1077,6 +1127,60 @@ declare class TaskAnalyzer {
1077
1127
  static clearCache(): void;
1078
1128
  }
1079
1129
 
1130
+ interface DeadModelRecord {
1131
+ /** `provider:modelId` — the same key shape the failover event reports. */
1132
+ key: string;
1133
+ provider: string;
1134
+ modelId: string;
1135
+ /** Provider's own words, kept verbatim so the user can act on them. */
1136
+ reason: string;
1137
+ firstSeenAt: number;
1138
+ lastSeenAt: number;
1139
+ /** How many times this model has been found dead. */
1140
+ hits: number;
1141
+ }
1142
+ /**
1143
+ * Where records live between runs. Desktop/CLI back this with a JSON file,
1144
+ * the cloud server with a per-user table. Both are synchronous-optional: a
1145
+ * store that fails to load must never stop a run.
1146
+ */
1147
+ interface DeadModelPersistence {
1148
+ load(): DeadModelRecord[];
1149
+ save(records: DeadModelRecord[]): void;
1150
+ }
1151
+ declare class DeadModelStore {
1152
+ private persistence?;
1153
+ private records;
1154
+ private readonly ttlMs;
1155
+ private readonly now;
1156
+ constructor(persistence?: DeadModelPersistence | undefined, opts?: {
1157
+ ttlMs?: number;
1158
+ now?: () => number;
1159
+ });
1160
+ private restore;
1161
+ private isExpired;
1162
+ private flush;
1163
+ /**
1164
+ * Record that a model was found dead. Returns true when this is the first
1165
+ * time (so the caller can log it once rather than once per concurrent
1166
+ * worker in the wave that discovered it).
1167
+ */
1168
+ record(provider: string, modelId: string, reason: string): boolean;
1169
+ /** True when this model is known dead and the verdict has not expired. */
1170
+ isDead(provider: string, modelId: string): boolean;
1171
+ /** Why a model is considered dead, for /why and the models command. */
1172
+ reasonFor(provider: string, modelId: string): string | null;
1173
+ /** Live records, expired ones filtered out. */
1174
+ list(): DeadModelRecord[];
1175
+ /**
1176
+ * Forget a verdict — used when the user fixes the underlying problem (adds
1177
+ * the model to their project, gets quota) and wants it retried now rather
1178
+ * than after the TTL.
1179
+ */
1180
+ forget(provider: string, modelId: string): boolean;
1181
+ clear(): void;
1182
+ }
1183
+
1080
1184
  declare class MemoryStore {
1081
1185
  private db;
1082
1186
  constructor(dbPath: string);
@@ -1505,10 +1609,38 @@ declare class CascadeRouter extends EventEmitter {
1505
1609
  */
1506
1610
  private budgetState;
1507
1611
  private budgetExceededReason;
1612
+ /**
1613
+ * Whether the run/session budget is permanently tripped, and why.
1614
+ *
1615
+ * A tier or worker that catches `BudgetExceededError` internally (T3Worker
1616
+ * does, so one dead-end subtask doesn't crash the whole run) absorbs the
1617
+ * exception into a normal FAILED/PARTIAL result — the tier returns as if
1618
+ * nothing unusual happened. That silently skips `Cascade.run()`'s dedicated
1619
+ * budget-exceeded handling (the `run:budget-exceeded` event and
1620
+ * `lastInterruptedRun` for `/continue`). This lets a caller check the
1621
+ * router's own state directly after a tier returns, regardless of how that
1622
+ * tier chose to report the error internally.
1623
+ */
1624
+ budgetExceededInfo(): {
1625
+ reason: string;
1626
+ } | null;
1508
1627
  private tpmLimiter;
1509
1628
  private localQueue;
1510
1629
  private taskAnalyzer?;
1511
1630
  private pendingFeedbackSource?;
1631
+ /** Models known dead for this account (404 / retired / not enabled). */
1632
+ private deadModels;
1633
+ /**
1634
+ * Back the dead-model memory with durable storage (a JSON file on desktop, a
1635
+ * per-user table in the cloud). Without one the verdicts still work, but only
1636
+ * for the life of the process — so tomorrow's run pays the same wave of 404s
1637
+ * to learn the same fact.
1638
+ */
1639
+ setDeadModelStore(store: DeadModelStore): void;
1640
+ /** Models known dead for this account, for `cascade models` and /why. */
1641
+ listDeadModels(): ReturnType<DeadModelStore['list']>;
1642
+ /** Retry a model the user has since fixed, without waiting out the TTL. */
1643
+ forgetDeadModel(provider: string, modelId: string): boolean;
1512
1644
  private worldStateDB?;
1513
1645
  private privacyPaths?;
1514
1646
  private guidanceQueue?;
@@ -1866,8 +1998,17 @@ declare class ToolRegistry extends EventEmitter {
1866
1998
  registerPlugin(plugin: ToolPlugin): void;
1867
1999
  /** Returns the names of all registered plugins */
1868
2000
  getRegisteredPlugins(): string[];
1869
- /** Registers all tools from an MCP client */
1870
- registerMcpTools(mcpClient: McpClient): void;
2001
+ /**
2002
+ * Registers all tools from an MCP client.
2003
+ *
2004
+ * `isAllowed` receives the FINAL registered name (`mcp__server__tool`) — the
2005
+ * same string the user sees and the same one that reaches the provider — so
2006
+ * a per-tool opt-out in Settings can be expressed as a plain name list.
2007
+ * Filtering here rather than at call time means a deselected tool is genuinely
2008
+ * absent from the definitions handed to the model, so it is never proposed,
2009
+ * never counted against the tool budget, and never has to be refused.
2010
+ */
2011
+ registerMcpTools(mcpClient: McpClient, isAllowed?: (registeredName: string) => boolean): void;
1871
2012
  setIgnoredPaths(patterns: string[]): void;
1872
2013
  getToolDefinitions(): ToolDefinition[];
1873
2014
  getTool(name: string): BaseTool | undefined;
@@ -2134,6 +2275,7 @@ declare class T1Administrator extends BaseTier {
2134
2275
  private escalations;
2135
2276
  private store?;
2136
2277
  private runBreaker?;
2278
+ private escalationCallback?;
2137
2279
  private t2PeerBus;
2138
2280
  private permissionEscalator?;
2139
2281
  private toolCreator?;
@@ -2143,6 +2285,13 @@ declare class T1Administrator extends BaseTier {
2143
2285
  private peerMessageSessionId;
2144
2286
  private planApprovalCallback?;
2145
2287
  constructor(router: CascadeRouter, toolRegistry: ToolRegistry, config: CascadeConfig);
2288
+ /** Ask the user what to do when any section escalates. */
2289
+ setEscalationCallback(cb: (ctx: {
2290
+ sectionId: string;
2291
+ sectionTitle: string;
2292
+ issues: string[];
2293
+ summary: string;
2294
+ }) => Promise<EscalationDecision>): void;
2146
2295
  /** Share the run-wide circuit breaker with every section manager. */
2147
2296
  setRunBreaker(breaker: RunBreaker): void;
2148
2297
  setStore(store: MemoryStore): void;
@@ -2212,6 +2361,34 @@ interface GeneratedAsset {
2212
2361
  */
2213
2362
  type AssetSink = (asset: GeneratedAsset) => Promise<string>;
2214
2363
 
2364
+ /** What the host's browser view can tell us about the page on screen. */
2365
+ interface CurrentPageSnapshot {
2366
+ url: string;
2367
+ title: string;
2368
+ /** Rendered visible text, host-truncated if enormous. */
2369
+ text: string;
2370
+ }
2371
+ /**
2372
+ * Returns the page the user is currently viewing, or null when the browser is
2373
+ * closed / on a blank tab.
2374
+ */
2375
+ type CurrentPageProvider = () => Promise<CurrentPageSnapshot | null>;
2376
+ /** Characters of page text handed to a model in one call. */
2377
+ declare const MAX_PAGE_TEXT_CHARS = 40000;
2378
+ declare class CurrentPageTool extends BaseTool {
2379
+ readonly name = "read_current_page";
2380
+ readonly description: string;
2381
+ readonly inputSchema: {
2382
+ type: string;
2383
+ properties: {};
2384
+ required: never[];
2385
+ };
2386
+ private provider;
2387
+ constructor(provider: CurrentPageProvider);
2388
+ isDangerous(): boolean;
2389
+ execute(_input: Record<string, unknown>, _options: ToolExecuteOptions): Promise<string>;
2390
+ }
2391
+
2215
2392
  /** One entry in the per-run orchestration decision trail (see /why). */
2216
2393
  interface DecisionLogEntry {
2217
2394
  at: string;
@@ -2278,6 +2455,32 @@ declare class Cascade extends EventEmitter {
2278
2455
  resolveMcpApproval(serverName: string, approved: boolean): void;
2279
2456
  private pendingPlanApproval?;
2280
2457
  private requestPlanApproval;
2458
+ private pendingEscalations;
2459
+ private requestEscalationDecision;
2460
+ /**
2461
+ * Resolve a pending escalation from a REPL / dashboard / cloud listener.
2462
+ *
2463
+ * `requestId` comes back from the `escalation:decision-required` event and is
2464
+ * what makes an answer land on the section that asked. It is optional so a
2465
+ * host with only one escalation in flight (and any caller that predates the
2466
+ * id) still works: without it the oldest outstanding request is answered,
2467
+ * which is correct whenever there is exactly one.
2468
+ *
2469
+ * `automatic` defaults to false — a call into this method is normally a real
2470
+ * person answering the prompt. Pass `true` from a host that is itself
2471
+ * settling the gate on nobody's behalf (a REST caller who never had a
2472
+ * chance to connect, a session halt with no per-section review behind it),
2473
+ * so T2 doesn't set `userSkipped` for a decision no one actually reviewed —
2474
+ * see EscalationDecision.automatic.
2475
+ */
2476
+ resolveEscalation(action: EscalationDecision['action'], note?: string, requestId?: string, automatic?: boolean): void;
2477
+ /**
2478
+ * Release every parked escalation — run teardown, so none outlive the run.
2479
+ * `automatic: true`: teardown resolving a section nobody answered is the
2480
+ * same system-decided case as the early-return skips above, not a person
2481
+ * reviewing and accepting that section's output.
2482
+ */
2483
+ private releasePendingEscalations;
2281
2484
  /**
2282
2485
  * Resolve a pending boardroom plan approval from a REPL / dashboard listener.
2283
2486
  * An optional `note` re-plans and re-asks; an optional `editedPlan` is applied
@@ -2328,6 +2531,17 @@ declare class Cascade extends EventEmitter {
2328
2531
  * existing tools rather than duplicating them.
2329
2532
  */
2330
2533
  setMediaSink(sink: AssetSink): void;
2534
+ /**
2535
+ * Let the agent read the page the user has open in the host's browser.
2536
+ *
2537
+ * Host-supplied because only a host WITH a browser can answer it — so the
2538
+ * tool is simply absent in the CLI and in hosted runs, rather than present
2539
+ * and always failing. Registered outside `enabledTools` for the same reason
2540
+ * media tools are: that allowlist is a blast-radius control for tools that
2541
+ * touch the machine, and reading a page the user already opened touches
2542
+ * nothing. `disabledTools` still removes it.
2543
+ */
2544
+ setCurrentPageProvider(provider: CurrentPageProvider): void;
2331
2545
  private registerMediaTools;
2332
2546
  /**
2333
2547
  * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
@@ -2575,6 +2789,19 @@ declare class T2Manager extends BaseTier {
2575
2789
  private t2PeerBus?;
2576
2790
  private permissionEscalator?;
2577
2791
  private toolCreator?;
2792
+ /**
2793
+ * Asked when a section ends ESCALATED — i.e. a worker hit something it could
2794
+ * not decide alone. Without this the escalation was a dead end: the status
2795
+ * said "needs a decision" and no decision was ever requested, so an MCP run
2796
+ * that legitimately needed input just stopped there.
2797
+ */
2798
+ private escalationCallback?;
2799
+ /**
2800
+ * The one allowed retry has been spent. Retrying is bounded because an
2801
+ * escalation loop that can re-ask forever burns a budget on a question that
2802
+ * is not resolving; a second escalation is terminal instead.
2803
+ */
2804
+ private escalationRetryUsed;
2578
2805
  /** Optional boardroom gate (Moderate / root-T2 runs) — pauses after decomposition. */
2579
2806
  private planApprovalCallback?;
2580
2807
  /** AbortController for the current T3 wave — aborted on cancel-and-respawn */
@@ -2596,6 +2823,13 @@ declare class T2Manager extends BaseTier {
2596
2823
  */
2597
2824
  setPermissionEscalator(escalator: PermissionEscalator): void;
2598
2825
  setToolCreator(creator: ToolCreator): void;
2826
+ /** Ask the user what to do when a section escalates. */
2827
+ setEscalationCallback(cb: (ctx: {
2828
+ sectionId: string;
2829
+ sectionTitle: string;
2830
+ issues: string[];
2831
+ summary: string;
2832
+ }) => Promise<EscalationDecision>): void;
2599
2833
  /** Boardroom gate for Moderate (root-T2) runs: pause after decomposition. */
2600
2834
  setPlanApprovalCallback(cb: (subtasks: ReadonlyArray<{
2601
2835
  subtaskId: string;
@@ -2887,6 +3121,50 @@ declare class ConfigManager {
2887
3121
  getApiKey(provider: string): string | undefined;
2888
3122
  private loadConfig;
2889
3123
  private injectEnvKeys;
3124
+ /**
3125
+ * Follow a disambiguation rename into `tools.mcpTrusted`.
3126
+ *
3127
+ * `mcpTrusted` is matched by EXACT name (`McpClient.connect()`). Two shapes
3128
+ * of rename reach here, and they need OPPOSITE treatment:
3129
+ *
3130
+ * - Distinct raw names that only collide via sanitizing (`foo bar` /
3131
+ * `foo@bar`): each had its OWN trust entry, and the old string now belongs
3132
+ * to nothing — the server that used it moved away, so the entry has to
3133
+ * move with it. A plain rewrite is correct here.
3134
+ * - Literally identical names (a hand-edited or duplicated config: two rows
3135
+ * both named `foo`): there is only ONE trust entry for both, since
3136
+ * `mcpTrusted` itself is deduplicated. `this.config.tools.mcpServers` has
3137
+ * already been updated to the disambiguated list by the time this runs
3138
+ * (see `load()`), so the untouched survivor is STILL named `foo` — a plain
3139
+ * rewrite would move the one entry entirely onto the renamed row and
3140
+ * leave the survivor untrusted. The entry must be kept for the survivor
3141
+ * AND granted to the renamed identity, not moved.
3142
+ *
3143
+ * Renames are grouped by `from` before either treatment is applied, and
3144
+ * each group is settled in one step. Two DIFFERENT rows can start out with
3145
+ * the exact same raw name and BOTH get renamed away (e.g. three servers
3146
+ * named `foo bar`, `foo@bar`, `foo@bar` — the two `foo@bar` rows collide
3147
+ * with `foo bar` and with each other, so both are renamed to distinct
3148
+ * identities while `foo bar` survives untouched). Processing renames one at
3149
+ * a time in that shape drops coverage: the first rename's REPLACE branch
3150
+ * would erase `from` from `trusted` entirely, so the guard on the second
3151
+ * rename with the same `from` sees it already gone and silently skips —
3152
+ * granting trust to only one of the two renamed identities. Grouping first
3153
+ * means every renamed identity that shares a `from` is added or substituted
3154
+ * together, in the one pass that still sees the original entry.
3155
+ *
3156
+ * `tools.disabledTools` deliberately gets no equivalent treatment. It's
3157
+ * matched by sanitized PREFIX, and a prefix collision is exactly what made
3158
+ * an existing entry ambiguous between the two servers in the first place —
3159
+ * there is no way to tell, from the stored string alone, which of the two
3160
+ * a denial was meant for. Leaving those entries untouched resolves that
3161
+ * ambiguity for free: the survivor keeps the original (now unique) prefix,
3162
+ * so an old entry keeps applying to it, while the renamed server starts
3163
+ * clean under its new prefix. Rewriting the entry to follow the rename
3164
+ * would do the opposite — move a denial that already worked for the
3165
+ * survivor onto a server it may never have meant to cover.
3166
+ */
3167
+ private renameMcpServerReferences;
2890
3168
  private ensureDefaultIdentity;
2891
3169
  }
2892
3170
 
@@ -2900,6 +3178,10 @@ declare class DashboardSocket {
2900
3178
  private authRequired;
2901
3179
  private secret;
2902
3180
  constructor(httpServer: Server, options: DashboardSocketOptions);
3181
+ /** How many sockets are currently in a room — 0 means nobody can answer. */
3182
+ roomSize(room: string): number;
3183
+ /** Is this exact connection still live? A reconnect issues a NEW socket id. */
3184
+ hasSocket(socketId: string): boolean;
2903
3185
  broadcastToRoom(room: string, event: string, data: unknown): void;
2904
3186
  broadcast(event: string, data: unknown): void;
2905
3187
  emitCascadeEvent(ev: CascadeEvent): void;
@@ -2909,6 +3191,15 @@ declare class DashboardSocket {
2909
3191
  emitApprovalRequest(request: PermissionRequest): void;
2910
3192
  onApprovalResponse(callback: (data: PermissionDecisionPayload) => void): void;
2911
3193
  private setupHandlers;
3194
+ /**
3195
+ * A client subscribed to a session's room.
3196
+ *
3197
+ * The host uses this to replay a gate the run is already parked on. Socket.IO
3198
+ * does not buffer room emissions, so a prompt broadcast while the renderer was
3199
+ * reconnecting is simply gone; without a replay the section waits out its full
3200
+ * timeout with the user sitting in front of a screen that never asked.
3201
+ */
3202
+ onJoinSession(callback: (sessionId: string, socketId: string) => void): void;
2912
3203
  onSessionRate(callback: (sessionId: string, rating: 'good' | 'bad') => void): void;
2913
3204
  onSessionHalt(callback: (sessionId: string) => void): void;
2914
3205
  /**
@@ -2922,6 +3213,20 @@ declare class DashboardSocket {
2922
3213
  note?: string;
2923
3214
  editedPlan?: unknown;
2924
3215
  }) => void): void;
3216
+ /**
3217
+ * A section escalated and the run is parked waiting for an answer. The
3218
+ * desktop shows the escalation modal on `escalation:decision-required` and
3219
+ * answers here.
3220
+ *
3221
+ * Unlike a plan decision, silence is NOT taken as consent — the SDK fails
3222
+ * the section on timeout — so this handler only ever forwards a real choice.
3223
+ */
3224
+ onEscalationDecision(callback: (data: {
3225
+ sessionId: string;
3226
+ requestId?: string;
3227
+ action: 'retry' | 'skip' | 'guidance';
3228
+ note?: string;
3229
+ }) => void): void;
2925
3230
  onSessionSteer(callback: (message: string, sessionId?: string, nodeId?: string) => void): void;
2926
3231
  onConfigUpdate(callback: (data: {
2927
3232
  keys?: Record<string, string>;
@@ -2947,6 +3252,13 @@ declare class DashboardServer {
2947
3252
  private globalStore;
2948
3253
  private broadcastTimer;
2949
3254
  private activeSessions;
3255
+ /**
3256
+ * Supplied by the desktop shell so a run can read the page the user has
3257
+ * open in the built-in browser. Absent in the CLI, where there is no
3258
+ * browser — so the tool is simply not registered rather than present and
3259
+ * always failing.
3260
+ */
3261
+ private currentPageProvider?;
2950
3262
  private activeControllers;
2951
3263
  /**
2952
3264
  * Run taskIds per chat session — file snapshots are keyed by the run's
@@ -2961,6 +3273,17 @@ declare class DashboardServer {
2961
3273
  * map is how that answer reaches the run that's blocked on it.
2962
3274
  */
2963
3275
  private pendingApprovals;
3276
+ /**
3277
+ * The escalation prompt a session is currently parked on, kept so it can be
3278
+ * delivered to a client that was not reachable when it was raised.
3279
+ *
3280
+ * Socket.IO buffers nothing: a prompt emitted while the renderer is mid
3281
+ * reconnect is gone, and the section then waits out its full five minutes
3282
+ * with the user in front of a screen that never asked them anything. Holding
3283
+ * the payload lets `join:session` replay it, and lets a run with no reachable
3284
+ * client be settled instead of parked (see ORPHANED_ESCALATION_GRACE_MS).
3285
+ */
3286
+ private pendingEscalations;
2964
3287
  /**
2965
3288
  * The orchestration decision trail ("why") of each session's most recent
2966
3289
  * run — captured when the run ends so the desktop's Why panel can show it
@@ -2973,6 +3296,8 @@ declare class DashboardServer {
2973
3296
  private host;
2974
3297
  private workspacePath;
2975
3298
  constructor(config: CascadeConfig, store: MemoryStore, workspacePath?: string);
3299
+ /** Wire the host's browser view in (desktop only). */
3300
+ setCurrentPageProvider(provider: CurrentPageProvider): void;
2976
3301
  start(): Promise<void>;
2977
3302
  stop(): Promise<void>;
2978
3303
  getSocket(): DashboardSocket;
@@ -3039,6 +3364,66 @@ declare class DashboardServer {
3039
3364
  * client so an open desktop sees the run appear live.
3040
3365
  */
3041
3366
  private runScheduledTask;
3367
+ /**
3368
+ * Is anyone able to answer for this session right now?
3369
+ *
3370
+ * The originating socket first — that connection is definitely watching this
3371
+ * run — then the session room, which is where a client lands after a
3372
+ * reconnect issues it a new socket id.
3373
+ */
3374
+ private hasResponder;
3375
+ /**
3376
+ * Send a run event to whoever is watching this session.
3377
+ *
3378
+ * A captured socket id goes stale the moment Socket.IO reconnects — the
3379
+ * client is back, but under a new id, and `emitToSocket` on the old one is a
3380
+ * silent no-op. Falling back to the session room keeps the run addressable
3381
+ * across a reconnect; emitting to one OR the other (never both) keeps a
3382
+ * client that is both the originator and a room member from seeing every
3383
+ * token twice.
3384
+ */
3385
+ private emitToSessionClients;
3386
+ /**
3387
+ * Raise an escalation prompt, and make sure it can still be answered.
3388
+ *
3389
+ * Held in `pendingEscalations` — keyed by requestId, because a Complex wave
3390
+ * dispatches sections concurrently and two can be parked at once — so a
3391
+ * client that joins late (or rejoins after a reconnect) gets every waiting
3392
+ * prompt replayed. Keying by session would drop the first of a pair: its
3393
+ * grace timer would be cleared by the second, leaving it parked for the full
3394
+ * timeout with nobody able to answer it. If nobody is reachable at all, the gate is
3395
+ * settled as `skip` after a short grace period rather than parked for the
3396
+ * full timeout: that matches the SDK's policy when no listener exists, and
3397
+ * five minutes of a held run is a real cost for a REST caller who has already
3398
+ * hung up. The grace period exists so an ordinary two-second reconnect is not
3399
+ * mistaken for an absent user.
3400
+ */
3401
+ private raiseEscalation;
3402
+ /**
3403
+ * Replay EVERY parked prompt to a client that just subscribed to the session.
3404
+ *
3405
+ * Plural because a Complex run dispatches sections concurrently, so a wave can
3406
+ * leave more than one waiting at the same time — the same reason the SDK keys
3407
+ * its gates and the clients queue theirs.
3408
+ *
3409
+ * `timeoutMs` is recomputed as the time actually LEFT, because the SDK's timer
3410
+ * has been running since the prompt was raised. Sent verbatim, a client
3411
+ * reconnecting four minutes in would show a fresh five-minute countdown and
3412
+ * then watch the section fail with four of those minutes still on the clock.
3413
+ * A remaining duration is used rather than an absolute deadline so the two
3414
+ * sides never have to agree on a wall clock.
3415
+ */
3416
+ private replayEscalations;
3417
+ /** Forget ONE parked prompt — the one that was answered or timed out. */
3418
+ private clearPendingEscalation;
3419
+ /**
3420
+ * Forget the session's OLDEST prompt — for a client that answered without a
3421
+ * requestId, which is exactly the gate `resolveEscalation` will settle.
3422
+ * Insertion order makes `values()` oldest-first.
3423
+ */
3424
+ private clearOldestSessionEscalation;
3425
+ /** Forget every prompt a session was parked on (run ended, or halted). */
3426
+ private clearSessionEscalations;
3042
3427
  /** Deny + clear any approvals still pending for a session (run end / abort). */
3043
3428
  private denyPendingApprovals;
3044
3429
  private persistRuntimeRow;
@@ -3185,6 +3570,104 @@ declare function connectMcpWithLoopbackOAuth(opts: {
3185
3570
  timeoutMs?: number;
3186
3571
  }): Promise<LoopbackOAuthResult>;
3187
3572
 
3573
+ /** One tool a server advertises, named exactly as it is registered at run time. */
3574
+ interface DiscoveredMcpTool {
3575
+ /** The server that advertises it. */
3576
+ server: string;
3577
+ /** The server-side tool name (what the vendor calls it). */
3578
+ tool: string;
3579
+ /** The registered name — `mcp__server__tool`. This is the selection key. */
3580
+ name: string;
3581
+ description: string;
3582
+ }
3583
+ interface McpDiscoveryResult {
3584
+ server: string;
3585
+ tools: DiscoveredMcpTool[];
3586
+ /** Set when the server could not be reached or refused the connection. */
3587
+ error?: string;
3588
+ }
3589
+ /**
3590
+ * Connect to each server, list its tools, disconnect.
3591
+ *
3592
+ * Servers are pre-trusted here because the caller has already added them —
3593
+ * discovery reads a list, it never invokes anything. A server that fails is
3594
+ * reported with its error rather than throwing, so one dead connector cannot
3595
+ * blank out the Settings panel for the working ones.
3596
+ */
3597
+ declare function discoverMcpTools(servers: McpServerConfig$1[]): Promise<McpDiscoveryResult[]>;
3598
+
3599
+ /** Prefix marking a tool that came from an MCP server. */
3600
+ declare const MCP_TOOL_PREFIX = "mcp__";
3601
+ /** Provider-safe name for a tool exposed by an MCP server. Bounded — see MAX_MCP_TOOL_NAME_LENGTH. */
3602
+ declare function mcpToolName(serverName: string, toolName: string): string;
3603
+ declare function isMcpToolName(name: string): boolean;
3604
+ /**
3605
+ * A name for a NEW MCP server connection that won't collide, on its sanitized
3606
+ * tool prefix, with any name already in use.
3607
+ *
3608
+ * Compared by prefix, not by the raw name: `foo bar` and `foo@bar` are
3609
+ * different strings but both register as `mcp__foo_bar__…`, so a check against
3610
+ * raw names would accept a pair that still collides downstream — the exact gap
3611
+ * that let two connectors' tools overwrite each other in the registry.
3612
+ *
3613
+ * Mirrored on the cloud side (`cloud/server/src/db.ts` `uniqueMcpServerName`,
3614
+ * DB-backed instead of array-backed) — kept in sync by hand since the two sides
3615
+ * don't share a runtime, but the algorithm and suffix format must match or a
3616
+ * name that's unique on one side could still collide on the other after a sync
3617
+ * pull (see `mergeMcpServers` in `src/cloud/keysync.ts`, which performs the
3618
+ * same comparison for that reason).
3619
+ */
3620
+ declare function uniqueMcpServerName(desired: string, existingNames: string[]): string;
3621
+ /** One entry renamed by `disambiguateMcpServerNames`, so callers can follow it. */
3622
+ interface McpServerRename {
3623
+ from: string;
3624
+ to: string;
3625
+ }
3626
+ /**
3627
+ * Rename pre-existing entries whose sanitized prefixes collide.
3628
+ *
3629
+ * For config loaded from disk (desktop and CLI share one config file), rather
3630
+ * than a fresh connection — `uniqueMcpServerName` only guards a NEW entry, so a
3631
+ * file that already contains a colliding pair (hand-edited, or written by a
3632
+ * CLI version that predates the check) stays broken until something touches it.
3633
+ * The FIRST entry of each colliding group keeps its name, since that connection
3634
+ * is presumably already working; later ones are suffixed exactly as
3635
+ * `uniqueMcpServerName` would have produced had they been added after this
3636
+ * existed. A no-op when nothing collides, so callers can run it unconditionally
3637
+ * on every load without needing a one-time migration flag.
3638
+ *
3639
+ * A server's name is referenced elsewhere in config — `tools.mcpTrusted`
3640
+ * (`McpClient.connect()` matches it exactly) and `tools.disabledTools`
3641
+ * (matched by sanitized prefix). This function only owns `mcpServers`, so it
3642
+ * returns the rename list rather than reaching into config fields it doesn't
3643
+ * know about; the caller applies it to whatever else keys off the old name.
3644
+ */
3645
+ declare function disambiguateMcpServerNames<T extends {
3646
+ name: string;
3647
+ }>(servers: T[]): {
3648
+ servers: T[];
3649
+ renames: McpServerRename[];
3650
+ };
3651
+ /**
3652
+ * Prefix matching every tool from one server, for filtering.
3653
+ *
3654
+ * Also lets a host drop a server's per-tool selections when the server itself
3655
+ * is removed — otherwise a tool switched off before removal stays off when the
3656
+ * same connector is added back, with nothing on screen explaining why.
3657
+ */
3658
+ declare function mcpServerPrefix(serverName: string): string;
3659
+ /**
3660
+ * Drop one removed server's entries from a deny list.
3661
+ *
3662
+ * The deny list lives as one flat array, not scoped per server, so removing a
3663
+ * connector without this left its denials behind — reconnecting the SAME name
3664
+ * later silently re-disabled tools the user had switched off in a previous
3665
+ * life of that connection, with nothing on screen explaining why. Shared by
3666
+ * every removal path (desktop IPC, `cascade mcp remove`) so the behavior can't
3667
+ * drift between them the way the duplicated inline version risked.
3668
+ */
3669
+ declare function removeMcpServerDenials(disabledTools: string[] | undefined, serverName: string): string[] | undefined;
3670
+
3188
3671
  /** A unit of indexed text. `sourceId` groups chunks from one document/file. */
3189
3672
  interface Chunk {
3190
3673
  id: string;
@@ -3972,14 +4455,39 @@ declare class CloudClient {
3972
4455
  logout(): Promise<void>;
3973
4456
  }
3974
4457
 
4458
+ /**
4459
+ * Bundle format version.
4460
+ *
4461
+ * 1 — the original shape. Knows nothing about per-tool selections.
4462
+ * 2 — carries `disabledTools`.
4463
+ *
4464
+ * The distinction is load-bearing, not bookkeeping. In a v2 bundle an absent
4465
+ * `disabledTools` is a real statement — "nothing is switched off on the device
4466
+ * I pushed from" — and pulling it must be able to switch tools back ON. In a v1
4467
+ * bundle the field is absent because the format had no way to express it, and
4468
+ * reading that as "nothing is switched off" would silently re-enable the
4469
+ * destructive connector tool a user had deliberately turned off. Absent and
4470
+ * empty are the same JSON; only the version tells them apart.
4471
+ */
4472
+ type SyncBundleVersion = 1 | 2;
3975
4473
  interface SyncBundle {
3976
- v: 1;
4474
+ v: SyncBundleVersion;
3977
4475
  /** LLM provider keys / endpoints. */
3978
4476
  providers?: ProviderConfig[];
3979
4477
  /** Web-search backend keys (Brave / Tavily) + SearXNG URL. */
3980
4478
  webSearch?: WebSearchConfig;
3981
4479
  /** MCP servers / connectors (headers carry auth). */
3982
4480
  mcpServers?: McpServerConfig$1[];
4481
+ /**
4482
+ * Per-tool selections for the servers above — the tools switched OFF.
4483
+ * Present from v2 on.
4484
+ *
4485
+ * Travels WITH the servers, and must: syncing a connector without its
4486
+ * selections hands the other device the connector, its credentials, and every
4487
+ * tool switched back on — including the destructive one the user had
4488
+ * deliberately turned off, with nothing on screen saying so.
4489
+ */
4490
+ disabledTools?: string[];
3983
4491
  /** Per-tier model overrides (non-secret). */
3984
4492
  models?: ModelOverrides;
3985
4493
  /** Budget caps (non-secret). */
@@ -3997,4 +4505,4 @@ declare function gatherSyncBundle(config: CascadeConfig): SyncBundle;
3997
4505
  /** Merge a pulled bundle into a config, returning the updated config to persist. */
3998
4506
  declare function applySyncBundle(bundle: SyncBundle, config: CascadeConfig): CascadeConfig;
3999
4507
 
4000
- export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompleteFn, ConfigManager, type ConversationMessage, 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, type DashboardConfig, DashboardServer, type DeviceStart, type DistilledFact, type Embedder, type EncryptedBlob, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, MODELS, type ManifestDiff, McpClient, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RankedFact, type RefreshResult, type Reranker, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, WorkspaceIndex, type WorkspaceIndexOptions, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, 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 };
4508
+ export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, type ApprovalRequest, type ApprovalResponse, type AuditEntry, AuditLogger, type BenchmarksConfig, type BudgetConfig, 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, type CascadeConfig, type CascadeEvent, type CascadeEventType, CascadeIgnore, type CascadeMessage, CascadeRouter, type CascadeRunOptions, type CascadeRunResult, type CascadeThemeName, CascadeToolError, type Chunk, type ChunkOptions, CloudClient, type CloudConversation, type CloudMessage, type CloudSession, type CloudSessionStore, type CloudTurnInput, type CloudUser, type CodeChunkOptions, type CodeChunker, CodeSearchTool, type CompleteFn, ConfigManager, type ConversationMessage, type CurrentPageProvider, type CurrentPageSnapshot, 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, type DashboardConfig, DashboardServer, type DeviceStart, type DiscoveredMcpTool, type DistilledFact, type Embedder, type EncryptedBlob, type EscalationDecision, type EscalationPayload, type ExtendedContextConfig, type FileManifest, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, type GenerateOptions, type GenerateResult, type GraphFactSource, GraphRetriever, type GraphSearchOptions, GraphSearchTool, type HookDefinition, type HooksConfig, HooksRunner, type Identity, type ImageAttachment, Keystore, LLMReranker, LM_STUDIO_BASE_URL, type LegacyThemeName, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, type ManifestDiff, McpClient, type McpDiscoveryResult, McpOAuthProvider, type McpOAuthState, type McpOAuthStore, type McpServerConfig$1 as McpServerConfig, type McpServerRename, type MemoryConfig, MemoryStore, type Message, type MessageContent, type MessagePayload, type MessageStatus, type MessageType, type ModelInfo, type ModelOverrides, type NativeProvider, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, type OpenAIEmbedderOptions, PROVIDER_DISPLAY_NAMES, type PeerMessage, type PeerMessageEvent, type PeerSyncPayload, type PeerSyncType, type PermissionDecision, type PermissionDecisionPayload, type PermissionRequest, type PermissionTrailEntry, type PlanReviewConfig, type ProviderConfig, type ProviderType, type RankedFact, type RefreshResult, type Reranker, type RetrievalContext, type RetrievalMode, type RetrievalPlan, Retriever, type RetrieverSearchOptions, type RuntimeNode, type RuntimeNodeLog, type RuntimeRefreshPayload, type RuntimeScope, type RuntimeSession, type RuntimeSnapshotPayload, type ScheduledTask, type ScoredChunk, type SearchOptions, type Session, type SessionCheckpoint, type SessionMetadata, type SessionSubscriptionPayload, SqliteVectorStore, type StatusUpdate, type StoredMessage, type StreamChunk, type SyncBundle, T1Administrator, type T1ToT2Assignment, T1_MODEL_PRIORITY, T2Manager, type T2Result, type T2ToT3Assignment, T2_MODEL_PRIORITY, type T3Result, type T3ResultPayload, type T3SubtaskSpec, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, type TaskComplexity, TaskScheduler, Telemetry, type TelemetryConfig, type Theme, type ThemeColors, type ThemeName, type TierConfig, type TierLimits, type TierRole, type TierStatus, type TokenUsage, type ToolCall, type ToolDefinition, type ToolExecuteOptions, ToolRegistry, type ToolResult, type ToolsConfig, VISION_MODEL_PRIORITY, type VectorStore, type WebSearchConfig, type WebhookConfig, type WorkspaceConfig, WorkspaceIndex, type WorkspaceIndexOptions, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, 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 };