cascade-ai 0.68.0 → 0.70.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 +85 -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/desktop-core.cjs
CHANGED
|
@@ -291781,7 +291781,7 @@ function reconcilePrice(model, live) {
|
|
|
291781
291781
|
}
|
|
291782
291782
|
|
|
291783
291783
|
// src/constants.ts
|
|
291784
|
-
var CASCADE_VERSION = "0.
|
|
291784
|
+
var CASCADE_VERSION = "0.70.0";
|
|
291785
291785
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
291786
291786
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
291787
291787
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -321846,6 +321846,21 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
321846
321846
|
* which node (Cockpit node panel / Why panel).
|
|
321847
321847
|
*/
|
|
321848
321848
|
servingModel;
|
|
321849
|
+
/**
|
|
321850
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
321851
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
321852
|
+
*
|
|
321853
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
321854
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
321855
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
321856
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
321857
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
321858
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
321859
|
+
* appears in the stream.
|
|
321860
|
+
*/
|
|
321861
|
+
graphNodeId;
|
|
321862
|
+
graphDependsOn;
|
|
321863
|
+
graphWave;
|
|
321849
321864
|
constructor(role, id, parentId) {
|
|
321850
321865
|
super();
|
|
321851
321866
|
this.role = role;
|
|
@@ -321857,6 +321872,33 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
321857
321872
|
setPresenter(on = true) {
|
|
321858
321873
|
this.isPresenter = on;
|
|
321859
321874
|
}
|
|
321875
|
+
/**
|
|
321876
|
+
* Record where this tier sits in the compiled task graph.
|
|
321877
|
+
*
|
|
321878
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
321879
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
321880
|
+
* are set independently rather than all at once.
|
|
321881
|
+
*/
|
|
321882
|
+
setGraphPosition(position) {
|
|
321883
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
321884
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
321885
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
321886
|
+
}
|
|
321887
|
+
/**
|
|
321888
|
+
* The graph fields, for spreading into a status payload.
|
|
321889
|
+
*
|
|
321890
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
321891
|
+
* two different call sites with two different payload shapes; adding a field
|
|
321892
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
321893
|
+
* consumer than never arriving at all.
|
|
321894
|
+
*/
|
|
321895
|
+
graphFields() {
|
|
321896
|
+
return {
|
|
321897
|
+
nodeId: this.graphNodeId,
|
|
321898
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
321899
|
+
waveId: this.graphWave
|
|
321900
|
+
};
|
|
321901
|
+
}
|
|
321860
321902
|
getStatus() {
|
|
321861
321903
|
return this.status;
|
|
321862
321904
|
}
|
|
@@ -321871,7 +321913,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
321871
321913
|
status,
|
|
321872
321914
|
timestamp,
|
|
321873
321915
|
output,
|
|
321874
|
-
model: this.servingModel
|
|
321916
|
+
model: this.servingModel,
|
|
321917
|
+
...this.graphFields()
|
|
321875
321918
|
};
|
|
321876
321919
|
this.emit("status", event);
|
|
321877
321920
|
this.emit("tier:status", event);
|
|
@@ -321911,7 +321954,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
321911
321954
|
progressPct: update.progressPct,
|
|
321912
321955
|
timestamp,
|
|
321913
321956
|
output: update.output,
|
|
321914
|
-
model: this.servingModel
|
|
321957
|
+
model: this.servingModel,
|
|
321958
|
+
...this.graphFields()
|
|
321915
321959
|
});
|
|
321916
321960
|
}
|
|
321917
321961
|
buildMessage(type, to, payload) {
|
|
@@ -322813,7 +322857,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
322813
322857
|
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
322814
322858
|
}
|
|
322815
322859
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
322816
|
-
const
|
|
322860
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
322861
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
322817
322862
|
checksRun.push(...testResult.checksRun);
|
|
322818
322863
|
passed.push(...testResult.passed);
|
|
322819
322864
|
failed.push(...testResult.failed);
|
|
@@ -322822,7 +322867,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
322822
322867
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
322823
322868
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
322824
322869
|
output = corrected;
|
|
322825
|
-
const retest = await this.selfTest(assignment, output);
|
|
322870
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
322826
322871
|
passed.push(...retest.passed);
|
|
322827
322872
|
if (retest.failed.length > 0) {
|
|
322828
322873
|
failed.push(...retest.failed);
|
|
@@ -324746,6 +324791,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
324746
324791
|
for (const a2 of assignments) {
|
|
324747
324792
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
324748
324793
|
if (this.store) worker.setStore(this.store, taskId);
|
|
324794
|
+
worker.setGraphPosition({ nodeId: a2.subtaskId, dependsOn: a2.dependsOn ?? [] });
|
|
324749
324795
|
worker.setPeerBus(this.t3PeerBus);
|
|
324750
324796
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
324751
324797
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -324866,6 +324912,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
324866
324912
|
runnableIds.push(fallbackId);
|
|
324867
324913
|
}
|
|
324868
324914
|
wave++;
|
|
324915
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
324869
324916
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
324870
324917
|
this.sendStatusUpdate({
|
|
324871
324918
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -325193,23 +325240,63 @@ var DependencyScheduler = class {
|
|
|
325193
325240
|
dependents.get(dependencyId)?.push(node.id);
|
|
325194
325241
|
}
|
|
325195
325242
|
}
|
|
325196
|
-
const
|
|
325243
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
325244
|
+
const settled = /* @__PURE__ */ new Set();
|
|
325197
325245
|
const results = /* @__PURE__ */ new Map();
|
|
325246
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
325198
325247
|
const waves = [];
|
|
325199
325248
|
let wave = 0;
|
|
325200
|
-
|
|
325201
|
-
const
|
|
325249
|
+
const blockDescendants = async (failedIds) => {
|
|
325250
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
325251
|
+
const queue2 = [];
|
|
325252
|
+
for (const rootId of failedIds) {
|
|
325253
|
+
for (const childId of dependents.get(rootId) ?? []) queue2.push({ id: childId, root: rootId });
|
|
325254
|
+
}
|
|
325255
|
+
while (queue2.length > 0) {
|
|
325256
|
+
const { id, root } = queue2.shift();
|
|
325257
|
+
if (settled.has(id)) continue;
|
|
325258
|
+
const known = rootsFor.get(id);
|
|
325259
|
+
if (known) {
|
|
325260
|
+
if (!known.has(root)) known.add(root);
|
|
325261
|
+
continue;
|
|
325262
|
+
}
|
|
325263
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
325264
|
+
for (const childId of dependents.get(id) ?? []) queue2.push({ id: childId, root });
|
|
325265
|
+
}
|
|
325266
|
+
for (const node of nodes) {
|
|
325267
|
+
const roots = rootsFor.get(node.id);
|
|
325268
|
+
if (!roots || settled.has(node.id)) continue;
|
|
325269
|
+
const causes = nodes.filter((n8) => roots.has(n8.id));
|
|
325270
|
+
const blockedBy = causes.map((n8) => n8.id);
|
|
325271
|
+
const blockedByTitles = causes.map((n8) => n8.title || n8.id);
|
|
325272
|
+
const record2 = {
|
|
325273
|
+
blockedBy,
|
|
325274
|
+
blockedByTitles,
|
|
325275
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t4) => `"${t4}"`).join(", ")} failed`
|
|
325276
|
+
};
|
|
325277
|
+
blocked.set(node.id, record2);
|
|
325278
|
+
settled.add(node.id);
|
|
325279
|
+
await this.hooks.onNodeBlocked?.(node, record2);
|
|
325280
|
+
}
|
|
325281
|
+
};
|
|
325282
|
+
while (settled.size < nodes.length) {
|
|
325283
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
325202
325284
|
if (ready.length === 0) {
|
|
325203
325285
|
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
325204
325286
|
}
|
|
325205
325287
|
waves.push(ready.map((node) => node.id));
|
|
325206
325288
|
await this.hooks.onWaveStart?.(ready, wave);
|
|
325289
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
325207
325290
|
const runOne = async (node) => {
|
|
325208
325291
|
const result = await this.hooks.execute(node);
|
|
325209
325292
|
results.set(node.id, result);
|
|
325210
|
-
|
|
325211
|
-
|
|
325212
|
-
|
|
325293
|
+
settled.add(node.id);
|
|
325294
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
325295
|
+
outcomes.set(node.id, outcome);
|
|
325296
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
325297
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
325298
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
325299
|
+
}
|
|
325213
325300
|
}
|
|
325214
325301
|
await this.hooks.onNodeComplete?.(node, result);
|
|
325215
325302
|
};
|
|
@@ -325218,9 +325305,13 @@ var DependencyScheduler = class {
|
|
|
325218
325305
|
} else {
|
|
325219
325306
|
await Promise.all(ready.map(runOne));
|
|
325220
325307
|
}
|
|
325308
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
325309
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
325310
|
+
await blockDescendants(failedThisWave);
|
|
325311
|
+
}
|
|
325221
325312
|
wave++;
|
|
325222
325313
|
}
|
|
325223
|
-
return { results, waves };
|
|
325314
|
+
return { results, waves, blocked };
|
|
325224
325315
|
}
|
|
325225
325316
|
};
|
|
325226
325317
|
|
|
@@ -325321,6 +325412,9 @@ QUALITY RULES:
|
|
|
325321
325412
|
|
|
325322
325413
|
${generation}` : ""}`;
|
|
325323
325414
|
}
|
|
325415
|
+
function producedOutput(result) {
|
|
325416
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
325417
|
+
}
|
|
325324
325418
|
var T1Administrator = class extends BaseTier {
|
|
325325
325419
|
router;
|
|
325326
325420
|
toolRegistry;
|
|
@@ -325455,7 +325549,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
325455
325549
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
325456
325550
|
let pass = 1;
|
|
325457
325551
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
325458
|
-
const okCount = (rs2) => rs2.filter(
|
|
325552
|
+
const okCount = (rs2) => rs2.filter(producedOutput).length;
|
|
325459
325553
|
while (pass <= maxReplanPasses) {
|
|
325460
325554
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
325461
325555
|
if (reviewResult.approved) {
|
|
@@ -325586,7 +325680,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
325586
325680
|
/** Structured, grounded summary of what's already done — used to keep
|
|
325587
325681
|
* corrective replan passes from re-emitting completed sections. */
|
|
325588
325682
|
summarizeCompletedSections(results) {
|
|
325589
|
-
const done = results.filter(
|
|
325683
|
+
const done = results.filter(producedOutput);
|
|
325590
325684
|
if (done.length === 0) return "";
|
|
325591
325685
|
const lines = done.map((r4) => `- "${r4.sectionTitle}" [${r4.status}]: ${(r4.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
325592
325686
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -325731,6 +325825,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
325731
325825
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
325732
325826
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
325733
325827
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
325828
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
325734
325829
|
if (this.store) {
|
|
325735
325830
|
manager.setStore(this.store);
|
|
325736
325831
|
}
|
|
@@ -325857,6 +325952,13 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
325857
325952
|
const scheduler = new DependencyScheduler(
|
|
325858
325953
|
compiled.graph,
|
|
325859
325954
|
{
|
|
325955
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
325956
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
325957
|
+
// Stamped before the wave executes so the first status event a node
|
|
325958
|
+
// emits already carries it.
|
|
325959
|
+
onWaveStart: (nodes, wave) => {
|
|
325960
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
325961
|
+
},
|
|
325860
325962
|
execute: async (node) => {
|
|
325861
325963
|
const section = node.payload;
|
|
325862
325964
|
const manager = managers[node.ordinal];
|
|
@@ -325891,6 +325993,62 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
325891
325993
|
};
|
|
325892
325994
|
}
|
|
325893
325995
|
},
|
|
325996
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
325997
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
325998
|
+
// the user more than letting it try.
|
|
325999
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
326000
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
326001
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
326002
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
326003
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
326004
|
+
// should have been skipped would run and be billed.
|
|
326005
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
326006
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
326007
|
+
const section = node.payload;
|
|
326008
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
326009
|
+
resultMap.set(node.id, {
|
|
326010
|
+
sectionId: section.sectionId,
|
|
326011
|
+
sectionTitle: section.sectionTitle,
|
|
326012
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
326013
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
326014
|
+
// that did (the upstream section named below).
|
|
326015
|
+
status: "BLOCKED",
|
|
326016
|
+
t3Results: [],
|
|
326017
|
+
sectionSummary: "",
|
|
326018
|
+
issues: [
|
|
326019
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
326020
|
+
],
|
|
326021
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
326022
|
+
});
|
|
326023
|
+
completedSections++;
|
|
326024
|
+
this.sendStatusUpdate({
|
|
326025
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
326026
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
326027
|
+
status: "IN_PROGRESS"
|
|
326028
|
+
});
|
|
326029
|
+
this.emit("tier:status", {
|
|
326030
|
+
tierId: section.sectionId,
|
|
326031
|
+
// A blocked section has no tier instance — it was never
|
|
326032
|
+
// constructed — so its runtime id and its graph id are the same
|
|
326033
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
326034
|
+
// stream addressable in the graph's id space, which is what
|
|
326035
|
+
// dependsOn edges point at.
|
|
326036
|
+
nodeId: section.sectionId,
|
|
326037
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
326038
|
+
role: "T2",
|
|
326039
|
+
label: section.sectionTitle,
|
|
326040
|
+
status: "BLOCKED",
|
|
326041
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
326042
|
+
output: blockedInfo.reason
|
|
326043
|
+
});
|
|
326044
|
+
this.emit("section:complete", {
|
|
326045
|
+
id: node.id,
|
|
326046
|
+
title: section.sectionTitle,
|
|
326047
|
+
status: "BLOCKED",
|
|
326048
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
326049
|
+
output: ""
|
|
326050
|
+
});
|
|
326051
|
+
},
|
|
325894
326052
|
onNodeComplete: (node, result) => {
|
|
325895
326053
|
resultMap.set(node.id, result);
|
|
325896
326054
|
completedSections++;
|
|
@@ -325907,14 +326065,18 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
325907
326065
|
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
325908
326066
|
// serialized T3 workers within a single section while every section's
|
|
325909
326067
|
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
325910
|
-
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
326068
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
326069
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
326070
|
+
// output. Running it after the dependency failed produces a second
|
|
326071
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
326072
|
+
onUpstreamFailure: "block"
|
|
325911
326073
|
}
|
|
325912
326074
|
);
|
|
325913
326075
|
await scheduler.run();
|
|
325914
326076
|
return sections.map((s3) => resultMap.get(s3.sectionId)).filter(Boolean);
|
|
325915
326077
|
}
|
|
325916
326078
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
325917
|
-
const completedSections = t2Results.filter(
|
|
326079
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
325918
326080
|
if (!completedSections.length) {
|
|
325919
326081
|
const allIssues = [
|
|
325920
326082
|
...t2Results.flatMap((r4) => r4.issues ?? []),
|
|
@@ -325934,7 +326096,6 @@ ${r4.sectionSummary}
|
|
|
325934
326096
|
Outputs:
|
|
325935
326097
|
${r4.t3Results.filter((t4) => t4.status === "COMPLETED").map((t4) => `\u2022 ${typeof t4.output === "string" ? t4.output : JSON.stringify(t4.output)}`).join("\n")}`).join("\n\n---\n\n");
|
|
325936
326098
|
const openIssues = t2Results.flatMap((r4) => r4.issues).filter(Boolean);
|
|
325937
|
-
t2Results.filter((r4) => r4.status === "FAILED" || r4.status === "PARTIAL");
|
|
325938
326099
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
325939
326100
|
|
|
325940
326101
|
Original request: ${originalPrompt}
|
|
@@ -326426,6 +326587,7 @@ Model: ${asset.modelId} \u2014 ${r4.selected.reason}`;
|
|
|
326426
326587
|
};
|
|
326427
326588
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
326428
326589
|
var MAX_VIDEO_SECONDS = 8;
|
|
326590
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
326429
326591
|
var GenerateVideoTool = class extends BaseTool {
|
|
326430
326592
|
constructor(deps) {
|
|
326431
326593
|
super();
|
|
@@ -326442,7 +326604,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
326442
326604
|
type: "object",
|
|
326443
326605
|
properties: {
|
|
326444
326606
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
326445
|
-
seconds: { type: "number", description: `Clip length,
|
|
326607
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
326446
326608
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
326447
326609
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
326448
326610
|
},
|
|
@@ -326454,7 +326616,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
326454
326616
|
const r4 = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
326455
326617
|
if (!r4.ok) return r4.error;
|
|
326456
326618
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
326457
|
-
const seconds = Math.max(
|
|
326619
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
326458
326620
|
const asset = await generateVideo(
|
|
326459
326621
|
r4.selected.capability,
|
|
326460
326622
|
r4.cfg,
|
|
@@ -326471,7 +326633,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
326471
326633
|
const location = await this.deps.sink(asset);
|
|
326472
326634
|
const unit = r4.selected.cost;
|
|
326473
326635
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
326474
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
326636
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
326475
326637
|
return `Video generated and saved to ${location}.
|
|
326476
326638
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
326477
326639
|
}
|
|
@@ -370519,7 +370681,24 @@ var TaskAnalyzer = class {
|
|
|
370519
370681
|
feedback;
|
|
370520
370682
|
bias;
|
|
370521
370683
|
lastProfile = null;
|
|
370522
|
-
|
|
370684
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
370685
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
370686
|
+
/**
|
|
370687
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
370688
|
+
* type they were chosen for.
|
|
370689
|
+
*
|
|
370690
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
370691
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
370692
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
370693
|
+
* model is good at creative writing, from a rating that never said so.
|
|
370694
|
+
*
|
|
370695
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
370696
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
370697
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
370698
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
370699
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
370700
|
+
*/
|
|
370701
|
+
lastCompletedRun;
|
|
370523
370702
|
constructor(tracker, bias = "balanced") {
|
|
370524
370703
|
this.tracker = tracker;
|
|
370525
370704
|
this.bias = bias;
|
|
@@ -370566,11 +370745,15 @@ var TaskAnalyzer = class {
|
|
|
370566
370745
|
*/
|
|
370567
370746
|
async selectModel(prompt, tier, selector, opts) {
|
|
370568
370747
|
const profile = await this.analyze(prompt);
|
|
370748
|
+
const recordSelection = (model) => {
|
|
370749
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
370750
|
+
return model;
|
|
370751
|
+
};
|
|
370569
370752
|
if (profile.requiresVision) {
|
|
370570
|
-
return selector.selectVisionModel();
|
|
370753
|
+
return recordSelection(selector.selectVisionModel());
|
|
370571
370754
|
}
|
|
370572
370755
|
let candidates = selector.getCandidatesForTier(tier);
|
|
370573
|
-
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
370756
|
+
if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
370574
370757
|
if (opts?.requiresToolUse) {
|
|
370575
370758
|
const toolCapable = candidates.filter((m3) => m3.supportsToolUse !== false);
|
|
370576
370759
|
if (toolCapable.length > 0) candidates = toolCapable;
|
|
@@ -370580,9 +370763,7 @@ var TaskAnalyzer = class {
|
|
|
370580
370763
|
score: this.scoreModel(m3, profile)
|
|
370581
370764
|
}));
|
|
370582
370765
|
scored.sort((a2, b3) => b3.score - a2.score);
|
|
370583
|
-
|
|
370584
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
370585
|
-
return best;
|
|
370766
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
370586
370767
|
}
|
|
370587
370768
|
/**
|
|
370588
370769
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -370591,25 +370772,38 @@ var TaskAnalyzer = class {
|
|
|
370591
370772
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
370592
370773
|
if (!this.tracker || !this.lastProfile) return;
|
|
370593
370774
|
const taskType = this.lastProfile.type;
|
|
370594
|
-
for (const [tier, model] of this.
|
|
370775
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
370595
370776
|
const cost = costByTier[tier] ?? 0;
|
|
370596
370777
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
370597
370778
|
}
|
|
370598
|
-
this.
|
|
370779
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
370780
|
+
this.currentRunSelections.clear();
|
|
370599
370781
|
void this.tracker.save();
|
|
370600
370782
|
}
|
|
370601
370783
|
/**
|
|
370602
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
370784
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
370603
370785
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
370604
|
-
*
|
|
370786
|
+
*
|
|
370787
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
370788
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
370789
|
+
* empty.
|
|
370790
|
+
*
|
|
370791
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
370792
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
370793
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
370794
|
+
* that reports false, which is what "rate this run" should mean.
|
|
370605
370795
|
*/
|
|
370606
370796
|
recordExplicitRating(rating) {
|
|
370607
|
-
|
|
370608
|
-
|
|
370609
|
-
|
|
370610
|
-
|
|
370611
|
-
|
|
370612
|
-
|
|
370797
|
+
const snapshot = this.lastCompletedRun;
|
|
370798
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
370799
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
370800
|
+
for (const [, model] of snapshot.selections) {
|
|
370801
|
+
if (ratedModels.has(model.id)) continue;
|
|
370802
|
+
ratedModels.add(model.id);
|
|
370803
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
370804
|
+
}
|
|
370805
|
+
this.lastCompletedRun = void 0;
|
|
370806
|
+
return true;
|
|
370613
370807
|
}
|
|
370614
370808
|
scoreModel(model, profile) {
|
|
370615
370809
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -371647,6 +371841,8 @@ var RESUME_SCHEMA_VERSION = 1;
|
|
|
371647
371841
|
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
371648
371842
|
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
371649
371843
|
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
371844
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
371845
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
371650
371846
|
function safeId(taskId) {
|
|
371651
371847
|
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
371652
371848
|
return cleaned.slice(0, 100) || "run";
|
|
@@ -371661,16 +371857,24 @@ var ResumeStore = class {
|
|
|
371661
371857
|
maxCheckpoints;
|
|
371662
371858
|
maxAgeMs;
|
|
371663
371859
|
now;
|
|
371860
|
+
claimTimeoutMs;
|
|
371664
371861
|
constructor(options) {
|
|
371665
371862
|
this.dir = options.dir;
|
|
371666
371863
|
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
371667
371864
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
371668
371865
|
this.now = options.now ?? Date.now;
|
|
371866
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
371669
371867
|
}
|
|
371670
371868
|
/**
|
|
371671
|
-
* Persist a checkpoint and prune.
|
|
371672
|
-
*
|
|
371673
|
-
*
|
|
371869
|
+
* Persist a checkpoint and prune.
|
|
371870
|
+
*
|
|
371871
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
371872
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
371873
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
371874
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
371875
|
+
* look alike to them.
|
|
371876
|
+
*
|
|
371877
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
371674
371878
|
*/
|
|
371675
371879
|
async save(checkpoint) {
|
|
371676
371880
|
try {
|
|
@@ -371686,7 +371890,9 @@ var ResumeStore = class {
|
|
|
371686
371890
|
await fs12.promises.writeFile(tempPath, JSON.stringify(record2, null, 2), "utf-8");
|
|
371687
371891
|
await fs12.promises.rename(tempPath, finalPath);
|
|
371688
371892
|
await this.prune();
|
|
371893
|
+
return true;
|
|
371689
371894
|
} catch {
|
|
371895
|
+
return false;
|
|
371690
371896
|
}
|
|
371691
371897
|
}
|
|
371692
371898
|
/** Valid, unexpired checkpoints, newest first. */
|
|
@@ -371716,15 +371922,87 @@ var ResumeStore = class {
|
|
|
371716
371922
|
return (await this.list())[0] ?? null;
|
|
371717
371923
|
}
|
|
371718
371924
|
/**
|
|
371719
|
-
*
|
|
371720
|
-
*
|
|
371925
|
+
* Claim a checkpoint for a resume attempt.
|
|
371926
|
+
*
|
|
371927
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
371928
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
371929
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
371930
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
371931
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
371932
|
+
* wrong trade for a recovery mechanism.
|
|
371933
|
+
*
|
|
371934
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
371935
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
371936
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
371937
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
371938
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
371721
371939
|
*/
|
|
371722
|
-
async
|
|
371940
|
+
async claim(taskId) {
|
|
371723
371941
|
const all = await this.list();
|
|
371724
371942
|
const target = taskId ? all.find((c4) => c4.taskId === taskId) : all[0];
|
|
371725
371943
|
if (!target) return null;
|
|
371726
|
-
|
|
371727
|
-
|
|
371944
|
+
try {
|
|
371945
|
+
const entries = await fs12.promises.readdir(this.dir);
|
|
371946
|
+
const wanted = safeId(target.taskId);
|
|
371947
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
371948
|
+
if (!source) return null;
|
|
371949
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
371950
|
+
await fs12.promises.rename(path28__namespace.default.join(this.dir, source), path28__namespace.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
371951
|
+
return { checkpoint: target, claimId };
|
|
371952
|
+
} catch {
|
|
371953
|
+
return null;
|
|
371954
|
+
}
|
|
371955
|
+
}
|
|
371956
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
371957
|
+
async release(claimId) {
|
|
371958
|
+
try {
|
|
371959
|
+
await fs12.promises.rename(
|
|
371960
|
+
path28__namespace.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
371961
|
+
path28__namespace.default.join(this.dir, `${claimId}.json`)
|
|
371962
|
+
);
|
|
371963
|
+
} catch {
|
|
371964
|
+
}
|
|
371965
|
+
}
|
|
371966
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
371967
|
+
async settle(claimId) {
|
|
371968
|
+
try {
|
|
371969
|
+
await fs12.promises.rm(path28__namespace.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
371970
|
+
} catch {
|
|
371971
|
+
}
|
|
371972
|
+
}
|
|
371973
|
+
/**
|
|
371974
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
371975
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
371976
|
+
*/
|
|
371977
|
+
async consume(taskId) {
|
|
371978
|
+
const claimed = await this.claim(taskId);
|
|
371979
|
+
if (!claimed) return null;
|
|
371980
|
+
await this.settle(claimed.claimId);
|
|
371981
|
+
return claimed.checkpoint;
|
|
371982
|
+
}
|
|
371983
|
+
/**
|
|
371984
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
371985
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
371986
|
+
* trading one loss window for another.
|
|
371987
|
+
*/
|
|
371988
|
+
async reclaimStale() {
|
|
371989
|
+
let reclaimed = 0;
|
|
371990
|
+
try {
|
|
371991
|
+
const entries = await fs12.promises.readdir(this.dir);
|
|
371992
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
371993
|
+
for (const name of entries) {
|
|
371994
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
371995
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
371996
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
371997
|
+
await fs12.promises.rename(
|
|
371998
|
+
path28__namespace.default.join(this.dir, name),
|
|
371999
|
+
path28__namespace.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
372000
|
+
);
|
|
372001
|
+
reclaimed++;
|
|
372002
|
+
}
|
|
372003
|
+
} catch {
|
|
372004
|
+
}
|
|
372005
|
+
return reclaimed;
|
|
371728
372006
|
}
|
|
371729
372007
|
/** Remove every checkpoint for a task id. */
|
|
371730
372008
|
async delete(taskId) {
|
|
@@ -372790,6 +373068,25 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
372790
373068
|
lastInterruptedRun;
|
|
372791
373069
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
372792
373070
|
resumeStore;
|
|
373071
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
373072
|
+
claimedResumeId;
|
|
373073
|
+
/**
|
|
373074
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
373075
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
373076
|
+
*/
|
|
373077
|
+
resumedFrom;
|
|
373078
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
373079
|
+
wroteCheckpointThisRun = false;
|
|
373080
|
+
/**
|
|
373081
|
+
* Whether the run reached the end of its normal path.
|
|
373082
|
+
*
|
|
373083
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
373084
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
373085
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
373086
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
373087
|
+
* the original claim and the finished sections with it.
|
|
373088
|
+
*/
|
|
373089
|
+
completedNormally = false;
|
|
372793
373090
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
372794
373091
|
currentRunCompleted = [];
|
|
372795
373092
|
initPromise;
|
|
@@ -373307,17 +373604,31 @@ ${input}` }],
|
|
|
373307
373604
|
* worse. Nothing here is allowed to throw.
|
|
373308
373605
|
*/
|
|
373309
373606
|
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
373310
|
-
|
|
373607
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
373608
|
+
const merged = /* @__PURE__ */ new Map();
|
|
373609
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
373610
|
+
for (const node of this.currentRunCompleted) {
|
|
373611
|
+
const existing = merged.get(node.id);
|
|
373612
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
373613
|
+
merged.set(node.id, node);
|
|
373614
|
+
}
|
|
373615
|
+
const completed = [...merged.values()];
|
|
373616
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
373617
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
373618
|
+
if (!completed.length && !carriedPartial) return false;
|
|
373311
373619
|
try {
|
|
373312
|
-
await this.resumeStore?.save({
|
|
373620
|
+
const saved = await this.resumeStore?.save({
|
|
373313
373621
|
taskId,
|
|
373314
|
-
prompt,
|
|
373622
|
+
prompt: canonicalPrompt,
|
|
373315
373623
|
reason,
|
|
373316
373624
|
...detail ? { detail } : {},
|
|
373317
|
-
partialOutput,
|
|
373318
|
-
completed
|
|
373625
|
+
partialOutput: carriedPartial,
|
|
373626
|
+
completed
|
|
373319
373627
|
});
|
|
373628
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
373629
|
+
return saved === true;
|
|
373320
373630
|
} catch {
|
|
373631
|
+
return false;
|
|
373321
373632
|
}
|
|
373322
373633
|
}
|
|
373323
373634
|
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
@@ -373355,11 +373666,55 @@ ${input}` }],
|
|
|
373355
373666
|
* checkpoint, so the same finished work is never restored into two runs.
|
|
373356
373667
|
*/
|
|
373357
373668
|
async prepareDurableResume(opts = {}) {
|
|
373358
|
-
|
|
373359
|
-
|
|
373360
|
-
|
|
373361
|
-
|
|
373362
|
-
|
|
373669
|
+
await this.resumeStore?.reclaimStale();
|
|
373670
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
373671
|
+
if (!claimed) return null;
|
|
373672
|
+
try {
|
|
373673
|
+
this.lastInterruptedRun = void 0;
|
|
373674
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
373675
|
+
const prompt = this.buildResumePrompt(
|
|
373676
|
+
claimed.checkpoint.prompt,
|
|
373677
|
+
claimed.checkpoint.partialOutput,
|
|
373678
|
+
claimed.checkpoint.completed
|
|
373679
|
+
);
|
|
373680
|
+
this.resumedFrom = {
|
|
373681
|
+
prompt: claimed.checkpoint.prompt,
|
|
373682
|
+
completed: claimed.checkpoint.completed,
|
|
373683
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
373684
|
+
};
|
|
373685
|
+
this.claimedResumeId = claimed.claimId;
|
|
373686
|
+
return prompt;
|
|
373687
|
+
} catch (err) {
|
|
373688
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
373689
|
+
throw err;
|
|
373690
|
+
}
|
|
373691
|
+
}
|
|
373692
|
+
/**
|
|
373693
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
373694
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
373695
|
+
* run loop; safe to call when nothing is claimed.
|
|
373696
|
+
*/
|
|
373697
|
+
async settleClaimedResume(succeeded) {
|
|
373698
|
+
const claimId = this.claimedResumeId;
|
|
373699
|
+
if (!claimId) return;
|
|
373700
|
+
this.claimedResumeId = void 0;
|
|
373701
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
373702
|
+
await this.resumeStore?.settle(claimId);
|
|
373703
|
+
} else {
|
|
373704
|
+
await this.resumeStore?.release(claimId);
|
|
373705
|
+
}
|
|
373706
|
+
}
|
|
373707
|
+
/**
|
|
373708
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
373709
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
373710
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
373711
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
373712
|
+
*/
|
|
373713
|
+
async releaseClaimedResume() {
|
|
373714
|
+
const claimId = this.claimedResumeId;
|
|
373715
|
+
if (!claimId) return;
|
|
373716
|
+
this.claimedResumeId = void 0;
|
|
373717
|
+
await this.resumeStore?.release(claimId);
|
|
373363
373718
|
}
|
|
373364
373719
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
373365
373720
|
raiseBudgetForResume(maxTokens) {
|
|
@@ -373395,7 +373750,12 @@ ${partialOutput}` : ""
|
|
|
373395
373750
|
async resumeRun(opts = {}) {
|
|
373396
373751
|
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
373397
373752
|
if (!prompt) return null;
|
|
373398
|
-
|
|
373753
|
+
try {
|
|
373754
|
+
return await this.run({ prompt });
|
|
373755
|
+
} catch (err) {
|
|
373756
|
+
await this.releaseClaimedResume();
|
|
373757
|
+
throw err;
|
|
373758
|
+
}
|
|
373399
373759
|
}
|
|
373400
373760
|
getWorkspacePath() {
|
|
373401
373761
|
return this.workspacePath;
|
|
@@ -373765,6 +374125,9 @@ ${prompt}` : prompt;
|
|
|
373765
374125
|
const taskId = crypto6.randomUUID();
|
|
373766
374126
|
this.decisionLog = [];
|
|
373767
374127
|
this.currentRunCompleted = [];
|
|
374128
|
+
this.wroteCheckpointThisRun = false;
|
|
374129
|
+
this.completedNormally = false;
|
|
374130
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
373768
374131
|
if (options.fastAnswer) {
|
|
373769
374132
|
return this.runFastAnswer(options, startMs, taskId);
|
|
373770
374133
|
}
|
|
@@ -374043,6 +374406,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial2;
|
|
|
374043
374406
|
${finalOutput}` : ""
|
|
374044
374407
|
].join("\n").trimEnd();
|
|
374045
374408
|
}
|
|
374409
|
+
this.completedNormally = trip == null;
|
|
374046
374410
|
} catch (err) {
|
|
374047
374411
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
374048
374412
|
this.emit("run:cancelled", {
|
|
@@ -374084,6 +374448,10 @@ ${finalOutput}` : ""
|
|
|
374084
374448
|
escalator.cancelAllPending();
|
|
374085
374449
|
} catch {
|
|
374086
374450
|
}
|
|
374451
|
+
try {
|
|
374452
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
374453
|
+
} catch {
|
|
374454
|
+
}
|
|
374087
374455
|
try {
|
|
374088
374456
|
this.releasePendingEscalations();
|
|
374089
374457
|
} catch {
|
|
@@ -376493,8 +376861,14 @@ var DashboardSocket = class {
|
|
|
376493
376861
|
emitCascadeEvent(ev) {
|
|
376494
376862
|
this.io.emit("cascade:event", ev);
|
|
376495
376863
|
}
|
|
376496
|
-
|
|
376497
|
-
|
|
376864
|
+
/**
|
|
376865
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
376866
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
376867
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
376868
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
376869
|
+
*/
|
|
376870
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
376871
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
376498
376872
|
this.io.emit("tier:status", payload);
|
|
376499
376873
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
376500
376874
|
}
|