cascade-ai 0.67.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 +1336 -390
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1330 -384
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1982 -1037
- package/dist/index.cjs +1286 -341
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -18
- package/dist/index.d.ts +520 -18
- package/dist/index.js +1270 -336
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import path26, { dirname } from 'path';
|
|
3
|
+
import fs25, { promises, mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
4
4
|
import os6 from 'os';
|
|
5
5
|
import crypto4, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
|
|
6
6
|
import EventEmitter, { setMaxListeners } from 'events';
|
|
@@ -61,12 +61,12 @@ var init_audit_logger = __esm({
|
|
|
61
61
|
constructor(workspacePath, debugMode = false) {
|
|
62
62
|
this.workspacePath = workspacePath;
|
|
63
63
|
this.debugMode = debugMode;
|
|
64
|
-
const cascadeDir =
|
|
65
|
-
if (!
|
|
66
|
-
|
|
64
|
+
const cascadeDir = path26.join(workspacePath, ".cascade");
|
|
65
|
+
if (!fs25.existsSync(cascadeDir)) {
|
|
66
|
+
fs25.mkdirSync(cascadeDir, { recursive: true });
|
|
67
67
|
}
|
|
68
|
-
this.keyPath =
|
|
69
|
-
this.dbPath =
|
|
68
|
+
this.keyPath = path26.join(cascadeDir, "audit_log.key");
|
|
69
|
+
this.dbPath = path26.join(cascadeDir, "audit_log.db");
|
|
70
70
|
this.initEncryptionKey();
|
|
71
71
|
this.db = new Database(this.dbPath);
|
|
72
72
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -96,11 +96,11 @@ var init_audit_logger = __esm({
|
|
|
96
96
|
return crypto4.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
97
97
|
}
|
|
98
98
|
initEncryptionKey() {
|
|
99
|
-
if (
|
|
100
|
-
this.encryptionKey =
|
|
99
|
+
if (fs25.existsSync(this.keyPath)) {
|
|
100
|
+
this.encryptionKey = fs25.readFileSync(this.keyPath);
|
|
101
101
|
} else {
|
|
102
102
|
this.encryptionKey = crypto4.randomBytes(32);
|
|
103
|
-
|
|
103
|
+
fs25.writeFileSync(this.keyPath, this.encryptionKey);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
encrypt(text) {
|
|
@@ -178,9 +178,9 @@ var init_audit_logger = __esm({
|
|
|
178
178
|
dumpDebugIfNeeded() {
|
|
179
179
|
if (!this.debugMode) return;
|
|
180
180
|
try {
|
|
181
|
-
const dumpPath =
|
|
181
|
+
const dumpPath = path26.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
|
|
182
182
|
const entries = this.getAllLogs();
|
|
183
|
-
|
|
183
|
+
fs25.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
184
184
|
} catch (err) {
|
|
185
185
|
console.error("Failed to dump debug audit logs", err);
|
|
186
186
|
}
|
|
@@ -1862,7 +1862,7 @@ function reconcilePrice(model, live) {
|
|
|
1862
1862
|
}
|
|
1863
1863
|
|
|
1864
1864
|
// src/constants.ts
|
|
1865
|
-
var CASCADE_VERSION = "0.
|
|
1865
|
+
var CASCADE_VERSION = "0.69.0";
|
|
1866
1866
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1867
1867
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1868
1868
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -4617,7 +4617,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
4617
4617
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
4618
4618
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
4619
4619
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
4620
|
-
var DEFAULT_CACHE_FILE =
|
|
4620
|
+
var DEFAULT_CACHE_FILE = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
4621
4621
|
function normalizeModelId2(id) {
|
|
4622
4622
|
let s = id.toLowerCase();
|
|
4623
4623
|
const slash = s.lastIndexOf("/");
|
|
@@ -4764,7 +4764,7 @@ var LiveDataProvider = class {
|
|
|
4764
4764
|
}
|
|
4765
4765
|
async saveCache() {
|
|
4766
4766
|
try {
|
|
4767
|
-
await fs5.mkdir(
|
|
4767
|
+
await fs5.mkdir(path26.dirname(this.opts.cacheFile), { recursive: true });
|
|
4768
4768
|
const cache = {
|
|
4769
4769
|
fetchedAt: this.fetchedAt,
|
|
4770
4770
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -6024,6 +6024,21 @@ var BaseTier = class extends EventEmitter {
|
|
|
6024
6024
|
* which node (Cockpit node panel / Why panel).
|
|
6025
6025
|
*/
|
|
6026
6026
|
servingModel;
|
|
6027
|
+
/**
|
|
6028
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
6029
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
6030
|
+
*
|
|
6031
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
6032
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
6033
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
6034
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
6035
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
6036
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
6037
|
+
* appears in the stream.
|
|
6038
|
+
*/
|
|
6039
|
+
graphNodeId;
|
|
6040
|
+
graphDependsOn;
|
|
6041
|
+
graphWave;
|
|
6027
6042
|
constructor(role, id, parentId) {
|
|
6028
6043
|
super();
|
|
6029
6044
|
this.role = role;
|
|
@@ -6035,6 +6050,33 @@ var BaseTier = class extends EventEmitter {
|
|
|
6035
6050
|
setPresenter(on = true) {
|
|
6036
6051
|
this.isPresenter = on;
|
|
6037
6052
|
}
|
|
6053
|
+
/**
|
|
6054
|
+
* Record where this tier sits in the compiled task graph.
|
|
6055
|
+
*
|
|
6056
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
6057
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
6058
|
+
* are set independently rather than all at once.
|
|
6059
|
+
*/
|
|
6060
|
+
setGraphPosition(position) {
|
|
6061
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
6062
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
6063
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
6064
|
+
}
|
|
6065
|
+
/**
|
|
6066
|
+
* The graph fields, for spreading into a status payload.
|
|
6067
|
+
*
|
|
6068
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
6069
|
+
* two different call sites with two different payload shapes; adding a field
|
|
6070
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
6071
|
+
* consumer than never arriving at all.
|
|
6072
|
+
*/
|
|
6073
|
+
graphFields() {
|
|
6074
|
+
return {
|
|
6075
|
+
nodeId: this.graphNodeId,
|
|
6076
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
6077
|
+
waveId: this.graphWave
|
|
6078
|
+
};
|
|
6079
|
+
}
|
|
6038
6080
|
getStatus() {
|
|
6039
6081
|
return this.status;
|
|
6040
6082
|
}
|
|
@@ -6049,7 +6091,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
6049
6091
|
status,
|
|
6050
6092
|
timestamp,
|
|
6051
6093
|
output,
|
|
6052
|
-
model: this.servingModel
|
|
6094
|
+
model: this.servingModel,
|
|
6095
|
+
...this.graphFields()
|
|
6053
6096
|
};
|
|
6054
6097
|
this.emit("status", event);
|
|
6055
6098
|
this.emit("tier:status", event);
|
|
@@ -6089,7 +6132,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
6089
6132
|
progressPct: update.progressPct,
|
|
6090
6133
|
timestamp,
|
|
6091
6134
|
output: update.output,
|
|
6092
|
-
model: this.servingModel
|
|
6135
|
+
model: this.servingModel,
|
|
6136
|
+
...this.graphFields()
|
|
6093
6137
|
});
|
|
6094
6138
|
}
|
|
6095
6139
|
buildMessage(type, to, payload) {
|
|
@@ -6556,6 +6600,79 @@ function describeProviderError(c, modelId) {
|
|
|
6556
6600
|
}
|
|
6557
6601
|
}
|
|
6558
6602
|
|
|
6603
|
+
// src/core/verification/acceptance.ts
|
|
6604
|
+
var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
|
|
6605
|
+
var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
|
|
6606
|
+
var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
|
|
6607
|
+
var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
|
|
6608
|
+
var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
|
|
6609
|
+
function targetFile(criterion, ownedFiles) {
|
|
6610
|
+
const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
|
|
6611
|
+
if (mentioned.length > 1) return null;
|
|
6612
|
+
if (mentioned.length === 1) {
|
|
6613
|
+
const candidate = mentioned[0];
|
|
6614
|
+
return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
|
|
6615
|
+
}
|
|
6616
|
+
return ownedFiles.length === 1 ? ownedFiles[0] : null;
|
|
6617
|
+
}
|
|
6618
|
+
async function evaluateAcceptance(criteria, ownedFiles, probe) {
|
|
6619
|
+
const results = [];
|
|
6620
|
+
for (const criterion of criteria) {
|
|
6621
|
+
const text = criterion.trim();
|
|
6622
|
+
if (!text) {
|
|
6623
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6624
|
+
continue;
|
|
6625
|
+
}
|
|
6626
|
+
if (NEGATION.test(text)) {
|
|
6627
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6628
|
+
continue;
|
|
6629
|
+
}
|
|
6630
|
+
const needle = text.match(QUOTED_NEEDLE)?.[1];
|
|
6631
|
+
const wantsExists = EXISTS.test(text);
|
|
6632
|
+
const wantsNonEmpty = NON_EMPTY.test(text);
|
|
6633
|
+
if (!needle && !wantsExists && !wantsNonEmpty) {
|
|
6634
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6635
|
+
continue;
|
|
6636
|
+
}
|
|
6637
|
+
const file = targetFile(text, ownedFiles);
|
|
6638
|
+
if (!file) {
|
|
6639
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6640
|
+
continue;
|
|
6641
|
+
}
|
|
6642
|
+
const stat = await probe.stat(file);
|
|
6643
|
+
if (!stat) {
|
|
6644
|
+
results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
|
|
6645
|
+
continue;
|
|
6646
|
+
}
|
|
6647
|
+
if (wantsNonEmpty && stat.size === 0) {
|
|
6648
|
+
results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
|
|
6649
|
+
continue;
|
|
6650
|
+
}
|
|
6651
|
+
if (needle) {
|
|
6652
|
+
const content = await probe.read(file);
|
|
6653
|
+
if (content === null) {
|
|
6654
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6655
|
+
continue;
|
|
6656
|
+
}
|
|
6657
|
+
const found = content.includes(needle);
|
|
6658
|
+
results.push({
|
|
6659
|
+
criterion,
|
|
6660
|
+
verdict: found ? "passed" : "failed",
|
|
6661
|
+
detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
|
|
6662
|
+
});
|
|
6663
|
+
continue;
|
|
6664
|
+
}
|
|
6665
|
+
results.push({ criterion, verdict: "passed", detail: `${file} exists` });
|
|
6666
|
+
}
|
|
6667
|
+
return results;
|
|
6668
|
+
}
|
|
6669
|
+
function undecided(results) {
|
|
6670
|
+
return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
|
|
6671
|
+
}
|
|
6672
|
+
function failures(results) {
|
|
6673
|
+
return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6559
6676
|
// src/core/tiers/t3-worker.ts
|
|
6560
6677
|
var CriticalToolError = class extends Error {
|
|
6561
6678
|
constructor(message, toolName) {
|
|
@@ -6872,7 +6989,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
6872
6989
|
}
|
|
6873
6990
|
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
6874
6991
|
if (!artifactCheck.ok) {
|
|
6875
|
-
correctionAttempts
|
|
6992
|
+
correctionAttempts++;
|
|
6876
6993
|
issues.push(...artifactCheck.issues);
|
|
6877
6994
|
output = await this.correctOutput(output, artifactCheck.issues);
|
|
6878
6995
|
const retryArtifactCheck = await this.verifyArtifacts(assignment);
|
|
@@ -6890,22 +7007,45 @@ Now execute your subtask using this context where relevant.`
|
|
|
6890
7007
|
this.tools.map((t) => t.name)
|
|
6891
7008
|
);
|
|
6892
7009
|
if (visualIssue) {
|
|
6893
|
-
correctionAttempts
|
|
7010
|
+
correctionAttempts++;
|
|
6894
7011
|
issues.push(visualIssue);
|
|
6895
7012
|
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
6896
7013
|
output = await this.correctOutput(output, [visualIssue]);
|
|
6897
7014
|
}
|
|
7015
|
+
const acceptanceResults = await this.checkAcceptance(assignment);
|
|
7016
|
+
const acceptanceFailures = failures(acceptanceResults);
|
|
7017
|
+
if (acceptanceFailures.length > 0) {
|
|
7018
|
+
correctionAttempts++;
|
|
7019
|
+
issues.push(...acceptanceFailures);
|
|
7020
|
+
this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
|
|
7021
|
+
output = await this.correctOutput(output, acceptanceFailures);
|
|
7022
|
+
const recheck = failures(await this.checkAcceptance(assignment));
|
|
7023
|
+
if (recheck.length > 0) {
|
|
7024
|
+
issues.push(...recheck);
|
|
7025
|
+
checksRun.push(...acceptanceResults.map((r) => r.criterion));
|
|
7026
|
+
failed.push(...recheck);
|
|
7027
|
+
this.setStatus("FAILED");
|
|
7028
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
|
|
7029
|
+
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
for (const result2 of acceptanceResults) {
|
|
7033
|
+
if (result2.verdict === "undecidable") continue;
|
|
7034
|
+
checksRun.push(result2.criterion);
|
|
7035
|
+
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
7036
|
+
}
|
|
6898
7037
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
6899
|
-
const
|
|
7038
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
7039
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
6900
7040
|
checksRun.push(...testResult.checksRun);
|
|
6901
7041
|
passed.push(...testResult.passed);
|
|
6902
7042
|
failed.push(...testResult.failed);
|
|
6903
7043
|
if (testResult.failed.length > 0) {
|
|
6904
|
-
correctionAttempts
|
|
7044
|
+
correctionAttempts++;
|
|
6905
7045
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
6906
7046
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
6907
7047
|
output = corrected;
|
|
6908
|
-
const retest = await this.selfTest(assignment, output);
|
|
7048
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
6909
7049
|
passed.push(...retest.passed);
|
|
6910
7050
|
if (retest.failed.length > 0) {
|
|
6911
7051
|
failed.push(...retest.failed);
|
|
@@ -7209,8 +7349,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
7209
7349
|
// Media generation can run for a minute; without this a cancelled run
|
|
7210
7350
|
// still pays for an image nobody will see.
|
|
7211
7351
|
...this.signal ? { signal: this.signal } : {},
|
|
7212
|
-
saveSnapshot: async (
|
|
7213
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
7352
|
+
saveSnapshot: async (path28, content) => {
|
|
7353
|
+
this.store?.addFileSnapshot(this.taskId, path28, content);
|
|
7214
7354
|
},
|
|
7215
7355
|
sendPeerSync: (to, syncType, content) => {
|
|
7216
7356
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -7397,7 +7537,7 @@ ${assignment.expectedOutput}`;
|
|
|
7397
7537
|
const { promisify: promisify4 } = await import('util');
|
|
7398
7538
|
const execAsync2 = promisify4(exec2);
|
|
7399
7539
|
for (const artifactPath of artifactPaths) {
|
|
7400
|
-
const absolutePath =
|
|
7540
|
+
const absolutePath = path26.resolve(process.cwd(), artifactPath);
|
|
7401
7541
|
try {
|
|
7402
7542
|
const stat = await fs5.stat(absolutePath);
|
|
7403
7543
|
if (!stat.isFile()) {
|
|
@@ -7432,7 +7572,7 @@ ${assignment.expectedOutput}`;
|
|
|
7432
7572
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
7433
7573
|
continue;
|
|
7434
7574
|
}
|
|
7435
|
-
const ext =
|
|
7575
|
+
const ext = path26.extname(absolutePath).toLowerCase();
|
|
7436
7576
|
try {
|
|
7437
7577
|
if (ext === ".ts" || ext === ".tsx") {
|
|
7438
7578
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -7518,14 +7658,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7518
7658
|
}
|
|
7519
7659
|
return current;
|
|
7520
7660
|
}
|
|
7521
|
-
|
|
7661
|
+
/**
|
|
7662
|
+
* Run the acceptance criteria that can be settled by looking at the workspace.
|
|
7663
|
+
* Reads go through the same workspace-relative resolution verifyArtifacts uses.
|
|
7664
|
+
*/
|
|
7665
|
+
async checkAcceptance(assignment) {
|
|
7666
|
+
const criteria = assignment.acceptance ?? [];
|
|
7667
|
+
if (!criteria.length) return [];
|
|
7668
|
+
const resolve2 = (target) => path26.resolve(process.cwd(), target);
|
|
7669
|
+
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
7670
|
+
stat: async (target) => {
|
|
7671
|
+
try {
|
|
7672
|
+
const stat = await fs5.stat(resolve2(target));
|
|
7673
|
+
return stat.isFile() ? { size: stat.size } : null;
|
|
7674
|
+
} catch {
|
|
7675
|
+
return null;
|
|
7676
|
+
}
|
|
7677
|
+
},
|
|
7678
|
+
read: async (target) => {
|
|
7679
|
+
try {
|
|
7680
|
+
const content = await fs5.readFile(resolve2(target), "utf-8");
|
|
7681
|
+
return content.includes("\uFFFD") ? null : content;
|
|
7682
|
+
} catch {
|
|
7683
|
+
return null;
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
});
|
|
7687
|
+
}
|
|
7688
|
+
async selfTest(assignment, output, pendingAcceptance) {
|
|
7689
|
+
const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
|
|
7522
7690
|
const prompt = `Self-test this output against the assignment requirements.
|
|
7523
7691
|
|
|
7524
7692
|
Assignment: ${assignment.description}
|
|
7525
7693
|
Expected output: ${assignment.expectedOutput}
|
|
7526
7694
|
Constraints: ${assignment.constraints.join("; ")}
|
|
7527
|
-
${
|
|
7528
|
-
${
|
|
7695
|
+
${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
7696
|
+
${acceptance.map((a) => `- ${a}`).join("\n")}
|
|
7529
7697
|
` : ""}
|
|
7530
7698
|
Output to test:
|
|
7531
7699
|
${output}
|
|
@@ -7592,8 +7760,8 @@ ${output.slice(0, 4e3)}`;
|
|
|
7592
7760
|
} catch {
|
|
7593
7761
|
}
|
|
7594
7762
|
}
|
|
7595
|
-
async correctOutput(originalOutput,
|
|
7596
|
-
const correctionPrompt = `The following output failed these checks: ${
|
|
7763
|
+
async correctOutput(originalOutput, failures2) {
|
|
7764
|
+
const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
|
|
7597
7765
|
|
|
7598
7766
|
Original output:
|
|
7599
7767
|
${originalOutput}
|
|
@@ -8254,12 +8422,220 @@ function describeGenerationForPlanner(has) {
|
|
|
8254
8422
|
return lines.join("\n");
|
|
8255
8423
|
}
|
|
8256
8424
|
|
|
8425
|
+
// src/core/orchestration/task-graph.ts
|
|
8426
|
+
var TaskGraphValidationError = class extends Error {
|
|
8427
|
+
constructor(issues) {
|
|
8428
|
+
super(issues.map((issue) => issue.message).join("; "));
|
|
8429
|
+
this.issues = issues;
|
|
8430
|
+
this.name = "TaskGraphValidationError";
|
|
8431
|
+
}
|
|
8432
|
+
issues;
|
|
8433
|
+
};
|
|
8434
|
+
function normalizeDependencies(input) {
|
|
8435
|
+
if (!input?.length) return [];
|
|
8436
|
+
return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
|
|
8437
|
+
}
|
|
8438
|
+
function cycleNodes(nodes) {
|
|
8439
|
+
const adjacency = new Map(
|
|
8440
|
+
nodes.map((node) => [node.id, node.dependsOn])
|
|
8441
|
+
);
|
|
8442
|
+
const index = /* @__PURE__ */ new Map();
|
|
8443
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
8444
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
8445
|
+
const stack = [];
|
|
8446
|
+
const inCycle = /* @__PURE__ */ new Set();
|
|
8447
|
+
let counter = 0;
|
|
8448
|
+
const open = (id) => {
|
|
8449
|
+
index.set(id, counter);
|
|
8450
|
+
lowlink.set(id, counter);
|
|
8451
|
+
counter++;
|
|
8452
|
+
stack.push(id);
|
|
8453
|
+
onStack.add(id);
|
|
8454
|
+
};
|
|
8455
|
+
for (const root of nodes) {
|
|
8456
|
+
if (index.has(root.id)) continue;
|
|
8457
|
+
open(root.id);
|
|
8458
|
+
const frames = [{ id: root.id, edge: 0 }];
|
|
8459
|
+
while (frames.length > 0) {
|
|
8460
|
+
const frame = frames[frames.length - 1];
|
|
8461
|
+
const neighbours = adjacency.get(frame.id) ?? [];
|
|
8462
|
+
if (frame.edge < neighbours.length) {
|
|
8463
|
+
const next = neighbours[frame.edge];
|
|
8464
|
+
frame.edge++;
|
|
8465
|
+
if (!adjacency.has(next)) continue;
|
|
8466
|
+
if (!index.has(next)) {
|
|
8467
|
+
open(next);
|
|
8468
|
+
frames.push({ id: next, edge: 0 });
|
|
8469
|
+
} else if (onStack.has(next)) {
|
|
8470
|
+
lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
|
|
8471
|
+
}
|
|
8472
|
+
continue;
|
|
8473
|
+
}
|
|
8474
|
+
frames.pop();
|
|
8475
|
+
const parent = frames[frames.length - 1];
|
|
8476
|
+
if (parent) {
|
|
8477
|
+
lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
|
|
8478
|
+
}
|
|
8479
|
+
if (lowlink.get(frame.id) === index.get(frame.id)) {
|
|
8480
|
+
const component = [];
|
|
8481
|
+
for (; ; ) {
|
|
8482
|
+
const member = stack.pop();
|
|
8483
|
+
onStack.delete(member);
|
|
8484
|
+
component.push(member);
|
|
8485
|
+
if (member === frame.id) break;
|
|
8486
|
+
}
|
|
8487
|
+
const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
8488
|
+
if (cyclic) for (const member of component) inCycle.add(member);
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
return inCycle;
|
|
8493
|
+
}
|
|
8494
|
+
function throwIfStrict(mode, issues) {
|
|
8495
|
+
if (mode === "strict" && issues.length > 0) {
|
|
8496
|
+
throw new TaskGraphValidationError(issues);
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8499
|
+
function compileTaskGraph(inputNodes, options = {}) {
|
|
8500
|
+
const mode = options.mode ?? "strict";
|
|
8501
|
+
const issues = [];
|
|
8502
|
+
if (inputNodes.length === 0) {
|
|
8503
|
+
issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
|
|
8504
|
+
throwIfStrict(mode, issues);
|
|
8505
|
+
}
|
|
8506
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8507
|
+
const nodes = [];
|
|
8508
|
+
for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
|
|
8509
|
+
const input = inputNodes[ordinal];
|
|
8510
|
+
const id = input.id?.trim();
|
|
8511
|
+
const title = input.title?.trim();
|
|
8512
|
+
if (!id || !title) {
|
|
8513
|
+
issues.push({
|
|
8514
|
+
code: "INVALID_NODE",
|
|
8515
|
+
nodeId: id || void 0,
|
|
8516
|
+
message: `Task graph node at index ${ordinal} requires a non-empty id and title`
|
|
8517
|
+
});
|
|
8518
|
+
continue;
|
|
8519
|
+
}
|
|
8520
|
+
if (seen.has(id)) {
|
|
8521
|
+
issues.push({
|
|
8522
|
+
code: "DUPLICATE_NODE_ID",
|
|
8523
|
+
nodeId: id,
|
|
8524
|
+
message: `Duplicate task graph node id: ${id}`
|
|
8525
|
+
});
|
|
8526
|
+
continue;
|
|
8527
|
+
}
|
|
8528
|
+
seen.add(id);
|
|
8529
|
+
nodes.push({
|
|
8530
|
+
id,
|
|
8531
|
+
title,
|
|
8532
|
+
kind: input.kind,
|
|
8533
|
+
dependsOn: normalizeDependencies(input.dependsOn),
|
|
8534
|
+
payload: input.payload,
|
|
8535
|
+
ordinal
|
|
8536
|
+
});
|
|
8537
|
+
}
|
|
8538
|
+
throwIfStrict(mode, issues);
|
|
8539
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
8540
|
+
const sanitized = nodes.map((node) => {
|
|
8541
|
+
const dependsOn = [];
|
|
8542
|
+
for (const dependencyId of node.dependsOn) {
|
|
8543
|
+
if (dependencyId === node.id) {
|
|
8544
|
+
issues.push({
|
|
8545
|
+
code: "SELF_DEPENDENCY",
|
|
8546
|
+
nodeId: node.id,
|
|
8547
|
+
dependencyId,
|
|
8548
|
+
message: `Task graph node ${node.id} cannot depend on itself`
|
|
8549
|
+
});
|
|
8550
|
+
continue;
|
|
8551
|
+
}
|
|
8552
|
+
if (!ids.has(dependencyId)) {
|
|
8553
|
+
issues.push({
|
|
8554
|
+
code: "UNKNOWN_DEPENDENCY",
|
|
8555
|
+
nodeId: node.id,
|
|
8556
|
+
dependencyId,
|
|
8557
|
+
message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
|
|
8558
|
+
});
|
|
8559
|
+
continue;
|
|
8560
|
+
}
|
|
8561
|
+
dependsOn.push(dependencyId);
|
|
8562
|
+
}
|
|
8563
|
+
return { ...node, dependsOn };
|
|
8564
|
+
});
|
|
8565
|
+
throwIfStrict(mode, issues);
|
|
8566
|
+
const cyclic = cycleNodes(sanitized);
|
|
8567
|
+
let repaired = sanitized;
|
|
8568
|
+
if (cyclic.size > 0) {
|
|
8569
|
+
const cycleIssues = [];
|
|
8570
|
+
repaired = sanitized.map((node) => {
|
|
8571
|
+
if (!cyclic.has(node.id)) return node;
|
|
8572
|
+
const dependsOn = node.dependsOn.filter((dependencyId) => {
|
|
8573
|
+
if (!cyclic.has(dependencyId)) return true;
|
|
8574
|
+
cycleIssues.push({
|
|
8575
|
+
code: "CYCLE_EDGE_REMOVED",
|
|
8576
|
+
nodeId: node.id,
|
|
8577
|
+
dependencyId,
|
|
8578
|
+
message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
|
|
8579
|
+
});
|
|
8580
|
+
return false;
|
|
8581
|
+
});
|
|
8582
|
+
return { ...node, dependsOn };
|
|
8583
|
+
});
|
|
8584
|
+
issues.push(...cycleIssues);
|
|
8585
|
+
throwIfStrict(mode, cycleIssues);
|
|
8586
|
+
}
|
|
8587
|
+
const remainingCycle = cycleNodes(repaired);
|
|
8588
|
+
if (remainingCycle.size > 0) {
|
|
8589
|
+
throw new TaskGraphValidationError([
|
|
8590
|
+
{
|
|
8591
|
+
code: "INVALID_NODE",
|
|
8592
|
+
message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
|
|
8593
|
+
}
|
|
8594
|
+
]);
|
|
8595
|
+
}
|
|
8596
|
+
return {
|
|
8597
|
+
graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
|
|
8598
|
+
issues
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
// src/core/orchestration/adapters.ts
|
|
8603
|
+
function compileSectionGraph(sections) {
|
|
8604
|
+
return compileTaskGraph(
|
|
8605
|
+
sections.map((section) => ({
|
|
8606
|
+
id: section.sectionId,
|
|
8607
|
+
title: section.sectionTitle,
|
|
8608
|
+
kind: "section",
|
|
8609
|
+
dependsOn: section.dependsOn,
|
|
8610
|
+
payload: section
|
|
8611
|
+
})),
|
|
8612
|
+
{ mode: "repair" }
|
|
8613
|
+
);
|
|
8614
|
+
}
|
|
8615
|
+
function compileSubtaskGraph(subtasks) {
|
|
8616
|
+
return compileTaskGraph(
|
|
8617
|
+
subtasks.map((subtask) => ({
|
|
8618
|
+
id: subtask.subtaskId,
|
|
8619
|
+
title: subtask.subtaskTitle,
|
|
8620
|
+
kind: "subtask",
|
|
8621
|
+
dependsOn: subtask.dependsOn,
|
|
8622
|
+
payload: subtask
|
|
8623
|
+
})),
|
|
8624
|
+
{ mode: "repair" }
|
|
8625
|
+
);
|
|
8626
|
+
}
|
|
8627
|
+
|
|
8257
8628
|
// src/core/tiers/t2-manager.ts
|
|
8258
8629
|
function buildT2SystemPrompt(has) {
|
|
8259
8630
|
const generation = describeGenerationForPlanner(has);
|
|
8260
8631
|
return [
|
|
8261
8632
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
8262
|
-
|
|
8633
|
+
// Must agree with the decomposition prompt in decomposeSection(), which is
|
|
8634
|
+
// the one carrying the actual instruction. It said 1-4 and "the FEWEST that
|
|
8635
|
+
// fully cover it"; this said 2-5. The model received both, so its floor was
|
|
8636
|
+
// simultaneously one and two, and the cheaper reading — pad to two — costs a
|
|
8637
|
+
// real worker call on every small section.
|
|
8638
|
+
"Your role is to analyze a section of a task and decompose it into 1-4 discrete subtasks for T3 Workers \u2014 the fewest that fully cover it.",
|
|
8263
8639
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
8264
8640
|
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
8265
8641
|
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
@@ -8593,6 +8969,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8593
8969
|
for (const a of assignments) {
|
|
8594
8970
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
8595
8971
|
if (this.store) worker.setStore(this.store, taskId);
|
|
8972
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
8596
8973
|
worker.setPeerBus(this.t3PeerBus);
|
|
8597
8974
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
8598
8975
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -8655,21 +9032,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8655
9032
|
* then reduce in-degrees of their dependents and repeat.
|
|
8656
9033
|
*/
|
|
8657
9034
|
async runWithDependencies(assignments, workerMap, taskId) {
|
|
9035
|
+
const compiled = compileSubtaskGraph(assignments);
|
|
9036
|
+
for (const issue of compiled.issues) {
|
|
9037
|
+
this.log(`\u26A0 Subtask graph: ${issue.message}`);
|
|
9038
|
+
}
|
|
9039
|
+
const sanitizedAssignments = compiled.graph.nodes.map((node) => {
|
|
9040
|
+
const assignment = node.payload;
|
|
9041
|
+
const original = assignment.dependsOn ?? [];
|
|
9042
|
+
const repaired = node.dependsOn;
|
|
9043
|
+
const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
|
|
9044
|
+
return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
|
|
9045
|
+
});
|
|
8658
9046
|
const adj = /* @__PURE__ */ new Map();
|
|
8659
9047
|
const inDegree = /* @__PURE__ */ new Map();
|
|
8660
9048
|
const resultMap = /* @__PURE__ */ new Map();
|
|
8661
|
-
for (const a of
|
|
9049
|
+
for (const a of sanitizedAssignments) {
|
|
8662
9050
|
if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
|
|
8663
9051
|
inDegree.set(a.subtaskId, 0);
|
|
8664
9052
|
}
|
|
8665
|
-
for (const a of
|
|
8666
|
-
const
|
|
8667
|
-
for (const dep of deps) {
|
|
9053
|
+
for (const a of sanitizedAssignments) {
|
|
9054
|
+
for (const dep of a.dependsOn ?? []) {
|
|
8668
9055
|
adj.get(dep).add(a.subtaskId);
|
|
8669
9056
|
inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
|
|
8670
9057
|
}
|
|
8671
9058
|
}
|
|
8672
|
-
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
8673
9059
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
8674
9060
|
let wave = 0;
|
|
8675
9061
|
let respawnBudget = 1;
|
|
@@ -8704,6 +9090,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8704
9090
|
runnableIds.push(fallbackId);
|
|
8705
9091
|
}
|
|
8706
9092
|
wave++;
|
|
9093
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
8707
9094
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
8708
9095
|
this.sendStatusUpdate({
|
|
8709
9096
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -8876,49 +9263,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8876
9263
|
}
|
|
8877
9264
|
return [...resultMap.values()];
|
|
8878
9265
|
}
|
|
8879
|
-
/**
|
|
8880
|
-
* Detects cyclic dependencies using Kahn's algorithm and breaks them
|
|
8881
|
-
* by removing back-edges. Returns a sanitized copy of assignments.
|
|
8882
|
-
*
|
|
8883
|
-
* A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
|
|
8884
|
-
* meaning t3 will start without waiting for t1, preventing deadlock.
|
|
8885
|
-
*/
|
|
8886
|
-
breakCycles(assignments, adj, inDegree) {
|
|
8887
|
-
const degree = new Map(inDegree);
|
|
8888
|
-
const queue = [];
|
|
8889
|
-
const visited = /* @__PURE__ */ new Set();
|
|
8890
|
-
for (const [id, d] of degree) {
|
|
8891
|
-
if (d === 0) queue.push(id);
|
|
8892
|
-
}
|
|
8893
|
-
while (queue.length > 0) {
|
|
8894
|
-
const id = queue.shift();
|
|
8895
|
-
visited.add(id);
|
|
8896
|
-
for (const dep of adj.get(id) ?? []) {
|
|
8897
|
-
const newDeg = (degree.get(dep) ?? 1) - 1;
|
|
8898
|
-
degree.set(dep, newDeg);
|
|
8899
|
-
if (newDeg === 0) queue.push(dep);
|
|
8900
|
-
}
|
|
8901
|
-
}
|
|
8902
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
8903
|
-
if (cycleNodes.length === 0) return assignments;
|
|
8904
|
-
this.log(
|
|
8905
|
-
`\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
|
|
8906
|
-
);
|
|
8907
|
-
return assignments.map((a) => {
|
|
8908
|
-
if (!cycleNodes.includes(a.subtaskId)) return a;
|
|
8909
|
-
const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
8910
|
-
if (safeDeps.length !== (a.dependsOn ?? []).length) {
|
|
8911
|
-
this.log(
|
|
8912
|
-
` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
|
|
8913
|
-
);
|
|
8914
|
-
for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
8915
|
-
inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
|
|
8916
|
-
adj.get(removed)?.delete(a.subtaskId);
|
|
8917
|
-
}
|
|
8918
|
-
}
|
|
8919
|
-
return { ...a, dependsOn: safeDeps };
|
|
8920
|
-
});
|
|
8921
|
-
}
|
|
8922
9266
|
async retryT3(assignment, taskId) {
|
|
8923
9267
|
this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
|
|
8924
9268
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
@@ -9054,6 +9398,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9054
9398
|
}
|
|
9055
9399
|
};
|
|
9056
9400
|
|
|
9401
|
+
// src/core/orchestration/scheduler.ts
|
|
9402
|
+
var DependencyScheduler = class {
|
|
9403
|
+
constructor(graph, hooks, options = {}) {
|
|
9404
|
+
this.graph = graph;
|
|
9405
|
+
this.hooks = hooks;
|
|
9406
|
+
this.options = options;
|
|
9407
|
+
}
|
|
9408
|
+
graph;
|
|
9409
|
+
hooks;
|
|
9410
|
+
options;
|
|
9411
|
+
async run() {
|
|
9412
|
+
const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
|
|
9413
|
+
const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
|
|
9414
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
9415
|
+
for (const node of nodes) dependents.set(node.id, []);
|
|
9416
|
+
for (const node of nodes) {
|
|
9417
|
+
for (const dependencyId of node.dependsOn) {
|
|
9418
|
+
dependents.get(dependencyId)?.push(node.id);
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
9422
|
+
const settled = /* @__PURE__ */ new Set();
|
|
9423
|
+
const results = /* @__PURE__ */ new Map();
|
|
9424
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
9425
|
+
const waves = [];
|
|
9426
|
+
let wave = 0;
|
|
9427
|
+
const blockDescendants = async (failedIds) => {
|
|
9428
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
9429
|
+
const queue = [];
|
|
9430
|
+
for (const rootId of failedIds) {
|
|
9431
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
9432
|
+
}
|
|
9433
|
+
while (queue.length > 0) {
|
|
9434
|
+
const { id, root } = queue.shift();
|
|
9435
|
+
if (settled.has(id)) continue;
|
|
9436
|
+
const known = rootsFor.get(id);
|
|
9437
|
+
if (known) {
|
|
9438
|
+
if (!known.has(root)) known.add(root);
|
|
9439
|
+
continue;
|
|
9440
|
+
}
|
|
9441
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
9442
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
9443
|
+
}
|
|
9444
|
+
for (const node of nodes) {
|
|
9445
|
+
const roots = rootsFor.get(node.id);
|
|
9446
|
+
if (!roots || settled.has(node.id)) continue;
|
|
9447
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
9448
|
+
const blockedBy = causes.map((n) => n.id);
|
|
9449
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
9450
|
+
const record = {
|
|
9451
|
+
blockedBy,
|
|
9452
|
+
blockedByTitles,
|
|
9453
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
9454
|
+
};
|
|
9455
|
+
blocked.set(node.id, record);
|
|
9456
|
+
settled.add(node.id);
|
|
9457
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
while (settled.size < nodes.length) {
|
|
9461
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
9462
|
+
if (ready.length === 0) {
|
|
9463
|
+
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
9464
|
+
}
|
|
9465
|
+
waves.push(ready.map((node) => node.id));
|
|
9466
|
+
await this.hooks.onWaveStart?.(ready, wave);
|
|
9467
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
9468
|
+
const runOne = async (node) => {
|
|
9469
|
+
const result = await this.hooks.execute(node);
|
|
9470
|
+
results.set(node.id, result);
|
|
9471
|
+
settled.add(node.id);
|
|
9472
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
9473
|
+
outcomes.set(node.id, outcome);
|
|
9474
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
9475
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
9476
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
await this.hooks.onNodeComplete?.(node, result);
|
|
9480
|
+
};
|
|
9481
|
+
if ((this.options.mode ?? "parallel") === "sequential") {
|
|
9482
|
+
for (const node of ready) await runOne(node);
|
|
9483
|
+
} else {
|
|
9484
|
+
await Promise.all(ready.map(runOne));
|
|
9485
|
+
}
|
|
9486
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
9487
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
9488
|
+
await blockDescendants(failedThisWave);
|
|
9489
|
+
}
|
|
9490
|
+
wave++;
|
|
9491
|
+
}
|
|
9492
|
+
return { results, waves, blocked };
|
|
9493
|
+
}
|
|
9494
|
+
};
|
|
9495
|
+
|
|
9057
9496
|
// src/utils/json-extract.ts
|
|
9058
9497
|
function extractFirstJsonObject(input) {
|
|
9059
9498
|
if (!input) return null;
|
|
@@ -9125,7 +9564,7 @@ function buildT1SystemPrompt(has) {
|
|
|
9125
9564
|
Your responsibilities:
|
|
9126
9565
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
9127
9566
|
2. Decompose the task into logical sections (one per T2 Manager)
|
|
9128
|
-
3. For each section, define
|
|
9567
|
+
3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
|
|
9129
9568
|
4. Return a structured plan as JSON
|
|
9130
9569
|
|
|
9131
9570
|
CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
|
|
@@ -9151,6 +9590,9 @@ QUALITY RULES:
|
|
|
9151
9590
|
|
|
9152
9591
|
${generation}` : ""}`;
|
|
9153
9592
|
}
|
|
9593
|
+
function producedOutput(result) {
|
|
9594
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
9595
|
+
}
|
|
9154
9596
|
var T1Administrator = class extends BaseTier {
|
|
9155
9597
|
router;
|
|
9156
9598
|
toolRegistry;
|
|
@@ -9285,7 +9727,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
9285
9727
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
9286
9728
|
let pass = 1;
|
|
9287
9729
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
9288
|
-
const okCount = (rs) => rs.filter(
|
|
9730
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
9289
9731
|
while (pass <= maxReplanPasses) {
|
|
9290
9732
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
9291
9733
|
if (reviewResult.approved) {
|
|
@@ -9416,7 +9858,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
9416
9858
|
/** Structured, grounded summary of what's already done — used to keep
|
|
9417
9859
|
* corrective replan passes from re-emitting completed sections. */
|
|
9418
9860
|
summarizeCompletedSections(results) {
|
|
9419
|
-
const done = results.filter(
|
|
9861
|
+
const done = results.filter(producedOutput);
|
|
9420
9862
|
if (done.length === 0) return "";
|
|
9421
9863
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
9422
9864
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -9540,7 +9982,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9540
9982
|
}
|
|
9541
9983
|
}
|
|
9542
9984
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
9543
|
-
if (plan.sections.length < min)
|
|
9985
|
+
if (plan.sections.length < min || plan.sections.length > max) {
|
|
9986
|
+
this.log(
|
|
9987
|
+
`\u26A0 Plan has ${plan.sections.length} section(s) but complexity "${plan.complexity}" expects ${min}-${max}. Keeping the plan as written \u2014 a right-sized plan is better than one padded to hit a floor.`
|
|
9988
|
+
);
|
|
9989
|
+
}
|
|
9544
9990
|
}
|
|
9545
9991
|
async dispatchT2Managers(sections) {
|
|
9546
9992
|
for (const section of sections) {
|
|
@@ -9557,6 +10003,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9557
10003
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
9558
10004
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
9559
10005
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10006
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
9560
10007
|
if (this.store) {
|
|
9561
10008
|
manager.setStore(this.store);
|
|
9562
10009
|
}
|
|
@@ -9652,118 +10099,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9652
10099
|
return t2Results;
|
|
9653
10100
|
}
|
|
9654
10101
|
/**
|
|
9655
|
-
* Runs T2 managers respecting dependsOn declarations
|
|
10102
|
+
* Runs T2 managers respecting dependsOn declarations.
|
|
10103
|
+
*
|
|
10104
|
+
* Graph construction, validation and cycle repair now live in
|
|
10105
|
+
* compileSectionGraph (orchestration/adapters.ts), and wave execution in
|
|
10106
|
+
* DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
|
|
10107
|
+
* ordered by one implementation rather than two that drifted.
|
|
10108
|
+
*
|
|
10109
|
+
* The hand-rolled Kahn pass this replaces treated every node it could not
|
|
10110
|
+
* reach as part of a cycle. That set also contains everything DOWNSTREAM of a
|
|
10111
|
+
* cycle, whose in-degree can never fall to zero while its dependency is stuck,
|
|
10112
|
+
* so breaking a cycle here also cut the dependencies of innocent later
|
|
10113
|
+
* sections and started them immediately — before the sections they consume.
|
|
10114
|
+
* compileTaskGraph uses strongly connected components, so only real cycle
|
|
10115
|
+
* members are touched.
|
|
9656
10116
|
*/
|
|
9657
10117
|
async runT2sWithDependencies(sections, managers, taskId) {
|
|
9658
|
-
const
|
|
9659
|
-
const
|
|
9660
|
-
|
|
9661
|
-
const allKeys = new Set(sections.map((s) => s.sectionId));
|
|
9662
|
-
for (const s of sections) {
|
|
9663
|
-
if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
|
|
9664
|
-
inDegree.set(s.sectionId, 0);
|
|
9665
|
-
s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
|
|
10118
|
+
const compiled = compileSectionGraph(sections);
|
|
10119
|
+
for (const issue of compiled.issues) {
|
|
10120
|
+
this.log(`\u26A0 Section graph: ${issue.message}`);
|
|
9666
10121
|
}
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
}
|
|
9672
|
-
}
|
|
9673
|
-
const queue = [];
|
|
9674
|
-
const degree = new Map(inDegree);
|
|
9675
|
-
for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
|
|
9676
|
-
const visited = /* @__PURE__ */ new Set();
|
|
9677
|
-
while (queue.length > 0) {
|
|
9678
|
-
const u = queue.shift();
|
|
9679
|
-
visited.add(u);
|
|
9680
|
-
for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
|
|
9681
|
-
const newDeg = (degree.get(v) ?? 1) - 1;
|
|
9682
|
-
degree.set(v, newDeg);
|
|
9683
|
-
if (newDeg === 0) queue.push(v);
|
|
9684
|
-
}
|
|
9685
|
-
}
|
|
9686
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
9687
|
-
if (cycleNodes.length > 0) {
|
|
9688
|
-
this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
|
|
9689
|
-
for (const s of sections) {
|
|
9690
|
-
if (cycleNodes.includes(s.sectionId)) {
|
|
9691
|
-
const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
9692
|
-
for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
9693
|
-
inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
|
|
9694
|
-
adj.get(removed)?.delete(s.sectionId);
|
|
9695
|
-
}
|
|
9696
|
-
s.dependsOn = safeDeps;
|
|
9697
|
-
}
|
|
9698
|
-
}
|
|
10122
|
+
const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
|
|
10123
|
+
for (const section of sections) {
|
|
10124
|
+
const node = byId.get(section.sectionId);
|
|
10125
|
+
if (node) section.dependsOn = [...node.dependsOn];
|
|
9699
10126
|
}
|
|
9700
|
-
const
|
|
10127
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
10128
|
+
const totalSections = compiled.graph.nodes.length;
|
|
9701
10129
|
let completedSections = 0;
|
|
9702
|
-
const
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
10130
|
+
const scheduler = new DependencyScheduler(
|
|
10131
|
+
compiled.graph,
|
|
10132
|
+
{
|
|
10133
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
10134
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
10135
|
+
// Stamped before the wave executes so the first status event a node
|
|
10136
|
+
// emits already carries it.
|
|
10137
|
+
onWaveStart: (nodes, wave) => {
|
|
10138
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
10139
|
+
},
|
|
10140
|
+
execute: async (node) => {
|
|
10141
|
+
const section = node.payload;
|
|
10142
|
+
const manager = managers[node.ordinal];
|
|
10143
|
+
const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
|
|
10144
|
+
this.sendStatusUpdate({
|
|
10145
|
+
progressPct,
|
|
10146
|
+
currentAction: `T2 working on: ${section.sectionTitle}`,
|
|
10147
|
+
status: "IN_PROGRESS"
|
|
10148
|
+
});
|
|
10149
|
+
this.throwIfCancelled();
|
|
10150
|
+
try {
|
|
10151
|
+
const result = await manager.execute(section, taskId, this.signal);
|
|
10152
|
+
manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
|
|
10153
|
+
if (result.status === "ESCALATED") {
|
|
10154
|
+
this.escalations.push({
|
|
10155
|
+
raisedBy: `T2_${section.sectionId}`,
|
|
10156
|
+
sectionId: section.sectionId,
|
|
10157
|
+
attempted: result.issues,
|
|
10158
|
+
blocker: result.issues.join("; "),
|
|
10159
|
+
needs: "Human review required"
|
|
10160
|
+
});
|
|
10161
|
+
}
|
|
10162
|
+
return result;
|
|
10163
|
+
} catch (err) {
|
|
10164
|
+
return {
|
|
9729
10165
|
sectionId: section.sectionId,
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
10166
|
+
sectionTitle: section.sectionTitle,
|
|
10167
|
+
status: "FAILED",
|
|
10168
|
+
t3Results: [],
|
|
10169
|
+
sectionSummary: "",
|
|
10170
|
+
issues: [err instanceof Error ? err.message : String(err)]
|
|
10171
|
+
};
|
|
9734
10172
|
}
|
|
9735
|
-
}
|
|
9736
|
-
|
|
10173
|
+
},
|
|
10174
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
10175
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
10176
|
+
// the user more than letting it try.
|
|
10177
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
10178
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
10179
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
10180
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
10181
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
10182
|
+
// should have been skipped would run and be billed.
|
|
10183
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
10184
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
10185
|
+
const section = node.payload;
|
|
10186
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
10187
|
+
resultMap.set(node.id, {
|
|
9737
10188
|
sectionId: section.sectionId,
|
|
9738
10189
|
sectionTitle: section.sectionTitle,
|
|
9739
|
-
|
|
10190
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
10191
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
10192
|
+
// that did (the upstream section named below).
|
|
10193
|
+
status: "BLOCKED",
|
|
9740
10194
|
t3Results: [],
|
|
9741
10195
|
sectionSummary: "",
|
|
9742
|
-
issues: [
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
10196
|
+
issues: [
|
|
10197
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
10198
|
+
],
|
|
10199
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
10200
|
+
});
|
|
10201
|
+
completedSections++;
|
|
10202
|
+
this.sendStatusUpdate({
|
|
10203
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
10204
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
10205
|
+
status: "IN_PROGRESS"
|
|
10206
|
+
});
|
|
10207
|
+
this.emit("tier:status", {
|
|
10208
|
+
tierId: section.sectionId,
|
|
10209
|
+
// A blocked section has no tier instance — it was never
|
|
10210
|
+
// constructed — so its runtime id and its graph id are the same
|
|
10211
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
10212
|
+
// stream addressable in the graph's id space, which is what
|
|
10213
|
+
// dependsOn edges point at.
|
|
10214
|
+
nodeId: section.sectionId,
|
|
10215
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
10216
|
+
role: "T2",
|
|
10217
|
+
label: section.sectionTitle,
|
|
10218
|
+
status: "BLOCKED",
|
|
10219
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10220
|
+
output: blockedInfo.reason
|
|
10221
|
+
});
|
|
10222
|
+
this.emit("section:complete", {
|
|
10223
|
+
id: node.id,
|
|
10224
|
+
title: section.sectionTitle,
|
|
10225
|
+
status: "BLOCKED",
|
|
10226
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10227
|
+
output: ""
|
|
10228
|
+
});
|
|
10229
|
+
},
|
|
10230
|
+
onNodeComplete: (node, result) => {
|
|
10231
|
+
resultMap.set(node.id, result);
|
|
10232
|
+
completedSections++;
|
|
10233
|
+
this.emit("section:complete", {
|
|
10234
|
+
id: node.id,
|
|
10235
|
+
title: result.sectionTitle,
|
|
10236
|
+
status: result.status,
|
|
10237
|
+
output: result.sectionSummary
|
|
10238
|
+
});
|
|
9754
10239
|
}
|
|
9755
|
-
}
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
10240
|
+
},
|
|
10241
|
+
{
|
|
10242
|
+
// Cross-section concurrency respects the same t3Execution mode as the
|
|
10243
|
+
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
10244
|
+
// serialized T3 workers within a single section while every section's
|
|
10245
|
+
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
10246
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
10247
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
10248
|
+
// output. Running it after the dependency failed produces a second
|
|
10249
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
10250
|
+
onUpstreamFailure: "block"
|
|
9760
10251
|
}
|
|
9761
|
-
|
|
9762
|
-
await
|
|
10252
|
+
);
|
|
10253
|
+
await scheduler.run();
|
|
9763
10254
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
9764
10255
|
}
|
|
9765
10256
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
9766
|
-
const completedSections = t2Results.filter(
|
|
10257
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
9767
10258
|
if (!completedSections.length) {
|
|
9768
10259
|
const allIssues = [
|
|
9769
10260
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -9783,7 +10274,6 @@ ${r.sectionSummary}
|
|
|
9783
10274
|
Outputs:
|
|
9784
10275
|
${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
|
|
9785
10276
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
9786
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
9787
10277
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
9788
10278
|
|
|
9789
10279
|
Original request: ${originalPrompt}
|
|
@@ -10275,6 +10765,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
10275
10765
|
};
|
|
10276
10766
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
10277
10767
|
var MAX_VIDEO_SECONDS = 8;
|
|
10768
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
10278
10769
|
var GenerateVideoTool = class extends BaseTool {
|
|
10279
10770
|
constructor(deps) {
|
|
10280
10771
|
super();
|
|
@@ -10291,7 +10782,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10291
10782
|
type: "object",
|
|
10292
10783
|
properties: {
|
|
10293
10784
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
10294
|
-
seconds: { type: "number", description: `Clip length,
|
|
10785
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
10295
10786
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
10296
10787
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
10297
10788
|
},
|
|
@@ -10303,7 +10794,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10303
10794
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
10304
10795
|
if (!r.ok) return r.error;
|
|
10305
10796
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
10306
|
-
const seconds = Math.max(
|
|
10797
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
10307
10798
|
const asset = await generateVideo(
|
|
10308
10799
|
r.selected.capability,
|
|
10309
10800
|
r.cfg,
|
|
@@ -10320,7 +10811,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10320
10811
|
const location = await this.deps.sink(asset);
|
|
10321
10812
|
const unit = r.selected.cost;
|
|
10322
10813
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
10323
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
10814
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
10324
10815
|
return `Video generated and saved to ${location}.
|
|
10325
10816
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
10326
10817
|
}
|
|
@@ -10389,16 +10880,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
10389
10880
|
if (typeof input !== "string" || input.length === 0) {
|
|
10390
10881
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
10391
10882
|
}
|
|
10392
|
-
const root =
|
|
10393
|
-
const abs =
|
|
10394
|
-
const rel =
|
|
10395
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
10883
|
+
const root = path26.resolve(workspaceRoot);
|
|
10884
|
+
const abs = path26.isAbsolute(input) ? path26.resolve(input) : path26.resolve(root, input);
|
|
10885
|
+
const rel = path26.relative(root, abs);
|
|
10886
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26.isAbsolute(rel)) {
|
|
10396
10887
|
throw new WorkspaceSandboxError(input, root);
|
|
10397
10888
|
}
|
|
10398
10889
|
try {
|
|
10399
|
-
const real =
|
|
10400
|
-
const realRel =
|
|
10401
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
10890
|
+
const real = fs25.realpathSync(abs);
|
|
10891
|
+
const realRel = path26.relative(root, real);
|
|
10892
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26.isAbsolute(realRel))) {
|
|
10402
10893
|
throw new WorkspaceSandboxError(input, root);
|
|
10403
10894
|
}
|
|
10404
10895
|
} catch (e) {
|
|
@@ -11056,7 +11547,7 @@ var GenerateDocumentTool = class extends BaseTool {
|
|
|
11056
11547
|
}
|
|
11057
11548
|
}
|
|
11058
11549
|
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
11059
|
-
await fs5.mkdir(
|
|
11550
|
+
await fs5.mkdir(path26.dirname(absPath), { recursive: true });
|
|
11060
11551
|
await fs5.writeFile(absPath, bytes);
|
|
11061
11552
|
const charts = countCharts(content);
|
|
11062
11553
|
return [
|
|
@@ -11451,7 +11942,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
11451
11942
|
} catch {
|
|
11452
11943
|
}
|
|
11453
11944
|
}
|
|
11454
|
-
await fs5.mkdir(
|
|
11945
|
+
await fs5.mkdir(path26.dirname(absPath), { recursive: true });
|
|
11455
11946
|
await fs5.writeFile(absPath, content, "utf-8");
|
|
11456
11947
|
return `Written ${content.length} characters to ${filePath}`;
|
|
11457
11948
|
}
|
|
@@ -11949,7 +12440,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
11949
12440
|
};
|
|
11950
12441
|
async function fileToImageAttachment(filePath) {
|
|
11951
12442
|
const data = await fs5.readFile(filePath);
|
|
11952
|
-
const ext =
|
|
12443
|
+
const ext = path26.extname(filePath).toLowerCase();
|
|
11953
12444
|
const mimeMap = {
|
|
11954
12445
|
".jpg": "image/jpeg",
|
|
11955
12446
|
".jpeg": "image/jpeg",
|
|
@@ -11983,14 +12474,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
11983
12474
|
const filePath = input["path"];
|
|
11984
12475
|
const content = input["content"];
|
|
11985
12476
|
const title = input["title"];
|
|
11986
|
-
const dir =
|
|
11987
|
-
if (!
|
|
11988
|
-
|
|
12477
|
+
const dir = path26.dirname(filePath);
|
|
12478
|
+
if (!fs25.existsSync(dir)) {
|
|
12479
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
11989
12480
|
}
|
|
11990
12481
|
return new Promise((resolve2, reject) => {
|
|
11991
12482
|
try {
|
|
11992
12483
|
const doc = new PDFDocument({ margin: 50 });
|
|
11993
|
-
const stream =
|
|
12484
|
+
const stream = fs25.createWriteStream(filePath);
|
|
11994
12485
|
doc.pipe(stream);
|
|
11995
12486
|
if (title) {
|
|
11996
12487
|
doc.info["Title"] = title;
|
|
@@ -12068,22 +12559,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
12068
12559
|
}
|
|
12069
12560
|
cmdPrefix = NODE_CMD;
|
|
12070
12561
|
}
|
|
12071
|
-
const tmpDir =
|
|
12072
|
-
if (!
|
|
12073
|
-
|
|
12562
|
+
const tmpDir = path26.join(this.workspaceRoot, ".cascade", "tmp");
|
|
12563
|
+
if (!fs25.existsSync(tmpDir)) {
|
|
12564
|
+
fs25.mkdirSync(tmpDir, { recursive: true });
|
|
12074
12565
|
}
|
|
12075
12566
|
const extension = language === "python" ? "py" : "js";
|
|
12076
12567
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
12077
|
-
const filePath =
|
|
12078
|
-
|
|
12568
|
+
const filePath = path26.join(tmpDir, fileName);
|
|
12569
|
+
fs25.writeFileSync(filePath, code, "utf-8");
|
|
12079
12570
|
const execArgs = [filePath, ...args];
|
|
12080
12571
|
return new Promise((resolve2) => {
|
|
12081
12572
|
const startMs = Date.now();
|
|
12082
12573
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
12083
12574
|
const duration = Date.now() - startMs;
|
|
12084
12575
|
try {
|
|
12085
|
-
if (
|
|
12086
|
-
|
|
12576
|
+
if (fs25.existsSync(filePath)) {
|
|
12577
|
+
fs25.unlinkSync(filePath);
|
|
12087
12578
|
}
|
|
12088
12579
|
} catch (cleanupErr) {
|
|
12089
12580
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -12395,7 +12886,7 @@ var GlobTool = class extends BaseTool {
|
|
|
12395
12886
|
};
|
|
12396
12887
|
async execute(input, _options) {
|
|
12397
12888
|
const pattern = input["pattern"];
|
|
12398
|
-
const searchPath = input["path"] ?
|
|
12889
|
+
const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
12399
12890
|
const matches = await glob(pattern, {
|
|
12400
12891
|
cwd: searchPath,
|
|
12401
12892
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -12408,7 +12899,7 @@ var GlobTool = class extends BaseTool {
|
|
|
12408
12899
|
const withMtime = await Promise.all(
|
|
12409
12900
|
matches.map(async (rel) => {
|
|
12410
12901
|
try {
|
|
12411
|
-
const stat = await fs5.stat(
|
|
12902
|
+
const stat = await fs5.stat(path26.join(searchPath, rel));
|
|
12412
12903
|
return { rel, mtime: stat.mtimeMs };
|
|
12413
12904
|
} catch {
|
|
12414
12905
|
return { rel, mtime: 0 };
|
|
@@ -12457,7 +12948,7 @@ var GrepTool = class extends BaseTool {
|
|
|
12457
12948
|
};
|
|
12458
12949
|
async execute(input, _options) {
|
|
12459
12950
|
const pattern = input["pattern"];
|
|
12460
|
-
const searchPath = input["path"] ?
|
|
12951
|
+
const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
12461
12952
|
const globPattern = input["glob"];
|
|
12462
12953
|
const outputMode = input["output_mode"] ?? "content";
|
|
12463
12954
|
const context = input["context"] ?? 0;
|
|
@@ -12511,12 +13002,12 @@ var GrepTool = class extends BaseTool {
|
|
|
12511
13002
|
nodir: true
|
|
12512
13003
|
});
|
|
12513
13004
|
} catch {
|
|
12514
|
-
files = [
|
|
13005
|
+
files = [path26.relative(searchPath, searchPath) || "."];
|
|
12515
13006
|
}
|
|
12516
13007
|
const results = [];
|
|
12517
13008
|
let totalCount = 0;
|
|
12518
13009
|
for (const rel of files) {
|
|
12519
|
-
const abs =
|
|
13010
|
+
const abs = path26.join(searchPath, rel);
|
|
12520
13011
|
let content;
|
|
12521
13012
|
try {
|
|
12522
13013
|
content = await fs5.readFile(abs, "utf-8");
|
|
@@ -12903,10 +13394,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
12903
13394
|
}
|
|
12904
13395
|
isIgnored(filePath) {
|
|
12905
13396
|
if (!filePath) return false;
|
|
12906
|
-
const abs =
|
|
12907
|
-
const rel =
|
|
12908
|
-
if (!rel || rel.startsWith("..") ||
|
|
12909
|
-
const posixRel = rel.split(
|
|
13397
|
+
const abs = path26.resolve(this.workspaceRoot, filePath);
|
|
13398
|
+
const rel = path26.relative(this.workspaceRoot, abs);
|
|
13399
|
+
if (!rel || rel.startsWith("..") || path26.isAbsolute(rel)) return true;
|
|
13400
|
+
const posixRel = rel.split(path26.sep).join("/");
|
|
12910
13401
|
return this.ignoreMatcher.ignores(posixRel);
|
|
12911
13402
|
}
|
|
12912
13403
|
};
|
|
@@ -13140,22 +13631,22 @@ var FileMcpOAuthStore = class {
|
|
|
13140
13631
|
filePath;
|
|
13141
13632
|
load() {
|
|
13142
13633
|
try {
|
|
13143
|
-
return JSON.parse(
|
|
13634
|
+
return JSON.parse(fs25.readFileSync(this.filePath, "utf-8"));
|
|
13144
13635
|
} catch {
|
|
13145
13636
|
return void 0;
|
|
13146
13637
|
}
|
|
13147
13638
|
}
|
|
13148
13639
|
save(state) {
|
|
13149
|
-
|
|
13150
|
-
|
|
13640
|
+
fs25.mkdirSync(path26.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
13641
|
+
fs25.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
13151
13642
|
try {
|
|
13152
|
-
|
|
13643
|
+
fs25.chmodSync(this.filePath, 384);
|
|
13153
13644
|
} catch {
|
|
13154
13645
|
}
|
|
13155
13646
|
}
|
|
13156
13647
|
clear() {
|
|
13157
13648
|
try {
|
|
13158
|
-
|
|
13649
|
+
fs25.rmSync(this.filePath);
|
|
13159
13650
|
} catch {
|
|
13160
13651
|
}
|
|
13161
13652
|
}
|
|
@@ -14052,6 +14543,14 @@ function heuristicAnalyze(prompt) {
|
|
|
14052
14543
|
const estimatedTokens = wordCount * 5;
|
|
14053
14544
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
14054
14545
|
}
|
|
14546
|
+
function hashPrompt(prompt) {
|
|
14547
|
+
let hash = 2166136261;
|
|
14548
|
+
for (let index = 0; index < prompt.length; index++) {
|
|
14549
|
+
hash ^= prompt.charCodeAt(index);
|
|
14550
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
14551
|
+
}
|
|
14552
|
+
return `${prompt.length}:${hash.toString(36)}`;
|
|
14553
|
+
}
|
|
14055
14554
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
14056
14555
|
var TASK_TYPE_TAGS = {
|
|
14057
14556
|
code: ["code", "instruction"],
|
|
@@ -14066,7 +14565,24 @@ var TaskAnalyzer = class {
|
|
|
14066
14565
|
feedback;
|
|
14067
14566
|
bias;
|
|
14068
14567
|
lastProfile = null;
|
|
14069
|
-
|
|
14568
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
14569
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
14570
|
+
/**
|
|
14571
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
14572
|
+
* type they were chosen for.
|
|
14573
|
+
*
|
|
14574
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
14575
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
14576
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
14577
|
+
* model is good at creative writing, from a rating that never said so.
|
|
14578
|
+
*
|
|
14579
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
14580
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
14581
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
14582
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
14583
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
14584
|
+
*/
|
|
14585
|
+
lastCompletedRun;
|
|
14070
14586
|
constructor(tracker, bias = "balanced") {
|
|
14071
14587
|
this.tracker = tracker;
|
|
14072
14588
|
this.bias = bias;
|
|
@@ -14095,7 +14611,7 @@ var TaskAnalyzer = class {
|
|
|
14095
14611
|
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
14096
14612
|
*/
|
|
14097
14613
|
async analyze(prompt) {
|
|
14098
|
-
const cacheKey = prompt
|
|
14614
|
+
const cacheKey = hashPrompt(prompt);
|
|
14099
14615
|
const cached = analysisCache.get(cacheKey);
|
|
14100
14616
|
if (cached) {
|
|
14101
14617
|
this.lastProfile = cached;
|
|
@@ -14113,11 +14629,15 @@ var TaskAnalyzer = class {
|
|
|
14113
14629
|
*/
|
|
14114
14630
|
async selectModel(prompt, tier, selector, opts) {
|
|
14115
14631
|
const profile = await this.analyze(prompt);
|
|
14632
|
+
const recordSelection = (model) => {
|
|
14633
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
14634
|
+
return model;
|
|
14635
|
+
};
|
|
14116
14636
|
if (profile.requiresVision) {
|
|
14117
|
-
return selector.selectVisionModel();
|
|
14637
|
+
return recordSelection(selector.selectVisionModel());
|
|
14118
14638
|
}
|
|
14119
14639
|
let candidates = selector.getCandidatesForTier(tier);
|
|
14120
|
-
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
14640
|
+
if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
14121
14641
|
if (opts?.requiresToolUse) {
|
|
14122
14642
|
const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
|
|
14123
14643
|
if (toolCapable.length > 0) candidates = toolCapable;
|
|
@@ -14127,9 +14647,7 @@ var TaskAnalyzer = class {
|
|
|
14127
14647
|
score: this.scoreModel(m, profile)
|
|
14128
14648
|
}));
|
|
14129
14649
|
scored.sort((a, b) => b.score - a.score);
|
|
14130
|
-
|
|
14131
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
14132
|
-
return best;
|
|
14650
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
14133
14651
|
}
|
|
14134
14652
|
/**
|
|
14135
14653
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -14138,25 +14656,38 @@ var TaskAnalyzer = class {
|
|
|
14138
14656
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
14139
14657
|
if (!this.tracker || !this.lastProfile) return;
|
|
14140
14658
|
const taskType = this.lastProfile.type;
|
|
14141
|
-
for (const [tier, model] of this.
|
|
14659
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
14142
14660
|
const cost = costByTier[tier] ?? 0;
|
|
14143
14661
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
14144
14662
|
}
|
|
14145
|
-
this.
|
|
14663
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
14664
|
+
this.currentRunSelections.clear();
|
|
14146
14665
|
void this.tracker.save();
|
|
14147
14666
|
}
|
|
14148
14667
|
/**
|
|
14149
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
14668
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
14150
14669
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
14151
|
-
*
|
|
14670
|
+
*
|
|
14671
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
14672
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
14673
|
+
* empty.
|
|
14674
|
+
*
|
|
14675
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
14676
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
14677
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
14678
|
+
* that reports false, which is what "rate this run" should mean.
|
|
14152
14679
|
*/
|
|
14153
14680
|
recordExplicitRating(rating) {
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14681
|
+
const snapshot = this.lastCompletedRun;
|
|
14682
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
14683
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
14684
|
+
for (const [, model] of snapshot.selections) {
|
|
14685
|
+
if (ratedModels.has(model.id)) continue;
|
|
14686
|
+
ratedModels.add(model.id);
|
|
14687
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
14688
|
+
}
|
|
14689
|
+
this.lastCompletedRun = void 0;
|
|
14690
|
+
return true;
|
|
14160
14691
|
}
|
|
14161
14692
|
scoreModel(model, profile) {
|
|
14162
14693
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -14194,7 +14725,7 @@ var TaskAnalyzer = class {
|
|
|
14194
14725
|
analysisCache.clear();
|
|
14195
14726
|
}
|
|
14196
14727
|
};
|
|
14197
|
-
var DEFAULT_STATS_FILE =
|
|
14728
|
+
var DEFAULT_STATS_FILE = path26.join(os6.homedir(), ".cascade", "model-perf.json");
|
|
14198
14729
|
var ModelPerformanceTracker = class {
|
|
14199
14730
|
stats = /* @__PURE__ */ new Map();
|
|
14200
14731
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -14234,7 +14765,7 @@ var ModelPerformanceTracker = class {
|
|
|
14234
14765
|
async save() {
|
|
14235
14766
|
if (this.readOnly) return;
|
|
14236
14767
|
try {
|
|
14237
|
-
await fs5.mkdir(
|
|
14768
|
+
await fs5.mkdir(path26.dirname(this.statsFile), { recursive: true });
|
|
14238
14769
|
const modelsObj = {};
|
|
14239
14770
|
const featuresObj = {};
|
|
14240
14771
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -14782,7 +15313,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
14782
15313
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
14783
15314
|
async loadPersistedTools() {
|
|
14784
15315
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
14785
|
-
const file =
|
|
15316
|
+
const file = path26.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
14786
15317
|
try {
|
|
14787
15318
|
const raw = await fs5.readFile(file, "utf-8");
|
|
14788
15319
|
const specs = JSON.parse(raw);
|
|
@@ -14806,8 +15337,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
14806
15337
|
}
|
|
14807
15338
|
async persist() {
|
|
14808
15339
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
14809
|
-
const dir =
|
|
14810
|
-
const file =
|
|
15340
|
+
const dir = path26.join(this.workspacePath, ".cascade");
|
|
15341
|
+
const file = path26.join(dir, DYNAMIC_TOOLS_FILE);
|
|
14811
15342
|
try {
|
|
14812
15343
|
await fs5.mkdir(dir, { recursive: true });
|
|
14813
15344
|
await fs5.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -14827,12 +15358,12 @@ var WorldStateDB = class {
|
|
|
14827
15358
|
constructor(workspacePath, debugMode = false) {
|
|
14828
15359
|
this.workspacePath = workspacePath;
|
|
14829
15360
|
this.debugMode = debugMode;
|
|
14830
|
-
const cascadeDir =
|
|
14831
|
-
if (!
|
|
14832
|
-
|
|
15361
|
+
const cascadeDir = path26.join(workspacePath, ".cascade");
|
|
15362
|
+
if (!fs25.existsSync(cascadeDir)) {
|
|
15363
|
+
fs25.mkdirSync(cascadeDir, { recursive: true });
|
|
14833
15364
|
}
|
|
14834
|
-
this.keyPath =
|
|
14835
|
-
this.dbPath =
|
|
15365
|
+
this.keyPath = path26.join(cascadeDir, "world_state.key");
|
|
15366
|
+
this.dbPath = path26.join(cascadeDir, "world_state.db");
|
|
14836
15367
|
this.initEncryptionKey();
|
|
14837
15368
|
this.db = new Database(this.dbPath);
|
|
14838
15369
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -14889,11 +15420,11 @@ var WorldStateDB = class {
|
|
|
14889
15420
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
14890
15421
|
}
|
|
14891
15422
|
initEncryptionKey() {
|
|
14892
|
-
if (
|
|
14893
|
-
this.encryptionKey =
|
|
15423
|
+
if (fs25.existsSync(this.keyPath)) {
|
|
15424
|
+
this.encryptionKey = fs25.readFileSync(this.keyPath);
|
|
14894
15425
|
} else {
|
|
14895
15426
|
this.encryptionKey = crypto4.randomBytes(32);
|
|
14896
|
-
|
|
15427
|
+
fs25.writeFileSync(this.keyPath, this.encryptionKey);
|
|
14897
15428
|
}
|
|
14898
15429
|
}
|
|
14899
15430
|
encrypt(text) {
|
|
@@ -15179,9 +15710,9 @@ var WorldStateDB = class {
|
|
|
15179
15710
|
dumpDebugIfNeeded() {
|
|
15180
15711
|
if (!this.debugMode) return;
|
|
15181
15712
|
try {
|
|
15182
|
-
const dumpPath =
|
|
15713
|
+
const dumpPath = path26.join(os6.tmpdir(), "cascade_world_state_debug.json");
|
|
15183
15714
|
const entries = this.getAllEntries();
|
|
15184
|
-
|
|
15715
|
+
fs25.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
15185
15716
|
} catch (err) {
|
|
15186
15717
|
console.error("Failed to dump debug world state", err);
|
|
15187
15718
|
}
|
|
@@ -15190,6 +15721,212 @@ var WorldStateDB = class {
|
|
|
15190
15721
|
this.db.close();
|
|
15191
15722
|
}
|
|
15192
15723
|
};
|
|
15724
|
+
var RESUME_SCHEMA_VERSION = 1;
|
|
15725
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
15726
|
+
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15727
|
+
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
15728
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
15729
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15730
|
+
function safeId(taskId) {
|
|
15731
|
+
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
15732
|
+
return cleaned.slice(0, 100) || "run";
|
|
15733
|
+
}
|
|
15734
|
+
function isCheckpoint(value) {
|
|
15735
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15736
|
+
const c = value;
|
|
15737
|
+
return c.version === RESUME_SCHEMA_VERSION && typeof c.taskId === "string" && typeof c.prompt === "string" && typeof c.createdAt === "string" && typeof c.partialOutput === "string" && Array.isArray(c.completed);
|
|
15738
|
+
}
|
|
15739
|
+
var ResumeStore = class {
|
|
15740
|
+
dir;
|
|
15741
|
+
maxCheckpoints;
|
|
15742
|
+
maxAgeMs;
|
|
15743
|
+
now;
|
|
15744
|
+
claimTimeoutMs;
|
|
15745
|
+
constructor(options) {
|
|
15746
|
+
this.dir = options.dir;
|
|
15747
|
+
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
15748
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
15749
|
+
this.now = options.now ?? Date.now;
|
|
15750
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
15751
|
+
}
|
|
15752
|
+
/**
|
|
15753
|
+
* Persist a checkpoint and prune.
|
|
15754
|
+
*
|
|
15755
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
15756
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
15757
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
15758
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
15759
|
+
* look alike to them.
|
|
15760
|
+
*
|
|
15761
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
15762
|
+
*/
|
|
15763
|
+
async save(checkpoint) {
|
|
15764
|
+
try {
|
|
15765
|
+
const timestamp = this.now();
|
|
15766
|
+
const record = {
|
|
15767
|
+
...checkpoint,
|
|
15768
|
+
version: RESUME_SCHEMA_VERSION,
|
|
15769
|
+
createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
|
|
15770
|
+
};
|
|
15771
|
+
await promises.mkdir(this.dir, { recursive: true });
|
|
15772
|
+
const finalPath = path26.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
|
|
15773
|
+
const tempPath = `${finalPath}.${process.pid}.tmp`;
|
|
15774
|
+
await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
15775
|
+
await promises.rename(tempPath, finalPath);
|
|
15776
|
+
await this.prune();
|
|
15777
|
+
return true;
|
|
15778
|
+
} catch {
|
|
15779
|
+
return false;
|
|
15780
|
+
}
|
|
15781
|
+
}
|
|
15782
|
+
/** Valid, unexpired checkpoints, newest first. */
|
|
15783
|
+
async list() {
|
|
15784
|
+
let entries;
|
|
15785
|
+
try {
|
|
15786
|
+
entries = await promises.readdir(this.dir);
|
|
15787
|
+
} catch {
|
|
15788
|
+
return [];
|
|
15789
|
+
}
|
|
15790
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
15791
|
+
const candidates = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).filter((e) => e.timestamp >= cutoff).sort((a, b) => b.timestamp - a.timestamp);
|
|
15792
|
+
const checkpoints = [];
|
|
15793
|
+
for (const candidate of candidates) {
|
|
15794
|
+
try {
|
|
15795
|
+
const raw = await promises.readFile(path26.join(this.dir, candidate.name), "utf-8");
|
|
15796
|
+
const parsed = JSON.parse(raw);
|
|
15797
|
+
if (isCheckpoint(parsed)) checkpoints.push(parsed);
|
|
15798
|
+
} catch {
|
|
15799
|
+
continue;
|
|
15800
|
+
}
|
|
15801
|
+
}
|
|
15802
|
+
return checkpoints;
|
|
15803
|
+
}
|
|
15804
|
+
/** The most recent resumable run, or null. */
|
|
15805
|
+
async latest() {
|
|
15806
|
+
return (await this.list())[0] ?? null;
|
|
15807
|
+
}
|
|
15808
|
+
/**
|
|
15809
|
+
* Claim a checkpoint for a resume attempt.
|
|
15810
|
+
*
|
|
15811
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
15812
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
15813
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
15814
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
15815
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
15816
|
+
* wrong trade for a recovery mechanism.
|
|
15817
|
+
*
|
|
15818
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
15819
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
15820
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
15821
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
15822
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
15823
|
+
*/
|
|
15824
|
+
async claim(taskId) {
|
|
15825
|
+
const all = await this.list();
|
|
15826
|
+
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
15827
|
+
if (!target) return null;
|
|
15828
|
+
try {
|
|
15829
|
+
const entries = await promises.readdir(this.dir);
|
|
15830
|
+
const wanted = safeId(target.taskId);
|
|
15831
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
15832
|
+
if (!source) return null;
|
|
15833
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
15834
|
+
await promises.rename(path26.join(this.dir, source), path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
15835
|
+
return { checkpoint: target, claimId };
|
|
15836
|
+
} catch {
|
|
15837
|
+
return null;
|
|
15838
|
+
}
|
|
15839
|
+
}
|
|
15840
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
15841
|
+
async release(claimId) {
|
|
15842
|
+
try {
|
|
15843
|
+
await promises.rename(
|
|
15844
|
+
path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
15845
|
+
path26.join(this.dir, `${claimId}.json`)
|
|
15846
|
+
);
|
|
15847
|
+
} catch {
|
|
15848
|
+
}
|
|
15849
|
+
}
|
|
15850
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
15851
|
+
async settle(claimId) {
|
|
15852
|
+
try {
|
|
15853
|
+
await promises.rm(path26.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
15854
|
+
} catch {
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15857
|
+
/**
|
|
15858
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
15859
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
15860
|
+
*/
|
|
15861
|
+
async consume(taskId) {
|
|
15862
|
+
const claimed = await this.claim(taskId);
|
|
15863
|
+
if (!claimed) return null;
|
|
15864
|
+
await this.settle(claimed.claimId);
|
|
15865
|
+
return claimed.checkpoint;
|
|
15866
|
+
}
|
|
15867
|
+
/**
|
|
15868
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
15869
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
15870
|
+
* trading one loss window for another.
|
|
15871
|
+
*/
|
|
15872
|
+
async reclaimStale() {
|
|
15873
|
+
let reclaimed = 0;
|
|
15874
|
+
try {
|
|
15875
|
+
const entries = await promises.readdir(this.dir);
|
|
15876
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
15877
|
+
for (const name of entries) {
|
|
15878
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
15879
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
15880
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
15881
|
+
await promises.rename(
|
|
15882
|
+
path26.join(this.dir, name),
|
|
15883
|
+
path26.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
15884
|
+
);
|
|
15885
|
+
reclaimed++;
|
|
15886
|
+
}
|
|
15887
|
+
} catch {
|
|
15888
|
+
}
|
|
15889
|
+
return reclaimed;
|
|
15890
|
+
}
|
|
15891
|
+
/** Remove every checkpoint for a task id. */
|
|
15892
|
+
async delete(taskId) {
|
|
15893
|
+
try {
|
|
15894
|
+
const entries = await promises.readdir(this.dir);
|
|
15895
|
+
const wanted = safeId(taskId);
|
|
15896
|
+
await Promise.all(entries.map(async (name) => {
|
|
15897
|
+
const match = FILE_PATTERN.exec(name);
|
|
15898
|
+
if (match?.[2] === wanted) {
|
|
15899
|
+
await promises.rm(path26.join(this.dir, name), { force: true });
|
|
15900
|
+
}
|
|
15901
|
+
}));
|
|
15902
|
+
} catch {
|
|
15903
|
+
}
|
|
15904
|
+
}
|
|
15905
|
+
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
15906
|
+
async prune() {
|
|
15907
|
+
try {
|
|
15908
|
+
const entries = await promises.readdir(this.dir);
|
|
15909
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
15910
|
+
const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
|
|
15911
|
+
const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
|
|
15912
|
+
const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
|
|
15913
|
+
await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
|
|
15914
|
+
(name) => promises.rm(path26.join(this.dir, name), { force: true })
|
|
15915
|
+
));
|
|
15916
|
+
} catch {
|
|
15917
|
+
}
|
|
15918
|
+
}
|
|
15919
|
+
};
|
|
15920
|
+
function summarizeCompleted(completed, perNodeChars = 500) {
|
|
15921
|
+
if (!completed.length) return "";
|
|
15922
|
+
const lines = completed.map((node) => {
|
|
15923
|
+
const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
|
|
15924
|
+
return `- [${node.status}] ${node.title}
|
|
15925
|
+
${body.replace(/\n/g, "\n ")}`;
|
|
15926
|
+
});
|
|
15927
|
+
return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
|
|
15928
|
+
${lines.join("\n")}`;
|
|
15929
|
+
}
|
|
15193
15930
|
var ignore2 = ignoreFactory.default ?? ignoreFactory;
|
|
15194
15931
|
var PrivacyPaths = class {
|
|
15195
15932
|
localOnly;
|
|
@@ -15236,7 +15973,7 @@ var CascadeIgnore = class {
|
|
|
15236
15973
|
]);
|
|
15237
15974
|
}
|
|
15238
15975
|
async load(workspacePath) {
|
|
15239
|
-
const filePath =
|
|
15976
|
+
const filePath = path26.join(workspacePath, ".cascadeignore");
|
|
15240
15977
|
try {
|
|
15241
15978
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
15242
15979
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -15247,7 +15984,7 @@ var CascadeIgnore = class {
|
|
|
15247
15984
|
}
|
|
15248
15985
|
isIgnored(filePath, workspacePath) {
|
|
15249
15986
|
try {
|
|
15250
|
-
const relative = workspacePath ?
|
|
15987
|
+
const relative = workspacePath ? path26.relative(workspacePath, filePath) : filePath;
|
|
15251
15988
|
return this.ig.ignores(relative);
|
|
15252
15989
|
} catch {
|
|
15253
15990
|
return false;
|
|
@@ -15590,7 +16327,7 @@ var WorkspaceIndex = class {
|
|
|
15590
16327
|
isIgnored;
|
|
15591
16328
|
chunker;
|
|
15592
16329
|
constructor(opts) {
|
|
15593
|
-
this.root =
|
|
16330
|
+
this.root = path26.resolve(opts.root);
|
|
15594
16331
|
this.db = opts.db;
|
|
15595
16332
|
this.namespace = opts.namespace ?? "code";
|
|
15596
16333
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -15622,14 +16359,14 @@ var WorkspaceIndex = class {
|
|
|
15622
16359
|
return;
|
|
15623
16360
|
}
|
|
15624
16361
|
for (const entry of entries) {
|
|
15625
|
-
const abs =
|
|
16362
|
+
const abs = path26.join(dir, entry.name);
|
|
15626
16363
|
if (entry.isDirectory()) {
|
|
15627
16364
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
15628
16365
|
await walk(abs);
|
|
15629
16366
|
} else if (entry.isFile()) {
|
|
15630
|
-
const ext =
|
|
16367
|
+
const ext = path26.extname(entry.name).slice(1).toLowerCase();
|
|
15631
16368
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
15632
|
-
out.push(
|
|
16369
|
+
out.push(path26.relative(this.root, abs));
|
|
15633
16370
|
}
|
|
15634
16371
|
}
|
|
15635
16372
|
};
|
|
@@ -15659,7 +16396,7 @@ var WorkspaceIndex = class {
|
|
|
15659
16396
|
const entries = [];
|
|
15660
16397
|
for (const rel of rels) {
|
|
15661
16398
|
try {
|
|
15662
|
-
const abs =
|
|
16399
|
+
const abs = path26.join(this.root, rel);
|
|
15663
16400
|
const stat = await fs5.stat(abs);
|
|
15664
16401
|
if (stat.size > this.maxFileBytes) continue;
|
|
15665
16402
|
const buf = await fs5.readFile(abs);
|
|
@@ -16197,8 +16934,35 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16197
16934
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
16198
16935
|
decisionLog = [];
|
|
16199
16936
|
initialized = false;
|
|
16200
|
-
/**
|
|
16937
|
+
/**
|
|
16938
|
+
* Last interrupted task — powers /continue (resumeRun). Kept in memory as a
|
|
16939
|
+
* fast path for the same-process case; the durable copy lives in `resumeStore`
|
|
16940
|
+
* so a crash or an exit no longer throws away everything already finished.
|
|
16941
|
+
*/
|
|
16201
16942
|
lastInterruptedRun;
|
|
16943
|
+
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
16944
|
+
resumeStore;
|
|
16945
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
16946
|
+
claimedResumeId;
|
|
16947
|
+
/**
|
|
16948
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
16949
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
16950
|
+
*/
|
|
16951
|
+
resumedFrom;
|
|
16952
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
16953
|
+
wroteCheckpointThisRun = false;
|
|
16954
|
+
/**
|
|
16955
|
+
* Whether the run reached the end of its normal path.
|
|
16956
|
+
*
|
|
16957
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
16958
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
16959
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
16960
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
16961
|
+
* the original claim and the finished sections with it.
|
|
16962
|
+
*/
|
|
16963
|
+
completedNormally = false;
|
|
16964
|
+
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
16965
|
+
currentRunCompleted = [];
|
|
16202
16966
|
initPromise;
|
|
16203
16967
|
store;
|
|
16204
16968
|
audit;
|
|
@@ -16231,10 +16995,16 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16231
16995
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16232
16996
|
try {
|
|
16233
16997
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
16234
|
-
fileDeadModelPersistence(
|
|
16998
|
+
fileDeadModelPersistence(path26.join(workspacePath, ".cascade", "dead-models.json"))
|
|
16235
16999
|
));
|
|
16236
17000
|
} catch {
|
|
16237
17001
|
}
|
|
17002
|
+
try {
|
|
17003
|
+
this.resumeStore = new ResumeStore({
|
|
17004
|
+
dir: path26.join(workspacePath, ".cascade", "resume")
|
|
17005
|
+
});
|
|
17006
|
+
} catch {
|
|
17007
|
+
}
|
|
16238
17008
|
this.multimodal = new MultimodalRegistry(
|
|
16239
17009
|
(this.config.providers ?? []).map((p) => p.type)
|
|
16240
17010
|
);
|
|
@@ -16319,8 +17089,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16319
17089
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
16320
17090
|
return;
|
|
16321
17091
|
}
|
|
16322
|
-
const dbPath = ci.dbPath ||
|
|
16323
|
-
|
|
17092
|
+
const dbPath = ci.dbPath || path26.join(this.workspacePath, ".cascade", "code-index.db");
|
|
17093
|
+
fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
|
|
16324
17094
|
const db = new Database(dbPath);
|
|
16325
17095
|
db.pragma("journal_mode = WAL");
|
|
16326
17096
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -16700,10 +17470,57 @@ ${input}` }],
|
|
|
16700
17470
|
setFeedbackSource(source) {
|
|
16701
17471
|
this.router.setFeedbackSource(source);
|
|
16702
17472
|
}
|
|
16703
|
-
/**
|
|
17473
|
+
/**
|
|
17474
|
+
* Write a durable checkpoint for an interrupted run.
|
|
17475
|
+
*
|
|
17476
|
+
* Best-effort throughout: a run is already ending badly on every path that
|
|
17477
|
+
* calls this, and failing to record a resume file must not make that ending
|
|
17478
|
+
* worse. Nothing here is allowed to throw.
|
|
17479
|
+
*/
|
|
17480
|
+
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
17481
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
17482
|
+
const merged = /* @__PURE__ */ new Map();
|
|
17483
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
17484
|
+
for (const node of this.currentRunCompleted) {
|
|
17485
|
+
const existing = merged.get(node.id);
|
|
17486
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
17487
|
+
merged.set(node.id, node);
|
|
17488
|
+
}
|
|
17489
|
+
const completed = [...merged.values()];
|
|
17490
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
17491
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
17492
|
+
if (!completed.length && !carriedPartial) return false;
|
|
17493
|
+
try {
|
|
17494
|
+
const saved = await this.resumeStore?.save({
|
|
17495
|
+
taskId,
|
|
17496
|
+
prompt: canonicalPrompt,
|
|
17497
|
+
reason,
|
|
17498
|
+
...detail ? { detail } : {},
|
|
17499
|
+
partialOutput: carriedPartial,
|
|
17500
|
+
completed
|
|
17501
|
+
});
|
|
17502
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
17503
|
+
return saved === true;
|
|
17504
|
+
} catch {
|
|
17505
|
+
return false;
|
|
17506
|
+
}
|
|
17507
|
+
}
|
|
17508
|
+
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
16704
17509
|
hasResumableRun() {
|
|
16705
17510
|
return this.lastInterruptedRun != null;
|
|
16706
17511
|
}
|
|
17512
|
+
/**
|
|
17513
|
+
* True when a checkpoint from ANY process is resumable. Async because the
|
|
17514
|
+
* durable check touches disk; `hasResumableRun` remains the sync in-memory
|
|
17515
|
+
* answer for callers already on a hot path.
|
|
17516
|
+
*/
|
|
17517
|
+
async hasDurableResume() {
|
|
17518
|
+
return await this.resumeStore?.latest() != null;
|
|
17519
|
+
}
|
|
17520
|
+
/** Interrupted runs available to resume, newest first. */
|
|
17521
|
+
async listResumableRuns() {
|
|
17522
|
+
return await this.resumeStore?.list() ?? [];
|
|
17523
|
+
}
|
|
16707
17524
|
/**
|
|
16708
17525
|
* Raise the per-run token budget for a resume and return the continuation
|
|
16709
17526
|
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
@@ -16714,24 +17531,105 @@ ${input}` }],
|
|
|
16714
17531
|
const last = this.lastInterruptedRun;
|
|
16715
17532
|
if (!last) return null;
|
|
16716
17533
|
this.lastInterruptedRun = void 0;
|
|
16717
|
-
|
|
17534
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17535
|
+
return this.buildResumePrompt(last.prompt, last.partialOutput, []);
|
|
17536
|
+
}
|
|
17537
|
+
/**
|
|
17538
|
+
* Durable counterpart to prepareResume: recovers a run interrupted in ANY
|
|
17539
|
+
* process, including one that crashed before it could return. Consumes the
|
|
17540
|
+
* checkpoint, so the same finished work is never restored into two runs.
|
|
17541
|
+
*/
|
|
17542
|
+
async prepareDurableResume(opts = {}) {
|
|
17543
|
+
await this.resumeStore?.reclaimStale();
|
|
17544
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
17545
|
+
if (!claimed) return null;
|
|
17546
|
+
try {
|
|
17547
|
+
this.lastInterruptedRun = void 0;
|
|
17548
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17549
|
+
const prompt = this.buildResumePrompt(
|
|
17550
|
+
claimed.checkpoint.prompt,
|
|
17551
|
+
claimed.checkpoint.partialOutput,
|
|
17552
|
+
claimed.checkpoint.completed
|
|
17553
|
+
);
|
|
17554
|
+
this.resumedFrom = {
|
|
17555
|
+
prompt: claimed.checkpoint.prompt,
|
|
17556
|
+
completed: claimed.checkpoint.completed,
|
|
17557
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
17558
|
+
};
|
|
17559
|
+
this.claimedResumeId = claimed.claimId;
|
|
17560
|
+
return prompt;
|
|
17561
|
+
} catch (err) {
|
|
17562
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
17563
|
+
throw err;
|
|
17564
|
+
}
|
|
17565
|
+
}
|
|
17566
|
+
/**
|
|
17567
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
17568
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
17569
|
+
* run loop; safe to call when nothing is claimed.
|
|
17570
|
+
*/
|
|
17571
|
+
async settleClaimedResume(succeeded) {
|
|
17572
|
+
const claimId = this.claimedResumeId;
|
|
17573
|
+
if (!claimId) return;
|
|
17574
|
+
this.claimedResumeId = void 0;
|
|
17575
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
17576
|
+
await this.resumeStore?.settle(claimId);
|
|
17577
|
+
} else {
|
|
17578
|
+
await this.resumeStore?.release(claimId);
|
|
17579
|
+
}
|
|
17580
|
+
}
|
|
17581
|
+
/**
|
|
17582
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
17583
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
17584
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
17585
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
17586
|
+
*/
|
|
17587
|
+
async releaseClaimedResume() {
|
|
17588
|
+
const claimId = this.claimedResumeId;
|
|
17589
|
+
if (!claimId) return;
|
|
17590
|
+
this.claimedResumeId = void 0;
|
|
17591
|
+
await this.resumeStore?.release(claimId);
|
|
17592
|
+
}
|
|
17593
|
+
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
17594
|
+
raiseBudgetForResume(maxTokens) {
|
|
17595
|
+
const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
16718
17596
|
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
16719
17597
|
this.router.setMaxTokensPerRun(raised);
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
17598
|
+
}
|
|
17599
|
+
/**
|
|
17600
|
+
* The resume prompt. Completed sections are named explicitly rather than
|
|
17601
|
+
* gestured at: "do not recreate the files" is an instruction a planner can
|
|
17602
|
+
* only guess at, whereas a list of what is already done, and what it produced,
|
|
17603
|
+
* is something it can act on. That difference is the entire reason node
|
|
17604
|
+
* results are kept — a re-plan that cannot see the finished work will plan the
|
|
17605
|
+
* finished work again, and the user pays for it a second time.
|
|
17606
|
+
*/
|
|
17607
|
+
buildResumePrompt(prompt, partialOutput, completed) {
|
|
17608
|
+
const done = summarizeCompleted(completed);
|
|
17609
|
+
return [
|
|
17610
|
+
"Continue and FINISH this task. A previous attempt was interrupted before completion; any files already created are on disk \u2014 build on them, do NOT recreate them. Plan ONLY the remaining work.",
|
|
17611
|
+
"",
|
|
17612
|
+
`Original task: ${prompt}`,
|
|
17613
|
+
done ? `
|
|
17614
|
+
${done}` : "",
|
|
17615
|
+
partialOutput ? `
|
|
16724
17616
|
Partial result so far:
|
|
16725
|
-
${
|
|
17617
|
+
${partialOutput}` : ""
|
|
17618
|
+
].filter(Boolean).join("\n");
|
|
16726
17619
|
}
|
|
16727
17620
|
/**
|
|
16728
17621
|
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
16729
17622
|
* Returns null when there is nothing to resume.
|
|
16730
17623
|
*/
|
|
16731
17624
|
async resumeRun(opts = {}) {
|
|
16732
|
-
const prompt = this.prepareResume(opts);
|
|
17625
|
+
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
16733
17626
|
if (!prompt) return null;
|
|
16734
|
-
|
|
17627
|
+
try {
|
|
17628
|
+
return await this.run({ prompt });
|
|
17629
|
+
} catch (err) {
|
|
17630
|
+
await this.releaseClaimedResume();
|
|
17631
|
+
throw err;
|
|
17632
|
+
}
|
|
16735
17633
|
}
|
|
16736
17634
|
getWorkspacePath() {
|
|
16737
17635
|
return this.workspacePath;
|
|
@@ -17100,6 +17998,10 @@ ${prompt}` : prompt;
|
|
|
17100
17998
|
const startMs = Date.now();
|
|
17101
17999
|
const taskId = randomUUID();
|
|
17102
18000
|
this.decisionLog = [];
|
|
18001
|
+
this.currentRunCompleted = [];
|
|
18002
|
+
this.wroteCheckpointThisRun = false;
|
|
18003
|
+
this.completedNormally = false;
|
|
18004
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
17103
18005
|
if (options.fastAnswer) {
|
|
17104
18006
|
return this.runFastAnswer(options, startMs, taskId);
|
|
17105
18007
|
}
|
|
@@ -17214,6 +18116,11 @@ ${prompt}` : prompt;
|
|
|
17214
18116
|
});
|
|
17215
18117
|
tier.on("log", (e) => this.emit("log", e));
|
|
17216
18118
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
18119
|
+
tier.on("section:complete", (e) => {
|
|
18120
|
+
if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
|
|
18121
|
+
this.currentRunCompleted.push(e);
|
|
18122
|
+
this.emit("section:complete", e);
|
|
18123
|
+
});
|
|
17217
18124
|
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
17218
18125
|
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
17219
18126
|
tier.on("tool:approval-request", async (request) => {
|
|
@@ -17360,6 +18267,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17360
18267
|
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
17361
18268
|
const trip = runBreaker.reason();
|
|
17362
18269
|
if (trip) {
|
|
18270
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
|
|
17363
18271
|
finalOutput = [
|
|
17364
18272
|
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
17365
18273
|
"",
|
|
@@ -17372,6 +18280,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17372
18280
|
${finalOutput}` : ""
|
|
17373
18281
|
].join("\n").trimEnd();
|
|
17374
18282
|
}
|
|
18283
|
+
this.completedNormally = trip == null;
|
|
17375
18284
|
} catch (err) {
|
|
17376
18285
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
17377
18286
|
this.emit("run:cancelled", {
|
|
@@ -17379,6 +18288,13 @@ ${finalOutput}` : ""
|
|
|
17379
18288
|
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
17380
18289
|
partialOutput: finalOutput || ""
|
|
17381
18290
|
});
|
|
18291
|
+
await this.checkpointRun(
|
|
18292
|
+
taskId,
|
|
18293
|
+
options.prompt,
|
|
18294
|
+
finalOutput || "",
|
|
18295
|
+
"cancelled",
|
|
18296
|
+
err instanceof Error ? err.message : "Task cancelled"
|
|
18297
|
+
);
|
|
17382
18298
|
runError = null;
|
|
17383
18299
|
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
17384
18300
|
this.emit("run:budget-exceeded", {
|
|
@@ -17387,9 +18303,17 @@ ${finalOutput}` : ""
|
|
|
17387
18303
|
partialOutput: finalOutput || ""
|
|
17388
18304
|
});
|
|
17389
18305
|
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
18306
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
|
|
17390
18307
|
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
17391
18308
|
runError = null;
|
|
17392
18309
|
} else {
|
|
18310
|
+
await this.checkpointRun(
|
|
18311
|
+
taskId,
|
|
18312
|
+
options.prompt,
|
|
18313
|
+
finalOutput || "",
|
|
18314
|
+
"error",
|
|
18315
|
+
err instanceof Error ? err.message : String(err)
|
|
18316
|
+
);
|
|
17393
18317
|
runError = err;
|
|
17394
18318
|
throw err;
|
|
17395
18319
|
}
|
|
@@ -17398,6 +18322,10 @@ ${finalOutput}` : ""
|
|
|
17398
18322
|
escalator.cancelAllPending();
|
|
17399
18323
|
} catch {
|
|
17400
18324
|
}
|
|
18325
|
+
try {
|
|
18326
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
18327
|
+
} catch {
|
|
18328
|
+
}
|
|
17401
18329
|
try {
|
|
17402
18330
|
this.releasePendingEscalations();
|
|
17403
18331
|
} catch {
|
|
@@ -17637,7 +18565,7 @@ var Keystore = class {
|
|
|
17637
18565
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
17638
18566
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
17639
18567
|
this.backend = "keytar";
|
|
17640
|
-
if (password &&
|
|
18568
|
+
if (password && fs25.existsSync(this.storePath)) {
|
|
17641
18569
|
try {
|
|
17642
18570
|
const fileEntries = this.decryptFile(password);
|
|
17643
18571
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -17656,7 +18584,7 @@ var Keystore = class {
|
|
|
17656
18584
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
17657
18585
|
);
|
|
17658
18586
|
}
|
|
17659
|
-
if (!
|
|
18587
|
+
if (!fs25.existsSync(this.storePath)) {
|
|
17660
18588
|
const salt = crypto4.randomBytes(SALT_LEN);
|
|
17661
18589
|
this.masterKey = this.deriveKey(password, salt);
|
|
17662
18590
|
this.writeWithSalt({}, salt);
|
|
@@ -17670,7 +18598,7 @@ var Keystore = class {
|
|
|
17670
18598
|
}
|
|
17671
18599
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
17672
18600
|
unlockSync(password) {
|
|
17673
|
-
if (!
|
|
18601
|
+
if (!fs25.existsSync(this.storePath)) {
|
|
17674
18602
|
const salt = crypto4.randomBytes(SALT_LEN);
|
|
17675
18603
|
this.masterKey = this.deriveKey(password, salt);
|
|
17676
18604
|
this.writeWithSalt({}, salt);
|
|
@@ -17728,7 +18656,7 @@ var Keystore = class {
|
|
|
17728
18656
|
}
|
|
17729
18657
|
}
|
|
17730
18658
|
decryptFile(password, knownSalt) {
|
|
17731
|
-
if (!
|
|
18659
|
+
if (!fs25.existsSync(this.storePath)) return {};
|
|
17732
18660
|
try {
|
|
17733
18661
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
17734
18662
|
const useSalt = knownSalt ?? salt;
|
|
@@ -17750,8 +18678,8 @@ var Keystore = class {
|
|
|
17750
18678
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
17751
18679
|
const tag = cipher.getAuthTag();
|
|
17752
18680
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
17753
|
-
|
|
17754
|
-
|
|
18681
|
+
fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
|
|
18682
|
+
fs25.writeFileSync(this.storePath, out, { mode: 384 });
|
|
17755
18683
|
}
|
|
17756
18684
|
writeWithSalt(data, salt) {
|
|
17757
18685
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -17761,11 +18689,11 @@ var Keystore = class {
|
|
|
17761
18689
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
17762
18690
|
const tag = cipher.getAuthTag();
|
|
17763
18691
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
17764
|
-
|
|
17765
|
-
|
|
18692
|
+
fs25.mkdirSync(path26.dirname(this.storePath), { recursive: true });
|
|
18693
|
+
fs25.writeFileSync(this.storePath, out, { mode: 384 });
|
|
17766
18694
|
}
|
|
17767
18695
|
readRaw() {
|
|
17768
|
-
const buf =
|
|
18696
|
+
const buf = fs25.readFileSync(this.storePath);
|
|
17769
18697
|
let offset = 0;
|
|
17770
18698
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
17771
18699
|
offset += SALT_LEN;
|
|
@@ -17781,7 +18709,7 @@ var Keystore = class {
|
|
|
17781
18709
|
}
|
|
17782
18710
|
};
|
|
17783
18711
|
async function loadCascadeMd(workspacePath) {
|
|
17784
|
-
const filePath =
|
|
18712
|
+
const filePath = path26.join(workspacePath, "CASCADE.md");
|
|
17785
18713
|
try {
|
|
17786
18714
|
const raw = await fs5.readFile(filePath, "utf-8");
|
|
17787
18715
|
return parseCascadeMd(raw);
|
|
@@ -17812,7 +18740,7 @@ ${raw.trim()}`;
|
|
|
17812
18740
|
var MemoryStore = class _MemoryStore {
|
|
17813
18741
|
db;
|
|
17814
18742
|
constructor(dbPath) {
|
|
17815
|
-
|
|
18743
|
+
fs25.mkdirSync(path26.dirname(dbPath), { recursive: true });
|
|
17816
18744
|
try {
|
|
17817
18745
|
this.db = new Database(dbPath, { timeout: 5e3 });
|
|
17818
18746
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -18648,7 +19576,7 @@ Original error: ${err.message}`
|
|
|
18648
19576
|
}
|
|
18649
19577
|
};
|
|
18650
19578
|
function credentialsPath(globalDir2) {
|
|
18651
|
-
return
|
|
19579
|
+
return path26.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
18652
19580
|
}
|
|
18653
19581
|
function providerKey(p) {
|
|
18654
19582
|
if (p.type === "azure") {
|
|
@@ -18661,7 +19589,7 @@ function isPersistable(p) {
|
|
|
18661
19589
|
}
|
|
18662
19590
|
function loadGlobalCredentials(globalDir2) {
|
|
18663
19591
|
try {
|
|
18664
|
-
const raw =
|
|
19592
|
+
const raw = fs25.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
18665
19593
|
const parsed = JSON.parse(raw);
|
|
18666
19594
|
if (!Array.isArray(parsed.providers)) return [];
|
|
18667
19595
|
return parsed.providers.filter(
|
|
@@ -18673,13 +19601,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
18673
19601
|
}
|
|
18674
19602
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
18675
19603
|
const filePath = credentialsPath(globalDir2);
|
|
18676
|
-
|
|
19604
|
+
fs25.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
18677
19605
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
18678
19606
|
const tmp = `${filePath}.tmp`;
|
|
18679
|
-
|
|
18680
|
-
|
|
19607
|
+
fs25.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
19608
|
+
fs25.renameSync(tmp, filePath);
|
|
18681
19609
|
try {
|
|
18682
|
-
|
|
19610
|
+
fs25.chmodSync(filePath, 384);
|
|
18683
19611
|
} catch {
|
|
18684
19612
|
}
|
|
18685
19613
|
}
|
|
@@ -18714,7 +19642,7 @@ var ConfigManager = class {
|
|
|
18714
19642
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
18715
19643
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
18716
19644
|
this.workspacePath = workspacePath;
|
|
18717
|
-
this.globalDir = globalDirOverride ??
|
|
19645
|
+
this.globalDir = globalDirOverride ?? path26.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
18718
19646
|
}
|
|
18719
19647
|
async load() {
|
|
18720
19648
|
this.config = await this.loadConfig();
|
|
@@ -18728,8 +19656,8 @@ var ConfigManager = class {
|
|
|
18728
19656
|
this.ignore = new CascadeIgnore();
|
|
18729
19657
|
await this.ignore.load(this.workspacePath);
|
|
18730
19658
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
18731
|
-
this.keystore = new Keystore(
|
|
18732
|
-
this.store = new MemoryStore(
|
|
19659
|
+
this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
19660
|
+
this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
|
|
18733
19661
|
await this.injectEnvKeys();
|
|
18734
19662
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
18735
19663
|
await this.ensureDefaultIdentity();
|
|
@@ -18754,8 +19682,8 @@ var ConfigManager = class {
|
|
|
18754
19682
|
return this.workspacePath;
|
|
18755
19683
|
}
|
|
18756
19684
|
async save() {
|
|
18757
|
-
const configPath =
|
|
18758
|
-
await fs5.mkdir(
|
|
19685
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
19686
|
+
await fs5.mkdir(path26.dirname(configPath), { recursive: true });
|
|
18759
19687
|
await fs5.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
18760
19688
|
try {
|
|
18761
19689
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
@@ -18791,7 +19719,7 @@ var ConfigManager = class {
|
|
|
18791
19719
|
return configProvider?.apiKey;
|
|
18792
19720
|
}
|
|
18793
19721
|
async loadConfig() {
|
|
18794
|
-
const configPath =
|
|
19722
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
18795
19723
|
try {
|
|
18796
19724
|
const raw = await fs5.readFile(configPath, "utf-8");
|
|
18797
19725
|
return validateConfig(JSON.parse(raw));
|
|
@@ -19026,8 +19954,14 @@ var DashboardSocket = class {
|
|
|
19026
19954
|
emitCascadeEvent(ev) {
|
|
19027
19955
|
this.io.emit("cascade:event", ev);
|
|
19028
19956
|
}
|
|
19029
|
-
|
|
19030
|
-
|
|
19957
|
+
/**
|
|
19958
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
19959
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
19960
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
19961
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
19962
|
+
*/
|
|
19963
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
19964
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
19031
19965
|
this.io.emit("tier:status", payload);
|
|
19032
19966
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
19033
19967
|
}
|
|
@@ -19323,7 +20257,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
19323
20257
|
}
|
|
19324
20258
|
|
|
19325
20259
|
// src/dashboard/server.ts
|
|
19326
|
-
var __dirname$1 =
|
|
20260
|
+
var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
|
|
19327
20261
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
19328
20262
|
var DashboardServer = class {
|
|
19329
20263
|
app;
|
|
@@ -19603,14 +20537,14 @@ var DashboardServer = class {
|
|
|
19603
20537
|
*/
|
|
19604
20538
|
persistConfig() {
|
|
19605
20539
|
try {
|
|
19606
|
-
const configPath =
|
|
19607
|
-
|
|
19608
|
-
|
|
20540
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
20541
|
+
fs25.mkdirSync(path26.dirname(configPath), { recursive: true });
|
|
20542
|
+
fs25.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
19609
20543
|
} catch (err) {
|
|
19610
20544
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
19611
20545
|
}
|
|
19612
20546
|
try {
|
|
19613
|
-
saveGlobalCredentials(
|
|
20547
|
+
saveGlobalCredentials(path26.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
19614
20548
|
} catch (err) {
|
|
19615
20549
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
19616
20550
|
}
|
|
@@ -19625,15 +20559,15 @@ var DashboardServer = class {
|
|
|
19625
20559
|
resolveDashboardSecret() {
|
|
19626
20560
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
19627
20561
|
if (fromConfig) return fromConfig;
|
|
19628
|
-
const secretPath =
|
|
20562
|
+
const secretPath = path26.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
19629
20563
|
try {
|
|
19630
|
-
if (
|
|
19631
|
-
const existing =
|
|
20564
|
+
if (fs25.existsSync(secretPath)) {
|
|
20565
|
+
const existing = fs25.readFileSync(secretPath, "utf-8").trim();
|
|
19632
20566
|
if (existing.length >= 16) return existing;
|
|
19633
20567
|
}
|
|
19634
20568
|
const generated = randomUUID();
|
|
19635
|
-
|
|
19636
|
-
|
|
20569
|
+
fs25.mkdirSync(path26.dirname(secretPath), { recursive: true });
|
|
20570
|
+
fs25.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
19637
20571
|
if (this.config.dashboard.auth) {
|
|
19638
20572
|
console.warn(
|
|
19639
20573
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -19660,7 +20594,7 @@ var DashboardServer = class {
|
|
|
19660
20594
|
// ── Setup ─────────────────────────────────────
|
|
19661
20595
|
getGlobalStore() {
|
|
19662
20596
|
if (!this.globalStore) {
|
|
19663
|
-
const globalDbPath =
|
|
20597
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
19664
20598
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
19665
20599
|
}
|
|
19666
20600
|
return this.globalStore;
|
|
@@ -20040,12 +20974,12 @@ ${prompt}`;
|
|
|
20040
20974
|
}
|
|
20041
20975
|
}
|
|
20042
20976
|
watchRuntimeChanges() {
|
|
20043
|
-
const workspaceDbPath =
|
|
20044
|
-
const globalDbPath =
|
|
20977
|
+
const workspaceDbPath = path26.join(this.workspacePath, CASCADE_DB_FILE);
|
|
20978
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20045
20979
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
20046
20980
|
for (const watchPath of watchPaths) {
|
|
20047
|
-
if (!
|
|
20048
|
-
|
|
20981
|
+
if (!fs25.existsSync(watchPath)) continue;
|
|
20982
|
+
fs25.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
20049
20983
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
20050
20984
|
});
|
|
20051
20985
|
}
|
|
@@ -20167,7 +21101,7 @@ ${prompt}`;
|
|
|
20167
21101
|
const sessionId = req.params.id;
|
|
20168
21102
|
this.store.deleteSession(sessionId);
|
|
20169
21103
|
this.store.deleteRuntimeSession(sessionId);
|
|
20170
|
-
const globalDbPath =
|
|
21104
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20171
21105
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20172
21106
|
try {
|
|
20173
21107
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -20260,7 +21194,7 @@ ${prompt}`;
|
|
|
20260
21194
|
});
|
|
20261
21195
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
20262
21196
|
const body = req.body;
|
|
20263
|
-
const globalDbPath =
|
|
21197
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20264
21198
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
20265
21199
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20266
21200
|
try {
|
|
@@ -20283,7 +21217,7 @@ ${prompt}`;
|
|
|
20283
21217
|
});
|
|
20284
21218
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
20285
21219
|
this.store.deleteAllRuntimeNodes();
|
|
20286
|
-
const globalDbPath =
|
|
21220
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20287
21221
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20288
21222
|
try {
|
|
20289
21223
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -20369,12 +21303,12 @@ ${prompt}`;
|
|
|
20369
21303
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
20370
21304
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
20371
21305
|
try {
|
|
20372
|
-
const configPath =
|
|
20373
|
-
const existing =
|
|
21306
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
21307
|
+
const existing = fs25.existsSync(configPath) ? JSON.parse(fs25.readFileSync(configPath, "utf-8")) : {};
|
|
20374
21308
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
20375
21309
|
const tmp = configPath + ".tmp";
|
|
20376
|
-
|
|
20377
|
-
|
|
21310
|
+
fs25.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
21311
|
+
fs25.renameSync(tmp, configPath);
|
|
20378
21312
|
res.json({ ok: true });
|
|
20379
21313
|
} catch (err) {
|
|
20380
21314
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -20402,7 +21336,7 @@ ${prompt}`;
|
|
|
20402
21336
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
20403
21337
|
const scope = req.query["scope"] ?? "workspace";
|
|
20404
21338
|
if (scope === "global") {
|
|
20405
|
-
const globalDbPath =
|
|
21339
|
+
const globalDbPath = path26.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20406
21340
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20407
21341
|
try {
|
|
20408
21342
|
res.json({
|
|
@@ -20743,13 +21677,13 @@ ${prompt}`;
|
|
|
20743
21677
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
20744
21678
|
}
|
|
20745
21679
|
});
|
|
20746
|
-
const prodPath =
|
|
20747
|
-
const devPath =
|
|
20748
|
-
const webDistPath =
|
|
20749
|
-
if (
|
|
21680
|
+
const prodPath = path26.resolve(__dirname$1, "../web/dist");
|
|
21681
|
+
const devPath = path26.resolve(__dirname$1, "../../web/dist");
|
|
21682
|
+
const webDistPath = fs25.existsSync(prodPath) ? prodPath : devPath;
|
|
21683
|
+
if (fs25.existsSync(webDistPath)) {
|
|
20750
21684
|
this.app.use(express.static(webDistPath));
|
|
20751
21685
|
this.app.get("*", (_req, res) => {
|
|
20752
|
-
res.sendFile(
|
|
21686
|
+
res.sendFile(path26.join(webDistPath, "index.html"));
|
|
20753
21687
|
});
|
|
20754
21688
|
} else {
|
|
20755
21689
|
this.app.get("/", (_req, res) => {
|
|
@@ -20936,14 +21870,14 @@ function planRetrieval(ctx) {
|
|
|
20936
21870
|
}
|
|
20937
21871
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
20938
21872
|
function globalDir(homeDir = os6.homedir()) {
|
|
20939
|
-
return
|
|
21873
|
+
return path26.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
20940
21874
|
}
|
|
20941
21875
|
function cloudSessionPath(dir = globalDir()) {
|
|
20942
|
-
return
|
|
21876
|
+
return path26.join(dir, CLOUD_SESSION_FILE);
|
|
20943
21877
|
}
|
|
20944
21878
|
function loadCloudSession(dir = globalDir()) {
|
|
20945
21879
|
try {
|
|
20946
|
-
const raw =
|
|
21880
|
+
const raw = fs25.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
20947
21881
|
const parsed = JSON.parse(raw);
|
|
20948
21882
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
20949
21883
|
return parsed;
|
|
@@ -20952,17 +21886,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
20952
21886
|
}
|
|
20953
21887
|
}
|
|
20954
21888
|
function saveCloudSession(session, dir = globalDir()) {
|
|
20955
|
-
|
|
21889
|
+
fs25.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
20956
21890
|
const file = cloudSessionPath(dir);
|
|
20957
|
-
|
|
21891
|
+
fs25.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
20958
21892
|
try {
|
|
20959
|
-
|
|
21893
|
+
fs25.chmodSync(file, 384);
|
|
20960
21894
|
} catch {
|
|
20961
21895
|
}
|
|
20962
21896
|
}
|
|
20963
21897
|
function clearCloudSession(dir = globalDir()) {
|
|
20964
21898
|
try {
|
|
20965
|
-
|
|
21899
|
+
fs25.rmSync(cloudSessionPath(dir));
|
|
20966
21900
|
} catch {
|
|
20967
21901
|
}
|
|
20968
21902
|
}
|
|
@@ -21134,16 +22068,16 @@ var CloudClient = class _CloudClient {
|
|
|
21134
22068
|
return (await this.refresh()).accessToken;
|
|
21135
22069
|
}
|
|
21136
22070
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
21137
|
-
async authed(
|
|
21138
|
-
const call = (token) => fetch(this.url(
|
|
22071
|
+
async authed(path28, init = {}) {
|
|
22072
|
+
const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
21139
22073
|
let res = await call(await this.accessToken());
|
|
21140
22074
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
21141
22075
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
21142
22076
|
const text = await res.text();
|
|
21143
22077
|
return text ? JSON.parse(text) : {};
|
|
21144
22078
|
}
|
|
21145
|
-
authedGet(
|
|
21146
|
-
return this.authed(
|
|
22079
|
+
authedGet(path28) {
|
|
22080
|
+
return this.authed(path28);
|
|
21147
22081
|
}
|
|
21148
22082
|
async fetchMe(token) {
|
|
21149
22083
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -21480,6 +22414,6 @@ function applySyncBundle(bundle, config) {
|
|
|
21480
22414
|
return next;
|
|
21481
22415
|
}
|
|
21482
22416
|
|
|
21483
|
-
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, extractCharts, fileExt, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, uniqueMcpServerName };
|
|
22417
|
+
export { AZURE_BASE_MODELS, AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, CurrentPageTool, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DOCUMENT_MIME, DashboardServer, DependencyScheduler, FileMcpOAuthStore, GITHUB_MODELS_API_VERSION, GITHUB_MODELS_CATALOG_URL, GITHUB_MODELS_INFERENCE_URL, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GenerateDocumentTool, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MAX_PAGE_TEXT_CHARS, MCP_TOOL_PREFIX, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, RESUME_SCHEMA_VERSION, ResumeStore, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskGraphValidationError, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildDocumentTools, buildManifest, buildSessionTranscript, bytesToBase64, cagCharBudget, chartKind, chartToTableRows, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, compileSectionGraph, compileSubtaskGraph, compileTaskGraph, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, disambiguateMcpServerNames, discoverMcpAuthServer, discoverMcpTools, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, evaluateAcceptance, extractCharts, failures, fileExt, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, inlineRuns, isDocumentFormat, isMcpToolName, matchImageLine, mcpServerPrefix, mcpToolName, nodeHttpFetch, parseBlocks, parseChartSpec, parseDelimited, parseRankOrder, parseSlide, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, removeMcpServerDenials, renderDocument, renderDocx, renderPptx, renderXlsx, runCascade, sessionWorthRemembering, sniffImage, splitSlides, streamCascade, stripInline, summarizeCompleted, undecided, uniqueMcpServerName };
|
|
21484
22418
|
//# sourceMappingURL=index.js.map
|
|
21485
22419
|
//# sourceMappingURL=index.js.map
|