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/cli.js
CHANGED
|
@@ -1757,7 +1757,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1757
1757
|
var init_constants = __esm({
|
|
1758
1758
|
"src/constants.ts"() {
|
|
1759
1759
|
init_pricing();
|
|
1760
|
-
CASCADE_VERSION = "0.
|
|
1760
|
+
CASCADE_VERSION = "0.69.0";
|
|
1761
1761
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1762
1762
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1763
1763
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -8482,6 +8482,21 @@ var BaseTier = class extends EventEmitter {
|
|
|
8482
8482
|
* which node (Cockpit node panel / Why panel).
|
|
8483
8483
|
*/
|
|
8484
8484
|
servingModel;
|
|
8485
|
+
/**
|
|
8486
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
8487
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
8488
|
+
*
|
|
8489
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
8490
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
8491
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
8492
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
8493
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
8494
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
8495
|
+
* appears in the stream.
|
|
8496
|
+
*/
|
|
8497
|
+
graphNodeId;
|
|
8498
|
+
graphDependsOn;
|
|
8499
|
+
graphWave;
|
|
8485
8500
|
constructor(role, id, parentId) {
|
|
8486
8501
|
super();
|
|
8487
8502
|
this.role = role;
|
|
@@ -8493,6 +8508,33 @@ var BaseTier = class extends EventEmitter {
|
|
|
8493
8508
|
setPresenter(on = true) {
|
|
8494
8509
|
this.isPresenter = on;
|
|
8495
8510
|
}
|
|
8511
|
+
/**
|
|
8512
|
+
* Record where this tier sits in the compiled task graph.
|
|
8513
|
+
*
|
|
8514
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
8515
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
8516
|
+
* are set independently rather than all at once.
|
|
8517
|
+
*/
|
|
8518
|
+
setGraphPosition(position) {
|
|
8519
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
8520
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
8521
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
8522
|
+
}
|
|
8523
|
+
/**
|
|
8524
|
+
* The graph fields, for spreading into a status payload.
|
|
8525
|
+
*
|
|
8526
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
8527
|
+
* two different call sites with two different payload shapes; adding a field
|
|
8528
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
8529
|
+
* consumer than never arriving at all.
|
|
8530
|
+
*/
|
|
8531
|
+
graphFields() {
|
|
8532
|
+
return {
|
|
8533
|
+
nodeId: this.graphNodeId,
|
|
8534
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
8535
|
+
waveId: this.graphWave
|
|
8536
|
+
};
|
|
8537
|
+
}
|
|
8496
8538
|
getStatus() {
|
|
8497
8539
|
return this.status;
|
|
8498
8540
|
}
|
|
@@ -8507,7 +8549,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
8507
8549
|
status,
|
|
8508
8550
|
timestamp,
|
|
8509
8551
|
output,
|
|
8510
|
-
model: this.servingModel
|
|
8552
|
+
model: this.servingModel,
|
|
8553
|
+
...this.graphFields()
|
|
8511
8554
|
};
|
|
8512
8555
|
this.emit("status", event);
|
|
8513
8556
|
this.emit("tier:status", event);
|
|
@@ -8547,7 +8590,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
8547
8590
|
progressPct: update.progressPct,
|
|
8548
8591
|
timestamp,
|
|
8549
8592
|
output: update.output,
|
|
8550
|
-
model: this.servingModel
|
|
8593
|
+
model: this.servingModel,
|
|
8594
|
+
...this.graphFields()
|
|
8551
8595
|
});
|
|
8552
8596
|
}
|
|
8553
8597
|
buildMessage(type, to, payload) {
|
|
@@ -9450,7 +9494,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
9450
9494
|
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
9451
9495
|
}
|
|
9452
9496
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9453
|
-
const
|
|
9497
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
9498
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9454
9499
|
checksRun.push(...testResult.checksRun);
|
|
9455
9500
|
passed.push(...testResult.passed);
|
|
9456
9501
|
failed.push(...testResult.failed);
|
|
@@ -9459,7 +9504,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
9459
9504
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
9460
9505
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
9461
9506
|
output = corrected;
|
|
9462
|
-
const retest = await this.selfTest(assignment, output);
|
|
9507
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9463
9508
|
passed.push(...retest.passed);
|
|
9464
9509
|
if (retest.failed.length > 0) {
|
|
9465
9510
|
failed.push(...retest.failed);
|
|
@@ -11384,6 +11429,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11384
11429
|
for (const a of assignments) {
|
|
11385
11430
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
11386
11431
|
if (this.store) worker.setStore(this.store, taskId);
|
|
11432
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
11387
11433
|
worker.setPeerBus(this.t3PeerBus);
|
|
11388
11434
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
11389
11435
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -11504,6 +11550,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11504
11550
|
runnableIds.push(fallbackId);
|
|
11505
11551
|
}
|
|
11506
11552
|
wave++;
|
|
11553
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
11507
11554
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
11508
11555
|
this.sendStatusUpdate({
|
|
11509
11556
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -11834,23 +11881,63 @@ var DependencyScheduler = class {
|
|
|
11834
11881
|
dependents.get(dependencyId)?.push(node.id);
|
|
11835
11882
|
}
|
|
11836
11883
|
}
|
|
11837
|
-
const
|
|
11884
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
11885
|
+
const settled = /* @__PURE__ */ new Set();
|
|
11838
11886
|
const results = /* @__PURE__ */ new Map();
|
|
11887
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
11839
11888
|
const waves = [];
|
|
11840
11889
|
let wave = 0;
|
|
11841
|
-
|
|
11842
|
-
const
|
|
11890
|
+
const blockDescendants = async (failedIds) => {
|
|
11891
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
11892
|
+
const queue = [];
|
|
11893
|
+
for (const rootId of failedIds) {
|
|
11894
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
11895
|
+
}
|
|
11896
|
+
while (queue.length > 0) {
|
|
11897
|
+
const { id, root } = queue.shift();
|
|
11898
|
+
if (settled.has(id)) continue;
|
|
11899
|
+
const known = rootsFor.get(id);
|
|
11900
|
+
if (known) {
|
|
11901
|
+
if (!known.has(root)) known.add(root);
|
|
11902
|
+
continue;
|
|
11903
|
+
}
|
|
11904
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
11905
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
11906
|
+
}
|
|
11907
|
+
for (const node of nodes) {
|
|
11908
|
+
const roots = rootsFor.get(node.id);
|
|
11909
|
+
if (!roots || settled.has(node.id)) continue;
|
|
11910
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
11911
|
+
const blockedBy = causes.map((n) => n.id);
|
|
11912
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
11913
|
+
const record = {
|
|
11914
|
+
blockedBy,
|
|
11915
|
+
blockedByTitles,
|
|
11916
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
11917
|
+
};
|
|
11918
|
+
blocked.set(node.id, record);
|
|
11919
|
+
settled.add(node.id);
|
|
11920
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
11921
|
+
}
|
|
11922
|
+
};
|
|
11923
|
+
while (settled.size < nodes.length) {
|
|
11924
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
11843
11925
|
if (ready.length === 0) {
|
|
11844
11926
|
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
11845
11927
|
}
|
|
11846
11928
|
waves.push(ready.map((node) => node.id));
|
|
11847
11929
|
await this.hooks.onWaveStart?.(ready, wave);
|
|
11930
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
11848
11931
|
const runOne = async (node) => {
|
|
11849
11932
|
const result = await this.hooks.execute(node);
|
|
11850
11933
|
results.set(node.id, result);
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11934
|
+
settled.add(node.id);
|
|
11935
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
11936
|
+
outcomes.set(node.id, outcome);
|
|
11937
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
11938
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
11939
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
11940
|
+
}
|
|
11854
11941
|
}
|
|
11855
11942
|
await this.hooks.onNodeComplete?.(node, result);
|
|
11856
11943
|
};
|
|
@@ -11859,9 +11946,13 @@ var DependencyScheduler = class {
|
|
|
11859
11946
|
} else {
|
|
11860
11947
|
await Promise.all(ready.map(runOne));
|
|
11861
11948
|
}
|
|
11949
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
11950
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
11951
|
+
await blockDescendants(failedThisWave);
|
|
11952
|
+
}
|
|
11862
11953
|
wave++;
|
|
11863
11954
|
}
|
|
11864
|
-
return { results, waves };
|
|
11955
|
+
return { results, waves, blocked };
|
|
11865
11956
|
}
|
|
11866
11957
|
};
|
|
11867
11958
|
|
|
@@ -11962,6 +12053,9 @@ QUALITY RULES:
|
|
|
11962
12053
|
|
|
11963
12054
|
${generation}` : ""}`;
|
|
11964
12055
|
}
|
|
12056
|
+
function producedOutput(result) {
|
|
12057
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
12058
|
+
}
|
|
11965
12059
|
var T1Administrator = class extends BaseTier {
|
|
11966
12060
|
router;
|
|
11967
12061
|
toolRegistry;
|
|
@@ -12096,7 +12190,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
12096
12190
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
12097
12191
|
let pass = 1;
|
|
12098
12192
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
12099
|
-
const okCount = (rs) => rs.filter(
|
|
12193
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
12100
12194
|
while (pass <= maxReplanPasses) {
|
|
12101
12195
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
12102
12196
|
if (reviewResult.approved) {
|
|
@@ -12227,7 +12321,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
12227
12321
|
/** Structured, grounded summary of what's already done — used to keep
|
|
12228
12322
|
* corrective replan passes from re-emitting completed sections. */
|
|
12229
12323
|
summarizeCompletedSections(results) {
|
|
12230
|
-
const done = results.filter(
|
|
12324
|
+
const done = results.filter(producedOutput);
|
|
12231
12325
|
if (done.length === 0) return "";
|
|
12232
12326
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
12233
12327
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -12372,6 +12466,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12372
12466
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
12373
12467
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
12374
12468
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
12469
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
12375
12470
|
if (this.store) {
|
|
12376
12471
|
manager.setStore(this.store);
|
|
12377
12472
|
}
|
|
@@ -12498,6 +12593,13 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12498
12593
|
const scheduler = new DependencyScheduler(
|
|
12499
12594
|
compiled.graph,
|
|
12500
12595
|
{
|
|
12596
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
12597
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
12598
|
+
// Stamped before the wave executes so the first status event a node
|
|
12599
|
+
// emits already carries it.
|
|
12600
|
+
onWaveStart: (nodes, wave) => {
|
|
12601
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
12602
|
+
},
|
|
12501
12603
|
execute: async (node) => {
|
|
12502
12604
|
const section = node.payload;
|
|
12503
12605
|
const manager = managers[node.ordinal];
|
|
@@ -12532,6 +12634,62 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12532
12634
|
};
|
|
12533
12635
|
}
|
|
12534
12636
|
},
|
|
12637
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
12638
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
12639
|
+
// the user more than letting it try.
|
|
12640
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
12641
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
12642
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
12643
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
12644
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
12645
|
+
// should have been skipped would run and be billed.
|
|
12646
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
12647
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
12648
|
+
const section = node.payload;
|
|
12649
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
12650
|
+
resultMap.set(node.id, {
|
|
12651
|
+
sectionId: section.sectionId,
|
|
12652
|
+
sectionTitle: section.sectionTitle,
|
|
12653
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
12654
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
12655
|
+
// that did (the upstream section named below).
|
|
12656
|
+
status: "BLOCKED",
|
|
12657
|
+
t3Results: [],
|
|
12658
|
+
sectionSummary: "",
|
|
12659
|
+
issues: [
|
|
12660
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
12661
|
+
],
|
|
12662
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
12663
|
+
});
|
|
12664
|
+
completedSections++;
|
|
12665
|
+
this.sendStatusUpdate({
|
|
12666
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
12667
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
12668
|
+
status: "IN_PROGRESS"
|
|
12669
|
+
});
|
|
12670
|
+
this.emit("tier:status", {
|
|
12671
|
+
tierId: section.sectionId,
|
|
12672
|
+
// A blocked section has no tier instance — it was never
|
|
12673
|
+
// constructed — so its runtime id and its graph id are the same
|
|
12674
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
12675
|
+
// stream addressable in the graph's id space, which is what
|
|
12676
|
+
// dependsOn edges point at.
|
|
12677
|
+
nodeId: section.sectionId,
|
|
12678
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
12679
|
+
role: "T2",
|
|
12680
|
+
label: section.sectionTitle,
|
|
12681
|
+
status: "BLOCKED",
|
|
12682
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12683
|
+
output: blockedInfo.reason
|
|
12684
|
+
});
|
|
12685
|
+
this.emit("section:complete", {
|
|
12686
|
+
id: node.id,
|
|
12687
|
+
title: section.sectionTitle,
|
|
12688
|
+
status: "BLOCKED",
|
|
12689
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12690
|
+
output: ""
|
|
12691
|
+
});
|
|
12692
|
+
},
|
|
12535
12693
|
onNodeComplete: (node, result) => {
|
|
12536
12694
|
resultMap.set(node.id, result);
|
|
12537
12695
|
completedSections++;
|
|
@@ -12548,14 +12706,18 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12548
12706
|
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
12549
12707
|
// serialized T3 workers within a single section while every section's
|
|
12550
12708
|
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
12551
|
-
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
12709
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
12710
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
12711
|
+
// output. Running it after the dependency failed produces a second
|
|
12712
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
12713
|
+
onUpstreamFailure: "block"
|
|
12552
12714
|
}
|
|
12553
12715
|
);
|
|
12554
12716
|
await scheduler.run();
|
|
12555
12717
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
12556
12718
|
}
|
|
12557
12719
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
12558
|
-
const completedSections = t2Results.filter(
|
|
12720
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
12559
12721
|
if (!completedSections.length) {
|
|
12560
12722
|
const allIssues = [
|
|
12561
12723
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -12575,7 +12737,6 @@ ${r.sectionSummary}
|
|
|
12575
12737
|
Outputs:
|
|
12576
12738
|
${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");
|
|
12577
12739
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
12578
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
12579
12740
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
12580
12741
|
|
|
12581
12742
|
Original request: ${originalPrompt}
|
|
@@ -13070,6 +13231,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
13070
13231
|
};
|
|
13071
13232
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
13072
13233
|
var MAX_VIDEO_SECONDS = 8;
|
|
13234
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
13073
13235
|
var GenerateVideoTool = class extends BaseTool {
|
|
13074
13236
|
constructor(deps) {
|
|
13075
13237
|
super();
|
|
@@ -13086,7 +13248,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13086
13248
|
type: "object",
|
|
13087
13249
|
properties: {
|
|
13088
13250
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
13089
|
-
seconds: { type: "number", description: `Clip length,
|
|
13251
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
13090
13252
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
13091
13253
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
13092
13254
|
},
|
|
@@ -13098,7 +13260,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13098
13260
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
13099
13261
|
if (!r.ok) return r.error;
|
|
13100
13262
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
13101
|
-
const seconds = Math.max(
|
|
13263
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
13102
13264
|
const asset = await generateVideo(
|
|
13103
13265
|
r.selected.capability,
|
|
13104
13266
|
r.cfg,
|
|
@@ -13115,7 +13277,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13115
13277
|
const location = await this.deps.sink(asset);
|
|
13116
13278
|
const unit = r.selected.cost;
|
|
13117
13279
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
13118
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
13280
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
13119
13281
|
return `Video generated and saved to ${location}.
|
|
13120
13282
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
13121
13283
|
}
|
|
@@ -16256,7 +16418,24 @@ var TaskAnalyzer = class {
|
|
|
16256
16418
|
feedback;
|
|
16257
16419
|
bias;
|
|
16258
16420
|
lastProfile = null;
|
|
16259
|
-
|
|
16421
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
16422
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
16423
|
+
/**
|
|
16424
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
16425
|
+
* type they were chosen for.
|
|
16426
|
+
*
|
|
16427
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
16428
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
16429
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
16430
|
+
* model is good at creative writing, from a rating that never said so.
|
|
16431
|
+
*
|
|
16432
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
16433
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
16434
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
16435
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
16436
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
16437
|
+
*/
|
|
16438
|
+
lastCompletedRun;
|
|
16260
16439
|
constructor(tracker, bias = "balanced") {
|
|
16261
16440
|
this.tracker = tracker;
|
|
16262
16441
|
this.bias = bias;
|
|
@@ -16303,11 +16482,15 @@ var TaskAnalyzer = class {
|
|
|
16303
16482
|
*/
|
|
16304
16483
|
async selectModel(prompt, tier, selector, opts) {
|
|
16305
16484
|
const profile = await this.analyze(prompt);
|
|
16485
|
+
const recordSelection = (model) => {
|
|
16486
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
16487
|
+
return model;
|
|
16488
|
+
};
|
|
16306
16489
|
if (profile.requiresVision) {
|
|
16307
|
-
return selector.selectVisionModel();
|
|
16490
|
+
return recordSelection(selector.selectVisionModel());
|
|
16308
16491
|
}
|
|
16309
16492
|
let candidates2 = selector.getCandidatesForTier(tier);
|
|
16310
|
-
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
16493
|
+
if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
16311
16494
|
if (opts?.requiresToolUse) {
|
|
16312
16495
|
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
16313
16496
|
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
@@ -16317,9 +16500,7 @@ var TaskAnalyzer = class {
|
|
|
16317
16500
|
score: this.scoreModel(m, profile)
|
|
16318
16501
|
}));
|
|
16319
16502
|
scored.sort((a, b) => b.score - a.score);
|
|
16320
|
-
|
|
16321
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
16322
|
-
return best;
|
|
16503
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
16323
16504
|
}
|
|
16324
16505
|
/**
|
|
16325
16506
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -16328,25 +16509,38 @@ var TaskAnalyzer = class {
|
|
|
16328
16509
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
16329
16510
|
if (!this.tracker || !this.lastProfile) return;
|
|
16330
16511
|
const taskType = this.lastProfile.type;
|
|
16331
|
-
for (const [tier, model] of this.
|
|
16512
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
16332
16513
|
const cost = costByTier[tier] ?? 0;
|
|
16333
16514
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
16334
16515
|
}
|
|
16335
|
-
this.
|
|
16516
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
16517
|
+
this.currentRunSelections.clear();
|
|
16336
16518
|
void this.tracker.save();
|
|
16337
16519
|
}
|
|
16338
16520
|
/**
|
|
16339
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
16521
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
16340
16522
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
16341
|
-
*
|
|
16523
|
+
*
|
|
16524
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
16525
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
16526
|
+
* empty.
|
|
16527
|
+
*
|
|
16528
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
16529
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
16530
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
16531
|
+
* that reports false, which is what "rate this run" should mean.
|
|
16342
16532
|
*/
|
|
16343
16533
|
recordExplicitRating(rating) {
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16534
|
+
const snapshot = this.lastCompletedRun;
|
|
16535
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
16536
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
16537
|
+
for (const [, model] of snapshot.selections) {
|
|
16538
|
+
if (ratedModels.has(model.id)) continue;
|
|
16539
|
+
ratedModels.add(model.id);
|
|
16540
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
16541
|
+
}
|
|
16542
|
+
this.lastCompletedRun = void 0;
|
|
16543
|
+
return true;
|
|
16350
16544
|
}
|
|
16351
16545
|
scoreModel(model, profile) {
|
|
16352
16546
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -17387,6 +17581,8 @@ var RESUME_SCHEMA_VERSION = 1;
|
|
|
17387
17581
|
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
17388
17582
|
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17389
17583
|
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
17584
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
17585
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
17390
17586
|
function safeId(taskId) {
|
|
17391
17587
|
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
17392
17588
|
return cleaned.slice(0, 100) || "run";
|
|
@@ -17401,16 +17597,24 @@ var ResumeStore = class {
|
|
|
17401
17597
|
maxCheckpoints;
|
|
17402
17598
|
maxAgeMs;
|
|
17403
17599
|
now;
|
|
17600
|
+
claimTimeoutMs;
|
|
17404
17601
|
constructor(options) {
|
|
17405
17602
|
this.dir = options.dir;
|
|
17406
17603
|
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
17407
17604
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
17408
17605
|
this.now = options.now ?? Date.now;
|
|
17606
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
17409
17607
|
}
|
|
17410
17608
|
/**
|
|
17411
|
-
* Persist a checkpoint and prune.
|
|
17412
|
-
*
|
|
17413
|
-
*
|
|
17609
|
+
* Persist a checkpoint and prune.
|
|
17610
|
+
*
|
|
17611
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
17612
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
17613
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
17614
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
17615
|
+
* look alike to them.
|
|
17616
|
+
*
|
|
17617
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
17414
17618
|
*/
|
|
17415
17619
|
async save(checkpoint) {
|
|
17416
17620
|
try {
|
|
@@ -17426,7 +17630,9 @@ var ResumeStore = class {
|
|
|
17426
17630
|
await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
17427
17631
|
await promises.rename(tempPath, finalPath);
|
|
17428
17632
|
await this.prune();
|
|
17633
|
+
return true;
|
|
17429
17634
|
} catch {
|
|
17635
|
+
return false;
|
|
17430
17636
|
}
|
|
17431
17637
|
}
|
|
17432
17638
|
/** Valid, unexpired checkpoints, newest first. */
|
|
@@ -17456,15 +17662,87 @@ var ResumeStore = class {
|
|
|
17456
17662
|
return (await this.list())[0] ?? null;
|
|
17457
17663
|
}
|
|
17458
17664
|
/**
|
|
17459
|
-
*
|
|
17460
|
-
*
|
|
17665
|
+
* Claim a checkpoint for a resume attempt.
|
|
17666
|
+
*
|
|
17667
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
17668
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
17669
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
17670
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
17671
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
17672
|
+
* wrong trade for a recovery mechanism.
|
|
17673
|
+
*
|
|
17674
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
17675
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
17676
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
17677
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
17678
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
17461
17679
|
*/
|
|
17462
|
-
async
|
|
17680
|
+
async claim(taskId) {
|
|
17463
17681
|
const all = await this.list();
|
|
17464
17682
|
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
17465
17683
|
if (!target) return null;
|
|
17466
|
-
|
|
17467
|
-
|
|
17684
|
+
try {
|
|
17685
|
+
const entries = await promises.readdir(this.dir);
|
|
17686
|
+
const wanted = safeId(target.taskId);
|
|
17687
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
17688
|
+
if (!source) return null;
|
|
17689
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
17690
|
+
await promises.rename(path34.join(this.dir, source), path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
17691
|
+
return { checkpoint: target, claimId };
|
|
17692
|
+
} catch {
|
|
17693
|
+
return null;
|
|
17694
|
+
}
|
|
17695
|
+
}
|
|
17696
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
17697
|
+
async release(claimId) {
|
|
17698
|
+
try {
|
|
17699
|
+
await promises.rename(
|
|
17700
|
+
path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
17701
|
+
path34.join(this.dir, `${claimId}.json`)
|
|
17702
|
+
);
|
|
17703
|
+
} catch {
|
|
17704
|
+
}
|
|
17705
|
+
}
|
|
17706
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
17707
|
+
async settle(claimId) {
|
|
17708
|
+
try {
|
|
17709
|
+
await promises.rm(path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
17710
|
+
} catch {
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
17713
|
+
/**
|
|
17714
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
17715
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
17716
|
+
*/
|
|
17717
|
+
async consume(taskId) {
|
|
17718
|
+
const claimed = await this.claim(taskId);
|
|
17719
|
+
if (!claimed) return null;
|
|
17720
|
+
await this.settle(claimed.claimId);
|
|
17721
|
+
return claimed.checkpoint;
|
|
17722
|
+
}
|
|
17723
|
+
/**
|
|
17724
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
17725
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
17726
|
+
* trading one loss window for another.
|
|
17727
|
+
*/
|
|
17728
|
+
async reclaimStale() {
|
|
17729
|
+
let reclaimed = 0;
|
|
17730
|
+
try {
|
|
17731
|
+
const entries = await promises.readdir(this.dir);
|
|
17732
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
17733
|
+
for (const name of entries) {
|
|
17734
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
17735
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
17736
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
17737
|
+
await promises.rename(
|
|
17738
|
+
path34.join(this.dir, name),
|
|
17739
|
+
path34.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
17740
|
+
);
|
|
17741
|
+
reclaimed++;
|
|
17742
|
+
}
|
|
17743
|
+
} catch {
|
|
17744
|
+
}
|
|
17745
|
+
return reclaimed;
|
|
17468
17746
|
}
|
|
17469
17747
|
/** Remove every checkpoint for a task id. */
|
|
17470
17748
|
async delete(taskId) {
|
|
@@ -18481,6 +18759,25 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
18481
18759
|
lastInterruptedRun;
|
|
18482
18760
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
18483
18761
|
resumeStore;
|
|
18762
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
18763
|
+
claimedResumeId;
|
|
18764
|
+
/**
|
|
18765
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
18766
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
18767
|
+
*/
|
|
18768
|
+
resumedFrom;
|
|
18769
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
18770
|
+
wroteCheckpointThisRun = false;
|
|
18771
|
+
/**
|
|
18772
|
+
* Whether the run reached the end of its normal path.
|
|
18773
|
+
*
|
|
18774
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
18775
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
18776
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
18777
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
18778
|
+
* the original claim and the finished sections with it.
|
|
18779
|
+
*/
|
|
18780
|
+
completedNormally = false;
|
|
18484
18781
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
18485
18782
|
currentRunCompleted = [];
|
|
18486
18783
|
initPromise;
|
|
@@ -18998,17 +19295,31 @@ ${input}` }],
|
|
|
18998
19295
|
* worse. Nothing here is allowed to throw.
|
|
18999
19296
|
*/
|
|
19000
19297
|
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
19001
|
-
|
|
19298
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
19299
|
+
const merged = /* @__PURE__ */ new Map();
|
|
19300
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
19301
|
+
for (const node of this.currentRunCompleted) {
|
|
19302
|
+
const existing = merged.get(node.id);
|
|
19303
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
19304
|
+
merged.set(node.id, node);
|
|
19305
|
+
}
|
|
19306
|
+
const completed = [...merged.values()];
|
|
19307
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
19308
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
19309
|
+
if (!completed.length && !carriedPartial) return false;
|
|
19002
19310
|
try {
|
|
19003
|
-
await this.resumeStore?.save({
|
|
19311
|
+
const saved = await this.resumeStore?.save({
|
|
19004
19312
|
taskId,
|
|
19005
|
-
prompt,
|
|
19313
|
+
prompt: canonicalPrompt,
|
|
19006
19314
|
reason,
|
|
19007
19315
|
...detail ? { detail } : {},
|
|
19008
|
-
partialOutput,
|
|
19009
|
-
completed
|
|
19316
|
+
partialOutput: carriedPartial,
|
|
19317
|
+
completed
|
|
19010
19318
|
});
|
|
19319
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
19320
|
+
return saved === true;
|
|
19011
19321
|
} catch {
|
|
19322
|
+
return false;
|
|
19012
19323
|
}
|
|
19013
19324
|
}
|
|
19014
19325
|
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
@@ -19046,11 +19357,55 @@ ${input}` }],
|
|
|
19046
19357
|
* checkpoint, so the same finished work is never restored into two runs.
|
|
19047
19358
|
*/
|
|
19048
19359
|
async prepareDurableResume(opts = {}) {
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19360
|
+
await this.resumeStore?.reclaimStale();
|
|
19361
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
19362
|
+
if (!claimed) return null;
|
|
19363
|
+
try {
|
|
19364
|
+
this.lastInterruptedRun = void 0;
|
|
19365
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19366
|
+
const prompt = this.buildResumePrompt(
|
|
19367
|
+
claimed.checkpoint.prompt,
|
|
19368
|
+
claimed.checkpoint.partialOutput,
|
|
19369
|
+
claimed.checkpoint.completed
|
|
19370
|
+
);
|
|
19371
|
+
this.resumedFrom = {
|
|
19372
|
+
prompt: claimed.checkpoint.prompt,
|
|
19373
|
+
completed: claimed.checkpoint.completed,
|
|
19374
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
19375
|
+
};
|
|
19376
|
+
this.claimedResumeId = claimed.claimId;
|
|
19377
|
+
return prompt;
|
|
19378
|
+
} catch (err) {
|
|
19379
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
19380
|
+
throw err;
|
|
19381
|
+
}
|
|
19382
|
+
}
|
|
19383
|
+
/**
|
|
19384
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
19385
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
19386
|
+
* run loop; safe to call when nothing is claimed.
|
|
19387
|
+
*/
|
|
19388
|
+
async settleClaimedResume(succeeded) {
|
|
19389
|
+
const claimId = this.claimedResumeId;
|
|
19390
|
+
if (!claimId) return;
|
|
19391
|
+
this.claimedResumeId = void 0;
|
|
19392
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
19393
|
+
await this.resumeStore?.settle(claimId);
|
|
19394
|
+
} else {
|
|
19395
|
+
await this.resumeStore?.release(claimId);
|
|
19396
|
+
}
|
|
19397
|
+
}
|
|
19398
|
+
/**
|
|
19399
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
19400
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
19401
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
19402
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
19403
|
+
*/
|
|
19404
|
+
async releaseClaimedResume() {
|
|
19405
|
+
const claimId = this.claimedResumeId;
|
|
19406
|
+
if (!claimId) return;
|
|
19407
|
+
this.claimedResumeId = void 0;
|
|
19408
|
+
await this.resumeStore?.release(claimId);
|
|
19054
19409
|
}
|
|
19055
19410
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
19056
19411
|
raiseBudgetForResume(maxTokens) {
|
|
@@ -19086,7 +19441,12 @@ ${partialOutput}` : ""
|
|
|
19086
19441
|
async resumeRun(opts = {}) {
|
|
19087
19442
|
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
19088
19443
|
if (!prompt) return null;
|
|
19089
|
-
|
|
19444
|
+
try {
|
|
19445
|
+
return await this.run({ prompt });
|
|
19446
|
+
} catch (err) {
|
|
19447
|
+
await this.releaseClaimedResume();
|
|
19448
|
+
throw err;
|
|
19449
|
+
}
|
|
19090
19450
|
}
|
|
19091
19451
|
getWorkspacePath() {
|
|
19092
19452
|
return this.workspacePath;
|
|
@@ -19456,6 +19816,9 @@ ${prompt}` : prompt;
|
|
|
19456
19816
|
const taskId = randomUUID();
|
|
19457
19817
|
this.decisionLog = [];
|
|
19458
19818
|
this.currentRunCompleted = [];
|
|
19819
|
+
this.wroteCheckpointThisRun = false;
|
|
19820
|
+
this.completedNormally = false;
|
|
19821
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
19459
19822
|
if (options.fastAnswer) {
|
|
19460
19823
|
return this.runFastAnswer(options, startMs, taskId);
|
|
19461
19824
|
}
|
|
@@ -19734,6 +20097,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19734
20097
|
${finalOutput}` : ""
|
|
19735
20098
|
].join("\n").trimEnd();
|
|
19736
20099
|
}
|
|
20100
|
+
this.completedNormally = trip == null;
|
|
19737
20101
|
} catch (err) {
|
|
19738
20102
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
19739
20103
|
this.emit("run:cancelled", {
|
|
@@ -19775,6 +20139,10 @@ ${finalOutput}` : ""
|
|
|
19775
20139
|
escalator.cancelAllPending();
|
|
19776
20140
|
} catch {
|
|
19777
20141
|
}
|
|
20142
|
+
try {
|
|
20143
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
20144
|
+
} catch {
|
|
20145
|
+
}
|
|
19778
20146
|
try {
|
|
19779
20147
|
this.releasePendingEscalations();
|
|
19780
20148
|
} catch {
|
|
@@ -20965,7 +21333,8 @@ function AgentTreeInternal({
|
|
|
20965
21333
|
] }),
|
|
20966
21334
|
node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20967
21335
|
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
|
|
20968
|
-
node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" })
|
|
21336
|
+
node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" }),
|
|
21337
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20969
21338
|
] }, `t2-${node.id}-${i}`);
|
|
20970
21339
|
}
|
|
20971
21340
|
if (row.type === "t3") {
|
|
@@ -20990,7 +21359,8 @@ function AgentTreeInternal({
|
|
|
20990
21359
|
/* @__PURE__ */ jsx(Spinner, { type: "dots" })
|
|
20991
21360
|
] }),
|
|
20992
21361
|
node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20993
|
-
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" })
|
|
21362
|
+
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
|
|
21363
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20994
21364
|
] }, `t3-${node.id}-${i}`);
|
|
20995
21365
|
}
|
|
20996
21366
|
return null;
|
|
@@ -21989,7 +22359,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21989
22359
|
}, []);
|
|
21990
22360
|
const recordNodeEvent = useCallback((event) => {
|
|
21991
22361
|
const existing = treeNodesRef.current.get(event.tierId);
|
|
21992
|
-
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, children: [] };
|
|
22362
|
+
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, nodeId: event.nodeId ?? existing?.nodeId, dependsOn: event.dependsOn ?? existing?.dependsOn, waveId: event.waveId ?? existing?.waveId, blockedBy: event.blockedBy ?? existing?.blockedBy, children: [] };
|
|
21993
22363
|
treeNodesRef.current.set(event.tierId, node);
|
|
21994
22364
|
const store = storeRef.current;
|
|
21995
22365
|
if (store) {
|
|
@@ -22326,13 +22696,21 @@ ${lastUser.content}`;
|
|
|
22326
22696
|
onContinue: async (args) => {
|
|
22327
22697
|
const cascade = cascadeRef.current;
|
|
22328
22698
|
if (!cascade) return "Not ready yet.";
|
|
22699
|
+
if (state.isExecuting) {
|
|
22700
|
+
return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
|
|
22701
|
+
}
|
|
22329
22702
|
const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
|
|
22330
22703
|
const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
|
|
22331
22704
|
const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
|
|
22332
22705
|
if (!prompt) {
|
|
22333
22706
|
return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
|
|
22334
22707
|
}
|
|
22335
|
-
|
|
22708
|
+
try {
|
|
22709
|
+
await handleSubmit(prompt);
|
|
22710
|
+
} catch (err) {
|
|
22711
|
+
await cascade.releaseClaimedResume();
|
|
22712
|
+
throw err;
|
|
22713
|
+
}
|
|
22336
22714
|
return "";
|
|
22337
22715
|
}
|
|
22338
22716
|
});
|
|
@@ -24825,8 +25203,14 @@ var DashboardSocket = class {
|
|
|
24825
25203
|
emitCascadeEvent(ev) {
|
|
24826
25204
|
this.io.emit("cascade:event", ev);
|
|
24827
25205
|
}
|
|
24828
|
-
|
|
24829
|
-
|
|
25206
|
+
/**
|
|
25207
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
25208
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
25209
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
25210
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
25211
|
+
*/
|
|
25212
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
25213
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
24830
25214
|
this.io.emit("tier:status", payload);
|
|
24831
25215
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
24832
25216
|
}
|