cascade-ai 0.68.0 → 0.69.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
@@ -61,30 +61,69 @@ interface ResumeStoreOptions {
61
61
  maxAgeMs?: number;
62
62
  /** Injectable clock, for tests. */
63
63
  now?: () => number;
64
+ /** How long a claim may be held before it is considered abandoned. */
65
+ claimTimeoutMs?: number;
64
66
  }
65
67
  declare class ResumeStore {
66
68
  private readonly dir;
67
69
  private readonly maxCheckpoints;
68
70
  private readonly maxAgeMs;
69
71
  private readonly now;
72
+ private readonly claimTimeoutMs;
70
73
  constructor(options: ResumeStoreOptions);
71
74
  /**
72
- * Persist a checkpoint and prune. Never throws: failing to save a resume file
73
- * must not turn a recoverable stop into a crash, which would be a strictly
74
- * worse outcome than the one this store exists to improve.
75
+ * Persist a checkpoint and prune.
76
+ *
77
+ * Never throws — failing to save a resume file must not turn a recoverable
78
+ * stop into a crash — but DOES report whether the atomic rename landed.
79
+ * Callers decide whether an original claim is safe to discard based on that
80
+ * answer, so "I swallowed the error" and "the checkpoint is on disk" must not
81
+ * look alike to them.
82
+ *
83
+ * @returns true only when the checkpoint is durably on disk.
75
84
  */
76
85
  save(checkpoint: Omit<ResumeCheckpoint, 'version' | 'createdAt'> & {
77
86
  createdAt?: string;
78
- }): Promise<void>;
87
+ }): Promise<boolean>;
79
88
  /** Valid, unexpired checkpoints, newest first. */
80
89
  list(): Promise<ResumeCheckpoint[]>;
81
90
  /** The most recent resumable run, or null. */
82
91
  latest(): Promise<ResumeCheckpoint | null>;
83
92
  /**
84
- * Load a checkpoint and delete it in the same step. Resuming twice from one
85
- * checkpoint would re-run restored work, so consumption is part of reading.
93
+ * Claim a checkpoint for a resume attempt.
94
+ *
95
+ * This used to DELETE the file and return it, which opened a window with no
96
+ * recovery record at all: between preparing the continuation and the resumed
97
+ * run writing its own checkpoint, a crash lost the only copy — and a crash is
98
+ * precisely what this store exists to survive. Deleting on read optimised for
99
+ * "never resume twice" at the cost of "never lose the work", which is the
100
+ * wrong trade for a recovery mechanism.
101
+ *
102
+ * Instead the file is RENAMED to `.claimed`, which still hides it from
103
+ * `list()` (so a concurrent resume cannot take the same work) but keeps it on
104
+ * disk. The caller then either `release`s it — restoring it for another
105
+ * attempt — or `settle`s it once the resumed run is safely under way. A claim
106
+ * abandoned by a dead process is reclaimed after `claimTimeoutMs`.
107
+ */
108
+ claim(taskId?: string): Promise<{
109
+ checkpoint: ResumeCheckpoint;
110
+ claimId: string;
111
+ } | null>;
112
+ /** Give a claim back, so the work stays recoverable after a failed start. */
113
+ release(claimId: string): Promise<void>;
114
+ /** Discard a claim for good — the resumed run has taken ownership. */
115
+ settle(claimId: string): Promise<void>;
116
+ /**
117
+ * Claim-and-discard in one step. Kept for callers that genuinely want the old
118
+ * semantics; prefer claim/settle so a crash mid-resume stays recoverable.
86
119
  */
87
120
  consume(taskId?: string): Promise<ResumeCheckpoint | null>;
121
+ /**
122
+ * Return claims abandoned by a process that died mid-resume. Without this a
123
+ * crash between claim and settle would strand the checkpoint invisibly —
124
+ * trading one loss window for another.
125
+ */
126
+ reclaimStale(): Promise<number>;
88
127
  /** Remove every checkpoint for a task id. */
89
128
  delete(taskId: string): Promise<void>;
90
129
  /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
@@ -336,7 +375,17 @@ interface ToolExecuteOptions {
336
375
  }>;
337
376
  }
338
377
  type TierRole = 'T1' | 'T2' | 'T3';
339
- type TierStatus = 'IDLE' | 'ACTIVE' | 'COMPLETED' | 'FAILED' | 'ESCALATED';
378
+ /**
379
+ * A tier's lifecycle state.
380
+ *
381
+ * BLOCKED is terminal like COMPLETED and FAILED — the tier will never run,
382
+ * because work it depended on failed first. It is NOT the same word as
383
+ * `MessageStatus.BLOCKED` below, which means "running but stuck": one describes
384
+ * a tier that never started, the other a tier that started and cannot finish.
385
+ * Anything rendering a status label should treat BLOCKED as "skipped", not as
386
+ * an error and not as in-progress.
387
+ */
388
+ type TierStatus = 'IDLE' | 'ACTIVE' | 'COMPLETED' | 'FAILED' | 'ESCALATED' | 'BLOCKED';
340
389
  type TaskComplexity = 'Simple' | 'Moderate' | 'Complex' | 'Highly Complex';
341
390
  interface TierConfig {
342
391
  role: TierRole;
@@ -409,10 +458,27 @@ interface StatusUpdate {
409
458
  interface T2Result {
410
459
  sectionId: string;
411
460
  sectionTitle: string;
412
- status: 'COMPLETED' | 'PARTIAL' | 'FAILED' | 'ESCALATED';
461
+ /**
462
+ * BLOCKED is not a kind of FAILED. A failed section was attempted and did not
463
+ * work; a blocked one was never attempted, because something it depended on
464
+ * failed first — so it spent no tokens and says nothing about whether it
465
+ * would have worked. Reporting it as FAILED (which is what happened until
466
+ * now) tells the user their work broke when in fact the scheduler protected
467
+ * them from paying for work that could not succeed yet.
468
+ */
469
+ status: 'COMPLETED' | 'PARTIAL' | 'FAILED' | 'ESCALATED' | 'BLOCKED';
413
470
  t3Results: T3Result[];
414
471
  sectionSummary: string;
415
472
  issues: string[];
473
+ /**
474
+ * Set only on BLOCKED sections: which upstream work stopped this one, by
475
+ * title and by id. Structured rather than flattened into `issues`, so a UI
476
+ * can link a skipped section back to its cause instead of parsing prose.
477
+ */
478
+ blockedBy?: {
479
+ ids: string[];
480
+ titles: string[];
481
+ };
416
482
  /**
417
483
  * Set when this section reached PARTIAL because the user was asked about an
418
484
  * escalation and chose "skip" — not because a T2/T3 pipeline shortfall left
@@ -1179,7 +1245,24 @@ declare class TaskAnalyzer {
1179
1245
  private feedback?;
1180
1246
  private bias;
1181
1247
  private lastProfile;
1182
- private lastSelectedModels;
1248
+ /** Models chosen by the run currently in flight. Cleared when it completes. */
1249
+ private currentRunSelections;
1250
+ /**
1251
+ * Immutable snapshot of the last COMPLETED run: its selections AND the task
1252
+ * type they were chosen for.
1253
+ *
1254
+ * The task type has to travel WITH the selections. `lastProfile` is overwritten
1255
+ * by the next analyze(), so if run B started before the user rated run A, A's
1256
+ * models were credited under B's task type — teaching the router that a coding
1257
+ * model is good at creative writing, from a rating that never said so.
1258
+ *
1259
+ * Explicit ratings arrive after the run finishes — rateLastRun() is by nature
1260
+ * a reaction to a finished answer — but the run finalizer calls
1261
+ * recordRunOutcome() first, and that used to clear the only map there was. So
1262
+ * every explicit rating iterated an empty map, recorded nothing, and returned
1263
+ * false: the 3x-weighted user signal never reached the tracker at all.
1264
+ */
1265
+ private lastCompletedRun?;
1183
1266
  constructor(tracker?: ModelPerformanceTracker, bias?: AutoBias);
1184
1267
  /**
1185
1268
  * Supply per-model rating counts. Optional by design: a host that collects no
@@ -1211,9 +1294,17 @@ declare class TaskAnalyzer {
1211
1294
  */
1212
1295
  recordRunOutcome(outcome: 'success' | 'failure', costByTier: Record<string, number>, contextTokens?: number): void;
1213
1296
  /**
1214
- * Record an explicit user rating (good/bad) for the last run's selected models.
1297
+ * Record an explicit user rating (good/bad) for the last COMPLETED run's models.
1215
1298
  * Explicit ratings carry 3× the weight of auto-detected outcomes.
1216
- * Does NOT clear lastSelectedModels — the auto record already did that.
1299
+ *
1300
+ * Reads the completed-run snapshot, not the in-flight map: by the time a user
1301
+ * can rate an answer, the finalizer has already run and the in-flight map is
1302
+ * empty.
1303
+ *
1304
+ * The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
1305
+ * three samples, so a double submit would inject six — a stutter on the
1306
+ * button would count as two opinions. Consuming makes a second call a no-op
1307
+ * that reports false, which is what "rate this run" should mean.
1217
1308
  */
1218
1309
  recordExplicitRating(rating: 'good' | 'bad'): boolean;
1219
1310
  private scoreModel;
@@ -2150,9 +2241,49 @@ declare abstract class BaseTier extends EventEmitter {
2150
2241
  * which node (Cockpit node panel / Why panel).
2151
2242
  */
2152
2243
  protected servingModel?: string;
2244
+ /**
2245
+ * This tier's identity IN THE TASK GRAPH — the section or subtask id the
2246
+ * planner assigned — plus where it sat in the dependency order.
2247
+ *
2248
+ * `id` and `graphNodeId` are deliberately different id spaces, and both are
2249
+ * needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
2250
+ * construction); `graphNodeId` identifies the WORK ("s1"), which is what
2251
+ * dependency edges point at. Without the distinction a graph view cannot be
2252
+ * drawn at all: `dependsOn` lists section ids, while every running tier
2253
+ * reports a generated tier id, so every edge would name a node that never
2254
+ * appears in the stream.
2255
+ */
2256
+ protected graphNodeId?: string;
2257
+ protected graphDependsOn?: readonly string[];
2258
+ protected graphWave?: number;
2153
2259
  constructor(role: TierRole, id?: string, parentId?: string);
2154
2260
  /** Mark this tier as the run's presenter (root tier). */
2155
2261
  setPresenter(on?: boolean): void;
2262
+ /**
2263
+ * Record where this tier sits in the compiled task graph.
2264
+ *
2265
+ * Called by whichever tier scheduled this one: the id and edges are known at
2266
+ * construction, the wave only once the scheduler reaches it, so the fields
2267
+ * are set independently rather than all at once.
2268
+ */
2269
+ setGraphPosition(position: {
2270
+ nodeId?: string;
2271
+ dependsOn?: readonly string[];
2272
+ wave?: number;
2273
+ }): void;
2274
+ /**
2275
+ * The graph fields, for spreading into a status payload.
2276
+ *
2277
+ * One helper rather than two literals because `tier:status` is emitted from
2278
+ * two different call sites with two different payload shapes; adding a field
2279
+ * to only one of them makes it arrive intermittently, which is worse for a
2280
+ * consumer than never arriving at all.
2281
+ */
2282
+ protected graphFields(): {
2283
+ nodeId?: string;
2284
+ dependsOn?: string[];
2285
+ waveId?: number;
2286
+ };
2156
2287
  getStatus(): TierStatus;
2157
2288
  protected setStatus(status: TierStatus, output?: string): void;
2158
2289
  /** Record the model serving this tier; future status events carry it. */
@@ -2532,6 +2663,25 @@ declare class Cascade extends EventEmitter {
2532
2663
  private lastInterruptedRun?;
2533
2664
  /** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
2534
2665
  private resumeStore?;
2666
+ /** Claim held by an in-flight resume, settled once the run owns the work. */
2667
+ private claimedResumeId?;
2668
+ /**
2669
+ * Facts inherited from the checkpoint this run is resuming, so its own
2670
+ * checkpoint is cumulative rather than only covering this attempt.
2671
+ */
2672
+ private resumedFrom?;
2673
+ /** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
2674
+ private wroteCheckpointThisRun;
2675
+ /**
2676
+ * Whether the run reached the end of its normal path.
2677
+ *
2678
+ * NOT the same as `runError == null`: the cancellation and budget handlers
2679
+ * both null `runError` deliberately (an intentional stop is not a telemetry
2680
+ * error) and the breaker path never sets it at all. Treating that as success
2681
+ * meant an immediate cancel — no output, no replacement checkpoint — deleted
2682
+ * the original claim and the finished sections with it.
2683
+ */
2684
+ private completedNormally;
2535
2685
  /** Section results of the CURRENT run, so an interruption can checkpoint them. */
2536
2686
  private currentRunCompleted;
2537
2687
  private initPromise?;
@@ -2732,6 +2882,19 @@ declare class Cascade extends EventEmitter {
2732
2882
  maxTokens?: number;
2733
2883
  taskId?: string;
2734
2884
  }): Promise<string | null>;
2885
+ /**
2886
+ * Discard the claimed checkpoint once the resumed run has taken ownership of
2887
+ * the work (it has produced its own checkpoint, or finished). Called by the
2888
+ * run loop; safe to call when nothing is claimed.
2889
+ */
2890
+ private settleClaimedResume;
2891
+ /**
2892
+ * Hand back a claim taken by prepareDurableResume() when the caller never
2893
+ * gets as far as run(). The REPL prepares the continuation and submits it
2894
+ * separately, so a failure in between would otherwise strand the checkpoint
2895
+ * for the whole lease timeout before reclaimStale() could recover it.
2896
+ */
2897
+ releaseClaimedResume(): Promise<void>;
2735
2898
  /** A resume gets a bigger budget — the previous attempt exhausted the old one. */
2736
2899
  private raiseBudgetForResume;
2737
2900
  /**
@@ -3215,17 +3378,67 @@ declare class TaskGraphValidationError extends Error {
3215
3378
  declare function compileTaskGraph<TPayload>(inputNodes: readonly TaskGraphInputNode<TPayload>[], options?: CompileTaskGraphOptions): CompiledTaskGraph<TPayload>;
3216
3379
 
3217
3380
  type SchedulerExecutionMode = 'parallel' | 'sequential';
3381
+ /**
3382
+ * How a finished node turned out.
3383
+ *
3384
+ * The scheduler used to know only "the promise resolved", which is not the same
3385
+ * question. T1 catches worker errors into an ordinary `T2Result { status:
3386
+ * 'FAILED' }` so one dead section cannot crash the run — a resolved value. The
3387
+ * scheduler saw success and released the dependents anyway, so "run the
3388
+ * integration tests" would start after "implement the API" had failed outright,
3389
+ * and then fail too, and bill for the privilege.
3390
+ *
3391
+ * `partial` deliberately does NOT block: partial output is usable, and treating
3392
+ * it as failure would cancel work that a degraded-but-real result can support.
3393
+ */
3394
+ type NodeOutcome = 'succeeded' | 'partial' | 'failed';
3395
+ /** What to do with the dependents of a failed node. */
3396
+ type UpstreamFailurePolicy =
3397
+ /** Run them anyway — the historical behaviour, kept as the default. */
3398
+ 'continue'
3399
+ /** Skip them, transitively, and report why. */
3400
+ | 'block';
3401
+ /** Why a node never ran. */
3402
+ interface BlockedNode {
3403
+ /**
3404
+ * Ids of the failed ancestors responsible, nearest first. Stable and
3405
+ * machine-readable — use these to link a blocked node back to its cause.
3406
+ */
3407
+ blockedBy: string[];
3408
+ /**
3409
+ * The same ancestors by title, in the same order. Node ids are internal
3410
+ * ("s1", "s2"); anything a person reads should use these instead.
3411
+ */
3412
+ blockedByTitles: string[];
3413
+ /** A sentence naming the cause, written with titles rather than ids. */
3414
+ reason: string;
3415
+ }
3218
3416
  interface SchedulerHooks<TPayload, TResult> {
3219
3417
  execute(node: TaskGraphNode<TPayload>): Promise<TResult>;
3220
3418
  onWaveStart?(nodes: readonly TaskGraphNode<TPayload>[], wave: number): void | Promise<void>;
3221
3419
  onNodeComplete?(node: TaskGraphNode<TPayload>, result: TResult): void | Promise<void>;
3420
+ /**
3421
+ * Decide whether a result counts as success. Omit it and every result counts
3422
+ * as `succeeded`, which is exactly the previous behaviour — so adding failure
3423
+ * awareness to a caller is opt-in and cannot silently change an existing one.
3424
+ */
3425
+ classify?(node: TaskGraphNode<TPayload>, result: TResult): NodeOutcome;
3426
+ /**
3427
+ * A node that will never run because an ancestor failed. Callers use this to
3428
+ * synthesize a result for it, so downstream reporting still sees every node.
3429
+ */
3430
+ onNodeBlocked?(node: TaskGraphNode<TPayload>, blocked: BlockedNode): void | Promise<void>;
3222
3431
  }
3223
3432
  interface SchedulerOptions {
3224
3433
  mode?: SchedulerExecutionMode;
3434
+ /** Defaults to 'continue' so existing callers behave exactly as before. */
3435
+ onUpstreamFailure?: UpstreamFailurePolicy;
3225
3436
  }
3226
3437
  interface SchedulerResult<TResult> {
3227
3438
  results: ReadonlyMap<string, TResult>;
3228
3439
  waves: readonly (readonly string[])[];
3440
+ /** Nodes skipped because an ancestor failed. Empty under 'continue'. */
3441
+ blocked: ReadonlyMap<string, BlockedNode>;
3229
3442
  }
3230
3443
  /**
3231
3444
  * Shared deterministic dependency scheduler for T1 sections and T2 subtasks.
@@ -3507,7 +3720,13 @@ declare class DashboardSocket {
3507
3720
  broadcastToRoom(room: string, event: string, data: unknown): void;
3508
3721
  broadcast(event: string, data: unknown): void;
3509
3722
  emitCascadeEvent(ev: CascadeEvent): void;
3510
- emitTierStatus(tierId: string, role: string, status: string, sessionId: string, action?: string): void;
3723
+ /**
3724
+ * `extra` exists because this signature is positional: every field the tier
3725
+ * emits that is not named here is silently dropped on this path, while the
3726
+ * other two forwarders spread the whole event through. That asymmetry is how
3727
+ * a field can reach the web dashboard from one code path and not another.
3728
+ */
3729
+ emitTierStatus(tierId: string, role: string, status: string, sessionId: string, action?: string, extra?: Record<string, unknown>): void;
3511
3730
  emitStreamToken(tierId: string, text: string, sessionId: string): void;
3512
3731
  emitPeerMessage(event: PeerMessageEvent): void;
3513
3732
  emitApprovalRequest(request: PermissionRequest): void;