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/README.md +50 -59
- package/dist/cli.cjs +445 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +445 -61
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +431 -57
- package/dist/index.cjs +431 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -12
- package/dist/index.d.ts +231 -12
- package/dist/index.js +431 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
|
|
1910
1910
|
// src/constants.ts
|
|
1911
|
-
var CASCADE_VERSION = "0.
|
|
1911
|
+
var CASCADE_VERSION = "0.69.0";
|
|
1912
1912
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1913
1913
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1914
1914
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -6070,6 +6070,21 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6070
6070
|
* which node (Cockpit node panel / Why panel).
|
|
6071
6071
|
*/
|
|
6072
6072
|
servingModel;
|
|
6073
|
+
/**
|
|
6074
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
6075
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
6076
|
+
*
|
|
6077
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
6078
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
6079
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
6080
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
6081
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
6082
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
6083
|
+
* appears in the stream.
|
|
6084
|
+
*/
|
|
6085
|
+
graphNodeId;
|
|
6086
|
+
graphDependsOn;
|
|
6087
|
+
graphWave;
|
|
6073
6088
|
constructor(role, id, parentId) {
|
|
6074
6089
|
super();
|
|
6075
6090
|
this.role = role;
|
|
@@ -6081,6 +6096,33 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6081
6096
|
setPresenter(on = true) {
|
|
6082
6097
|
this.isPresenter = on;
|
|
6083
6098
|
}
|
|
6099
|
+
/**
|
|
6100
|
+
* Record where this tier sits in the compiled task graph.
|
|
6101
|
+
*
|
|
6102
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
6103
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
6104
|
+
* are set independently rather than all at once.
|
|
6105
|
+
*/
|
|
6106
|
+
setGraphPosition(position) {
|
|
6107
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
6108
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
6109
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
6110
|
+
}
|
|
6111
|
+
/**
|
|
6112
|
+
* The graph fields, for spreading into a status payload.
|
|
6113
|
+
*
|
|
6114
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
6115
|
+
* two different call sites with two different payload shapes; adding a field
|
|
6116
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
6117
|
+
* consumer than never arriving at all.
|
|
6118
|
+
*/
|
|
6119
|
+
graphFields() {
|
|
6120
|
+
return {
|
|
6121
|
+
nodeId: this.graphNodeId,
|
|
6122
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
6123
|
+
waveId: this.graphWave
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6084
6126
|
getStatus() {
|
|
6085
6127
|
return this.status;
|
|
6086
6128
|
}
|
|
@@ -6095,7 +6137,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6095
6137
|
status,
|
|
6096
6138
|
timestamp,
|
|
6097
6139
|
output,
|
|
6098
|
-
model: this.servingModel
|
|
6140
|
+
model: this.servingModel,
|
|
6141
|
+
...this.graphFields()
|
|
6099
6142
|
};
|
|
6100
6143
|
this.emit("status", event);
|
|
6101
6144
|
this.emit("tier:status", event);
|
|
@@ -6135,7 +6178,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6135
6178
|
progressPct: update.progressPct,
|
|
6136
6179
|
timestamp,
|
|
6137
6180
|
output: update.output,
|
|
6138
|
-
model: this.servingModel
|
|
6181
|
+
model: this.servingModel,
|
|
6182
|
+
...this.graphFields()
|
|
6139
6183
|
});
|
|
6140
6184
|
}
|
|
6141
6185
|
buildMessage(type, to, payload) {
|
|
@@ -7037,7 +7081,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
7037
7081
|
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
7038
7082
|
}
|
|
7039
7083
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
7040
|
-
const
|
|
7084
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
7085
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
7041
7086
|
checksRun.push(...testResult.checksRun);
|
|
7042
7087
|
passed.push(...testResult.passed);
|
|
7043
7088
|
failed.push(...testResult.failed);
|
|
@@ -7046,7 +7091,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
7046
7091
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
7047
7092
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
7048
7093
|
output = corrected;
|
|
7049
|
-
const retest = await this.selfTest(assignment, output);
|
|
7094
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
7050
7095
|
passed.push(...retest.passed);
|
|
7051
7096
|
if (retest.failed.length > 0) {
|
|
7052
7097
|
failed.push(...retest.failed);
|
|
@@ -8970,6 +9015,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8970
9015
|
for (const a of assignments) {
|
|
8971
9016
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
8972
9017
|
if (this.store) worker.setStore(this.store, taskId);
|
|
9018
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
8973
9019
|
worker.setPeerBus(this.t3PeerBus);
|
|
8974
9020
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
8975
9021
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -9090,6 +9136,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9090
9136
|
runnableIds.push(fallbackId);
|
|
9091
9137
|
}
|
|
9092
9138
|
wave++;
|
|
9139
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
9093
9140
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
9094
9141
|
this.sendStatusUpdate({
|
|
9095
9142
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -9417,23 +9464,63 @@ var DependencyScheduler = class {
|
|
|
9417
9464
|
dependents.get(dependencyId)?.push(node.id);
|
|
9418
9465
|
}
|
|
9419
9466
|
}
|
|
9420
|
-
const
|
|
9467
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
9468
|
+
const settled = /* @__PURE__ */ new Set();
|
|
9421
9469
|
const results = /* @__PURE__ */ new Map();
|
|
9470
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
9422
9471
|
const waves = [];
|
|
9423
9472
|
let wave = 0;
|
|
9424
|
-
|
|
9425
|
-
const
|
|
9473
|
+
const blockDescendants = async (failedIds) => {
|
|
9474
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
9475
|
+
const queue = [];
|
|
9476
|
+
for (const rootId of failedIds) {
|
|
9477
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
9478
|
+
}
|
|
9479
|
+
while (queue.length > 0) {
|
|
9480
|
+
const { id, root } = queue.shift();
|
|
9481
|
+
if (settled.has(id)) continue;
|
|
9482
|
+
const known = rootsFor.get(id);
|
|
9483
|
+
if (known) {
|
|
9484
|
+
if (!known.has(root)) known.add(root);
|
|
9485
|
+
continue;
|
|
9486
|
+
}
|
|
9487
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
9488
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
9489
|
+
}
|
|
9490
|
+
for (const node of nodes) {
|
|
9491
|
+
const roots = rootsFor.get(node.id);
|
|
9492
|
+
if (!roots || settled.has(node.id)) continue;
|
|
9493
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
9494
|
+
const blockedBy = causes.map((n) => n.id);
|
|
9495
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
9496
|
+
const record = {
|
|
9497
|
+
blockedBy,
|
|
9498
|
+
blockedByTitles,
|
|
9499
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
9500
|
+
};
|
|
9501
|
+
blocked.set(node.id, record);
|
|
9502
|
+
settled.add(node.id);
|
|
9503
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
9504
|
+
}
|
|
9505
|
+
};
|
|
9506
|
+
while (settled.size < nodes.length) {
|
|
9507
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
9426
9508
|
if (ready.length === 0) {
|
|
9427
9509
|
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
9428
9510
|
}
|
|
9429
9511
|
waves.push(ready.map((node) => node.id));
|
|
9430
9512
|
await this.hooks.onWaveStart?.(ready, wave);
|
|
9513
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
9431
9514
|
const runOne = async (node) => {
|
|
9432
9515
|
const result = await this.hooks.execute(node);
|
|
9433
9516
|
results.set(node.id, result);
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9517
|
+
settled.add(node.id);
|
|
9518
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
9519
|
+
outcomes.set(node.id, outcome);
|
|
9520
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
9521
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
9522
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
9523
|
+
}
|
|
9437
9524
|
}
|
|
9438
9525
|
await this.hooks.onNodeComplete?.(node, result);
|
|
9439
9526
|
};
|
|
@@ -9442,9 +9529,13 @@ var DependencyScheduler = class {
|
|
|
9442
9529
|
} else {
|
|
9443
9530
|
await Promise.all(ready.map(runOne));
|
|
9444
9531
|
}
|
|
9532
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
9533
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
9534
|
+
await blockDescendants(failedThisWave);
|
|
9535
|
+
}
|
|
9445
9536
|
wave++;
|
|
9446
9537
|
}
|
|
9447
|
-
return { results, waves };
|
|
9538
|
+
return { results, waves, blocked };
|
|
9448
9539
|
}
|
|
9449
9540
|
};
|
|
9450
9541
|
|
|
@@ -9545,6 +9636,9 @@ QUALITY RULES:
|
|
|
9545
9636
|
|
|
9546
9637
|
${generation}` : ""}`;
|
|
9547
9638
|
}
|
|
9639
|
+
function producedOutput(result) {
|
|
9640
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
9641
|
+
}
|
|
9548
9642
|
var T1Administrator = class extends BaseTier {
|
|
9549
9643
|
router;
|
|
9550
9644
|
toolRegistry;
|
|
@@ -9679,7 +9773,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
9679
9773
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
9680
9774
|
let pass = 1;
|
|
9681
9775
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
9682
|
-
const okCount = (rs) => rs.filter(
|
|
9776
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
9683
9777
|
while (pass <= maxReplanPasses) {
|
|
9684
9778
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
9685
9779
|
if (reviewResult.approved) {
|
|
@@ -9810,7 +9904,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
9810
9904
|
/** Structured, grounded summary of what's already done — used to keep
|
|
9811
9905
|
* corrective replan passes from re-emitting completed sections. */
|
|
9812
9906
|
summarizeCompletedSections(results) {
|
|
9813
|
-
const done = results.filter(
|
|
9907
|
+
const done = results.filter(producedOutput);
|
|
9814
9908
|
if (done.length === 0) return "";
|
|
9815
9909
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
9816
9910
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -9955,6 +10049,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9955
10049
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
9956
10050
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
9957
10051
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10052
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
9958
10053
|
if (this.store) {
|
|
9959
10054
|
manager.setStore(this.store);
|
|
9960
10055
|
}
|
|
@@ -10081,6 +10176,13 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10081
10176
|
const scheduler = new DependencyScheduler(
|
|
10082
10177
|
compiled.graph,
|
|
10083
10178
|
{
|
|
10179
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
10180
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
10181
|
+
// Stamped before the wave executes so the first status event a node
|
|
10182
|
+
// emits already carries it.
|
|
10183
|
+
onWaveStart: (nodes, wave) => {
|
|
10184
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
10185
|
+
},
|
|
10084
10186
|
execute: async (node) => {
|
|
10085
10187
|
const section = node.payload;
|
|
10086
10188
|
const manager = managers[node.ordinal];
|
|
@@ -10115,6 +10217,62 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10115
10217
|
};
|
|
10116
10218
|
}
|
|
10117
10219
|
},
|
|
10220
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
10221
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
10222
|
+
// the user more than letting it try.
|
|
10223
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
10224
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
10225
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
10226
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
10227
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
10228
|
+
// should have been skipped would run and be billed.
|
|
10229
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
10230
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
10231
|
+
const section = node.payload;
|
|
10232
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
10233
|
+
resultMap.set(node.id, {
|
|
10234
|
+
sectionId: section.sectionId,
|
|
10235
|
+
sectionTitle: section.sectionTitle,
|
|
10236
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
10237
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
10238
|
+
// that did (the upstream section named below).
|
|
10239
|
+
status: "BLOCKED",
|
|
10240
|
+
t3Results: [],
|
|
10241
|
+
sectionSummary: "",
|
|
10242
|
+
issues: [
|
|
10243
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
10244
|
+
],
|
|
10245
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
10246
|
+
});
|
|
10247
|
+
completedSections++;
|
|
10248
|
+
this.sendStatusUpdate({
|
|
10249
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
10250
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
10251
|
+
status: "IN_PROGRESS"
|
|
10252
|
+
});
|
|
10253
|
+
this.emit("tier:status", {
|
|
10254
|
+
tierId: section.sectionId,
|
|
10255
|
+
// A blocked section has no tier instance — it was never
|
|
10256
|
+
// constructed — so its runtime id and its graph id are the same
|
|
10257
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
10258
|
+
// stream addressable in the graph's id space, which is what
|
|
10259
|
+
// dependsOn edges point at.
|
|
10260
|
+
nodeId: section.sectionId,
|
|
10261
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
10262
|
+
role: "T2",
|
|
10263
|
+
label: section.sectionTitle,
|
|
10264
|
+
status: "BLOCKED",
|
|
10265
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10266
|
+
output: blockedInfo.reason
|
|
10267
|
+
});
|
|
10268
|
+
this.emit("section:complete", {
|
|
10269
|
+
id: node.id,
|
|
10270
|
+
title: section.sectionTitle,
|
|
10271
|
+
status: "BLOCKED",
|
|
10272
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10273
|
+
output: ""
|
|
10274
|
+
});
|
|
10275
|
+
},
|
|
10118
10276
|
onNodeComplete: (node, result) => {
|
|
10119
10277
|
resultMap.set(node.id, result);
|
|
10120
10278
|
completedSections++;
|
|
@@ -10131,14 +10289,18 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10131
10289
|
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
10132
10290
|
// serialized T3 workers within a single section while every section's
|
|
10133
10291
|
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
10134
|
-
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
10292
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
10293
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
10294
|
+
// output. Running it after the dependency failed produces a second
|
|
10295
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
10296
|
+
onUpstreamFailure: "block"
|
|
10135
10297
|
}
|
|
10136
10298
|
);
|
|
10137
10299
|
await scheduler.run();
|
|
10138
10300
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
10139
10301
|
}
|
|
10140
10302
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
10141
|
-
const completedSections = t2Results.filter(
|
|
10303
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
10142
10304
|
if (!completedSections.length) {
|
|
10143
10305
|
const allIssues = [
|
|
10144
10306
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -10158,7 +10320,6 @@ ${r.sectionSummary}
|
|
|
10158
10320
|
Outputs:
|
|
10159
10321
|
${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
|
|
10160
10322
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
10161
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
10162
10323
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
10163
10324
|
|
|
10164
10325
|
Original request: ${originalPrompt}
|
|
@@ -10650,6 +10811,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
10650
10811
|
};
|
|
10651
10812
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
10652
10813
|
var MAX_VIDEO_SECONDS = 8;
|
|
10814
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
10653
10815
|
var GenerateVideoTool = class extends BaseTool {
|
|
10654
10816
|
constructor(deps) {
|
|
10655
10817
|
super();
|
|
@@ -10666,7 +10828,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10666
10828
|
type: "object",
|
|
10667
10829
|
properties: {
|
|
10668
10830
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
10669
|
-
seconds: { type: "number", description: `Clip length,
|
|
10831
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
10670
10832
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
10671
10833
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
10672
10834
|
},
|
|
@@ -10678,7 +10840,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10678
10840
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
10679
10841
|
if (!r.ok) return r.error;
|
|
10680
10842
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
10681
|
-
const seconds = Math.max(
|
|
10843
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
10682
10844
|
const asset = await generateVideo(
|
|
10683
10845
|
r.selected.capability,
|
|
10684
10846
|
r.cfg,
|
|
@@ -10695,7 +10857,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10695
10857
|
const location = await this.deps.sink(asset);
|
|
10696
10858
|
const unit = r.selected.cost;
|
|
10697
10859
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
10698
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
10860
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
10699
10861
|
return `Video generated and saved to ${location}.
|
|
10700
10862
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
10701
10863
|
}
|
|
@@ -14449,7 +14611,24 @@ var TaskAnalyzer = class {
|
|
|
14449
14611
|
feedback;
|
|
14450
14612
|
bias;
|
|
14451
14613
|
lastProfile = null;
|
|
14452
|
-
|
|
14614
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
14615
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
14616
|
+
/**
|
|
14617
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
14618
|
+
* type they were chosen for.
|
|
14619
|
+
*
|
|
14620
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
14621
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
14622
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
14623
|
+
* model is good at creative writing, from a rating that never said so.
|
|
14624
|
+
*
|
|
14625
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
14626
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
14627
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
14628
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
14629
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
14630
|
+
*/
|
|
14631
|
+
lastCompletedRun;
|
|
14453
14632
|
constructor(tracker, bias = "balanced") {
|
|
14454
14633
|
this.tracker = tracker;
|
|
14455
14634
|
this.bias = bias;
|
|
@@ -14496,11 +14675,15 @@ var TaskAnalyzer = class {
|
|
|
14496
14675
|
*/
|
|
14497
14676
|
async selectModel(prompt, tier, selector, opts) {
|
|
14498
14677
|
const profile = await this.analyze(prompt);
|
|
14678
|
+
const recordSelection = (model) => {
|
|
14679
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
14680
|
+
return model;
|
|
14681
|
+
};
|
|
14499
14682
|
if (profile.requiresVision) {
|
|
14500
|
-
return selector.selectVisionModel();
|
|
14683
|
+
return recordSelection(selector.selectVisionModel());
|
|
14501
14684
|
}
|
|
14502
14685
|
let candidates = selector.getCandidatesForTier(tier);
|
|
14503
|
-
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
14686
|
+
if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
14504
14687
|
if (opts?.requiresToolUse) {
|
|
14505
14688
|
const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
|
|
14506
14689
|
if (toolCapable.length > 0) candidates = toolCapable;
|
|
@@ -14510,9 +14693,7 @@ var TaskAnalyzer = class {
|
|
|
14510
14693
|
score: this.scoreModel(m, profile)
|
|
14511
14694
|
}));
|
|
14512
14695
|
scored.sort((a, b) => b.score - a.score);
|
|
14513
|
-
|
|
14514
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
14515
|
-
return best;
|
|
14696
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
14516
14697
|
}
|
|
14517
14698
|
/**
|
|
14518
14699
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -14521,25 +14702,38 @@ var TaskAnalyzer = class {
|
|
|
14521
14702
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
14522
14703
|
if (!this.tracker || !this.lastProfile) return;
|
|
14523
14704
|
const taskType = this.lastProfile.type;
|
|
14524
|
-
for (const [tier, model] of this.
|
|
14705
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
14525
14706
|
const cost = costByTier[tier] ?? 0;
|
|
14526
14707
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
14527
14708
|
}
|
|
14528
|
-
this.
|
|
14709
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
14710
|
+
this.currentRunSelections.clear();
|
|
14529
14711
|
void this.tracker.save();
|
|
14530
14712
|
}
|
|
14531
14713
|
/**
|
|
14532
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
14714
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
14533
14715
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
14534
|
-
*
|
|
14716
|
+
*
|
|
14717
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
14718
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
14719
|
+
* empty.
|
|
14720
|
+
*
|
|
14721
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
14722
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
14723
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
14724
|
+
* that reports false, which is what "rate this run" should mean.
|
|
14535
14725
|
*/
|
|
14536
14726
|
recordExplicitRating(rating) {
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14727
|
+
const snapshot = this.lastCompletedRun;
|
|
14728
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
14729
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
14730
|
+
for (const [, model] of snapshot.selections) {
|
|
14731
|
+
if (ratedModels.has(model.id)) continue;
|
|
14732
|
+
ratedModels.add(model.id);
|
|
14733
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
14734
|
+
}
|
|
14735
|
+
this.lastCompletedRun = void 0;
|
|
14736
|
+
return true;
|
|
14543
14737
|
}
|
|
14544
14738
|
scoreModel(model, profile) {
|
|
14545
14739
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -15577,6 +15771,8 @@ var RESUME_SCHEMA_VERSION = 1;
|
|
|
15577
15771
|
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
15578
15772
|
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15579
15773
|
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
15774
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
15775
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15580
15776
|
function safeId(taskId) {
|
|
15581
15777
|
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
15582
15778
|
return cleaned.slice(0, 100) || "run";
|
|
@@ -15591,16 +15787,24 @@ var ResumeStore = class {
|
|
|
15591
15787
|
maxCheckpoints;
|
|
15592
15788
|
maxAgeMs;
|
|
15593
15789
|
now;
|
|
15790
|
+
claimTimeoutMs;
|
|
15594
15791
|
constructor(options) {
|
|
15595
15792
|
this.dir = options.dir;
|
|
15596
15793
|
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
15597
15794
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
15598
15795
|
this.now = options.now ?? Date.now;
|
|
15796
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
15599
15797
|
}
|
|
15600
15798
|
/**
|
|
15601
|
-
* Persist a checkpoint and prune.
|
|
15602
|
-
*
|
|
15603
|
-
*
|
|
15799
|
+
* Persist a checkpoint and prune.
|
|
15800
|
+
*
|
|
15801
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
15802
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
15803
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
15804
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
15805
|
+
* look alike to them.
|
|
15806
|
+
*
|
|
15807
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
15604
15808
|
*/
|
|
15605
15809
|
async save(checkpoint) {
|
|
15606
15810
|
try {
|
|
@@ -15616,7 +15820,9 @@ var ResumeStore = class {
|
|
|
15616
15820
|
await fs25.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
15617
15821
|
await fs25.promises.rename(tempPath, finalPath);
|
|
15618
15822
|
await this.prune();
|
|
15823
|
+
return true;
|
|
15619
15824
|
} catch {
|
|
15825
|
+
return false;
|
|
15620
15826
|
}
|
|
15621
15827
|
}
|
|
15622
15828
|
/** Valid, unexpired checkpoints, newest first. */
|
|
@@ -15646,15 +15852,87 @@ var ResumeStore = class {
|
|
|
15646
15852
|
return (await this.list())[0] ?? null;
|
|
15647
15853
|
}
|
|
15648
15854
|
/**
|
|
15649
|
-
*
|
|
15650
|
-
*
|
|
15855
|
+
* Claim a checkpoint for a resume attempt.
|
|
15856
|
+
*
|
|
15857
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
15858
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
15859
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
15860
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
15861
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
15862
|
+
* wrong trade for a recovery mechanism.
|
|
15863
|
+
*
|
|
15864
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
15865
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
15866
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
15867
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
15868
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
15651
15869
|
*/
|
|
15652
|
-
async
|
|
15870
|
+
async claim(taskId) {
|
|
15653
15871
|
const all = await this.list();
|
|
15654
15872
|
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
15655
15873
|
if (!target) return null;
|
|
15656
|
-
|
|
15657
|
-
|
|
15874
|
+
try {
|
|
15875
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15876
|
+
const wanted = safeId(target.taskId);
|
|
15877
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
15878
|
+
if (!source) return null;
|
|
15879
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
15880
|
+
await fs25.promises.rename(path26__default.default.join(this.dir, source), path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
15881
|
+
return { checkpoint: target, claimId };
|
|
15882
|
+
} catch {
|
|
15883
|
+
return null;
|
|
15884
|
+
}
|
|
15885
|
+
}
|
|
15886
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
15887
|
+
async release(claimId) {
|
|
15888
|
+
try {
|
|
15889
|
+
await fs25.promises.rename(
|
|
15890
|
+
path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
15891
|
+
path26__default.default.join(this.dir, `${claimId}.json`)
|
|
15892
|
+
);
|
|
15893
|
+
} catch {
|
|
15894
|
+
}
|
|
15895
|
+
}
|
|
15896
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
15897
|
+
async settle(claimId) {
|
|
15898
|
+
try {
|
|
15899
|
+
await fs25.promises.rm(path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
15900
|
+
} catch {
|
|
15901
|
+
}
|
|
15902
|
+
}
|
|
15903
|
+
/**
|
|
15904
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
15905
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
15906
|
+
*/
|
|
15907
|
+
async consume(taskId) {
|
|
15908
|
+
const claimed = await this.claim(taskId);
|
|
15909
|
+
if (!claimed) return null;
|
|
15910
|
+
await this.settle(claimed.claimId);
|
|
15911
|
+
return claimed.checkpoint;
|
|
15912
|
+
}
|
|
15913
|
+
/**
|
|
15914
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
15915
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
15916
|
+
* trading one loss window for another.
|
|
15917
|
+
*/
|
|
15918
|
+
async reclaimStale() {
|
|
15919
|
+
let reclaimed = 0;
|
|
15920
|
+
try {
|
|
15921
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15922
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
15923
|
+
for (const name of entries) {
|
|
15924
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
15925
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
15926
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
15927
|
+
await fs25.promises.rename(
|
|
15928
|
+
path26__default.default.join(this.dir, name),
|
|
15929
|
+
path26__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
15930
|
+
);
|
|
15931
|
+
reclaimed++;
|
|
15932
|
+
}
|
|
15933
|
+
} catch {
|
|
15934
|
+
}
|
|
15935
|
+
return reclaimed;
|
|
15658
15936
|
}
|
|
15659
15937
|
/** Remove every checkpoint for a task id. */
|
|
15660
15938
|
async delete(taskId) {
|
|
@@ -16710,6 +16988,25 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16710
16988
|
lastInterruptedRun;
|
|
16711
16989
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
16712
16990
|
resumeStore;
|
|
16991
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
16992
|
+
claimedResumeId;
|
|
16993
|
+
/**
|
|
16994
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
16995
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
16996
|
+
*/
|
|
16997
|
+
resumedFrom;
|
|
16998
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
16999
|
+
wroteCheckpointThisRun = false;
|
|
17000
|
+
/**
|
|
17001
|
+
* Whether the run reached the end of its normal path.
|
|
17002
|
+
*
|
|
17003
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
17004
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
17005
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
17006
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
17007
|
+
* the original claim and the finished sections with it.
|
|
17008
|
+
*/
|
|
17009
|
+
completedNormally = false;
|
|
16713
17010
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
16714
17011
|
currentRunCompleted = [];
|
|
16715
17012
|
initPromise;
|
|
@@ -17227,17 +17524,31 @@ ${input}` }],
|
|
|
17227
17524
|
* worse. Nothing here is allowed to throw.
|
|
17228
17525
|
*/
|
|
17229
17526
|
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
17230
|
-
|
|
17527
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
17528
|
+
const merged = /* @__PURE__ */ new Map();
|
|
17529
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
17530
|
+
for (const node of this.currentRunCompleted) {
|
|
17531
|
+
const existing = merged.get(node.id);
|
|
17532
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
17533
|
+
merged.set(node.id, node);
|
|
17534
|
+
}
|
|
17535
|
+
const completed = [...merged.values()];
|
|
17536
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
17537
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
17538
|
+
if (!completed.length && !carriedPartial) return false;
|
|
17231
17539
|
try {
|
|
17232
|
-
await this.resumeStore?.save({
|
|
17540
|
+
const saved = await this.resumeStore?.save({
|
|
17233
17541
|
taskId,
|
|
17234
|
-
prompt,
|
|
17542
|
+
prompt: canonicalPrompt,
|
|
17235
17543
|
reason,
|
|
17236
17544
|
...detail ? { detail } : {},
|
|
17237
|
-
partialOutput,
|
|
17238
|
-
completed
|
|
17545
|
+
partialOutput: carriedPartial,
|
|
17546
|
+
completed
|
|
17239
17547
|
});
|
|
17548
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
17549
|
+
return saved === true;
|
|
17240
17550
|
} catch {
|
|
17551
|
+
return false;
|
|
17241
17552
|
}
|
|
17242
17553
|
}
|
|
17243
17554
|
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
@@ -17275,11 +17586,55 @@ ${input}` }],
|
|
|
17275
17586
|
* checkpoint, so the same finished work is never restored into two runs.
|
|
17276
17587
|
*/
|
|
17277
17588
|
async prepareDurableResume(opts = {}) {
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17589
|
+
await this.resumeStore?.reclaimStale();
|
|
17590
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
17591
|
+
if (!claimed) return null;
|
|
17592
|
+
try {
|
|
17593
|
+
this.lastInterruptedRun = void 0;
|
|
17594
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17595
|
+
const prompt = this.buildResumePrompt(
|
|
17596
|
+
claimed.checkpoint.prompt,
|
|
17597
|
+
claimed.checkpoint.partialOutput,
|
|
17598
|
+
claimed.checkpoint.completed
|
|
17599
|
+
);
|
|
17600
|
+
this.resumedFrom = {
|
|
17601
|
+
prompt: claimed.checkpoint.prompt,
|
|
17602
|
+
completed: claimed.checkpoint.completed,
|
|
17603
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
17604
|
+
};
|
|
17605
|
+
this.claimedResumeId = claimed.claimId;
|
|
17606
|
+
return prompt;
|
|
17607
|
+
} catch (err) {
|
|
17608
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
17609
|
+
throw err;
|
|
17610
|
+
}
|
|
17611
|
+
}
|
|
17612
|
+
/**
|
|
17613
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
17614
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
17615
|
+
* run loop; safe to call when nothing is claimed.
|
|
17616
|
+
*/
|
|
17617
|
+
async settleClaimedResume(succeeded) {
|
|
17618
|
+
const claimId = this.claimedResumeId;
|
|
17619
|
+
if (!claimId) return;
|
|
17620
|
+
this.claimedResumeId = void 0;
|
|
17621
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
17622
|
+
await this.resumeStore?.settle(claimId);
|
|
17623
|
+
} else {
|
|
17624
|
+
await this.resumeStore?.release(claimId);
|
|
17625
|
+
}
|
|
17626
|
+
}
|
|
17627
|
+
/**
|
|
17628
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
17629
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
17630
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
17631
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
17632
|
+
*/
|
|
17633
|
+
async releaseClaimedResume() {
|
|
17634
|
+
const claimId = this.claimedResumeId;
|
|
17635
|
+
if (!claimId) return;
|
|
17636
|
+
this.claimedResumeId = void 0;
|
|
17637
|
+
await this.resumeStore?.release(claimId);
|
|
17283
17638
|
}
|
|
17284
17639
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
17285
17640
|
raiseBudgetForResume(maxTokens) {
|
|
@@ -17315,7 +17670,12 @@ ${partialOutput}` : ""
|
|
|
17315
17670
|
async resumeRun(opts = {}) {
|
|
17316
17671
|
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
17317
17672
|
if (!prompt) return null;
|
|
17318
|
-
|
|
17673
|
+
try {
|
|
17674
|
+
return await this.run({ prompt });
|
|
17675
|
+
} catch (err) {
|
|
17676
|
+
await this.releaseClaimedResume();
|
|
17677
|
+
throw err;
|
|
17678
|
+
}
|
|
17319
17679
|
}
|
|
17320
17680
|
getWorkspacePath() {
|
|
17321
17681
|
return this.workspacePath;
|
|
@@ -17685,6 +18045,9 @@ ${prompt}` : prompt;
|
|
|
17685
18045
|
const taskId = crypto4.randomUUID();
|
|
17686
18046
|
this.decisionLog = [];
|
|
17687
18047
|
this.currentRunCompleted = [];
|
|
18048
|
+
this.wroteCheckpointThisRun = false;
|
|
18049
|
+
this.completedNormally = false;
|
|
18050
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
17688
18051
|
if (options.fastAnswer) {
|
|
17689
18052
|
return this.runFastAnswer(options, startMs, taskId);
|
|
17690
18053
|
}
|
|
@@ -17963,6 +18326,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17963
18326
|
${finalOutput}` : ""
|
|
17964
18327
|
].join("\n").trimEnd();
|
|
17965
18328
|
}
|
|
18329
|
+
this.completedNormally = trip == null;
|
|
17966
18330
|
} catch (err) {
|
|
17967
18331
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
17968
18332
|
this.emit("run:cancelled", {
|
|
@@ -18004,6 +18368,10 @@ ${finalOutput}` : ""
|
|
|
18004
18368
|
escalator.cancelAllPending();
|
|
18005
18369
|
} catch {
|
|
18006
18370
|
}
|
|
18371
|
+
try {
|
|
18372
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
18373
|
+
} catch {
|
|
18374
|
+
}
|
|
18007
18375
|
try {
|
|
18008
18376
|
this.releasePendingEscalations();
|
|
18009
18377
|
} catch {
|
|
@@ -19632,8 +20000,14 @@ var DashboardSocket = class {
|
|
|
19632
20000
|
emitCascadeEvent(ev) {
|
|
19633
20001
|
this.io.emit("cascade:event", ev);
|
|
19634
20002
|
}
|
|
19635
|
-
|
|
19636
|
-
|
|
20003
|
+
/**
|
|
20004
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
20005
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
20006
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
20007
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
20008
|
+
*/
|
|
20009
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
20010
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
19637
20011
|
this.io.emit("tier:status", payload);
|
|
19638
20012
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
19639
20013
|
}
|