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/cli.cjs
CHANGED
|
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1810
1810
|
var init_constants = __esm({
|
|
1811
1811
|
"src/constants.ts"() {
|
|
1812
1812
|
init_pricing();
|
|
1813
|
-
CASCADE_VERSION = "0.
|
|
1813
|
+
CASCADE_VERSION = "0.70.0";
|
|
1814
1814
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1815
1815
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1816
1816
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -8535,6 +8535,21 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8535
8535
|
* which node (Cockpit node panel / Why panel).
|
|
8536
8536
|
*/
|
|
8537
8537
|
servingModel;
|
|
8538
|
+
/**
|
|
8539
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
8540
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
8541
|
+
*
|
|
8542
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
8543
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
8544
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
8545
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
8546
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
8547
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
8548
|
+
* appears in the stream.
|
|
8549
|
+
*/
|
|
8550
|
+
graphNodeId;
|
|
8551
|
+
graphDependsOn;
|
|
8552
|
+
graphWave;
|
|
8538
8553
|
constructor(role, id, parentId) {
|
|
8539
8554
|
super();
|
|
8540
8555
|
this.role = role;
|
|
@@ -8546,6 +8561,33 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8546
8561
|
setPresenter(on = true) {
|
|
8547
8562
|
this.isPresenter = on;
|
|
8548
8563
|
}
|
|
8564
|
+
/**
|
|
8565
|
+
* Record where this tier sits in the compiled task graph.
|
|
8566
|
+
*
|
|
8567
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
8568
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
8569
|
+
* are set independently rather than all at once.
|
|
8570
|
+
*/
|
|
8571
|
+
setGraphPosition(position) {
|
|
8572
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
8573
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
8574
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
8575
|
+
}
|
|
8576
|
+
/**
|
|
8577
|
+
* The graph fields, for spreading into a status payload.
|
|
8578
|
+
*
|
|
8579
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
8580
|
+
* two different call sites with two different payload shapes; adding a field
|
|
8581
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
8582
|
+
* consumer than never arriving at all.
|
|
8583
|
+
*/
|
|
8584
|
+
graphFields() {
|
|
8585
|
+
return {
|
|
8586
|
+
nodeId: this.graphNodeId,
|
|
8587
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
8588
|
+
waveId: this.graphWave
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8549
8591
|
getStatus() {
|
|
8550
8592
|
return this.status;
|
|
8551
8593
|
}
|
|
@@ -8560,7 +8602,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8560
8602
|
status,
|
|
8561
8603
|
timestamp,
|
|
8562
8604
|
output,
|
|
8563
|
-
model: this.servingModel
|
|
8605
|
+
model: this.servingModel,
|
|
8606
|
+
...this.graphFields()
|
|
8564
8607
|
};
|
|
8565
8608
|
this.emit("status", event);
|
|
8566
8609
|
this.emit("tier:status", event);
|
|
@@ -8600,7 +8643,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8600
8643
|
progressPct: update.progressPct,
|
|
8601
8644
|
timestamp,
|
|
8602
8645
|
output: update.output,
|
|
8603
|
-
model: this.servingModel
|
|
8646
|
+
model: this.servingModel,
|
|
8647
|
+
...this.graphFields()
|
|
8604
8648
|
});
|
|
8605
8649
|
}
|
|
8606
8650
|
buildMessage(type, to, payload) {
|
|
@@ -9503,7 +9547,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
9503
9547
|
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
9504
9548
|
}
|
|
9505
9549
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9506
|
-
const
|
|
9550
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
9551
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9507
9552
|
checksRun.push(...testResult.checksRun);
|
|
9508
9553
|
passed.push(...testResult.passed);
|
|
9509
9554
|
failed.push(...testResult.failed);
|
|
@@ -9512,7 +9557,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
9512
9557
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
9513
9558
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
9514
9559
|
output = corrected;
|
|
9515
|
-
const retest = await this.selfTest(assignment, output);
|
|
9560
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9516
9561
|
passed.push(...retest.passed);
|
|
9517
9562
|
if (retest.failed.length > 0) {
|
|
9518
9563
|
failed.push(...retest.failed);
|
|
@@ -11437,6 +11482,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11437
11482
|
for (const a of assignments) {
|
|
11438
11483
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
11439
11484
|
if (this.store) worker.setStore(this.store, taskId);
|
|
11485
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
11440
11486
|
worker.setPeerBus(this.t3PeerBus);
|
|
11441
11487
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
11442
11488
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -11557,6 +11603,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11557
11603
|
runnableIds.push(fallbackId);
|
|
11558
11604
|
}
|
|
11559
11605
|
wave++;
|
|
11606
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
11560
11607
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
11561
11608
|
this.sendStatusUpdate({
|
|
11562
11609
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -11887,23 +11934,63 @@ var DependencyScheduler = class {
|
|
|
11887
11934
|
dependents.get(dependencyId)?.push(node.id);
|
|
11888
11935
|
}
|
|
11889
11936
|
}
|
|
11890
|
-
const
|
|
11937
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
11938
|
+
const settled = /* @__PURE__ */ new Set();
|
|
11891
11939
|
const results = /* @__PURE__ */ new Map();
|
|
11940
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
11892
11941
|
const waves = [];
|
|
11893
11942
|
let wave = 0;
|
|
11894
|
-
|
|
11895
|
-
const
|
|
11943
|
+
const blockDescendants = async (failedIds) => {
|
|
11944
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
11945
|
+
const queue = [];
|
|
11946
|
+
for (const rootId of failedIds) {
|
|
11947
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
11948
|
+
}
|
|
11949
|
+
while (queue.length > 0) {
|
|
11950
|
+
const { id, root } = queue.shift();
|
|
11951
|
+
if (settled.has(id)) continue;
|
|
11952
|
+
const known = rootsFor.get(id);
|
|
11953
|
+
if (known) {
|
|
11954
|
+
if (!known.has(root)) known.add(root);
|
|
11955
|
+
continue;
|
|
11956
|
+
}
|
|
11957
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
11958
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
11959
|
+
}
|
|
11960
|
+
for (const node of nodes) {
|
|
11961
|
+
const roots = rootsFor.get(node.id);
|
|
11962
|
+
if (!roots || settled.has(node.id)) continue;
|
|
11963
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
11964
|
+
const blockedBy = causes.map((n) => n.id);
|
|
11965
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
11966
|
+
const record = {
|
|
11967
|
+
blockedBy,
|
|
11968
|
+
blockedByTitles,
|
|
11969
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
11970
|
+
};
|
|
11971
|
+
blocked.set(node.id, record);
|
|
11972
|
+
settled.add(node.id);
|
|
11973
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
11974
|
+
}
|
|
11975
|
+
};
|
|
11976
|
+
while (settled.size < nodes.length) {
|
|
11977
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
11896
11978
|
if (ready.length === 0) {
|
|
11897
11979
|
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
11898
11980
|
}
|
|
11899
11981
|
waves.push(ready.map((node) => node.id));
|
|
11900
11982
|
await this.hooks.onWaveStart?.(ready, wave);
|
|
11983
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
11901
11984
|
const runOne = async (node) => {
|
|
11902
11985
|
const result = await this.hooks.execute(node);
|
|
11903
11986
|
results.set(node.id, result);
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11987
|
+
settled.add(node.id);
|
|
11988
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
11989
|
+
outcomes.set(node.id, outcome);
|
|
11990
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
11991
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
11992
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
11993
|
+
}
|
|
11907
11994
|
}
|
|
11908
11995
|
await this.hooks.onNodeComplete?.(node, result);
|
|
11909
11996
|
};
|
|
@@ -11912,9 +11999,13 @@ var DependencyScheduler = class {
|
|
|
11912
11999
|
} else {
|
|
11913
12000
|
await Promise.all(ready.map(runOne));
|
|
11914
12001
|
}
|
|
12002
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
12003
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
12004
|
+
await blockDescendants(failedThisWave);
|
|
12005
|
+
}
|
|
11915
12006
|
wave++;
|
|
11916
12007
|
}
|
|
11917
|
-
return { results, waves };
|
|
12008
|
+
return { results, waves, blocked };
|
|
11918
12009
|
}
|
|
11919
12010
|
};
|
|
11920
12011
|
|
|
@@ -12015,6 +12106,9 @@ QUALITY RULES:
|
|
|
12015
12106
|
|
|
12016
12107
|
${generation}` : ""}`;
|
|
12017
12108
|
}
|
|
12109
|
+
function producedOutput(result) {
|
|
12110
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
12111
|
+
}
|
|
12018
12112
|
var T1Administrator = class extends BaseTier {
|
|
12019
12113
|
router;
|
|
12020
12114
|
toolRegistry;
|
|
@@ -12149,7 +12243,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
12149
12243
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
12150
12244
|
let pass = 1;
|
|
12151
12245
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
12152
|
-
const okCount = (rs) => rs.filter(
|
|
12246
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
12153
12247
|
while (pass <= maxReplanPasses) {
|
|
12154
12248
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
12155
12249
|
if (reviewResult.approved) {
|
|
@@ -12280,7 +12374,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
12280
12374
|
/** Structured, grounded summary of what's already done — used to keep
|
|
12281
12375
|
* corrective replan passes from re-emitting completed sections. */
|
|
12282
12376
|
summarizeCompletedSections(results) {
|
|
12283
|
-
const done = results.filter(
|
|
12377
|
+
const done = results.filter(producedOutput);
|
|
12284
12378
|
if (done.length === 0) return "";
|
|
12285
12379
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
12286
12380
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -12425,6 +12519,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12425
12519
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
12426
12520
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
12427
12521
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
12522
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
12428
12523
|
if (this.store) {
|
|
12429
12524
|
manager.setStore(this.store);
|
|
12430
12525
|
}
|
|
@@ -12551,6 +12646,13 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12551
12646
|
const scheduler = new DependencyScheduler(
|
|
12552
12647
|
compiled.graph,
|
|
12553
12648
|
{
|
|
12649
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
12650
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
12651
|
+
// Stamped before the wave executes so the first status event a node
|
|
12652
|
+
// emits already carries it.
|
|
12653
|
+
onWaveStart: (nodes, wave) => {
|
|
12654
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
12655
|
+
},
|
|
12554
12656
|
execute: async (node) => {
|
|
12555
12657
|
const section = node.payload;
|
|
12556
12658
|
const manager = managers[node.ordinal];
|
|
@@ -12585,6 +12687,62 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12585
12687
|
};
|
|
12586
12688
|
}
|
|
12587
12689
|
},
|
|
12690
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
12691
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
12692
|
+
// the user more than letting it try.
|
|
12693
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
12694
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
12695
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
12696
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
12697
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
12698
|
+
// should have been skipped would run and be billed.
|
|
12699
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
12700
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
12701
|
+
const section = node.payload;
|
|
12702
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
12703
|
+
resultMap.set(node.id, {
|
|
12704
|
+
sectionId: section.sectionId,
|
|
12705
|
+
sectionTitle: section.sectionTitle,
|
|
12706
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
12707
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
12708
|
+
// that did (the upstream section named below).
|
|
12709
|
+
status: "BLOCKED",
|
|
12710
|
+
t3Results: [],
|
|
12711
|
+
sectionSummary: "",
|
|
12712
|
+
issues: [
|
|
12713
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
12714
|
+
],
|
|
12715
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
12716
|
+
});
|
|
12717
|
+
completedSections++;
|
|
12718
|
+
this.sendStatusUpdate({
|
|
12719
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
12720
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
12721
|
+
status: "IN_PROGRESS"
|
|
12722
|
+
});
|
|
12723
|
+
this.emit("tier:status", {
|
|
12724
|
+
tierId: section.sectionId,
|
|
12725
|
+
// A blocked section has no tier instance — it was never
|
|
12726
|
+
// constructed — so its runtime id and its graph id are the same
|
|
12727
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
12728
|
+
// stream addressable in the graph's id space, which is what
|
|
12729
|
+
// dependsOn edges point at.
|
|
12730
|
+
nodeId: section.sectionId,
|
|
12731
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
12732
|
+
role: "T2",
|
|
12733
|
+
label: section.sectionTitle,
|
|
12734
|
+
status: "BLOCKED",
|
|
12735
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12736
|
+
output: blockedInfo.reason
|
|
12737
|
+
});
|
|
12738
|
+
this.emit("section:complete", {
|
|
12739
|
+
id: node.id,
|
|
12740
|
+
title: section.sectionTitle,
|
|
12741
|
+
status: "BLOCKED",
|
|
12742
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12743
|
+
output: ""
|
|
12744
|
+
});
|
|
12745
|
+
},
|
|
12588
12746
|
onNodeComplete: (node, result) => {
|
|
12589
12747
|
resultMap.set(node.id, result);
|
|
12590
12748
|
completedSections++;
|
|
@@ -12601,14 +12759,18 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12601
12759
|
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
12602
12760
|
// serialized T3 workers within a single section while every section's
|
|
12603
12761
|
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
12604
|
-
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel"
|
|
12762
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
12763
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
12764
|
+
// output. Running it after the dependency failed produces a second
|
|
12765
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
12766
|
+
onUpstreamFailure: "block"
|
|
12605
12767
|
}
|
|
12606
12768
|
);
|
|
12607
12769
|
await scheduler.run();
|
|
12608
12770
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
12609
12771
|
}
|
|
12610
12772
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
12611
|
-
const completedSections = t2Results.filter(
|
|
12773
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
12612
12774
|
if (!completedSections.length) {
|
|
12613
12775
|
const allIssues = [
|
|
12614
12776
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -12628,7 +12790,6 @@ ${r.sectionSummary}
|
|
|
12628
12790
|
Outputs:
|
|
12629
12791
|
${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");
|
|
12630
12792
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
12631
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
12632
12793
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
12633
12794
|
|
|
12634
12795
|
Original request: ${originalPrompt}
|
|
@@ -13123,6 +13284,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
13123
13284
|
};
|
|
13124
13285
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
13125
13286
|
var MAX_VIDEO_SECONDS = 8;
|
|
13287
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
13126
13288
|
var GenerateVideoTool = class extends BaseTool {
|
|
13127
13289
|
constructor(deps) {
|
|
13128
13290
|
super();
|
|
@@ -13139,7 +13301,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13139
13301
|
type: "object",
|
|
13140
13302
|
properties: {
|
|
13141
13303
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
13142
|
-
seconds: { type: "number", description: `Clip length,
|
|
13304
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
13143
13305
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
13144
13306
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
13145
13307
|
},
|
|
@@ -13151,7 +13313,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13151
13313
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
13152
13314
|
if (!r.ok) return r.error;
|
|
13153
13315
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
13154
|
-
const seconds = Math.max(
|
|
13316
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
13155
13317
|
const asset = await generateVideo(
|
|
13156
13318
|
r.selected.capability,
|
|
13157
13319
|
r.cfg,
|
|
@@ -13168,7 +13330,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
13168
13330
|
const location = await this.deps.sink(asset);
|
|
13169
13331
|
const unit = r.selected.cost;
|
|
13170
13332
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
13171
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
13333
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
13172
13334
|
return `Video generated and saved to ${location}.
|
|
13173
13335
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
13174
13336
|
}
|
|
@@ -16309,7 +16471,24 @@ var TaskAnalyzer = class {
|
|
|
16309
16471
|
feedback;
|
|
16310
16472
|
bias;
|
|
16311
16473
|
lastProfile = null;
|
|
16312
|
-
|
|
16474
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
16475
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
16476
|
+
/**
|
|
16477
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
16478
|
+
* type they were chosen for.
|
|
16479
|
+
*
|
|
16480
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
16481
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
16482
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
16483
|
+
* model is good at creative writing, from a rating that never said so.
|
|
16484
|
+
*
|
|
16485
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
16486
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
16487
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
16488
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
16489
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
16490
|
+
*/
|
|
16491
|
+
lastCompletedRun;
|
|
16313
16492
|
constructor(tracker, bias = "balanced") {
|
|
16314
16493
|
this.tracker = tracker;
|
|
16315
16494
|
this.bias = bias;
|
|
@@ -16356,11 +16535,15 @@ var TaskAnalyzer = class {
|
|
|
16356
16535
|
*/
|
|
16357
16536
|
async selectModel(prompt, tier, selector, opts) {
|
|
16358
16537
|
const profile = await this.analyze(prompt);
|
|
16538
|
+
const recordSelection = (model) => {
|
|
16539
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
16540
|
+
return model;
|
|
16541
|
+
};
|
|
16359
16542
|
if (profile.requiresVision) {
|
|
16360
|
-
return selector.selectVisionModel();
|
|
16543
|
+
return recordSelection(selector.selectVisionModel());
|
|
16361
16544
|
}
|
|
16362
16545
|
let candidates2 = selector.getCandidatesForTier(tier);
|
|
16363
|
-
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
16546
|
+
if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
16364
16547
|
if (opts?.requiresToolUse) {
|
|
16365
16548
|
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
16366
16549
|
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
@@ -16370,9 +16553,7 @@ var TaskAnalyzer = class {
|
|
|
16370
16553
|
score: this.scoreModel(m, profile)
|
|
16371
16554
|
}));
|
|
16372
16555
|
scored.sort((a, b) => b.score - a.score);
|
|
16373
|
-
|
|
16374
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
16375
|
-
return best;
|
|
16556
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
16376
16557
|
}
|
|
16377
16558
|
/**
|
|
16378
16559
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -16381,25 +16562,38 @@ var TaskAnalyzer = class {
|
|
|
16381
16562
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
16382
16563
|
if (!this.tracker || !this.lastProfile) return;
|
|
16383
16564
|
const taskType = this.lastProfile.type;
|
|
16384
|
-
for (const [tier, model] of this.
|
|
16565
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
16385
16566
|
const cost = costByTier[tier] ?? 0;
|
|
16386
16567
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
16387
16568
|
}
|
|
16388
|
-
this.
|
|
16569
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
16570
|
+
this.currentRunSelections.clear();
|
|
16389
16571
|
void this.tracker.save();
|
|
16390
16572
|
}
|
|
16391
16573
|
/**
|
|
16392
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
16574
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
16393
16575
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
16394
|
-
*
|
|
16576
|
+
*
|
|
16577
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
16578
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
16579
|
+
* empty.
|
|
16580
|
+
*
|
|
16581
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
16582
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
16583
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
16584
|
+
* that reports false, which is what "rate this run" should mean.
|
|
16395
16585
|
*/
|
|
16396
16586
|
recordExplicitRating(rating) {
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16587
|
+
const snapshot = this.lastCompletedRun;
|
|
16588
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
16589
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
16590
|
+
for (const [, model] of snapshot.selections) {
|
|
16591
|
+
if (ratedModels.has(model.id)) continue;
|
|
16592
|
+
ratedModels.add(model.id);
|
|
16593
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
16594
|
+
}
|
|
16595
|
+
this.lastCompletedRun = void 0;
|
|
16596
|
+
return true;
|
|
16403
16597
|
}
|
|
16404
16598
|
scoreModel(model, profile) {
|
|
16405
16599
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -17440,6 +17634,8 @@ var RESUME_SCHEMA_VERSION = 1;
|
|
|
17440
17634
|
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
17441
17635
|
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17442
17636
|
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
17637
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
17638
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
17443
17639
|
function safeId(taskId) {
|
|
17444
17640
|
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
17445
17641
|
return cleaned.slice(0, 100) || "run";
|
|
@@ -17454,16 +17650,24 @@ var ResumeStore = class {
|
|
|
17454
17650
|
maxCheckpoints;
|
|
17455
17651
|
maxAgeMs;
|
|
17456
17652
|
now;
|
|
17653
|
+
claimTimeoutMs;
|
|
17457
17654
|
constructor(options) {
|
|
17458
17655
|
this.dir = options.dir;
|
|
17459
17656
|
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
17460
17657
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
17461
17658
|
this.now = options.now ?? Date.now;
|
|
17659
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
17462
17660
|
}
|
|
17463
17661
|
/**
|
|
17464
|
-
* Persist a checkpoint and prune.
|
|
17465
|
-
*
|
|
17466
|
-
*
|
|
17662
|
+
* Persist a checkpoint and prune.
|
|
17663
|
+
*
|
|
17664
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
17665
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
17666
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
17667
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
17668
|
+
* look alike to them.
|
|
17669
|
+
*
|
|
17670
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
17467
17671
|
*/
|
|
17468
17672
|
async save(checkpoint) {
|
|
17469
17673
|
try {
|
|
@@ -17479,7 +17683,9 @@ var ResumeStore = class {
|
|
|
17479
17683
|
await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
17480
17684
|
await fs31.promises.rename(tempPath, finalPath);
|
|
17481
17685
|
await this.prune();
|
|
17686
|
+
return true;
|
|
17482
17687
|
} catch {
|
|
17688
|
+
return false;
|
|
17483
17689
|
}
|
|
17484
17690
|
}
|
|
17485
17691
|
/** Valid, unexpired checkpoints, newest first. */
|
|
@@ -17509,15 +17715,87 @@ var ResumeStore = class {
|
|
|
17509
17715
|
return (await this.list())[0] ?? null;
|
|
17510
17716
|
}
|
|
17511
17717
|
/**
|
|
17512
|
-
*
|
|
17513
|
-
*
|
|
17718
|
+
* Claim a checkpoint for a resume attempt.
|
|
17719
|
+
*
|
|
17720
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
17721
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
17722
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
17723
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
17724
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
17725
|
+
* wrong trade for a recovery mechanism.
|
|
17726
|
+
*
|
|
17727
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
17728
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
17729
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
17730
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
17731
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
17514
17732
|
*/
|
|
17515
|
-
async
|
|
17733
|
+
async claim(taskId) {
|
|
17516
17734
|
const all = await this.list();
|
|
17517
17735
|
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
17518
17736
|
if (!target) return null;
|
|
17519
|
-
|
|
17520
|
-
|
|
17737
|
+
try {
|
|
17738
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17739
|
+
const wanted = safeId(target.taskId);
|
|
17740
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
17741
|
+
if (!source) return null;
|
|
17742
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
17743
|
+
await fs31.promises.rename(path34__default.default.join(this.dir, source), path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
17744
|
+
return { checkpoint: target, claimId };
|
|
17745
|
+
} catch {
|
|
17746
|
+
return null;
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
17750
|
+
async release(claimId) {
|
|
17751
|
+
try {
|
|
17752
|
+
await fs31.promises.rename(
|
|
17753
|
+
path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
17754
|
+
path34__default.default.join(this.dir, `${claimId}.json`)
|
|
17755
|
+
);
|
|
17756
|
+
} catch {
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
17760
|
+
async settle(claimId) {
|
|
17761
|
+
try {
|
|
17762
|
+
await fs31.promises.rm(path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
17763
|
+
} catch {
|
|
17764
|
+
}
|
|
17765
|
+
}
|
|
17766
|
+
/**
|
|
17767
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
17768
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
17769
|
+
*/
|
|
17770
|
+
async consume(taskId) {
|
|
17771
|
+
const claimed = await this.claim(taskId);
|
|
17772
|
+
if (!claimed) return null;
|
|
17773
|
+
await this.settle(claimed.claimId);
|
|
17774
|
+
return claimed.checkpoint;
|
|
17775
|
+
}
|
|
17776
|
+
/**
|
|
17777
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
17778
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
17779
|
+
* trading one loss window for another.
|
|
17780
|
+
*/
|
|
17781
|
+
async reclaimStale() {
|
|
17782
|
+
let reclaimed = 0;
|
|
17783
|
+
try {
|
|
17784
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17785
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
17786
|
+
for (const name of entries) {
|
|
17787
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
17788
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
17789
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
17790
|
+
await fs31.promises.rename(
|
|
17791
|
+
path34__default.default.join(this.dir, name),
|
|
17792
|
+
path34__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
17793
|
+
);
|
|
17794
|
+
reclaimed++;
|
|
17795
|
+
}
|
|
17796
|
+
} catch {
|
|
17797
|
+
}
|
|
17798
|
+
return reclaimed;
|
|
17521
17799
|
}
|
|
17522
17800
|
/** Remove every checkpoint for a task id. */
|
|
17523
17801
|
async delete(taskId) {
|
|
@@ -18534,6 +18812,25 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
18534
18812
|
lastInterruptedRun;
|
|
18535
18813
|
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
18536
18814
|
resumeStore;
|
|
18815
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
18816
|
+
claimedResumeId;
|
|
18817
|
+
/**
|
|
18818
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
18819
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
18820
|
+
*/
|
|
18821
|
+
resumedFrom;
|
|
18822
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
18823
|
+
wroteCheckpointThisRun = false;
|
|
18824
|
+
/**
|
|
18825
|
+
* Whether the run reached the end of its normal path.
|
|
18826
|
+
*
|
|
18827
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
18828
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
18829
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
18830
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
18831
|
+
* the original claim and the finished sections with it.
|
|
18832
|
+
*/
|
|
18833
|
+
completedNormally = false;
|
|
18537
18834
|
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
18538
18835
|
currentRunCompleted = [];
|
|
18539
18836
|
initPromise;
|
|
@@ -19051,17 +19348,31 @@ ${input}` }],
|
|
|
19051
19348
|
* worse. Nothing here is allowed to throw.
|
|
19052
19349
|
*/
|
|
19053
19350
|
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
19054
|
-
|
|
19351
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
19352
|
+
const merged = /* @__PURE__ */ new Map();
|
|
19353
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
19354
|
+
for (const node of this.currentRunCompleted) {
|
|
19355
|
+
const existing = merged.get(node.id);
|
|
19356
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
19357
|
+
merged.set(node.id, node);
|
|
19358
|
+
}
|
|
19359
|
+
const completed = [...merged.values()];
|
|
19360
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
19361
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
19362
|
+
if (!completed.length && !carriedPartial) return false;
|
|
19055
19363
|
try {
|
|
19056
|
-
await this.resumeStore?.save({
|
|
19364
|
+
const saved = await this.resumeStore?.save({
|
|
19057
19365
|
taskId,
|
|
19058
|
-
prompt,
|
|
19366
|
+
prompt: canonicalPrompt,
|
|
19059
19367
|
reason,
|
|
19060
19368
|
...detail ? { detail } : {},
|
|
19061
|
-
partialOutput,
|
|
19062
|
-
completed
|
|
19369
|
+
partialOutput: carriedPartial,
|
|
19370
|
+
completed
|
|
19063
19371
|
});
|
|
19372
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
19373
|
+
return saved === true;
|
|
19064
19374
|
} catch {
|
|
19375
|
+
return false;
|
|
19065
19376
|
}
|
|
19066
19377
|
}
|
|
19067
19378
|
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
@@ -19099,11 +19410,55 @@ ${input}` }],
|
|
|
19099
19410
|
* checkpoint, so the same finished work is never restored into two runs.
|
|
19100
19411
|
*/
|
|
19101
19412
|
async prepareDurableResume(opts = {}) {
|
|
19102
|
-
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19413
|
+
await this.resumeStore?.reclaimStale();
|
|
19414
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
19415
|
+
if (!claimed) return null;
|
|
19416
|
+
try {
|
|
19417
|
+
this.lastInterruptedRun = void 0;
|
|
19418
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19419
|
+
const prompt = this.buildResumePrompt(
|
|
19420
|
+
claimed.checkpoint.prompt,
|
|
19421
|
+
claimed.checkpoint.partialOutput,
|
|
19422
|
+
claimed.checkpoint.completed
|
|
19423
|
+
);
|
|
19424
|
+
this.resumedFrom = {
|
|
19425
|
+
prompt: claimed.checkpoint.prompt,
|
|
19426
|
+
completed: claimed.checkpoint.completed,
|
|
19427
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
19428
|
+
};
|
|
19429
|
+
this.claimedResumeId = claimed.claimId;
|
|
19430
|
+
return prompt;
|
|
19431
|
+
} catch (err) {
|
|
19432
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
19433
|
+
throw err;
|
|
19434
|
+
}
|
|
19435
|
+
}
|
|
19436
|
+
/**
|
|
19437
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
19438
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
19439
|
+
* run loop; safe to call when nothing is claimed.
|
|
19440
|
+
*/
|
|
19441
|
+
async settleClaimedResume(succeeded) {
|
|
19442
|
+
const claimId = this.claimedResumeId;
|
|
19443
|
+
if (!claimId) return;
|
|
19444
|
+
this.claimedResumeId = void 0;
|
|
19445
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
19446
|
+
await this.resumeStore?.settle(claimId);
|
|
19447
|
+
} else {
|
|
19448
|
+
await this.resumeStore?.release(claimId);
|
|
19449
|
+
}
|
|
19450
|
+
}
|
|
19451
|
+
/**
|
|
19452
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
19453
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
19454
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
19455
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
19456
|
+
*/
|
|
19457
|
+
async releaseClaimedResume() {
|
|
19458
|
+
const claimId = this.claimedResumeId;
|
|
19459
|
+
if (!claimId) return;
|
|
19460
|
+
this.claimedResumeId = void 0;
|
|
19461
|
+
await this.resumeStore?.release(claimId);
|
|
19107
19462
|
}
|
|
19108
19463
|
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
19109
19464
|
raiseBudgetForResume(maxTokens) {
|
|
@@ -19139,7 +19494,12 @@ ${partialOutput}` : ""
|
|
|
19139
19494
|
async resumeRun(opts = {}) {
|
|
19140
19495
|
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
19141
19496
|
if (!prompt) return null;
|
|
19142
|
-
|
|
19497
|
+
try {
|
|
19498
|
+
return await this.run({ prompt });
|
|
19499
|
+
} catch (err) {
|
|
19500
|
+
await this.releaseClaimedResume();
|
|
19501
|
+
throw err;
|
|
19502
|
+
}
|
|
19143
19503
|
}
|
|
19144
19504
|
getWorkspacePath() {
|
|
19145
19505
|
return this.workspacePath;
|
|
@@ -19509,6 +19869,9 @@ ${prompt}` : prompt;
|
|
|
19509
19869
|
const taskId = crypto.randomUUID();
|
|
19510
19870
|
this.decisionLog = [];
|
|
19511
19871
|
this.currentRunCompleted = [];
|
|
19872
|
+
this.wroteCheckpointThisRun = false;
|
|
19873
|
+
this.completedNormally = false;
|
|
19874
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
19512
19875
|
if (options.fastAnswer) {
|
|
19513
19876
|
return this.runFastAnswer(options, startMs, taskId);
|
|
19514
19877
|
}
|
|
@@ -19787,6 +20150,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19787
20150
|
${finalOutput}` : ""
|
|
19788
20151
|
].join("\n").trimEnd();
|
|
19789
20152
|
}
|
|
20153
|
+
this.completedNormally = trip == null;
|
|
19790
20154
|
} catch (err) {
|
|
19791
20155
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
19792
20156
|
this.emit("run:cancelled", {
|
|
@@ -19828,6 +20192,10 @@ ${finalOutput}` : ""
|
|
|
19828
20192
|
escalator.cancelAllPending();
|
|
19829
20193
|
} catch {
|
|
19830
20194
|
}
|
|
20195
|
+
try {
|
|
20196
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
20197
|
+
} catch {
|
|
20198
|
+
}
|
|
19831
20199
|
try {
|
|
19832
20200
|
this.releasePendingEscalations();
|
|
19833
20201
|
} catch {
|
|
@@ -21018,7 +21386,8 @@ function AgentTreeInternal({
|
|
|
21018
21386
|
] }),
|
|
21019
21387
|
node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
|
|
21020
21388
|
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
|
|
21021
|
-
node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" })
|
|
21389
|
+
node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" }),
|
|
21390
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
21022
21391
|
] }, `t2-${node.id}-${i}`);
|
|
21023
21392
|
}
|
|
21024
21393
|
if (row.type === "t3") {
|
|
@@ -21043,7 +21412,8 @@ function AgentTreeInternal({
|
|
|
21043
21412
|
/* @__PURE__ */ jsxRuntime.jsx(Spinner__default.default, { type: "dots" })
|
|
21044
21413
|
] }),
|
|
21045
21414
|
node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
|
|
21046
|
-
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" })
|
|
21415
|
+
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
|
|
21416
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
21047
21417
|
] }, `t3-${node.id}-${i}`);
|
|
21048
21418
|
}
|
|
21049
21419
|
return null;
|
|
@@ -22042,7 +22412,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
22042
22412
|
}, []);
|
|
22043
22413
|
const recordNodeEvent = React2.useCallback((event) => {
|
|
22044
22414
|
const existing = treeNodesRef.current.get(event.tierId);
|
|
22045
|
-
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: [] };
|
|
22415
|
+
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: [] };
|
|
22046
22416
|
treeNodesRef.current.set(event.tierId, node);
|
|
22047
22417
|
const store = storeRef.current;
|
|
22048
22418
|
if (store) {
|
|
@@ -22379,13 +22749,21 @@ ${lastUser.content}`;
|
|
|
22379
22749
|
onContinue: async (args) => {
|
|
22380
22750
|
const cascade = cascadeRef.current;
|
|
22381
22751
|
if (!cascade) return "Not ready yet.";
|
|
22752
|
+
if (state.isExecuting) {
|
|
22753
|
+
return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
|
|
22754
|
+
}
|
|
22382
22755
|
const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
|
|
22383
22756
|
const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
|
|
22384
22757
|
const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
|
|
22385
22758
|
if (!prompt) {
|
|
22386
22759
|
return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
|
|
22387
22760
|
}
|
|
22388
|
-
|
|
22761
|
+
try {
|
|
22762
|
+
await handleSubmit(prompt);
|
|
22763
|
+
} catch (err) {
|
|
22764
|
+
await cascade.releaseClaimedResume();
|
|
22765
|
+
throw err;
|
|
22766
|
+
}
|
|
22389
22767
|
return "";
|
|
22390
22768
|
}
|
|
22391
22769
|
});
|
|
@@ -24878,8 +25256,14 @@ var DashboardSocket = class {
|
|
|
24878
25256
|
emitCascadeEvent(ev) {
|
|
24879
25257
|
this.io.emit("cascade:event", ev);
|
|
24880
25258
|
}
|
|
24881
|
-
|
|
24882
|
-
|
|
25259
|
+
/**
|
|
25260
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
25261
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
25262
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
25263
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
25264
|
+
*/
|
|
25265
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
25266
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
24883
25267
|
this.io.emit("tier:status", payload);
|
|
24884
25268
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
24885
25269
|
}
|