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.js
CHANGED
|
@@ -1862,7 +1862,7 @@ function reconcilePrice(model, live) {
|
|
|
1862
1862
|
}
|
|
1863
1863
|
|
|
1864
1864
|
// src/constants.ts
|
|
1865
|
-
var CASCADE_VERSION = "0.
|
|
1865
|
+
var CASCADE_VERSION = "0.69.0";
|
|
1866
1866
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1867
1867
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1868
1868
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -6024,6 +6024,21 @@ var BaseTier = class extends EventEmitter {
|
|
|
6024
6024
|
* which node (Cockpit node panel / Why panel).
|
|
6025
6025
|
*/
|
|
6026
6026
|
servingModel;
|
|
6027
|
+
/**
|
|
6028
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
6029
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
6030
|
+
*
|
|
6031
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
6032
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
6033
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
6034
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
6035
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
6036
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
6037
|
+
* appears in the stream.
|
|
6038
|
+
*/
|
|
6039
|
+
graphNodeId;
|
|
6040
|
+
graphDependsOn;
|
|
6041
|
+
graphWave;
|
|
6027
6042
|
constructor(role, id, parentId) {
|
|
6028
6043
|
super();
|
|
6029
6044
|
this.role = role;
|
|
@@ -6035,6 +6050,33 @@ var BaseTier = class extends EventEmitter {
|
|
|
6035
6050
|
setPresenter(on = true) {
|
|
6036
6051
|
this.isPresenter = on;
|
|
6037
6052
|
}
|
|
6053
|
+
/**
|
|
6054
|
+
* Record where this tier sits in the compiled task graph.
|
|
6055
|
+
*
|
|
6056
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
6057
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
6058
|
+
* are set independently rather than all at once.
|
|
6059
|
+
*/
|
|
6060
|
+
setGraphPosition(position) {
|
|
6061
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
6062
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
6063
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
6064
|
+
}
|
|
6065
|
+
/**
|
|
6066
|
+
* The graph fields, for spreading into a status payload.
|
|
6067
|
+
*
|
|
6068
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
6069
|
+
* two different call sites with two different payload shapes; adding a field
|
|
6070
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
6071
|
+
* consumer than never arriving at all.
|
|
6072
|
+
*/
|
|
6073
|
+
graphFields() {
|
|
6074
|
+
return {
|
|
6075
|
+
nodeId: this.graphNodeId,
|
|
6076
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
6077
|
+
waveId: this.graphWave
|
|
6078
|
+
};
|
|
6079
|
+
}
|
|
6038
6080
|
getStatus() {
|
|
6039
6081
|
return this.status;
|
|
6040
6082
|
}
|
|
@@ -6049,7 +6091,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
6049
6091
|
status,
|
|
6050
6092
|
timestamp,
|
|
6051
6093
|
output,
|
|
6052
|
-
model: this.servingModel
|
|
6094
|
+
model: this.servingModel,
|
|
6095
|
+
...this.graphFields()
|
|
6053
6096
|
};
|
|
6054
6097
|
this.emit("status", event);
|
|
6055
6098
|
this.emit("tier:status", event);
|
|
@@ -6089,7 +6132,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
6089
6132
|
progressPct: update.progressPct,
|
|
6090
6133
|
timestamp,
|
|
6091
6134
|
output: update.output,
|
|
6092
|
-
model: this.servingModel
|
|
6135
|
+
model: this.servingModel,
|
|
6136
|
+
...this.graphFields()
|
|
6093
6137
|
});
|
|
6094
6138
|
}
|
|
6095
6139
|
buildMessage(type, to, payload) {
|
|
@@ -6991,7 +7035,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
6991
7035
|
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
6992
7036
|
}
|
|
6993
7037
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
6994
|
-
const
|
|
7038
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
7039
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
6995
7040
|
checksRun.push(...testResult.checksRun);
|
|
6996
7041
|
passed.push(...testResult.passed);
|
|
6997
7042
|
failed.push(...testResult.failed);
|
|
@@ -7000,7 +7045,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
7000
7045
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
7001
7046
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
7002
7047
|
output = corrected;
|
|
7003
|
-
const retest = await this.selfTest(assignment, output);
|
|
7048
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
7004
7049
|
passed.push(...retest.passed);
|
|
7005
7050
|
if (retest.failed.length > 0) {
|
|
7006
7051
|
failed.push(...retest.failed);
|
|
@@ -8924,6 +8969,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8924
8969
|
for (const a of assignments) {
|
|
8925
8970
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
8926
8971
|
if (this.store) worker.setStore(this.store, taskId);
|
|
8972
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
8927
8973
|
worker.setPeerBus(this.t3PeerBus);
|
|
8928
8974
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
8929
8975
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -9044,6 +9090,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9044
9090
|
runnableIds.push(fallbackId);
|
|
9045
9091
|
}
|
|
9046
9092
|
wave++;
|
|
9093
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
9047
9094
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
9048
9095
|
this.sendStatusUpdate({
|
|
9049
9096
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -9371,23 +9418,63 @@ var DependencyScheduler = class {
|
|
|
9371
9418
|
dependents.get(dependencyId)?.push(node.id);
|
|
9372
9419
|
}
|
|
9373
9420
|
}
|
|
9374
|
-
const
|
|
9421
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
9422
|
+
const settled = /* @__PURE__ */ new Set();
|
|
9375
9423
|
const results = /* @__PURE__ */ new Map();
|
|
9424
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
9376
9425
|
const waves = [];
|
|
9377
9426
|
let wave = 0;
|
|
9378
|
-
|
|
9379
|
-
const
|
|
9427
|
+
const blockDescendants = async (failedIds) => {
|
|
9428
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
9429
|
+
const queue = [];
|
|
9430
|
+
for (const rootId of failedIds) {
|
|
9431
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
9432
|
+
}
|
|
9433
|
+
while (queue.length > 0) {
|
|
9434
|
+
const { id, root } = queue.shift();
|
|
9435
|
+
if (settled.has(id)) continue;
|
|
9436
|
+
const known = rootsFor.get(id);
|
|
9437
|
+
if (known) {
|
|
9438
|
+
if (!known.has(root)) known.add(root);
|
|
9439
|
+
continue;
|
|
9440
|
+
}
|
|
9441
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
9442
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
9443
|
+
}
|
|
9444
|
+
for (const node of nodes) {
|
|
9445
|
+
const roots = rootsFor.get(node.id);
|
|
9446
|
+
if (!roots || settled.has(node.id)) continue;
|
|
9447
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
9448
|
+
const blockedBy = causes.map((n) => n.id);
|
|
9449
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
9450
|
+
const record = {
|
|
9451
|
+
blockedBy,
|
|
9452
|
+
blockedByTitles,
|
|
9453
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
9454
|
+
};
|
|
9455
|
+
blocked.set(node.id, record);
|
|
9456
|
+
settled.add(node.id);
|
|
9457
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
while (settled.size < nodes.length) {
|
|
9461
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
9380
9462
|
if (ready.length === 0) {
|
|
9381
9463
|
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
9382
9464
|
}
|
|
9383
9465
|
waves.push(ready.map((node) => node.id));
|
|
9384
9466
|
await this.hooks.onWaveStart?.(ready, wave);
|
|
9467
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
9385
9468
|
const runOne = async (node) => {
|
|
9386
9469
|
const result = await this.hooks.execute(node);
|
|
9387
9470
|
results.set(node.id, result);
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9471
|
+
settled.add(node.id);
|
|
9472
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
9473
|
+
outcomes.set(node.id, outcome);
|
|
9474
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
9475
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
9476
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
9477
|
+
}
|
|
9391
9478
|
}
|
|
9392
9479
|
await this.hooks.onNodeComplete?.(node, result);
|
|
9393
9480
|
};
|
|
@@ -9396,9 +9483,13 @@ var DependencyScheduler = class {
|
|
|
9396
9483
|
} else {
|
|
9397
9484
|
await Promise.all(ready.map(runOne));
|
|
9398
9485
|
}
|
|
9486
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
9487
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
9488
|
+
await blockDescendants(failedThisWave);
|
|
9489
|
+
}
|
|
9399
9490
|
wave++;
|
|
9400
9491
|
}
|
|
9401
|
-
return { results, waves };
|
|
9492
|
+
return { results, waves, blocked };
|
|
9402
9493
|
}
|
|
9403
9494
|
};
|
|
9404
9495
|
|
|
@@ -9499,6 +9590,9 @@ QUALITY RULES:
|
|
|
9499
9590
|
|
|
9500
9591
|
${generation}` : ""}`;
|
|
9501
9592
|
}
|
|
9593
|
+
function producedOutput(result) {
|
|
9594
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
9595
|
+
}
|
|
9502
9596
|
var T1Administrator = class extends BaseTier {
|
|
9503
9597
|
router;
|
|
9504
9598
|
toolRegistry;
|
|
@@ -9633,7 +9727,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
9633
9727
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
9634
9728
|
let pass = 1;
|
|
9635
9729
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
9636
|
-
const okCount = (rs) => rs.filter(
|
|
9730
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
9637
9731
|
while (pass <= maxReplanPasses) {
|
|
9638
9732
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
9639
9733
|
if (reviewResult.approved) {
|
|
@@ -9764,7 +9858,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
9764
9858
|
/** Structured, grounded summary of what's already done — used to keep
|
|
9765
9859
|
* corrective replan passes from re-emitting completed sections. */
|
|
9766
9860
|
summarizeCompletedSections(results) {
|
|
9767
|
-
const done = results.filter(
|
|
9861
|
+
const done = results.filter(producedOutput);
|
|
9768
9862
|
if (done.length === 0) return "";
|
|
9769
9863
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
9770
9864
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -9909,6 +10003,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9909
10003
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
9910
10004
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
9911
10005
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10006
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
9912
10007
|
if (this.store) {
|
|
9913
10008
|
manager.setStore(this.store);
|
|
9914
10009
|
}
|
|
@@ -10035,6 +10130,13 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10035
10130
|
const scheduler = new DependencyScheduler(
|
|
10036
10131
|
compiled.graph,
|
|
10037
10132
|
{
|
|
10133
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
10134
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
10135
|
+
// Stamped before the wave executes so the first status event a node
|
|
10136
|
+
// emits already carries it.
|
|
10137
|
+
onWaveStart: (nodes, wave) => {
|
|
10138
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
10139
|
+
},
|
|
10038
10140
|
execute: async (node) => {
|
|
10039
10141
|
const section = node.payload;
|
|
10040
10142
|
const manager = managers[node.ordinal];
|
|
@@ -10069,6 +10171,62 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10069
10171
|
};
|
|
10070
10172
|
}
|
|
10071
10173
|
},
|
|
10174
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
10175
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
10176
|
+
// the user more than letting it try.
|
|
10177
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
10178
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
10179
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
10180
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
10181
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
10182
|
+
// should have been skipped would run and be billed.
|
|
10183
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
10184
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
10185
|
+
const section = node.payload;
|
|
10186
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
10187
|
+
resultMap.set(node.id, {
|
|
10188
|
+
sectionId: section.sectionId,
|
|
10189
|
+
sectionTitle: section.sectionTitle,
|
|
10190
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
10191
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
10192
|
+
// that did (the upstream section named below).
|
|
10193
|
+
status: "BLOCKED",
|
|
10194
|
+
t3Results: [],
|
|
10195
|
+
sectionSummary: "",
|
|
10196
|
+
issues: [
|
|
10197
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
10198
|
+
],
|
|
10199
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
10200
|
+
});
|
|
10201
|
+
completedSections++;
|
|
10202
|
+
this.sendStatusUpdate({
|
|
10203
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
10204
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
10205
|
+
status: "IN_PROGRESS"
|
|
10206
|
+
});
|
|
10207
|
+
this.emit("tier:status", {
|
|
10208
|
+
tierId: section.sectionId,
|
|
10209
|
+
// A blocked section has no tier instance — it was never
|
|
10210
|
+
// constructed — so its runtime id and its graph id are the same
|
|
10211
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
10212
|
+
// stream addressable in the graph's id space, which is what
|
|
10213
|
+
// dependsOn edges point at.
|
|
10214
|
+
nodeId: section.sectionId,
|
|
10215
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
10216
|
+
role: "T2",
|
|
10217
|
+
label: section.sectionTitle,
|
|
10218
|
+
status: "BLOCKED",
|
|
10219
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10220
|
+
output: blockedInfo.reason
|
|
10221
|
+
});
|
|
10222
|
+
this.emit("section:complete", {
|
|
10223
|
+
id: node.id,
|
|
10224
|
+
title: section.sectionTitle,
|
|
10225
|
+
status: "BLOCKED",
|
|
10226
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10227
|
+
output: ""
|
|
10228
|
+
});
|
|
10229
|
+
},
|
|
10072
10230
|
onNodeComplete: (node, result) => {
|
|
10073
10231
|
resultMap.set(node.id, result);
|
|
10074
10232
|
completedSections++;
|
|
@@ -10085,14 +10243,18 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
10085
10243
|
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
10086
10244
|
// serialized T3 workers within a single section while every section's
|
|
10087
10245
|
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
10088
|
-
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
10246
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
10247
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
10248
|
+
// output. Running it after the dependency failed produces a second
|
|
10249
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
10250
|
+
onUpstreamFailure: "block"
|
|
10089
10251
|
}
|
|
10090
10252
|
);
|
|
10091
10253
|
await scheduler.run();
|
|
10092
10254
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
10093
10255
|
}
|
|
10094
10256
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
10095
|
-
const completedSections = t2Results.filter(
|
|
10257
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
10096
10258
|
if (!completedSections.length) {
|
|
10097
10259
|
const allIssues = [
|
|
10098
10260
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -10112,7 +10274,6 @@ ${r.sectionSummary}
|
|
|
10112
10274
|
Outputs:
|
|
10113
10275
|
${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");
|
|
10114
10276
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
10115
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
10116
10277
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
10117
10278
|
|
|
10118
10279
|
Original request: ${originalPrompt}
|
|
@@ -10604,6 +10765,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
10604
10765
|
};
|
|
10605
10766
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
10606
10767
|
var MAX_VIDEO_SECONDS = 8;
|
|
10768
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
10607
10769
|
var GenerateVideoTool = class extends BaseTool {
|
|
10608
10770
|
constructor(deps) {
|
|
10609
10771
|
super();
|
|
@@ -10620,7 +10782,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10620
10782
|
type: "object",
|
|
10621
10783
|
properties: {
|
|
10622
10784
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
10623
|
-
seconds: { type: "number", description: `Clip length,
|
|
10785
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
10624
10786
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
10625
10787
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
10626
10788
|
},
|
|
@@ -10632,7 +10794,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10632
10794
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
10633
10795
|
if (!r.ok) return r.error;
|
|
10634
10796
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
10635
|
-
const seconds = Math.max(
|
|
10797
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
10636
10798
|
const asset = await generateVideo(
|
|
10637
10799
|
r.selected.capability,
|
|
10638
10800
|
r.cfg,
|
|
@@ -10649,7 +10811,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10649
10811
|
const location = await this.deps.sink(asset);
|
|
10650
10812
|
const unit = r.selected.cost;
|
|
10651
10813
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
10652
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
10814
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
10653
10815
|
return `Video generated and saved to ${location}.
|
|
10654
10816
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
10655
10817
|
}
|
|
@@ -14403,7 +14565,24 @@ var TaskAnalyzer = class {
|
|
|
14403
14565
|
feedback;
|
|
14404
14566
|
bias;
|
|
14405
14567
|
lastProfile = null;
|
|
14406
|
-
|
|
14568
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
14569
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
14570
|
+
/**
|
|
14571
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
14572
|
+
* type they were chosen for.
|
|
14573
|
+
*
|
|
14574
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
14575
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
14576
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
14577
|
+
* model is good at creative writing, from a rating that never said so.
|
|
14578
|
+
*
|
|
14579
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
14580
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
14581
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
14582
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
14583
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
14584
|
+
*/
|
|
14585
|
+
lastCompletedRun;
|
|
14407
14586
|
constructor(tracker, bias = "balanced") {
|
|
14408
14587
|
this.tracker = tracker;
|
|
14409
14588
|
this.bias = bias;
|
|
@@ -14450,11 +14629,15 @@ var TaskAnalyzer = class {
|
|
|
14450
14629
|
*/
|
|
14451
14630
|
async selectModel(prompt, tier, selector, opts) {
|
|
14452
14631
|
const profile = await this.analyze(prompt);
|
|
14632
|
+
const recordSelection = (model) => {
|
|
14633
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
14634
|
+
return model;
|
|
14635
|
+
};
|
|
14453
14636
|
if (profile.requiresVision) {
|
|
14454
|
-
return selector.selectVisionModel();
|
|
14637
|
+
return recordSelection(selector.selectVisionModel());
|
|
14455
14638
|
}
|
|
14456
14639
|
let candidates = selector.getCandidatesForTier(tier);
|
|
14457
|
-
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
14640
|
+
if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
14458
14641
|
if (opts?.requiresToolUse) {
|
|
14459
14642
|
const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
|
|
14460
14643
|
if (toolCapable.length > 0) candidates = toolCapable;
|
|
@@ -14464,9 +14647,7 @@ var TaskAnalyzer = class {
|
|
|
14464
14647
|
score: this.scoreModel(m, profile)
|
|
14465
14648
|
}));
|
|
14466
14649
|
scored.sort((a, b) => b.score - a.score);
|
|
14467
|
-
|
|
14468
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
14469
|
-
return best;
|
|
14650
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
14470
14651
|
}
|
|
14471
14652
|
/**
|
|
14472
14653
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -14475,25 +14656,38 @@ var TaskAnalyzer = class {
|
|
|
14475
14656
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
14476
14657
|
if (!this.tracker || !this.lastProfile) return;
|
|
14477
14658
|
const taskType = this.lastProfile.type;
|
|
14478
|
-
for (const [tier, model] of this.
|
|
14659
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
14479
14660
|
const cost = costByTier[tier] ?? 0;
|
|
14480
14661
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
14481
14662
|
}
|
|
14482
|
-
this.
|
|
14663
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
14664
|
+
this.currentRunSelections.clear();
|
|
14483
14665
|
void this.tracker.save();
|
|
14484
14666
|
}
|
|
14485
14667
|
/**
|
|
14486
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
14668
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
14487
14669
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
14488
|
-
*
|
|
14670
|
+
*
|
|
14671
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
14672
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
14673
|
+
* empty.
|
|
14674
|
+
*
|
|
14675
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
14676
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
14677
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
14678
|
+
* that reports false, which is what "rate this run" should mean.
|
|
14489
14679
|
*/
|
|
14490
14680
|
recordExplicitRating(rating) {
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14681
|
+
const snapshot = this.lastCompletedRun;
|
|
14682
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
14683
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
14684
|
+
for (const [, model] of snapshot.selections) {
|
|
14685
|
+
if (ratedModels.has(model.id)) continue;
|
|
14686
|
+
ratedModels.add(model.id);
|
|
14687
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
14688
|
+
}
|
|
14689
|
+
this.lastCompletedRun = void 0;
|
|
14690
|
+
return true;
|
|
14497
14691
|
}
|
|
14498
14692
|
scoreModel(model, profile) {
|
|
14499
14693
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -15531,6 +15725,8 @@ var RESUME_SCHEMA_VERSION = 1;
|
|
|
15531
15725
|
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
15532
15726
|
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15533
15727
|
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
15728
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
15729
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15534
15730
|
function safeId(taskId) {
|
|
15535
15731
|
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
15536
15732
|
return cleaned.slice(0, 100) || "run";
|
|
@@ -15545,16 +15741,24 @@ var ResumeStore = class {
|
|
|
15545
15741
|
maxCheckpoints;
|
|
15546
15742
|
maxAgeMs;
|
|
15547
15743
|
now;
|
|
15744
|
+
claimTimeoutMs;
|
|
15548
15745
|
constructor(options) {
|
|
15549
15746
|
this.dir = options.dir;
|
|
15550
15747
|
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
15551
15748
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
15552
15749
|
this.now = options.now ?? Date.now;
|
|
15750
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
15553
15751
|
}
|
|
15554
15752
|
/**
|
|
15555
|
-
* Persist a checkpoint and prune.
|
|
15556
|
-
*
|
|
15557
|
-
*
|
|
15753
|
+
* Persist a checkpoint and prune.
|
|
15754
|
+
*
|
|
15755
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
15756
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
15757
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
15758
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
15759
|
+
* look alike to them.
|
|
15760
|
+
*
|
|
15761
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
15558
15762
|
*/
|
|
15559
15763
|
async save(checkpoint) {
|
|
15560
15764
|
try {
|
|
@@ -15570,7 +15774,9 @@ var ResumeStore = class {
|
|
|
15570
15774
|
await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
15571
15775
|
await promises.rename(tempPath, finalPath);
|
|
15572
15776
|
await this.prune();
|
|
15777
|
+
return true;
|
|
15573
15778
|
} catch {
|
|
15779
|
+
return false;
|
|
15574
15780
|
}
|
|
15575
15781
|
}
|
|
15576
15782
|
/** Valid, unexpired checkpoints, newest first. */
|
|
@@ -15600,15 +15806,87 @@ var ResumeStore = class {
|
|
|
15600
15806
|
return (await this.list())[0] ?? null;
|
|
15601
15807
|
}
|
|
15602
15808
|
/**
|
|
15603
|
-
*
|
|
15604
|
-
*
|
|
15809
|
+
* Claim a checkpoint for a resume attempt.
|
|
15810
|
+
*
|
|
15811
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
15812
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
15813
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
15814
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
15815
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
15816
|
+
* wrong trade for a recovery mechanism.
|
|
15817
|
+
*
|
|
15818
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
15819
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
15820
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
15821
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
15822
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
15605
15823
|
*/
|
|
15606
|
-
async
|
|
15824
|
+
async claim(taskId) {
|
|
15607
15825
|
const all = await this.list();
|
|
15608
15826
|
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
15609
15827
|
if (!target) return null;
|
|
15610
|
-
|
|
15611
|
-
|
|
15828
|
+
try {
|
|
15829
|
+
const entries = await promises.readdir(this.dir);
|
|
15830
|
+
const wanted = safeId(target.taskId);
|
|
15831
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
15832
|
+
if (!source) return null;
|
|
15833
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
15834
|
+
await promises.rename(path26.join(this.dir, source), path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
15835
|
+
return { checkpoint: target, claimId };
|
|
15836
|
+
} catch {
|
|
15837
|
+
return null;
|
|
15838
|
+
}
|
|
15839
|
+
}
|
|
15840
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
15841
|
+
async release(claimId) {
|
|
15842
|
+
try {
|
|
15843
|
+
await promises.rename(
|
|
15844
|
+
path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
15845
|
+
path26.join(this.dir, `${claimId}.json`)
|
|
15846
|
+
);
|
|
15847
|
+
} catch {
|
|
15848
|
+
}
|
|
15849
|
+
}
|
|
15850
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
15851
|
+
async settle(claimId) {
|
|
15852
|
+
try {
|
|
15853
|
+
await promises.rm(path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
15854
|
+
} catch {
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15857
|
+
/**
|
|
15858
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
15859
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
15860
|
+
*/
|
|
15861
|
+
async consume(taskId) {
|
|
15862
|
+
const claimed = await this.claim(taskId);
|
|
15863
|
+
if (!claimed) return null;
|
|
15864
|
+
await this.settle(claimed.claimId);
|
|
15865
|
+
return claimed.checkpoint;
|
|
15866
|
+
}
|
|
15867
|
+
/**
|
|
15868
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
15869
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
15870
|
+
* trading one loss window for another.
|
|
15871
|
+
*/
|
|
15872
|
+
async reclaimStale() {
|
|
15873
|
+
let reclaimed = 0;
|
|
15874
|
+
try {
|
|
15875
|
+
const entries = await promises.readdir(this.dir);
|
|
15876
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
15877
|
+
for (const name of entries) {
|
|
15878
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
15879
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
15880
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
15881
|
+
await promises.rename(
|
|
15882
|
+
path26.join(this.dir, name),
|
|
15883
|
+
path26.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
15884
|
+
);
|
|
15885
|
+
reclaimed++;
|
|
15886
|
+
}
|
|
15887
|
+
} catch {
|
|
15888
|
+
}
|
|
15889
|
+
return reclaimed;
|
|
15612
15890
|
}
|
|
15613
15891
|
/** Remove every checkpoint for a task id. */
|
|
15614
15892
|
async delete(taskId) {
|
|
@@ -16664,6 +16942,25 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16664
16942
|
lastInterruptedRun;
|
|
16665
16943
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
16666
16944
|
resumeStore;
|
|
16945
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
16946
|
+
claimedResumeId;
|
|
16947
|
+
/**
|
|
16948
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
16949
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
16950
|
+
*/
|
|
16951
|
+
resumedFrom;
|
|
16952
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
16953
|
+
wroteCheckpointThisRun = false;
|
|
16954
|
+
/**
|
|
16955
|
+
* Whether the run reached the end of its normal path.
|
|
16956
|
+
*
|
|
16957
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
16958
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
16959
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
16960
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
16961
|
+
* the original claim and the finished sections with it.
|
|
16962
|
+
*/
|
|
16963
|
+
completedNormally = false;
|
|
16667
16964
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
16668
16965
|
currentRunCompleted = [];
|
|
16669
16966
|
initPromise;
|
|
@@ -17181,17 +17478,31 @@ ${input}` }],
|
|
|
17181
17478
|
* worse. Nothing here is allowed to throw.
|
|
17182
17479
|
*/
|
|
17183
17480
|
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
17184
|
-
|
|
17481
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
17482
|
+
const merged = /* @__PURE__ */ new Map();
|
|
17483
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
17484
|
+
for (const node of this.currentRunCompleted) {
|
|
17485
|
+
const existing = merged.get(node.id);
|
|
17486
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
17487
|
+
merged.set(node.id, node);
|
|
17488
|
+
}
|
|
17489
|
+
const completed = [...merged.values()];
|
|
17490
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
17491
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
17492
|
+
if (!completed.length && !carriedPartial) return false;
|
|
17185
17493
|
try {
|
|
17186
|
-
await this.resumeStore?.save({
|
|
17494
|
+
const saved = await this.resumeStore?.save({
|
|
17187
17495
|
taskId,
|
|
17188
|
-
prompt,
|
|
17496
|
+
prompt: canonicalPrompt,
|
|
17189
17497
|
reason,
|
|
17190
17498
|
...detail ? { detail } : {},
|
|
17191
|
-
partialOutput,
|
|
17192
|
-
completed
|
|
17499
|
+
partialOutput: carriedPartial,
|
|
17500
|
+
completed
|
|
17193
17501
|
});
|
|
17502
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
17503
|
+
return saved === true;
|
|
17194
17504
|
} catch {
|
|
17505
|
+
return false;
|
|
17195
17506
|
}
|
|
17196
17507
|
}
|
|
17197
17508
|
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
@@ -17229,11 +17540,55 @@ ${input}` }],
|
|
|
17229
17540
|
* checkpoint, so the same finished work is never restored into two runs.
|
|
17230
17541
|
*/
|
|
17231
17542
|
async prepareDurableResume(opts = {}) {
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17543
|
+
await this.resumeStore?.reclaimStale();
|
|
17544
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
17545
|
+
if (!claimed) return null;
|
|
17546
|
+
try {
|
|
17547
|
+
this.lastInterruptedRun = void 0;
|
|
17548
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17549
|
+
const prompt = this.buildResumePrompt(
|
|
17550
|
+
claimed.checkpoint.prompt,
|
|
17551
|
+
claimed.checkpoint.partialOutput,
|
|
17552
|
+
claimed.checkpoint.completed
|
|
17553
|
+
);
|
|
17554
|
+
this.resumedFrom = {
|
|
17555
|
+
prompt: claimed.checkpoint.prompt,
|
|
17556
|
+
completed: claimed.checkpoint.completed,
|
|
17557
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
17558
|
+
};
|
|
17559
|
+
this.claimedResumeId = claimed.claimId;
|
|
17560
|
+
return prompt;
|
|
17561
|
+
} catch (err) {
|
|
17562
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
17563
|
+
throw err;
|
|
17564
|
+
}
|
|
17565
|
+
}
|
|
17566
|
+
/**
|
|
17567
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
17568
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
17569
|
+
* run loop; safe to call when nothing is claimed.
|
|
17570
|
+
*/
|
|
17571
|
+
async settleClaimedResume(succeeded) {
|
|
17572
|
+
const claimId = this.claimedResumeId;
|
|
17573
|
+
if (!claimId) return;
|
|
17574
|
+
this.claimedResumeId = void 0;
|
|
17575
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
17576
|
+
await this.resumeStore?.settle(claimId);
|
|
17577
|
+
} else {
|
|
17578
|
+
await this.resumeStore?.release(claimId);
|
|
17579
|
+
}
|
|
17580
|
+
}
|
|
17581
|
+
/**
|
|
17582
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
17583
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
17584
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
17585
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
17586
|
+
*/
|
|
17587
|
+
async releaseClaimedResume() {
|
|
17588
|
+
const claimId = this.claimedResumeId;
|
|
17589
|
+
if (!claimId) return;
|
|
17590
|
+
this.claimedResumeId = void 0;
|
|
17591
|
+
await this.resumeStore?.release(claimId);
|
|
17237
17592
|
}
|
|
17238
17593
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
17239
17594
|
raiseBudgetForResume(maxTokens) {
|
|
@@ -17269,7 +17624,12 @@ ${partialOutput}` : ""
|
|
|
17269
17624
|
async resumeRun(opts = {}) {
|
|
17270
17625
|
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
17271
17626
|
if (!prompt) return null;
|
|
17272
|
-
|
|
17627
|
+
try {
|
|
17628
|
+
return await this.run({ prompt });
|
|
17629
|
+
} catch (err) {
|
|
17630
|
+
await this.releaseClaimedResume();
|
|
17631
|
+
throw err;
|
|
17632
|
+
}
|
|
17273
17633
|
}
|
|
17274
17634
|
getWorkspacePath() {
|
|
17275
17635
|
return this.workspacePath;
|
|
@@ -17639,6 +17999,9 @@ ${prompt}` : prompt;
|
|
|
17639
17999
|
const taskId = randomUUID();
|
|
17640
18000
|
this.decisionLog = [];
|
|
17641
18001
|
this.currentRunCompleted = [];
|
|
18002
|
+
this.wroteCheckpointThisRun = false;
|
|
18003
|
+
this.completedNormally = false;
|
|
18004
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
17642
18005
|
if (options.fastAnswer) {
|
|
17643
18006
|
return this.runFastAnswer(options, startMs, taskId);
|
|
17644
18007
|
}
|
|
@@ -17917,6 +18280,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17917
18280
|
${finalOutput}` : ""
|
|
17918
18281
|
].join("\n").trimEnd();
|
|
17919
18282
|
}
|
|
18283
|
+
this.completedNormally = trip == null;
|
|
17920
18284
|
} catch (err) {
|
|
17921
18285
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
17922
18286
|
this.emit("run:cancelled", {
|
|
@@ -17958,6 +18322,10 @@ ${finalOutput}` : ""
|
|
|
17958
18322
|
escalator.cancelAllPending();
|
|
17959
18323
|
} catch {
|
|
17960
18324
|
}
|
|
18325
|
+
try {
|
|
18326
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
18327
|
+
} catch {
|
|
18328
|
+
}
|
|
17961
18329
|
try {
|
|
17962
18330
|
this.releasePendingEscalations();
|
|
17963
18331
|
} catch {
|
|
@@ -19586,8 +19954,14 @@ var DashboardSocket = class {
|
|
|
19586
19954
|
emitCascadeEvent(ev) {
|
|
19587
19955
|
this.io.emit("cascade:event", ev);
|
|
19588
19956
|
}
|
|
19589
|
-
|
|
19590
|
-
|
|
19957
|
+
/**
|
|
19958
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
19959
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
19960
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
19961
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
19962
|
+
*/
|
|
19963
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
19964
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
19591
19965
|
this.io.emit("tier:status", payload);
|
|
19592
19966
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
19593
19967
|
}
|