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.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var Database = require('better-sqlite3');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var path26 = require('path');
|
|
5
|
+
var fs25 = require('fs');
|
|
6
6
|
var os6 = require('os');
|
|
7
7
|
var crypto4 = require('crypto');
|
|
8
8
|
var EventEmitter = require('events');
|
|
@@ -61,8 +61,8 @@ function _interopNamespace(e) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
64
|
-
var
|
|
65
|
-
var
|
|
64
|
+
var path26__default = /*#__PURE__*/_interopDefault(path26);
|
|
65
|
+
var fs25__default = /*#__PURE__*/_interopDefault(fs25);
|
|
66
66
|
var os6__default = /*#__PURE__*/_interopDefault(os6);
|
|
67
67
|
var crypto4__default = /*#__PURE__*/_interopDefault(crypto4);
|
|
68
68
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
@@ -107,12 +107,12 @@ var init_audit_logger = __esm({
|
|
|
107
107
|
constructor(workspacePath, debugMode = false) {
|
|
108
108
|
this.workspacePath = workspacePath;
|
|
109
109
|
this.debugMode = debugMode;
|
|
110
|
-
const cascadeDir =
|
|
111
|
-
if (!
|
|
112
|
-
|
|
110
|
+
const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
|
|
111
|
+
if (!fs25__default.default.existsSync(cascadeDir)) {
|
|
112
|
+
fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
113
113
|
}
|
|
114
|
-
this.keyPath =
|
|
115
|
-
this.dbPath =
|
|
114
|
+
this.keyPath = path26__default.default.join(cascadeDir, "audit_log.key");
|
|
115
|
+
this.dbPath = path26__default.default.join(cascadeDir, "audit_log.db");
|
|
116
116
|
this.initEncryptionKey();
|
|
117
117
|
this.db = new Database__default.default(this.dbPath);
|
|
118
118
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -142,11 +142,11 @@ var init_audit_logger = __esm({
|
|
|
142
142
|
return crypto4__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
143
143
|
}
|
|
144
144
|
initEncryptionKey() {
|
|
145
|
-
if (
|
|
146
|
-
this.encryptionKey =
|
|
145
|
+
if (fs25__default.default.existsSync(this.keyPath)) {
|
|
146
|
+
this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
|
|
147
147
|
} else {
|
|
148
148
|
this.encryptionKey = crypto4__default.default.randomBytes(32);
|
|
149
|
-
|
|
149
|
+
fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
encrypt(text) {
|
|
@@ -224,9 +224,9 @@ var init_audit_logger = __esm({
|
|
|
224
224
|
dumpDebugIfNeeded() {
|
|
225
225
|
if (!this.debugMode) return;
|
|
226
226
|
try {
|
|
227
|
-
const dumpPath =
|
|
227
|
+
const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
228
228
|
const entries = this.getAllLogs();
|
|
229
|
-
|
|
229
|
+
fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
230
230
|
} catch (err) {
|
|
231
231
|
console.error("Failed to dump debug audit logs", err);
|
|
232
232
|
}
|
|
@@ -1908,7 +1908,7 @@ function reconcilePrice(model, live) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
|
|
1910
1910
|
// src/constants.ts
|
|
1911
|
-
var CASCADE_VERSION = "0.
|
|
1911
|
+
var CASCADE_VERSION = "0.69.0";
|
|
1912
1912
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1913
1913
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1914
1914
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -4494,13 +4494,13 @@ var DeadModelStore = class {
|
|
|
4494
4494
|
function fileDeadModelPersistence(filePath) {
|
|
4495
4495
|
return {
|
|
4496
4496
|
load() {
|
|
4497
|
-
if (!
|
|
4498
|
-
const raw = JSON.parse(
|
|
4497
|
+
if (!fs25.existsSync(filePath)) return [];
|
|
4498
|
+
const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
4499
4499
|
return Array.isArray(raw) ? raw : [];
|
|
4500
4500
|
},
|
|
4501
4501
|
save(records) {
|
|
4502
|
-
|
|
4503
|
-
|
|
4502
|
+
fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
|
|
4503
|
+
fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4504
4504
|
}
|
|
4505
4505
|
};
|
|
4506
4506
|
}
|
|
@@ -4663,7 +4663,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
4663
4663
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
4664
4664
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
4665
4665
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
4666
|
-
var DEFAULT_CACHE_FILE =
|
|
4666
|
+
var DEFAULT_CACHE_FILE = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
4667
4667
|
function normalizeModelId2(id) {
|
|
4668
4668
|
let s = id.toLowerCase();
|
|
4669
4669
|
const slash = s.lastIndexOf("/");
|
|
@@ -4810,7 +4810,7 @@ var LiveDataProvider = class {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
async saveCache() {
|
|
4812
4812
|
try {
|
|
4813
|
-
await fs5__default.default.mkdir(
|
|
4813
|
+
await fs5__default.default.mkdir(path26__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
4814
4814
|
const cache = {
|
|
4815
4815
|
fetchedAt: this.fetchedAt,
|
|
4816
4816
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -6070,6 +6070,21 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6070
6070
|
* which node (Cockpit node panel / Why panel).
|
|
6071
6071
|
*/
|
|
6072
6072
|
servingModel;
|
|
6073
|
+
/**
|
|
6074
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
6075
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
6076
|
+
*
|
|
6077
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
6078
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
6079
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
6080
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
6081
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
6082
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
6083
|
+
* appears in the stream.
|
|
6084
|
+
*/
|
|
6085
|
+
graphNodeId;
|
|
6086
|
+
graphDependsOn;
|
|
6087
|
+
graphWave;
|
|
6073
6088
|
constructor(role, id, parentId) {
|
|
6074
6089
|
super();
|
|
6075
6090
|
this.role = role;
|
|
@@ -6081,6 +6096,33 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6081
6096
|
setPresenter(on = true) {
|
|
6082
6097
|
this.isPresenter = on;
|
|
6083
6098
|
}
|
|
6099
|
+
/**
|
|
6100
|
+
* Record where this tier sits in the compiled task graph.
|
|
6101
|
+
*
|
|
6102
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
6103
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
6104
|
+
* are set independently rather than all at once.
|
|
6105
|
+
*/
|
|
6106
|
+
setGraphPosition(position) {
|
|
6107
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
6108
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
6109
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
6110
|
+
}
|
|
6111
|
+
/**
|
|
6112
|
+
* The graph fields, for spreading into a status payload.
|
|
6113
|
+
*
|
|
6114
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
6115
|
+
* two different call sites with two different payload shapes; adding a field
|
|
6116
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
6117
|
+
* consumer than never arriving at all.
|
|
6118
|
+
*/
|
|
6119
|
+
graphFields() {
|
|
6120
|
+
return {
|
|
6121
|
+
nodeId: this.graphNodeId,
|
|
6122
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
6123
|
+
waveId: this.graphWave
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6084
6126
|
getStatus() {
|
|
6085
6127
|
return this.status;
|
|
6086
6128
|
}
|
|
@@ -6095,7 +6137,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6095
6137
|
status,
|
|
6096
6138
|
timestamp,
|
|
6097
6139
|
output,
|
|
6098
|
-
model: this.servingModel
|
|
6140
|
+
model: this.servingModel,
|
|
6141
|
+
...this.graphFields()
|
|
6099
6142
|
};
|
|
6100
6143
|
this.emit("status", event);
|
|
6101
6144
|
this.emit("tier:status", event);
|
|
@@ -6135,7 +6178,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
6135
6178
|
progressPct: update.progressPct,
|
|
6136
6179
|
timestamp,
|
|
6137
6180
|
output: update.output,
|
|
6138
|
-
model: this.servingModel
|
|
6181
|
+
model: this.servingModel,
|
|
6182
|
+
...this.graphFields()
|
|
6139
6183
|
});
|
|
6140
6184
|
}
|
|
6141
6185
|
buildMessage(type, to, payload) {
|
|
@@ -6602,6 +6646,79 @@ function describeProviderError(c, modelId) {
|
|
|
6602
6646
|
}
|
|
6603
6647
|
}
|
|
6604
6648
|
|
|
6649
|
+
// src/core/verification/acceptance.ts
|
|
6650
|
+
var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
|
|
6651
|
+
var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
|
|
6652
|
+
var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
|
|
6653
|
+
var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
|
|
6654
|
+
var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
|
|
6655
|
+
function targetFile(criterion, ownedFiles) {
|
|
6656
|
+
const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
|
|
6657
|
+
if (mentioned.length > 1) return null;
|
|
6658
|
+
if (mentioned.length === 1) {
|
|
6659
|
+
const candidate = mentioned[0];
|
|
6660
|
+
return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
|
|
6661
|
+
}
|
|
6662
|
+
return ownedFiles.length === 1 ? ownedFiles[0] : null;
|
|
6663
|
+
}
|
|
6664
|
+
async function evaluateAcceptance(criteria, ownedFiles, probe) {
|
|
6665
|
+
const results = [];
|
|
6666
|
+
for (const criterion of criteria) {
|
|
6667
|
+
const text = criterion.trim();
|
|
6668
|
+
if (!text) {
|
|
6669
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6670
|
+
continue;
|
|
6671
|
+
}
|
|
6672
|
+
if (NEGATION.test(text)) {
|
|
6673
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
const needle = text.match(QUOTED_NEEDLE)?.[1];
|
|
6677
|
+
const wantsExists = EXISTS.test(text);
|
|
6678
|
+
const wantsNonEmpty = NON_EMPTY.test(text);
|
|
6679
|
+
if (!needle && !wantsExists && !wantsNonEmpty) {
|
|
6680
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6681
|
+
continue;
|
|
6682
|
+
}
|
|
6683
|
+
const file = targetFile(text, ownedFiles);
|
|
6684
|
+
if (!file) {
|
|
6685
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6686
|
+
continue;
|
|
6687
|
+
}
|
|
6688
|
+
const stat = await probe.stat(file);
|
|
6689
|
+
if (!stat) {
|
|
6690
|
+
results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
|
|
6691
|
+
continue;
|
|
6692
|
+
}
|
|
6693
|
+
if (wantsNonEmpty && stat.size === 0) {
|
|
6694
|
+
results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
|
|
6695
|
+
continue;
|
|
6696
|
+
}
|
|
6697
|
+
if (needle) {
|
|
6698
|
+
const content = await probe.read(file);
|
|
6699
|
+
if (content === null) {
|
|
6700
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
const found = content.includes(needle);
|
|
6704
|
+
results.push({
|
|
6705
|
+
criterion,
|
|
6706
|
+
verdict: found ? "passed" : "failed",
|
|
6707
|
+
detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
|
|
6708
|
+
});
|
|
6709
|
+
continue;
|
|
6710
|
+
}
|
|
6711
|
+
results.push({ criterion, verdict: "passed", detail: `${file} exists` });
|
|
6712
|
+
}
|
|
6713
|
+
return results;
|
|
6714
|
+
}
|
|
6715
|
+
function undecided(results) {
|
|
6716
|
+
return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
|
|
6717
|
+
}
|
|
6718
|
+
function failures(results) {
|
|
6719
|
+
return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6605
6722
|
// src/core/tiers/t3-worker.ts
|
|
6606
6723
|
var CriticalToolError = class extends Error {
|
|
6607
6724
|
constructor(message, toolName) {
|
|
@@ -6918,7 +7035,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
6918
7035
|
}
|
|
6919
7036
|
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
6920
7037
|
if (!artifactCheck.ok) {
|
|
6921
|
-
correctionAttempts
|
|
7038
|
+
correctionAttempts++;
|
|
6922
7039
|
issues.push(...artifactCheck.issues);
|
|
6923
7040
|
output = await this.correctOutput(output, artifactCheck.issues);
|
|
6924
7041
|
const retryArtifactCheck = await this.verifyArtifacts(assignment);
|
|
@@ -6936,22 +7053,45 @@ Now execute your subtask using this context where relevant.`
|
|
|
6936
7053
|
this.tools.map((t) => t.name)
|
|
6937
7054
|
);
|
|
6938
7055
|
if (visualIssue) {
|
|
6939
|
-
correctionAttempts
|
|
7056
|
+
correctionAttempts++;
|
|
6940
7057
|
issues.push(visualIssue);
|
|
6941
7058
|
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
6942
7059
|
output = await this.correctOutput(output, [visualIssue]);
|
|
6943
7060
|
}
|
|
7061
|
+
const acceptanceResults = await this.checkAcceptance(assignment);
|
|
7062
|
+
const acceptanceFailures = failures(acceptanceResults);
|
|
7063
|
+
if (acceptanceFailures.length > 0) {
|
|
7064
|
+
correctionAttempts++;
|
|
7065
|
+
issues.push(...acceptanceFailures);
|
|
7066
|
+
this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
|
|
7067
|
+
output = await this.correctOutput(output, acceptanceFailures);
|
|
7068
|
+
const recheck = failures(await this.checkAcceptance(assignment));
|
|
7069
|
+
if (recheck.length > 0) {
|
|
7070
|
+
issues.push(...recheck);
|
|
7071
|
+
checksRun.push(...acceptanceResults.map((r) => r.criterion));
|
|
7072
|
+
failed.push(...recheck);
|
|
7073
|
+
this.setStatus("FAILED");
|
|
7074
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
|
|
7075
|
+
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
for (const result2 of acceptanceResults) {
|
|
7079
|
+
if (result2.verdict === "undecidable") continue;
|
|
7080
|
+
checksRun.push(result2.criterion);
|
|
7081
|
+
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
7082
|
+
}
|
|
6944
7083
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
6945
|
-
const
|
|
7084
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
7085
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
6946
7086
|
checksRun.push(...testResult.checksRun);
|
|
6947
7087
|
passed.push(...testResult.passed);
|
|
6948
7088
|
failed.push(...testResult.failed);
|
|
6949
7089
|
if (testResult.failed.length > 0) {
|
|
6950
|
-
correctionAttempts
|
|
7090
|
+
correctionAttempts++;
|
|
6951
7091
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
6952
7092
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
6953
7093
|
output = corrected;
|
|
6954
|
-
const retest = await this.selfTest(assignment, output);
|
|
7094
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
6955
7095
|
passed.push(...retest.passed);
|
|
6956
7096
|
if (retest.failed.length > 0) {
|
|
6957
7097
|
failed.push(...retest.failed);
|
|
@@ -7255,8 +7395,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
7255
7395
|
// Media generation can run for a minute; without this a cancelled run
|
|
7256
7396
|
// still pays for an image nobody will see.
|
|
7257
7397
|
...this.signal ? { signal: this.signal } : {},
|
|
7258
|
-
saveSnapshot: async (
|
|
7259
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
7398
|
+
saveSnapshot: async (path28, content) => {
|
|
7399
|
+
this.store?.addFileSnapshot(this.taskId, path28, content);
|
|
7260
7400
|
},
|
|
7261
7401
|
sendPeerSync: (to, syncType, content) => {
|
|
7262
7402
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -7443,7 +7583,7 @@ ${assignment.expectedOutput}`;
|
|
|
7443
7583
|
const { promisify: promisify4 } = await import('util');
|
|
7444
7584
|
const execAsync2 = promisify4(exec2);
|
|
7445
7585
|
for (const artifactPath of artifactPaths) {
|
|
7446
|
-
const absolutePath =
|
|
7586
|
+
const absolutePath = path26__default.default.resolve(process.cwd(), artifactPath);
|
|
7447
7587
|
try {
|
|
7448
7588
|
const stat = await fs5__default.default.stat(absolutePath);
|
|
7449
7589
|
if (!stat.isFile()) {
|
|
@@ -7478,7 +7618,7 @@ ${assignment.expectedOutput}`;
|
|
|
7478
7618
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
7479
7619
|
continue;
|
|
7480
7620
|
}
|
|
7481
|
-
const ext =
|
|
7621
|
+
const ext = path26__default.default.extname(absolutePath).toLowerCase();
|
|
7482
7622
|
try {
|
|
7483
7623
|
if (ext === ".ts" || ext === ".tsx") {
|
|
7484
7624
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -7564,14 +7704,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7564
7704
|
}
|
|
7565
7705
|
return current;
|
|
7566
7706
|
}
|
|
7567
|
-
|
|
7707
|
+
/**
|
|
7708
|
+
* Run the acceptance criteria that can be settled by looking at the workspace.
|
|
7709
|
+
* Reads go through the same workspace-relative resolution verifyArtifacts uses.
|
|
7710
|
+
*/
|
|
7711
|
+
async checkAcceptance(assignment) {
|
|
7712
|
+
const criteria = assignment.acceptance ?? [];
|
|
7713
|
+
if (!criteria.length) return [];
|
|
7714
|
+
const resolve2 = (target) => path26__default.default.resolve(process.cwd(), target);
|
|
7715
|
+
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
7716
|
+
stat: async (target) => {
|
|
7717
|
+
try {
|
|
7718
|
+
const stat = await fs5__default.default.stat(resolve2(target));
|
|
7719
|
+
return stat.isFile() ? { size: stat.size } : null;
|
|
7720
|
+
} catch {
|
|
7721
|
+
return null;
|
|
7722
|
+
}
|
|
7723
|
+
},
|
|
7724
|
+
read: async (target) => {
|
|
7725
|
+
try {
|
|
7726
|
+
const content = await fs5__default.default.readFile(resolve2(target), "utf-8");
|
|
7727
|
+
return content.includes("\uFFFD") ? null : content;
|
|
7728
|
+
} catch {
|
|
7729
|
+
return null;
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
});
|
|
7733
|
+
}
|
|
7734
|
+
async selfTest(assignment, output, pendingAcceptance) {
|
|
7735
|
+
const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
|
|
7568
7736
|
const prompt = `Self-test this output against the assignment requirements.
|
|
7569
7737
|
|
|
7570
7738
|
Assignment: ${assignment.description}
|
|
7571
7739
|
Expected output: ${assignment.expectedOutput}
|
|
7572
7740
|
Constraints: ${assignment.constraints.join("; ")}
|
|
7573
|
-
${
|
|
7574
|
-
${
|
|
7741
|
+
${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
7742
|
+
${acceptance.map((a) => `- ${a}`).join("\n")}
|
|
7575
7743
|
` : ""}
|
|
7576
7744
|
Output to test:
|
|
7577
7745
|
${output}
|
|
@@ -7638,8 +7806,8 @@ ${output.slice(0, 4e3)}`;
|
|
|
7638
7806
|
} catch {
|
|
7639
7807
|
}
|
|
7640
7808
|
}
|
|
7641
|
-
async correctOutput(originalOutput,
|
|
7642
|
-
const correctionPrompt = `The following output failed these checks: ${
|
|
7809
|
+
async correctOutput(originalOutput, failures2) {
|
|
7810
|
+
const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
|
|
7643
7811
|
|
|
7644
7812
|
Original output:
|
|
7645
7813
|
${originalOutput}
|
|
@@ -8300,12 +8468,220 @@ function describeGenerationForPlanner(has) {
|
|
|
8300
8468
|
return lines.join("\n");
|
|
8301
8469
|
}
|
|
8302
8470
|
|
|
8471
|
+
// src/core/orchestration/task-graph.ts
|
|
8472
|
+
var TaskGraphValidationError = class extends Error {
|
|
8473
|
+
constructor(issues) {
|
|
8474
|
+
super(issues.map((issue) => issue.message).join("; "));
|
|
8475
|
+
this.issues = issues;
|
|
8476
|
+
this.name = "TaskGraphValidationError";
|
|
8477
|
+
}
|
|
8478
|
+
issues;
|
|
8479
|
+
};
|
|
8480
|
+
function normalizeDependencies(input) {
|
|
8481
|
+
if (!input?.length) return [];
|
|
8482
|
+
return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
|
|
8483
|
+
}
|
|
8484
|
+
function cycleNodes(nodes) {
|
|
8485
|
+
const adjacency = new Map(
|
|
8486
|
+
nodes.map((node) => [node.id, node.dependsOn])
|
|
8487
|
+
);
|
|
8488
|
+
const index = /* @__PURE__ */ new Map();
|
|
8489
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
8490
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
8491
|
+
const stack = [];
|
|
8492
|
+
const inCycle = /* @__PURE__ */ new Set();
|
|
8493
|
+
let counter = 0;
|
|
8494
|
+
const open = (id) => {
|
|
8495
|
+
index.set(id, counter);
|
|
8496
|
+
lowlink.set(id, counter);
|
|
8497
|
+
counter++;
|
|
8498
|
+
stack.push(id);
|
|
8499
|
+
onStack.add(id);
|
|
8500
|
+
};
|
|
8501
|
+
for (const root of nodes) {
|
|
8502
|
+
if (index.has(root.id)) continue;
|
|
8503
|
+
open(root.id);
|
|
8504
|
+
const frames = [{ id: root.id, edge: 0 }];
|
|
8505
|
+
while (frames.length > 0) {
|
|
8506
|
+
const frame = frames[frames.length - 1];
|
|
8507
|
+
const neighbours = adjacency.get(frame.id) ?? [];
|
|
8508
|
+
if (frame.edge < neighbours.length) {
|
|
8509
|
+
const next = neighbours[frame.edge];
|
|
8510
|
+
frame.edge++;
|
|
8511
|
+
if (!adjacency.has(next)) continue;
|
|
8512
|
+
if (!index.has(next)) {
|
|
8513
|
+
open(next);
|
|
8514
|
+
frames.push({ id: next, edge: 0 });
|
|
8515
|
+
} else if (onStack.has(next)) {
|
|
8516
|
+
lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
|
|
8517
|
+
}
|
|
8518
|
+
continue;
|
|
8519
|
+
}
|
|
8520
|
+
frames.pop();
|
|
8521
|
+
const parent = frames[frames.length - 1];
|
|
8522
|
+
if (parent) {
|
|
8523
|
+
lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
|
|
8524
|
+
}
|
|
8525
|
+
if (lowlink.get(frame.id) === index.get(frame.id)) {
|
|
8526
|
+
const component = [];
|
|
8527
|
+
for (; ; ) {
|
|
8528
|
+
const member = stack.pop();
|
|
8529
|
+
onStack.delete(member);
|
|
8530
|
+
component.push(member);
|
|
8531
|
+
if (member === frame.id) break;
|
|
8532
|
+
}
|
|
8533
|
+
const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
8534
|
+
if (cyclic) for (const member of component) inCycle.add(member);
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
return inCycle;
|
|
8539
|
+
}
|
|
8540
|
+
function throwIfStrict(mode, issues) {
|
|
8541
|
+
if (mode === "strict" && issues.length > 0) {
|
|
8542
|
+
throw new TaskGraphValidationError(issues);
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8545
|
+
function compileTaskGraph(inputNodes, options = {}) {
|
|
8546
|
+
const mode = options.mode ?? "strict";
|
|
8547
|
+
const issues = [];
|
|
8548
|
+
if (inputNodes.length === 0) {
|
|
8549
|
+
issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
|
|
8550
|
+
throwIfStrict(mode, issues);
|
|
8551
|
+
}
|
|
8552
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8553
|
+
const nodes = [];
|
|
8554
|
+
for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
|
|
8555
|
+
const input = inputNodes[ordinal];
|
|
8556
|
+
const id = input.id?.trim();
|
|
8557
|
+
const title = input.title?.trim();
|
|
8558
|
+
if (!id || !title) {
|
|
8559
|
+
issues.push({
|
|
8560
|
+
code: "INVALID_NODE",
|
|
8561
|
+
nodeId: id || void 0,
|
|
8562
|
+
message: `Task graph node at index ${ordinal} requires a non-empty id and title`
|
|
8563
|
+
});
|
|
8564
|
+
continue;
|
|
8565
|
+
}
|
|
8566
|
+
if (seen.has(id)) {
|
|
8567
|
+
issues.push({
|
|
8568
|
+
code: "DUPLICATE_NODE_ID",
|
|
8569
|
+
nodeId: id,
|
|
8570
|
+
message: `Duplicate task graph node id: ${id}`
|
|
8571
|
+
});
|
|
8572
|
+
continue;
|
|
8573
|
+
}
|
|
8574
|
+
seen.add(id);
|
|
8575
|
+
nodes.push({
|
|
8576
|
+
id,
|
|
8577
|
+
title,
|
|
8578
|
+
kind: input.kind,
|
|
8579
|
+
dependsOn: normalizeDependencies(input.dependsOn),
|
|
8580
|
+
payload: input.payload,
|
|
8581
|
+
ordinal
|
|
8582
|
+
});
|
|
8583
|
+
}
|
|
8584
|
+
throwIfStrict(mode, issues);
|
|
8585
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
8586
|
+
const sanitized = nodes.map((node) => {
|
|
8587
|
+
const dependsOn = [];
|
|
8588
|
+
for (const dependencyId of node.dependsOn) {
|
|
8589
|
+
if (dependencyId === node.id) {
|
|
8590
|
+
issues.push({
|
|
8591
|
+
code: "SELF_DEPENDENCY",
|
|
8592
|
+
nodeId: node.id,
|
|
8593
|
+
dependencyId,
|
|
8594
|
+
message: `Task graph node ${node.id} cannot depend on itself`
|
|
8595
|
+
});
|
|
8596
|
+
continue;
|
|
8597
|
+
}
|
|
8598
|
+
if (!ids.has(dependencyId)) {
|
|
8599
|
+
issues.push({
|
|
8600
|
+
code: "UNKNOWN_DEPENDENCY",
|
|
8601
|
+
nodeId: node.id,
|
|
8602
|
+
dependencyId,
|
|
8603
|
+
message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
|
|
8604
|
+
});
|
|
8605
|
+
continue;
|
|
8606
|
+
}
|
|
8607
|
+
dependsOn.push(dependencyId);
|
|
8608
|
+
}
|
|
8609
|
+
return { ...node, dependsOn };
|
|
8610
|
+
});
|
|
8611
|
+
throwIfStrict(mode, issues);
|
|
8612
|
+
const cyclic = cycleNodes(sanitized);
|
|
8613
|
+
let repaired = sanitized;
|
|
8614
|
+
if (cyclic.size > 0) {
|
|
8615
|
+
const cycleIssues = [];
|
|
8616
|
+
repaired = sanitized.map((node) => {
|
|
8617
|
+
if (!cyclic.has(node.id)) return node;
|
|
8618
|
+
const dependsOn = node.dependsOn.filter((dependencyId) => {
|
|
8619
|
+
if (!cyclic.has(dependencyId)) return true;
|
|
8620
|
+
cycleIssues.push({
|
|
8621
|
+
code: "CYCLE_EDGE_REMOVED",
|
|
8622
|
+
nodeId: node.id,
|
|
8623
|
+
dependencyId,
|
|
8624
|
+
message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
|
|
8625
|
+
});
|
|
8626
|
+
return false;
|
|
8627
|
+
});
|
|
8628
|
+
return { ...node, dependsOn };
|
|
8629
|
+
});
|
|
8630
|
+
issues.push(...cycleIssues);
|
|
8631
|
+
throwIfStrict(mode, cycleIssues);
|
|
8632
|
+
}
|
|
8633
|
+
const remainingCycle = cycleNodes(repaired);
|
|
8634
|
+
if (remainingCycle.size > 0) {
|
|
8635
|
+
throw new TaskGraphValidationError([
|
|
8636
|
+
{
|
|
8637
|
+
code: "INVALID_NODE",
|
|
8638
|
+
message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
|
|
8639
|
+
}
|
|
8640
|
+
]);
|
|
8641
|
+
}
|
|
8642
|
+
return {
|
|
8643
|
+
graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
|
|
8644
|
+
issues
|
|
8645
|
+
};
|
|
8646
|
+
}
|
|
8647
|
+
|
|
8648
|
+
// src/core/orchestration/adapters.ts
|
|
8649
|
+
function compileSectionGraph(sections) {
|
|
8650
|
+
return compileTaskGraph(
|
|
8651
|
+
sections.map((section) => ({
|
|
8652
|
+
id: section.sectionId,
|
|
8653
|
+
title: section.sectionTitle,
|
|
8654
|
+
kind: "section",
|
|
8655
|
+
dependsOn: section.dependsOn,
|
|
8656
|
+
payload: section
|
|
8657
|
+
})),
|
|
8658
|
+
{ mode: "repair" }
|
|
8659
|
+
);
|
|
8660
|
+
}
|
|
8661
|
+
function compileSubtaskGraph(subtasks) {
|
|
8662
|
+
return compileTaskGraph(
|
|
8663
|
+
subtasks.map((subtask) => ({
|
|
8664
|
+
id: subtask.subtaskId,
|
|
8665
|
+
title: subtask.subtaskTitle,
|
|
8666
|
+
kind: "subtask",
|
|
8667
|
+
dependsOn: subtask.dependsOn,
|
|
8668
|
+
payload: subtask
|
|
8669
|
+
})),
|
|
8670
|
+
{ mode: "repair" }
|
|
8671
|
+
);
|
|
8672
|
+
}
|
|
8673
|
+
|
|
8303
8674
|
// src/core/tiers/t2-manager.ts
|
|
8304
8675
|
function buildT2SystemPrompt(has) {
|
|
8305
8676
|
const generation = describeGenerationForPlanner(has);
|
|
8306
8677
|
return [
|
|
8307
8678
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
8308
|
-
|
|
8679
|
+
// Must agree with the decomposition prompt in decomposeSection(), which is
|
|
8680
|
+
// the one carrying the actual instruction. It said 1-4 and "the FEWEST that
|
|
8681
|
+
// fully cover it"; this said 2-5. The model received both, so its floor was
|
|
8682
|
+
// simultaneously one and two, and the cheaper reading — pad to two — costs a
|
|
8683
|
+
// real worker call on every small section.
|
|
8684
|
+
"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.",
|
|
8309
8685
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
8310
8686
|
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
8311
8687
|
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
@@ -8639,6 +9015,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8639
9015
|
for (const a of assignments) {
|
|
8640
9016
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
8641
9017
|
if (this.store) worker.setStore(this.store, taskId);
|
|
9018
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
8642
9019
|
worker.setPeerBus(this.t3PeerBus);
|
|
8643
9020
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
8644
9021
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -8701,21 +9078,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8701
9078
|
* then reduce in-degrees of their dependents and repeat.
|
|
8702
9079
|
*/
|
|
8703
9080
|
async runWithDependencies(assignments, workerMap, taskId) {
|
|
9081
|
+
const compiled = compileSubtaskGraph(assignments);
|
|
9082
|
+
for (const issue of compiled.issues) {
|
|
9083
|
+
this.log(`\u26A0 Subtask graph: ${issue.message}`);
|
|
9084
|
+
}
|
|
9085
|
+
const sanitizedAssignments = compiled.graph.nodes.map((node) => {
|
|
9086
|
+
const assignment = node.payload;
|
|
9087
|
+
const original = assignment.dependsOn ?? [];
|
|
9088
|
+
const repaired = node.dependsOn;
|
|
9089
|
+
const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
|
|
9090
|
+
return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
|
|
9091
|
+
});
|
|
8704
9092
|
const adj = /* @__PURE__ */ new Map();
|
|
8705
9093
|
const inDegree = /* @__PURE__ */ new Map();
|
|
8706
9094
|
const resultMap = /* @__PURE__ */ new Map();
|
|
8707
|
-
for (const a of
|
|
9095
|
+
for (const a of sanitizedAssignments) {
|
|
8708
9096
|
if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
|
|
8709
9097
|
inDegree.set(a.subtaskId, 0);
|
|
8710
9098
|
}
|
|
8711
|
-
for (const a of
|
|
8712
|
-
const
|
|
8713
|
-
for (const dep of deps) {
|
|
9099
|
+
for (const a of sanitizedAssignments) {
|
|
9100
|
+
for (const dep of a.dependsOn ?? []) {
|
|
8714
9101
|
adj.get(dep).add(a.subtaskId);
|
|
8715
9102
|
inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
|
|
8716
9103
|
}
|
|
8717
9104
|
}
|
|
8718
|
-
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
8719
9105
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
8720
9106
|
let wave = 0;
|
|
8721
9107
|
let respawnBudget = 1;
|
|
@@ -8750,6 +9136,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8750
9136
|
runnableIds.push(fallbackId);
|
|
8751
9137
|
}
|
|
8752
9138
|
wave++;
|
|
9139
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
8753
9140
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
8754
9141
|
this.sendStatusUpdate({
|
|
8755
9142
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -8922,49 +9309,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8922
9309
|
}
|
|
8923
9310
|
return [...resultMap.values()];
|
|
8924
9311
|
}
|
|
8925
|
-
/**
|
|
8926
|
-
* Detects cyclic dependencies using Kahn's algorithm and breaks them
|
|
8927
|
-
* by removing back-edges. Returns a sanitized copy of assignments.
|
|
8928
|
-
*
|
|
8929
|
-
* A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
|
|
8930
|
-
* meaning t3 will start without waiting for t1, preventing deadlock.
|
|
8931
|
-
*/
|
|
8932
|
-
breakCycles(assignments, adj, inDegree) {
|
|
8933
|
-
const degree = new Map(inDegree);
|
|
8934
|
-
const queue = [];
|
|
8935
|
-
const visited = /* @__PURE__ */ new Set();
|
|
8936
|
-
for (const [id, d] of degree) {
|
|
8937
|
-
if (d === 0) queue.push(id);
|
|
8938
|
-
}
|
|
8939
|
-
while (queue.length > 0) {
|
|
8940
|
-
const id = queue.shift();
|
|
8941
|
-
visited.add(id);
|
|
8942
|
-
for (const dep of adj.get(id) ?? []) {
|
|
8943
|
-
const newDeg = (degree.get(dep) ?? 1) - 1;
|
|
8944
|
-
degree.set(dep, newDeg);
|
|
8945
|
-
if (newDeg === 0) queue.push(dep);
|
|
8946
|
-
}
|
|
8947
|
-
}
|
|
8948
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
8949
|
-
if (cycleNodes.length === 0) return assignments;
|
|
8950
|
-
this.log(
|
|
8951
|
-
`\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
|
|
8952
|
-
);
|
|
8953
|
-
return assignments.map((a) => {
|
|
8954
|
-
if (!cycleNodes.includes(a.subtaskId)) return a;
|
|
8955
|
-
const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
8956
|
-
if (safeDeps.length !== (a.dependsOn ?? []).length) {
|
|
8957
|
-
this.log(
|
|
8958
|
-
` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
|
|
8959
|
-
);
|
|
8960
|
-
for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
8961
|
-
inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
|
|
8962
|
-
adj.get(removed)?.delete(a.subtaskId);
|
|
8963
|
-
}
|
|
8964
|
-
}
|
|
8965
|
-
return { ...a, dependsOn: safeDeps };
|
|
8966
|
-
});
|
|
8967
|
-
}
|
|
8968
9312
|
async retryT3(assignment, taskId) {
|
|
8969
9313
|
this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
|
|
8970
9314
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
@@ -9100,6 +9444,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9100
9444
|
}
|
|
9101
9445
|
};
|
|
9102
9446
|
|
|
9447
|
+
// src/core/orchestration/scheduler.ts
|
|
9448
|
+
var DependencyScheduler = class {
|
|
9449
|
+
constructor(graph, hooks, options = {}) {
|
|
9450
|
+
this.graph = graph;
|
|
9451
|
+
this.hooks = hooks;
|
|
9452
|
+
this.options = options;
|
|
9453
|
+
}
|
|
9454
|
+
graph;
|
|
9455
|
+
hooks;
|
|
9456
|
+
options;
|
|
9457
|
+
async run() {
|
|
9458
|
+
const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
|
|
9459
|
+
const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
|
|
9460
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
9461
|
+
for (const node of nodes) dependents.set(node.id, []);
|
|
9462
|
+
for (const node of nodes) {
|
|
9463
|
+
for (const dependencyId of node.dependsOn) {
|
|
9464
|
+
dependents.get(dependencyId)?.push(node.id);
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
9468
|
+
const settled = /* @__PURE__ */ new Set();
|
|
9469
|
+
const results = /* @__PURE__ */ new Map();
|
|
9470
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
9471
|
+
const waves = [];
|
|
9472
|
+
let wave = 0;
|
|
9473
|
+
const blockDescendants = async (failedIds) => {
|
|
9474
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
9475
|
+
const queue = [];
|
|
9476
|
+
for (const rootId of failedIds) {
|
|
9477
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
9478
|
+
}
|
|
9479
|
+
while (queue.length > 0) {
|
|
9480
|
+
const { id, root } = queue.shift();
|
|
9481
|
+
if (settled.has(id)) continue;
|
|
9482
|
+
const known = rootsFor.get(id);
|
|
9483
|
+
if (known) {
|
|
9484
|
+
if (!known.has(root)) known.add(root);
|
|
9485
|
+
continue;
|
|
9486
|
+
}
|
|
9487
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
9488
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
9489
|
+
}
|
|
9490
|
+
for (const node of nodes) {
|
|
9491
|
+
const roots = rootsFor.get(node.id);
|
|
9492
|
+
if (!roots || settled.has(node.id)) continue;
|
|
9493
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
9494
|
+
const blockedBy = causes.map((n) => n.id);
|
|
9495
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
9496
|
+
const record = {
|
|
9497
|
+
blockedBy,
|
|
9498
|
+
blockedByTitles,
|
|
9499
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
9500
|
+
};
|
|
9501
|
+
blocked.set(node.id, record);
|
|
9502
|
+
settled.add(node.id);
|
|
9503
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
9504
|
+
}
|
|
9505
|
+
};
|
|
9506
|
+
while (settled.size < nodes.length) {
|
|
9507
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
9508
|
+
if (ready.length === 0) {
|
|
9509
|
+
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
9510
|
+
}
|
|
9511
|
+
waves.push(ready.map((node) => node.id));
|
|
9512
|
+
await this.hooks.onWaveStart?.(ready, wave);
|
|
9513
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
9514
|
+
const runOne = async (node) => {
|
|
9515
|
+
const result = await this.hooks.execute(node);
|
|
9516
|
+
results.set(node.id, result);
|
|
9517
|
+
settled.add(node.id);
|
|
9518
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
9519
|
+
outcomes.set(node.id, outcome);
|
|
9520
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
9521
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
9522
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
await this.hooks.onNodeComplete?.(node, result);
|
|
9526
|
+
};
|
|
9527
|
+
if ((this.options.mode ?? "parallel") === "sequential") {
|
|
9528
|
+
for (const node of ready) await runOne(node);
|
|
9529
|
+
} else {
|
|
9530
|
+
await Promise.all(ready.map(runOne));
|
|
9531
|
+
}
|
|
9532
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
9533
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
9534
|
+
await blockDescendants(failedThisWave);
|
|
9535
|
+
}
|
|
9536
|
+
wave++;
|
|
9537
|
+
}
|
|
9538
|
+
return { results, waves, blocked };
|
|
9539
|
+
}
|
|
9540
|
+
};
|
|
9541
|
+
|
|
9103
9542
|
// src/utils/json-extract.ts
|
|
9104
9543
|
function extractFirstJsonObject(input) {
|
|
9105
9544
|
if (!input) return null;
|
|
@@ -9171,7 +9610,7 @@ function buildT1SystemPrompt(has) {
|
|
|
9171
9610
|
Your responsibilities:
|
|
9172
9611
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
9173
9612
|
2. Decompose the task into logical sections (one per T2 Manager)
|
|
9174
|
-
3. For each section, define
|
|
9613
|
+
3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
|
|
9175
9614
|
4. Return a structured plan as JSON
|
|
9176
9615
|
|
|
9177
9616
|
CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
|
|
@@ -9197,6 +9636,9 @@ QUALITY RULES:
|
|
|
9197
9636
|
|
|
9198
9637
|
${generation}` : ""}`;
|
|
9199
9638
|
}
|
|
9639
|
+
function producedOutput(result) {
|
|
9640
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
9641
|
+
}
|
|
9200
9642
|
var T1Administrator = class extends BaseTier {
|
|
9201
9643
|
router;
|
|
9202
9644
|
toolRegistry;
|
|
@@ -9331,7 +9773,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
9331
9773
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
9332
9774
|
let pass = 1;
|
|
9333
9775
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
9334
|
-
const okCount = (rs) => rs.filter(
|
|
9776
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
9335
9777
|
while (pass <= maxReplanPasses) {
|
|
9336
9778
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
9337
9779
|
if (reviewResult.approved) {
|
|
@@ -9462,7 +9904,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
9462
9904
|
/** Structured, grounded summary of what's already done — used to keep
|
|
9463
9905
|
* corrective replan passes from re-emitting completed sections. */
|
|
9464
9906
|
summarizeCompletedSections(results) {
|
|
9465
|
-
const done = results.filter(
|
|
9907
|
+
const done = results.filter(producedOutput);
|
|
9466
9908
|
if (done.length === 0) return "";
|
|
9467
9909
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
9468
9910
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -9586,7 +10028,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9586
10028
|
}
|
|
9587
10029
|
}
|
|
9588
10030
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
9589
|
-
if (plan.sections.length < min)
|
|
10031
|
+
if (plan.sections.length < min || plan.sections.length > max) {
|
|
10032
|
+
this.log(
|
|
10033
|
+
`\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.`
|
|
10034
|
+
);
|
|
10035
|
+
}
|
|
9590
10036
|
}
|
|
9591
10037
|
async dispatchT2Managers(sections) {
|
|
9592
10038
|
for (const section of sections) {
|
|
@@ -9603,6 +10049,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9603
10049
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
9604
10050
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
9605
10051
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
10052
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
9606
10053
|
if (this.store) {
|
|
9607
10054
|
manager.setStore(this.store);
|
|
9608
10055
|
}
|
|
@@ -9698,118 +10145,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
9698
10145
|
return t2Results;
|
|
9699
10146
|
}
|
|
9700
10147
|
/**
|
|
9701
|
-
* Runs T2 managers respecting dependsOn declarations
|
|
10148
|
+
* Runs T2 managers respecting dependsOn declarations.
|
|
10149
|
+
*
|
|
10150
|
+
* Graph construction, validation and cycle repair now live in
|
|
10151
|
+
* compileSectionGraph (orchestration/adapters.ts), and wave execution in
|
|
10152
|
+
* DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
|
|
10153
|
+
* ordered by one implementation rather than two that drifted.
|
|
10154
|
+
*
|
|
10155
|
+
* The hand-rolled Kahn pass this replaces treated every node it could not
|
|
10156
|
+
* reach as part of a cycle. That set also contains everything DOWNSTREAM of a
|
|
10157
|
+
* cycle, whose in-degree can never fall to zero while its dependency is stuck,
|
|
10158
|
+
* so breaking a cycle here also cut the dependencies of innocent later
|
|
10159
|
+
* sections and started them immediately — before the sections they consume.
|
|
10160
|
+
* compileTaskGraph uses strongly connected components, so only real cycle
|
|
10161
|
+
* members are touched.
|
|
9702
10162
|
*/
|
|
9703
10163
|
async runT2sWithDependencies(sections, managers, taskId) {
|
|
9704
|
-
const
|
|
9705
|
-
const
|
|
9706
|
-
|
|
9707
|
-
const allKeys = new Set(sections.map((s) => s.sectionId));
|
|
9708
|
-
for (const s of sections) {
|
|
9709
|
-
if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
|
|
9710
|
-
inDegree.set(s.sectionId, 0);
|
|
9711
|
-
s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
|
|
10164
|
+
const compiled = compileSectionGraph(sections);
|
|
10165
|
+
for (const issue of compiled.issues) {
|
|
10166
|
+
this.log(`\u26A0 Section graph: ${issue.message}`);
|
|
9712
10167
|
}
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
}
|
|
9718
|
-
}
|
|
9719
|
-
const queue = [];
|
|
9720
|
-
const degree = new Map(inDegree);
|
|
9721
|
-
for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
|
|
9722
|
-
const visited = /* @__PURE__ */ new Set();
|
|
9723
|
-
while (queue.length > 0) {
|
|
9724
|
-
const u = queue.shift();
|
|
9725
|
-
visited.add(u);
|
|
9726
|
-
for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
|
|
9727
|
-
const newDeg = (degree.get(v) ?? 1) - 1;
|
|
9728
|
-
degree.set(v, newDeg);
|
|
9729
|
-
if (newDeg === 0) queue.push(v);
|
|
9730
|
-
}
|
|
9731
|
-
}
|
|
9732
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
9733
|
-
if (cycleNodes.length > 0) {
|
|
9734
|
-
this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
|
|
9735
|
-
for (const s of sections) {
|
|
9736
|
-
if (cycleNodes.includes(s.sectionId)) {
|
|
9737
|
-
const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
9738
|
-
for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
9739
|
-
inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
|
|
9740
|
-
adj.get(removed)?.delete(s.sectionId);
|
|
9741
|
-
}
|
|
9742
|
-
s.dependsOn = safeDeps;
|
|
9743
|
-
}
|
|
9744
|
-
}
|
|
10168
|
+
const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
|
|
10169
|
+
for (const section of sections) {
|
|
10170
|
+
const node = byId.get(section.sectionId);
|
|
10171
|
+
if (node) section.dependsOn = [...node.dependsOn];
|
|
9745
10172
|
}
|
|
9746
|
-
const
|
|
10173
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
10174
|
+
const totalSections = compiled.graph.nodes.length;
|
|
9747
10175
|
let completedSections = 0;
|
|
9748
|
-
const
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
10176
|
+
const scheduler = new DependencyScheduler(
|
|
10177
|
+
compiled.graph,
|
|
10178
|
+
{
|
|
10179
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
10180
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
10181
|
+
// Stamped before the wave executes so the first status event a node
|
|
10182
|
+
// emits already carries it.
|
|
10183
|
+
onWaveStart: (nodes, wave) => {
|
|
10184
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
10185
|
+
},
|
|
10186
|
+
execute: async (node) => {
|
|
10187
|
+
const section = node.payload;
|
|
10188
|
+
const manager = managers[node.ordinal];
|
|
10189
|
+
const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
|
|
10190
|
+
this.sendStatusUpdate({
|
|
10191
|
+
progressPct,
|
|
10192
|
+
currentAction: `T2 working on: ${section.sectionTitle}`,
|
|
10193
|
+
status: "IN_PROGRESS"
|
|
10194
|
+
});
|
|
10195
|
+
this.throwIfCancelled();
|
|
10196
|
+
try {
|
|
10197
|
+
const result = await manager.execute(section, taskId, this.signal);
|
|
10198
|
+
manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
|
|
10199
|
+
if (result.status === "ESCALATED") {
|
|
10200
|
+
this.escalations.push({
|
|
10201
|
+
raisedBy: `T2_${section.sectionId}`,
|
|
10202
|
+
sectionId: section.sectionId,
|
|
10203
|
+
attempted: result.issues,
|
|
10204
|
+
blocker: result.issues.join("; "),
|
|
10205
|
+
needs: "Human review required"
|
|
10206
|
+
});
|
|
10207
|
+
}
|
|
10208
|
+
return result;
|
|
10209
|
+
} catch (err) {
|
|
10210
|
+
return {
|
|
9775
10211
|
sectionId: section.sectionId,
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
10212
|
+
sectionTitle: section.sectionTitle,
|
|
10213
|
+
status: "FAILED",
|
|
10214
|
+
t3Results: [],
|
|
10215
|
+
sectionSummary: "",
|
|
10216
|
+
issues: [err instanceof Error ? err.message : String(err)]
|
|
10217
|
+
};
|
|
9780
10218
|
}
|
|
9781
|
-
}
|
|
9782
|
-
|
|
10219
|
+
},
|
|
10220
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
10221
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
10222
|
+
// the user more than letting it try.
|
|
10223
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
10224
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
10225
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
10226
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
10227
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
10228
|
+
// should have been skipped would run and be billed.
|
|
10229
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
10230
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
10231
|
+
const section = node.payload;
|
|
10232
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
10233
|
+
resultMap.set(node.id, {
|
|
9783
10234
|
sectionId: section.sectionId,
|
|
9784
10235
|
sectionTitle: section.sectionTitle,
|
|
9785
|
-
|
|
10236
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
10237
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
10238
|
+
// that did (the upstream section named below).
|
|
10239
|
+
status: "BLOCKED",
|
|
9786
10240
|
t3Results: [],
|
|
9787
10241
|
sectionSummary: "",
|
|
9788
|
-
issues: [
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
10242
|
+
issues: [
|
|
10243
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
10244
|
+
],
|
|
10245
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
10246
|
+
});
|
|
10247
|
+
completedSections++;
|
|
10248
|
+
this.sendStatusUpdate({
|
|
10249
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
10250
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
10251
|
+
status: "IN_PROGRESS"
|
|
10252
|
+
});
|
|
10253
|
+
this.emit("tier:status", {
|
|
10254
|
+
tierId: section.sectionId,
|
|
10255
|
+
// A blocked section has no tier instance — it was never
|
|
10256
|
+
// constructed — so its runtime id and its graph id are the same
|
|
10257
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
10258
|
+
// stream addressable in the graph's id space, which is what
|
|
10259
|
+
// dependsOn edges point at.
|
|
10260
|
+
nodeId: section.sectionId,
|
|
10261
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
10262
|
+
role: "T2",
|
|
10263
|
+
label: section.sectionTitle,
|
|
10264
|
+
status: "BLOCKED",
|
|
10265
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10266
|
+
output: blockedInfo.reason
|
|
10267
|
+
});
|
|
10268
|
+
this.emit("section:complete", {
|
|
10269
|
+
id: node.id,
|
|
10270
|
+
title: section.sectionTitle,
|
|
10271
|
+
status: "BLOCKED",
|
|
10272
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
10273
|
+
output: ""
|
|
10274
|
+
});
|
|
10275
|
+
},
|
|
10276
|
+
onNodeComplete: (node, result) => {
|
|
10277
|
+
resultMap.set(node.id, result);
|
|
10278
|
+
completedSections++;
|
|
10279
|
+
this.emit("section:complete", {
|
|
10280
|
+
id: node.id,
|
|
10281
|
+
title: result.sectionTitle,
|
|
10282
|
+
status: result.status,
|
|
10283
|
+
output: result.sectionSummary
|
|
10284
|
+
});
|
|
9800
10285
|
}
|
|
9801
|
-
}
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
10286
|
+
},
|
|
10287
|
+
{
|
|
10288
|
+
// Cross-section concurrency respects the same t3Execution mode as the
|
|
10289
|
+
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
10290
|
+
// serialized T3 workers within a single section while every section's
|
|
10291
|
+
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
10292
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
10293
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
10294
|
+
// output. Running it after the dependency failed produces a second
|
|
10295
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
10296
|
+
onUpstreamFailure: "block"
|
|
9806
10297
|
}
|
|
9807
|
-
|
|
9808
|
-
await
|
|
10298
|
+
);
|
|
10299
|
+
await scheduler.run();
|
|
9809
10300
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
9810
10301
|
}
|
|
9811
10302
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
9812
|
-
const completedSections = t2Results.filter(
|
|
10303
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
9813
10304
|
if (!completedSections.length) {
|
|
9814
10305
|
const allIssues = [
|
|
9815
10306
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -9829,7 +10320,6 @@ ${r.sectionSummary}
|
|
|
9829
10320
|
Outputs:
|
|
9830
10321
|
${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");
|
|
9831
10322
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
9832
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
9833
10323
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
9834
10324
|
|
|
9835
10325
|
Original request: ${originalPrompt}
|
|
@@ -10321,6 +10811,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
10321
10811
|
};
|
|
10322
10812
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
10323
10813
|
var MAX_VIDEO_SECONDS = 8;
|
|
10814
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
10324
10815
|
var GenerateVideoTool = class extends BaseTool {
|
|
10325
10816
|
constructor(deps) {
|
|
10326
10817
|
super();
|
|
@@ -10337,7 +10828,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10337
10828
|
type: "object",
|
|
10338
10829
|
properties: {
|
|
10339
10830
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
10340
|
-
seconds: { type: "number", description: `Clip length,
|
|
10831
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
10341
10832
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
10342
10833
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
10343
10834
|
},
|
|
@@ -10349,7 +10840,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10349
10840
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
10350
10841
|
if (!r.ok) return r.error;
|
|
10351
10842
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
10352
|
-
const seconds = Math.max(
|
|
10843
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
10353
10844
|
const asset = await generateVideo(
|
|
10354
10845
|
r.selected.capability,
|
|
10355
10846
|
r.cfg,
|
|
@@ -10366,7 +10857,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
10366
10857
|
const location = await this.deps.sink(asset);
|
|
10367
10858
|
const unit = r.selected.cost;
|
|
10368
10859
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
10369
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
10860
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
10370
10861
|
return `Video generated and saved to ${location}.
|
|
10371
10862
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
10372
10863
|
}
|
|
@@ -10435,16 +10926,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
10435
10926
|
if (typeof input !== "string" || input.length === 0) {
|
|
10436
10927
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
10437
10928
|
}
|
|
10438
|
-
const root =
|
|
10439
|
-
const abs =
|
|
10440
|
-
const rel =
|
|
10441
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
10929
|
+
const root = path26__default.default.resolve(workspaceRoot);
|
|
10930
|
+
const abs = path26__default.default.isAbsolute(input) ? path26__default.default.resolve(input) : path26__default.default.resolve(root, input);
|
|
10931
|
+
const rel = path26__default.default.relative(root, abs);
|
|
10932
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26__default.default.isAbsolute(rel)) {
|
|
10442
10933
|
throw new WorkspaceSandboxError(input, root);
|
|
10443
10934
|
}
|
|
10444
10935
|
try {
|
|
10445
|
-
const real =
|
|
10446
|
-
const realRel =
|
|
10447
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
10936
|
+
const real = fs25__default.default.realpathSync(abs);
|
|
10937
|
+
const realRel = path26__default.default.relative(root, real);
|
|
10938
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26__default.default.isAbsolute(realRel))) {
|
|
10448
10939
|
throw new WorkspaceSandboxError(input, root);
|
|
10449
10940
|
}
|
|
10450
10941
|
} catch (e) {
|
|
@@ -11102,7 +11593,7 @@ var GenerateDocumentTool = class extends BaseTool {
|
|
|
11102
11593
|
}
|
|
11103
11594
|
}
|
|
11104
11595
|
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
11105
|
-
await fs5__default.default.mkdir(
|
|
11596
|
+
await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
|
|
11106
11597
|
await fs5__default.default.writeFile(absPath, bytes);
|
|
11107
11598
|
const charts = countCharts(content);
|
|
11108
11599
|
return [
|
|
@@ -11497,7 +11988,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
11497
11988
|
} catch {
|
|
11498
11989
|
}
|
|
11499
11990
|
}
|
|
11500
|
-
await fs5__default.default.mkdir(
|
|
11991
|
+
await fs5__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
|
|
11501
11992
|
await fs5__default.default.writeFile(absPath, content, "utf-8");
|
|
11502
11993
|
return `Written ${content.length} characters to ${filePath}`;
|
|
11503
11994
|
}
|
|
@@ -11995,7 +12486,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
11995
12486
|
};
|
|
11996
12487
|
async function fileToImageAttachment(filePath) {
|
|
11997
12488
|
const data = await fs5__default.default.readFile(filePath);
|
|
11998
|
-
const ext =
|
|
12489
|
+
const ext = path26__default.default.extname(filePath).toLowerCase();
|
|
11999
12490
|
const mimeMap = {
|
|
12000
12491
|
".jpg": "image/jpeg",
|
|
12001
12492
|
".jpeg": "image/jpeg",
|
|
@@ -12029,14 +12520,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
12029
12520
|
const filePath = input["path"];
|
|
12030
12521
|
const content = input["content"];
|
|
12031
12522
|
const title = input["title"];
|
|
12032
|
-
const dir =
|
|
12033
|
-
if (!
|
|
12034
|
-
|
|
12523
|
+
const dir = path26__default.default.dirname(filePath);
|
|
12524
|
+
if (!fs25__default.default.existsSync(dir)) {
|
|
12525
|
+
fs25__default.default.mkdirSync(dir, { recursive: true });
|
|
12035
12526
|
}
|
|
12036
12527
|
return new Promise((resolve2, reject) => {
|
|
12037
12528
|
try {
|
|
12038
12529
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
12039
|
-
const stream =
|
|
12530
|
+
const stream = fs25__default.default.createWriteStream(filePath);
|
|
12040
12531
|
doc.pipe(stream);
|
|
12041
12532
|
if (title) {
|
|
12042
12533
|
doc.info["Title"] = title;
|
|
@@ -12114,22 +12605,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
12114
12605
|
}
|
|
12115
12606
|
cmdPrefix = NODE_CMD;
|
|
12116
12607
|
}
|
|
12117
|
-
const tmpDir =
|
|
12118
|
-
if (!
|
|
12119
|
-
|
|
12608
|
+
const tmpDir = path26__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
12609
|
+
if (!fs25__default.default.existsSync(tmpDir)) {
|
|
12610
|
+
fs25__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
12120
12611
|
}
|
|
12121
12612
|
const extension = language === "python" ? "py" : "js";
|
|
12122
12613
|
const fileName = `intp_${crypto4.randomUUID().slice(0, 8)}.${extension}`;
|
|
12123
|
-
const filePath =
|
|
12124
|
-
|
|
12614
|
+
const filePath = path26__default.default.join(tmpDir, fileName);
|
|
12615
|
+
fs25__default.default.writeFileSync(filePath, code, "utf-8");
|
|
12125
12616
|
const execArgs = [filePath, ...args];
|
|
12126
12617
|
return new Promise((resolve2) => {
|
|
12127
12618
|
const startMs = Date.now();
|
|
12128
12619
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
12129
12620
|
const duration = Date.now() - startMs;
|
|
12130
12621
|
try {
|
|
12131
|
-
if (
|
|
12132
|
-
|
|
12622
|
+
if (fs25__default.default.existsSync(filePath)) {
|
|
12623
|
+
fs25__default.default.unlinkSync(filePath);
|
|
12133
12624
|
}
|
|
12134
12625
|
} catch (cleanupErr) {
|
|
12135
12626
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -12441,7 +12932,7 @@ var GlobTool = class extends BaseTool {
|
|
|
12441
12932
|
};
|
|
12442
12933
|
async execute(input, _options) {
|
|
12443
12934
|
const pattern = input["pattern"];
|
|
12444
|
-
const searchPath = input["path"] ?
|
|
12935
|
+
const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
12445
12936
|
const matches = await glob.glob(pattern, {
|
|
12446
12937
|
cwd: searchPath,
|
|
12447
12938
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -12454,7 +12945,7 @@ var GlobTool = class extends BaseTool {
|
|
|
12454
12945
|
const withMtime = await Promise.all(
|
|
12455
12946
|
matches.map(async (rel) => {
|
|
12456
12947
|
try {
|
|
12457
|
-
const stat = await fs5__default.default.stat(
|
|
12948
|
+
const stat = await fs5__default.default.stat(path26__default.default.join(searchPath, rel));
|
|
12458
12949
|
return { rel, mtime: stat.mtimeMs };
|
|
12459
12950
|
} catch {
|
|
12460
12951
|
return { rel, mtime: 0 };
|
|
@@ -12503,7 +12994,7 @@ var GrepTool = class extends BaseTool {
|
|
|
12503
12994
|
};
|
|
12504
12995
|
async execute(input, _options) {
|
|
12505
12996
|
const pattern = input["pattern"];
|
|
12506
|
-
const searchPath = input["path"] ?
|
|
12997
|
+
const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
12507
12998
|
const globPattern = input["glob"];
|
|
12508
12999
|
const outputMode = input["output_mode"] ?? "content";
|
|
12509
13000
|
const context = input["context"] ?? 0;
|
|
@@ -12557,12 +13048,12 @@ var GrepTool = class extends BaseTool {
|
|
|
12557
13048
|
nodir: true
|
|
12558
13049
|
});
|
|
12559
13050
|
} catch {
|
|
12560
|
-
files = [
|
|
13051
|
+
files = [path26__default.default.relative(searchPath, searchPath) || "."];
|
|
12561
13052
|
}
|
|
12562
13053
|
const results = [];
|
|
12563
13054
|
let totalCount = 0;
|
|
12564
13055
|
for (const rel of files) {
|
|
12565
|
-
const abs =
|
|
13056
|
+
const abs = path26__default.default.join(searchPath, rel);
|
|
12566
13057
|
let content;
|
|
12567
13058
|
try {
|
|
12568
13059
|
content = await fs5__default.default.readFile(abs, "utf-8");
|
|
@@ -12949,10 +13440,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
12949
13440
|
}
|
|
12950
13441
|
isIgnored(filePath) {
|
|
12951
13442
|
if (!filePath) return false;
|
|
12952
|
-
const abs =
|
|
12953
|
-
const rel =
|
|
12954
|
-
if (!rel || rel.startsWith("..") ||
|
|
12955
|
-
const posixRel = rel.split(
|
|
13443
|
+
const abs = path26__default.default.resolve(this.workspaceRoot, filePath);
|
|
13444
|
+
const rel = path26__default.default.relative(this.workspaceRoot, abs);
|
|
13445
|
+
if (!rel || rel.startsWith("..") || path26__default.default.isAbsolute(rel)) return true;
|
|
13446
|
+
const posixRel = rel.split(path26__default.default.sep).join("/");
|
|
12956
13447
|
return this.ignoreMatcher.ignores(posixRel);
|
|
12957
13448
|
}
|
|
12958
13449
|
};
|
|
@@ -13186,22 +13677,22 @@ var FileMcpOAuthStore = class {
|
|
|
13186
13677
|
filePath;
|
|
13187
13678
|
load() {
|
|
13188
13679
|
try {
|
|
13189
|
-
return JSON.parse(
|
|
13680
|
+
return JSON.parse(fs25__default.default.readFileSync(this.filePath, "utf-8"));
|
|
13190
13681
|
} catch {
|
|
13191
13682
|
return void 0;
|
|
13192
13683
|
}
|
|
13193
13684
|
}
|
|
13194
13685
|
save(state) {
|
|
13195
|
-
|
|
13196
|
-
|
|
13686
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
13687
|
+
fs25__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
13197
13688
|
try {
|
|
13198
|
-
|
|
13689
|
+
fs25__default.default.chmodSync(this.filePath, 384);
|
|
13199
13690
|
} catch {
|
|
13200
13691
|
}
|
|
13201
13692
|
}
|
|
13202
13693
|
clear() {
|
|
13203
13694
|
try {
|
|
13204
|
-
|
|
13695
|
+
fs25__default.default.rmSync(this.filePath);
|
|
13205
13696
|
} catch {
|
|
13206
13697
|
}
|
|
13207
13698
|
}
|
|
@@ -14098,6 +14589,14 @@ function heuristicAnalyze(prompt) {
|
|
|
14098
14589
|
const estimatedTokens = wordCount * 5;
|
|
14099
14590
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
14100
14591
|
}
|
|
14592
|
+
function hashPrompt(prompt) {
|
|
14593
|
+
let hash = 2166136261;
|
|
14594
|
+
for (let index = 0; index < prompt.length; index++) {
|
|
14595
|
+
hash ^= prompt.charCodeAt(index);
|
|
14596
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
14597
|
+
}
|
|
14598
|
+
return `${prompt.length}:${hash.toString(36)}`;
|
|
14599
|
+
}
|
|
14101
14600
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
14102
14601
|
var TASK_TYPE_TAGS = {
|
|
14103
14602
|
code: ["code", "instruction"],
|
|
@@ -14112,7 +14611,24 @@ var TaskAnalyzer = class {
|
|
|
14112
14611
|
feedback;
|
|
14113
14612
|
bias;
|
|
14114
14613
|
lastProfile = null;
|
|
14115
|
-
|
|
14614
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
14615
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
14616
|
+
/**
|
|
14617
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
14618
|
+
* type they were chosen for.
|
|
14619
|
+
*
|
|
14620
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
14621
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
14622
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
14623
|
+
* model is good at creative writing, from a rating that never said so.
|
|
14624
|
+
*
|
|
14625
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
14626
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
14627
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
14628
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
14629
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
14630
|
+
*/
|
|
14631
|
+
lastCompletedRun;
|
|
14116
14632
|
constructor(tracker, bias = "balanced") {
|
|
14117
14633
|
this.tracker = tracker;
|
|
14118
14634
|
this.bias = bias;
|
|
@@ -14141,7 +14657,7 @@ var TaskAnalyzer = class {
|
|
|
14141
14657
|
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
14142
14658
|
*/
|
|
14143
14659
|
async analyze(prompt) {
|
|
14144
|
-
const cacheKey = prompt
|
|
14660
|
+
const cacheKey = hashPrompt(prompt);
|
|
14145
14661
|
const cached = analysisCache.get(cacheKey);
|
|
14146
14662
|
if (cached) {
|
|
14147
14663
|
this.lastProfile = cached;
|
|
@@ -14159,11 +14675,15 @@ var TaskAnalyzer = class {
|
|
|
14159
14675
|
*/
|
|
14160
14676
|
async selectModel(prompt, tier, selector, opts) {
|
|
14161
14677
|
const profile = await this.analyze(prompt);
|
|
14678
|
+
const recordSelection = (model) => {
|
|
14679
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
14680
|
+
return model;
|
|
14681
|
+
};
|
|
14162
14682
|
if (profile.requiresVision) {
|
|
14163
|
-
return selector.selectVisionModel();
|
|
14683
|
+
return recordSelection(selector.selectVisionModel());
|
|
14164
14684
|
}
|
|
14165
14685
|
let candidates = selector.getCandidatesForTier(tier);
|
|
14166
|
-
if (candidates.length === 0) return selector.selectForTier(tier);
|
|
14686
|
+
if (candidates.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
14167
14687
|
if (opts?.requiresToolUse) {
|
|
14168
14688
|
const toolCapable = candidates.filter((m) => m.supportsToolUse !== false);
|
|
14169
14689
|
if (toolCapable.length > 0) candidates = toolCapable;
|
|
@@ -14173,9 +14693,7 @@ var TaskAnalyzer = class {
|
|
|
14173
14693
|
score: this.scoreModel(m, profile)
|
|
14174
14694
|
}));
|
|
14175
14695
|
scored.sort((a, b) => b.score - a.score);
|
|
14176
|
-
|
|
14177
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
14178
|
-
return best;
|
|
14696
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
14179
14697
|
}
|
|
14180
14698
|
/**
|
|
14181
14699
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -14184,25 +14702,38 @@ var TaskAnalyzer = class {
|
|
|
14184
14702
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
14185
14703
|
if (!this.tracker || !this.lastProfile) return;
|
|
14186
14704
|
const taskType = this.lastProfile.type;
|
|
14187
|
-
for (const [tier, model] of this.
|
|
14705
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
14188
14706
|
const cost = costByTier[tier] ?? 0;
|
|
14189
14707
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
14190
14708
|
}
|
|
14191
|
-
this.
|
|
14709
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
14710
|
+
this.currentRunSelections.clear();
|
|
14192
14711
|
void this.tracker.save();
|
|
14193
14712
|
}
|
|
14194
14713
|
/**
|
|
14195
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
14714
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
14196
14715
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
14197
|
-
*
|
|
14716
|
+
*
|
|
14717
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
14718
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
14719
|
+
* empty.
|
|
14720
|
+
*
|
|
14721
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
14722
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
14723
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
14724
|
+
* that reports false, which is what "rate this run" should mean.
|
|
14198
14725
|
*/
|
|
14199
14726
|
recordExplicitRating(rating) {
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14727
|
+
const snapshot = this.lastCompletedRun;
|
|
14728
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
14729
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
14730
|
+
for (const [, model] of snapshot.selections) {
|
|
14731
|
+
if (ratedModels.has(model.id)) continue;
|
|
14732
|
+
ratedModels.add(model.id);
|
|
14733
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
14734
|
+
}
|
|
14735
|
+
this.lastCompletedRun = void 0;
|
|
14736
|
+
return true;
|
|
14206
14737
|
}
|
|
14207
14738
|
scoreModel(model, profile) {
|
|
14208
14739
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -14240,7 +14771,7 @@ var TaskAnalyzer = class {
|
|
|
14240
14771
|
analysisCache.clear();
|
|
14241
14772
|
}
|
|
14242
14773
|
};
|
|
14243
|
-
var DEFAULT_STATS_FILE =
|
|
14774
|
+
var DEFAULT_STATS_FILE = path26__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
|
|
14244
14775
|
var ModelPerformanceTracker = class {
|
|
14245
14776
|
stats = /* @__PURE__ */ new Map();
|
|
14246
14777
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -14280,7 +14811,7 @@ var ModelPerformanceTracker = class {
|
|
|
14280
14811
|
async save() {
|
|
14281
14812
|
if (this.readOnly) return;
|
|
14282
14813
|
try {
|
|
14283
|
-
await fs5__default.default.mkdir(
|
|
14814
|
+
await fs5__default.default.mkdir(path26__default.default.dirname(this.statsFile), { recursive: true });
|
|
14284
14815
|
const modelsObj = {};
|
|
14285
14816
|
const featuresObj = {};
|
|
14286
14817
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -14828,7 +15359,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
14828
15359
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
14829
15360
|
async loadPersistedTools() {
|
|
14830
15361
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
14831
|
-
const file =
|
|
15362
|
+
const file = path26__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
14832
15363
|
try {
|
|
14833
15364
|
const raw = await fs5__default.default.readFile(file, "utf-8");
|
|
14834
15365
|
const specs = JSON.parse(raw);
|
|
@@ -14852,8 +15383,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
14852
15383
|
}
|
|
14853
15384
|
async persist() {
|
|
14854
15385
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
14855
|
-
const dir =
|
|
14856
|
-
const file =
|
|
15386
|
+
const dir = path26__default.default.join(this.workspacePath, ".cascade");
|
|
15387
|
+
const file = path26__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
14857
15388
|
try {
|
|
14858
15389
|
await fs5__default.default.mkdir(dir, { recursive: true });
|
|
14859
15390
|
await fs5__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -14873,12 +15404,12 @@ var WorldStateDB = class {
|
|
|
14873
15404
|
constructor(workspacePath, debugMode = false) {
|
|
14874
15405
|
this.workspacePath = workspacePath;
|
|
14875
15406
|
this.debugMode = debugMode;
|
|
14876
|
-
const cascadeDir =
|
|
14877
|
-
if (!
|
|
14878
|
-
|
|
15407
|
+
const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
|
|
15408
|
+
if (!fs25__default.default.existsSync(cascadeDir)) {
|
|
15409
|
+
fs25__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
14879
15410
|
}
|
|
14880
|
-
this.keyPath =
|
|
14881
|
-
this.dbPath =
|
|
15411
|
+
this.keyPath = path26__default.default.join(cascadeDir, "world_state.key");
|
|
15412
|
+
this.dbPath = path26__default.default.join(cascadeDir, "world_state.db");
|
|
14882
15413
|
this.initEncryptionKey();
|
|
14883
15414
|
this.db = new Database__default.default(this.dbPath);
|
|
14884
15415
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -14935,11 +15466,11 @@ var WorldStateDB = class {
|
|
|
14935
15466
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
14936
15467
|
}
|
|
14937
15468
|
initEncryptionKey() {
|
|
14938
|
-
if (
|
|
14939
|
-
this.encryptionKey =
|
|
15469
|
+
if (fs25__default.default.existsSync(this.keyPath)) {
|
|
15470
|
+
this.encryptionKey = fs25__default.default.readFileSync(this.keyPath);
|
|
14940
15471
|
} else {
|
|
14941
15472
|
this.encryptionKey = crypto4__default.default.randomBytes(32);
|
|
14942
|
-
|
|
15473
|
+
fs25__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
14943
15474
|
}
|
|
14944
15475
|
}
|
|
14945
15476
|
encrypt(text) {
|
|
@@ -15225,9 +15756,9 @@ var WorldStateDB = class {
|
|
|
15225
15756
|
dumpDebugIfNeeded() {
|
|
15226
15757
|
if (!this.debugMode) return;
|
|
15227
15758
|
try {
|
|
15228
|
-
const dumpPath =
|
|
15759
|
+
const dumpPath = path26__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
15229
15760
|
const entries = this.getAllEntries();
|
|
15230
|
-
|
|
15761
|
+
fs25__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
15231
15762
|
} catch (err) {
|
|
15232
15763
|
console.error("Failed to dump debug world state", err);
|
|
15233
15764
|
}
|
|
@@ -15236,6 +15767,212 @@ var WorldStateDB = class {
|
|
|
15236
15767
|
this.db.close();
|
|
15237
15768
|
}
|
|
15238
15769
|
};
|
|
15770
|
+
var RESUME_SCHEMA_VERSION = 1;
|
|
15771
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
15772
|
+
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15773
|
+
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
15774
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
15775
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15776
|
+
function safeId(taskId) {
|
|
15777
|
+
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
15778
|
+
return cleaned.slice(0, 100) || "run";
|
|
15779
|
+
}
|
|
15780
|
+
function isCheckpoint(value) {
|
|
15781
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15782
|
+
const c = value;
|
|
15783
|
+
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);
|
|
15784
|
+
}
|
|
15785
|
+
var ResumeStore = class {
|
|
15786
|
+
dir;
|
|
15787
|
+
maxCheckpoints;
|
|
15788
|
+
maxAgeMs;
|
|
15789
|
+
now;
|
|
15790
|
+
claimTimeoutMs;
|
|
15791
|
+
constructor(options) {
|
|
15792
|
+
this.dir = options.dir;
|
|
15793
|
+
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
15794
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
15795
|
+
this.now = options.now ?? Date.now;
|
|
15796
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
15797
|
+
}
|
|
15798
|
+
/**
|
|
15799
|
+
* Persist a checkpoint and prune.
|
|
15800
|
+
*
|
|
15801
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
15802
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
15803
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
15804
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
15805
|
+
* look alike to them.
|
|
15806
|
+
*
|
|
15807
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
15808
|
+
*/
|
|
15809
|
+
async save(checkpoint) {
|
|
15810
|
+
try {
|
|
15811
|
+
const timestamp = this.now();
|
|
15812
|
+
const record = {
|
|
15813
|
+
...checkpoint,
|
|
15814
|
+
version: RESUME_SCHEMA_VERSION,
|
|
15815
|
+
createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
|
|
15816
|
+
};
|
|
15817
|
+
await fs25.promises.mkdir(this.dir, { recursive: true });
|
|
15818
|
+
const finalPath = path26__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
|
|
15819
|
+
const tempPath = `${finalPath}.${process.pid}.tmp`;
|
|
15820
|
+
await fs25.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
15821
|
+
await fs25.promises.rename(tempPath, finalPath);
|
|
15822
|
+
await this.prune();
|
|
15823
|
+
return true;
|
|
15824
|
+
} catch {
|
|
15825
|
+
return false;
|
|
15826
|
+
}
|
|
15827
|
+
}
|
|
15828
|
+
/** Valid, unexpired checkpoints, newest first. */
|
|
15829
|
+
async list() {
|
|
15830
|
+
let entries;
|
|
15831
|
+
try {
|
|
15832
|
+
entries = await fs25.promises.readdir(this.dir);
|
|
15833
|
+
} catch {
|
|
15834
|
+
return [];
|
|
15835
|
+
}
|
|
15836
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
15837
|
+
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);
|
|
15838
|
+
const checkpoints = [];
|
|
15839
|
+
for (const candidate of candidates) {
|
|
15840
|
+
try {
|
|
15841
|
+
const raw = await fs25.promises.readFile(path26__default.default.join(this.dir, candidate.name), "utf-8");
|
|
15842
|
+
const parsed = JSON.parse(raw);
|
|
15843
|
+
if (isCheckpoint(parsed)) checkpoints.push(parsed);
|
|
15844
|
+
} catch {
|
|
15845
|
+
continue;
|
|
15846
|
+
}
|
|
15847
|
+
}
|
|
15848
|
+
return checkpoints;
|
|
15849
|
+
}
|
|
15850
|
+
/** The most recent resumable run, or null. */
|
|
15851
|
+
async latest() {
|
|
15852
|
+
return (await this.list())[0] ?? null;
|
|
15853
|
+
}
|
|
15854
|
+
/**
|
|
15855
|
+
* Claim a checkpoint for a resume attempt.
|
|
15856
|
+
*
|
|
15857
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
15858
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
15859
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
15860
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
15861
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
15862
|
+
* wrong trade for a recovery mechanism.
|
|
15863
|
+
*
|
|
15864
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
15865
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
15866
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
15867
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
15868
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
15869
|
+
*/
|
|
15870
|
+
async claim(taskId) {
|
|
15871
|
+
const all = await this.list();
|
|
15872
|
+
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
15873
|
+
if (!target) return null;
|
|
15874
|
+
try {
|
|
15875
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15876
|
+
const wanted = safeId(target.taskId);
|
|
15877
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
15878
|
+
if (!source) return null;
|
|
15879
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
15880
|
+
await fs25.promises.rename(path26__default.default.join(this.dir, source), path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
15881
|
+
return { checkpoint: target, claimId };
|
|
15882
|
+
} catch {
|
|
15883
|
+
return null;
|
|
15884
|
+
}
|
|
15885
|
+
}
|
|
15886
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
15887
|
+
async release(claimId) {
|
|
15888
|
+
try {
|
|
15889
|
+
await fs25.promises.rename(
|
|
15890
|
+
path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
15891
|
+
path26__default.default.join(this.dir, `${claimId}.json`)
|
|
15892
|
+
);
|
|
15893
|
+
} catch {
|
|
15894
|
+
}
|
|
15895
|
+
}
|
|
15896
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
15897
|
+
async settle(claimId) {
|
|
15898
|
+
try {
|
|
15899
|
+
await fs25.promises.rm(path26__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
15900
|
+
} catch {
|
|
15901
|
+
}
|
|
15902
|
+
}
|
|
15903
|
+
/**
|
|
15904
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
15905
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
15906
|
+
*/
|
|
15907
|
+
async consume(taskId) {
|
|
15908
|
+
const claimed = await this.claim(taskId);
|
|
15909
|
+
if (!claimed) return null;
|
|
15910
|
+
await this.settle(claimed.claimId);
|
|
15911
|
+
return claimed.checkpoint;
|
|
15912
|
+
}
|
|
15913
|
+
/**
|
|
15914
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
15915
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
15916
|
+
* trading one loss window for another.
|
|
15917
|
+
*/
|
|
15918
|
+
async reclaimStale() {
|
|
15919
|
+
let reclaimed = 0;
|
|
15920
|
+
try {
|
|
15921
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15922
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
15923
|
+
for (const name of entries) {
|
|
15924
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
15925
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
15926
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
15927
|
+
await fs25.promises.rename(
|
|
15928
|
+
path26__default.default.join(this.dir, name),
|
|
15929
|
+
path26__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
15930
|
+
);
|
|
15931
|
+
reclaimed++;
|
|
15932
|
+
}
|
|
15933
|
+
} catch {
|
|
15934
|
+
}
|
|
15935
|
+
return reclaimed;
|
|
15936
|
+
}
|
|
15937
|
+
/** Remove every checkpoint for a task id. */
|
|
15938
|
+
async delete(taskId) {
|
|
15939
|
+
try {
|
|
15940
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15941
|
+
const wanted = safeId(taskId);
|
|
15942
|
+
await Promise.all(entries.map(async (name) => {
|
|
15943
|
+
const match = FILE_PATTERN.exec(name);
|
|
15944
|
+
if (match?.[2] === wanted) {
|
|
15945
|
+
await fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true });
|
|
15946
|
+
}
|
|
15947
|
+
}));
|
|
15948
|
+
} catch {
|
|
15949
|
+
}
|
|
15950
|
+
}
|
|
15951
|
+
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
15952
|
+
async prune() {
|
|
15953
|
+
try {
|
|
15954
|
+
const entries = await fs25.promises.readdir(this.dir);
|
|
15955
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
15956
|
+
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);
|
|
15957
|
+
const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
|
|
15958
|
+
const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
|
|
15959
|
+
await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
|
|
15960
|
+
(name) => fs25.promises.rm(path26__default.default.join(this.dir, name), { force: true })
|
|
15961
|
+
));
|
|
15962
|
+
} catch {
|
|
15963
|
+
}
|
|
15964
|
+
}
|
|
15965
|
+
};
|
|
15966
|
+
function summarizeCompleted(completed, perNodeChars = 500) {
|
|
15967
|
+
if (!completed.length) return "";
|
|
15968
|
+
const lines = completed.map((node) => {
|
|
15969
|
+
const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
|
|
15970
|
+
return `- [${node.status}] ${node.title}
|
|
15971
|
+
${body.replace(/\n/g, "\n ")}`;
|
|
15972
|
+
});
|
|
15973
|
+
return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
|
|
15974
|
+
${lines.join("\n")}`;
|
|
15975
|
+
}
|
|
15239
15976
|
var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
|
|
15240
15977
|
var PrivacyPaths = class {
|
|
15241
15978
|
localOnly;
|
|
@@ -15282,7 +16019,7 @@ var CascadeIgnore = class {
|
|
|
15282
16019
|
]);
|
|
15283
16020
|
}
|
|
15284
16021
|
async load(workspacePath) {
|
|
15285
|
-
const filePath =
|
|
16022
|
+
const filePath = path26__default.default.join(workspacePath, ".cascadeignore");
|
|
15286
16023
|
try {
|
|
15287
16024
|
const content = await fs5__default.default.readFile(filePath, "utf-8");
|
|
15288
16025
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -15293,7 +16030,7 @@ var CascadeIgnore = class {
|
|
|
15293
16030
|
}
|
|
15294
16031
|
isIgnored(filePath, workspacePath) {
|
|
15295
16032
|
try {
|
|
15296
|
-
const relative = workspacePath ?
|
|
16033
|
+
const relative = workspacePath ? path26__default.default.relative(workspacePath, filePath) : filePath;
|
|
15297
16034
|
return this.ig.ignores(relative);
|
|
15298
16035
|
} catch {
|
|
15299
16036
|
return false;
|
|
@@ -15636,7 +16373,7 @@ var WorkspaceIndex = class {
|
|
|
15636
16373
|
isIgnored;
|
|
15637
16374
|
chunker;
|
|
15638
16375
|
constructor(opts) {
|
|
15639
|
-
this.root =
|
|
16376
|
+
this.root = path26__default.default.resolve(opts.root);
|
|
15640
16377
|
this.db = opts.db;
|
|
15641
16378
|
this.namespace = opts.namespace ?? "code";
|
|
15642
16379
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -15668,14 +16405,14 @@ var WorkspaceIndex = class {
|
|
|
15668
16405
|
return;
|
|
15669
16406
|
}
|
|
15670
16407
|
for (const entry of entries) {
|
|
15671
|
-
const abs =
|
|
16408
|
+
const abs = path26__default.default.join(dir, entry.name);
|
|
15672
16409
|
if (entry.isDirectory()) {
|
|
15673
16410
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
15674
16411
|
await walk(abs);
|
|
15675
16412
|
} else if (entry.isFile()) {
|
|
15676
|
-
const ext =
|
|
16413
|
+
const ext = path26__default.default.extname(entry.name).slice(1).toLowerCase();
|
|
15677
16414
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
15678
|
-
out.push(
|
|
16415
|
+
out.push(path26__default.default.relative(this.root, abs));
|
|
15679
16416
|
}
|
|
15680
16417
|
}
|
|
15681
16418
|
};
|
|
@@ -15705,7 +16442,7 @@ var WorkspaceIndex = class {
|
|
|
15705
16442
|
const entries = [];
|
|
15706
16443
|
for (const rel of rels) {
|
|
15707
16444
|
try {
|
|
15708
|
-
const abs =
|
|
16445
|
+
const abs = path26__default.default.join(this.root, rel);
|
|
15709
16446
|
const stat = await fs5__default.default.stat(abs);
|
|
15710
16447
|
if (stat.size > this.maxFileBytes) continue;
|
|
15711
16448
|
const buf = await fs5__default.default.readFile(abs);
|
|
@@ -16243,8 +16980,35 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16243
16980
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
16244
16981
|
decisionLog = [];
|
|
16245
16982
|
initialized = false;
|
|
16246
|
-
/**
|
|
16983
|
+
/**
|
|
16984
|
+
* Last interrupted task — powers /continue (resumeRun). Kept in memory as a
|
|
16985
|
+
* fast path for the same-process case; the durable copy lives in `resumeStore`
|
|
16986
|
+
* so a crash or an exit no longer throws away everything already finished.
|
|
16987
|
+
*/
|
|
16247
16988
|
lastInterruptedRun;
|
|
16989
|
+
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
16990
|
+
resumeStore;
|
|
16991
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
16992
|
+
claimedResumeId;
|
|
16993
|
+
/**
|
|
16994
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
16995
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
16996
|
+
*/
|
|
16997
|
+
resumedFrom;
|
|
16998
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
16999
|
+
wroteCheckpointThisRun = false;
|
|
17000
|
+
/**
|
|
17001
|
+
* Whether the run reached the end of its normal path.
|
|
17002
|
+
*
|
|
17003
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
17004
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
17005
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
17006
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
17007
|
+
* the original claim and the finished sections with it.
|
|
17008
|
+
*/
|
|
17009
|
+
completedNormally = false;
|
|
17010
|
+
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
17011
|
+
currentRunCompleted = [];
|
|
16248
17012
|
initPromise;
|
|
16249
17013
|
store;
|
|
16250
17014
|
audit;
|
|
@@ -16277,10 +17041,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16277
17041
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16278
17042
|
try {
|
|
16279
17043
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
16280
|
-
fileDeadModelPersistence(
|
|
17044
|
+
fileDeadModelPersistence(path26__default.default.join(workspacePath, ".cascade", "dead-models.json"))
|
|
16281
17045
|
));
|
|
16282
17046
|
} catch {
|
|
16283
17047
|
}
|
|
17048
|
+
try {
|
|
17049
|
+
this.resumeStore = new ResumeStore({
|
|
17050
|
+
dir: path26__default.default.join(workspacePath, ".cascade", "resume")
|
|
17051
|
+
});
|
|
17052
|
+
} catch {
|
|
17053
|
+
}
|
|
16284
17054
|
this.multimodal = new MultimodalRegistry(
|
|
16285
17055
|
(this.config.providers ?? []).map((p) => p.type)
|
|
16286
17056
|
);
|
|
@@ -16365,8 +17135,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
16365
17135
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
16366
17136
|
return;
|
|
16367
17137
|
}
|
|
16368
|
-
const dbPath = ci.dbPath ||
|
|
16369
|
-
|
|
17138
|
+
const dbPath = ci.dbPath || path26__default.default.join(this.workspacePath, ".cascade", "code-index.db");
|
|
17139
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
|
|
16370
17140
|
const db = new Database__default.default(dbPath);
|
|
16371
17141
|
db.pragma("journal_mode = WAL");
|
|
16372
17142
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -16746,10 +17516,57 @@ ${input}` }],
|
|
|
16746
17516
|
setFeedbackSource(source) {
|
|
16747
17517
|
this.router.setFeedbackSource(source);
|
|
16748
17518
|
}
|
|
16749
|
-
/**
|
|
17519
|
+
/**
|
|
17520
|
+
* Write a durable checkpoint for an interrupted run.
|
|
17521
|
+
*
|
|
17522
|
+
* Best-effort throughout: a run is already ending badly on every path that
|
|
17523
|
+
* calls this, and failing to record a resume file must not make that ending
|
|
17524
|
+
* worse. Nothing here is allowed to throw.
|
|
17525
|
+
*/
|
|
17526
|
+
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
17527
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
17528
|
+
const merged = /* @__PURE__ */ new Map();
|
|
17529
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
17530
|
+
for (const node of this.currentRunCompleted) {
|
|
17531
|
+
const existing = merged.get(node.id);
|
|
17532
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
17533
|
+
merged.set(node.id, node);
|
|
17534
|
+
}
|
|
17535
|
+
const completed = [...merged.values()];
|
|
17536
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
17537
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
17538
|
+
if (!completed.length && !carriedPartial) return false;
|
|
17539
|
+
try {
|
|
17540
|
+
const saved = await this.resumeStore?.save({
|
|
17541
|
+
taskId,
|
|
17542
|
+
prompt: canonicalPrompt,
|
|
17543
|
+
reason,
|
|
17544
|
+
...detail ? { detail } : {},
|
|
17545
|
+
partialOutput: carriedPartial,
|
|
17546
|
+
completed
|
|
17547
|
+
});
|
|
17548
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
17549
|
+
return saved === true;
|
|
17550
|
+
} catch {
|
|
17551
|
+
return false;
|
|
17552
|
+
}
|
|
17553
|
+
}
|
|
17554
|
+
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
16750
17555
|
hasResumableRun() {
|
|
16751
17556
|
return this.lastInterruptedRun != null;
|
|
16752
17557
|
}
|
|
17558
|
+
/**
|
|
17559
|
+
* True when a checkpoint from ANY process is resumable. Async because the
|
|
17560
|
+
* durable check touches disk; `hasResumableRun` remains the sync in-memory
|
|
17561
|
+
* answer for callers already on a hot path.
|
|
17562
|
+
*/
|
|
17563
|
+
async hasDurableResume() {
|
|
17564
|
+
return await this.resumeStore?.latest() != null;
|
|
17565
|
+
}
|
|
17566
|
+
/** Interrupted runs available to resume, newest first. */
|
|
17567
|
+
async listResumableRuns() {
|
|
17568
|
+
return await this.resumeStore?.list() ?? [];
|
|
17569
|
+
}
|
|
16753
17570
|
/**
|
|
16754
17571
|
* Raise the per-run token budget for a resume and return the continuation
|
|
16755
17572
|
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
@@ -16760,24 +17577,105 @@ ${input}` }],
|
|
|
16760
17577
|
const last = this.lastInterruptedRun;
|
|
16761
17578
|
if (!last) return null;
|
|
16762
17579
|
this.lastInterruptedRun = void 0;
|
|
16763
|
-
|
|
17580
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17581
|
+
return this.buildResumePrompt(last.prompt, last.partialOutput, []);
|
|
17582
|
+
}
|
|
17583
|
+
/**
|
|
17584
|
+
* Durable counterpart to prepareResume: recovers a run interrupted in ANY
|
|
17585
|
+
* process, including one that crashed before it could return. Consumes the
|
|
17586
|
+
* checkpoint, so the same finished work is never restored into two runs.
|
|
17587
|
+
*/
|
|
17588
|
+
async prepareDurableResume(opts = {}) {
|
|
17589
|
+
await this.resumeStore?.reclaimStale();
|
|
17590
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
17591
|
+
if (!claimed) return null;
|
|
17592
|
+
try {
|
|
17593
|
+
this.lastInterruptedRun = void 0;
|
|
17594
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
17595
|
+
const prompt = this.buildResumePrompt(
|
|
17596
|
+
claimed.checkpoint.prompt,
|
|
17597
|
+
claimed.checkpoint.partialOutput,
|
|
17598
|
+
claimed.checkpoint.completed
|
|
17599
|
+
);
|
|
17600
|
+
this.resumedFrom = {
|
|
17601
|
+
prompt: claimed.checkpoint.prompt,
|
|
17602
|
+
completed: claimed.checkpoint.completed,
|
|
17603
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
17604
|
+
};
|
|
17605
|
+
this.claimedResumeId = claimed.claimId;
|
|
17606
|
+
return prompt;
|
|
17607
|
+
} catch (err) {
|
|
17608
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
17609
|
+
throw err;
|
|
17610
|
+
}
|
|
17611
|
+
}
|
|
17612
|
+
/**
|
|
17613
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
17614
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
17615
|
+
* run loop; safe to call when nothing is claimed.
|
|
17616
|
+
*/
|
|
17617
|
+
async settleClaimedResume(succeeded) {
|
|
17618
|
+
const claimId = this.claimedResumeId;
|
|
17619
|
+
if (!claimId) return;
|
|
17620
|
+
this.claimedResumeId = void 0;
|
|
17621
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
17622
|
+
await this.resumeStore?.settle(claimId);
|
|
17623
|
+
} else {
|
|
17624
|
+
await this.resumeStore?.release(claimId);
|
|
17625
|
+
}
|
|
17626
|
+
}
|
|
17627
|
+
/**
|
|
17628
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
17629
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
17630
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
17631
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
17632
|
+
*/
|
|
17633
|
+
async releaseClaimedResume() {
|
|
17634
|
+
const claimId = this.claimedResumeId;
|
|
17635
|
+
if (!claimId) return;
|
|
17636
|
+
this.claimedResumeId = void 0;
|
|
17637
|
+
await this.resumeStore?.release(claimId);
|
|
17638
|
+
}
|
|
17639
|
+
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
17640
|
+
raiseBudgetForResume(maxTokens) {
|
|
17641
|
+
const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
16764
17642
|
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
16765
17643
|
this.router.setMaxTokensPerRun(raised);
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
17644
|
+
}
|
|
17645
|
+
/**
|
|
17646
|
+
* The resume prompt. Completed sections are named explicitly rather than
|
|
17647
|
+
* gestured at: "do not recreate the files" is an instruction a planner can
|
|
17648
|
+
* only guess at, whereas a list of what is already done, and what it produced,
|
|
17649
|
+
* is something it can act on. That difference is the entire reason node
|
|
17650
|
+
* results are kept — a re-plan that cannot see the finished work will plan the
|
|
17651
|
+
* finished work again, and the user pays for it a second time.
|
|
17652
|
+
*/
|
|
17653
|
+
buildResumePrompt(prompt, partialOutput, completed) {
|
|
17654
|
+
const done = summarizeCompleted(completed);
|
|
17655
|
+
return [
|
|
17656
|
+
"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.",
|
|
17657
|
+
"",
|
|
17658
|
+
`Original task: ${prompt}`,
|
|
17659
|
+
done ? `
|
|
17660
|
+
${done}` : "",
|
|
17661
|
+
partialOutput ? `
|
|
16770
17662
|
Partial result so far:
|
|
16771
|
-
${
|
|
17663
|
+
${partialOutput}` : ""
|
|
17664
|
+
].filter(Boolean).join("\n");
|
|
16772
17665
|
}
|
|
16773
17666
|
/**
|
|
16774
17667
|
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
16775
17668
|
* Returns null when there is nothing to resume.
|
|
16776
17669
|
*/
|
|
16777
17670
|
async resumeRun(opts = {}) {
|
|
16778
|
-
const prompt = this.prepareResume(opts);
|
|
17671
|
+
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
16779
17672
|
if (!prompt) return null;
|
|
16780
|
-
|
|
17673
|
+
try {
|
|
17674
|
+
return await this.run({ prompt });
|
|
17675
|
+
} catch (err) {
|
|
17676
|
+
await this.releaseClaimedResume();
|
|
17677
|
+
throw err;
|
|
17678
|
+
}
|
|
16781
17679
|
}
|
|
16782
17680
|
getWorkspacePath() {
|
|
16783
17681
|
return this.workspacePath;
|
|
@@ -17146,6 +18044,10 @@ ${prompt}` : prompt;
|
|
|
17146
18044
|
const startMs = Date.now();
|
|
17147
18045
|
const taskId = crypto4.randomUUID();
|
|
17148
18046
|
this.decisionLog = [];
|
|
18047
|
+
this.currentRunCompleted = [];
|
|
18048
|
+
this.wroteCheckpointThisRun = false;
|
|
18049
|
+
this.completedNormally = false;
|
|
18050
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
17149
18051
|
if (options.fastAnswer) {
|
|
17150
18052
|
return this.runFastAnswer(options, startMs, taskId);
|
|
17151
18053
|
}
|
|
@@ -17260,6 +18162,11 @@ ${prompt}` : prompt;
|
|
|
17260
18162
|
});
|
|
17261
18163
|
tier.on("log", (e) => this.emit("log", e));
|
|
17262
18164
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
18165
|
+
tier.on("section:complete", (e) => {
|
|
18166
|
+
if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
|
|
18167
|
+
this.currentRunCompleted.push(e);
|
|
18168
|
+
this.emit("section:complete", e);
|
|
18169
|
+
});
|
|
17263
18170
|
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
17264
18171
|
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
17265
18172
|
tier.on("tool:approval-request", async (request) => {
|
|
@@ -17406,6 +18313,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17406
18313
|
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
17407
18314
|
const trip = runBreaker.reason();
|
|
17408
18315
|
if (trip) {
|
|
18316
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
|
|
17409
18317
|
finalOutput = [
|
|
17410
18318
|
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
17411
18319
|
"",
|
|
@@ -17418,6 +18326,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
17418
18326
|
${finalOutput}` : ""
|
|
17419
18327
|
].join("\n").trimEnd();
|
|
17420
18328
|
}
|
|
18329
|
+
this.completedNormally = trip == null;
|
|
17421
18330
|
} catch (err) {
|
|
17422
18331
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
17423
18332
|
this.emit("run:cancelled", {
|
|
@@ -17425,6 +18334,13 @@ ${finalOutput}` : ""
|
|
|
17425
18334
|
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
17426
18335
|
partialOutput: finalOutput || ""
|
|
17427
18336
|
});
|
|
18337
|
+
await this.checkpointRun(
|
|
18338
|
+
taskId,
|
|
18339
|
+
options.prompt,
|
|
18340
|
+
finalOutput || "",
|
|
18341
|
+
"cancelled",
|
|
18342
|
+
err instanceof Error ? err.message : "Task cancelled"
|
|
18343
|
+
);
|
|
17428
18344
|
runError = null;
|
|
17429
18345
|
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
17430
18346
|
this.emit("run:budget-exceeded", {
|
|
@@ -17433,9 +18349,17 @@ ${finalOutput}` : ""
|
|
|
17433
18349
|
partialOutput: finalOutput || ""
|
|
17434
18350
|
});
|
|
17435
18351
|
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
18352
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
|
|
17436
18353
|
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
17437
18354
|
runError = null;
|
|
17438
18355
|
} else {
|
|
18356
|
+
await this.checkpointRun(
|
|
18357
|
+
taskId,
|
|
18358
|
+
options.prompt,
|
|
18359
|
+
finalOutput || "",
|
|
18360
|
+
"error",
|
|
18361
|
+
err instanceof Error ? err.message : String(err)
|
|
18362
|
+
);
|
|
17439
18363
|
runError = err;
|
|
17440
18364
|
throw err;
|
|
17441
18365
|
}
|
|
@@ -17444,6 +18368,10 @@ ${finalOutput}` : ""
|
|
|
17444
18368
|
escalator.cancelAllPending();
|
|
17445
18369
|
} catch {
|
|
17446
18370
|
}
|
|
18371
|
+
try {
|
|
18372
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
18373
|
+
} catch {
|
|
18374
|
+
}
|
|
17447
18375
|
try {
|
|
17448
18376
|
this.releasePendingEscalations();
|
|
17449
18377
|
} catch {
|
|
@@ -17683,7 +18611,7 @@ var Keystore = class {
|
|
|
17683
18611
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
17684
18612
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
17685
18613
|
this.backend = "keytar";
|
|
17686
|
-
if (password &&
|
|
18614
|
+
if (password && fs25__default.default.existsSync(this.storePath)) {
|
|
17687
18615
|
try {
|
|
17688
18616
|
const fileEntries = this.decryptFile(password);
|
|
17689
18617
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -17702,7 +18630,7 @@ var Keystore = class {
|
|
|
17702
18630
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
17703
18631
|
);
|
|
17704
18632
|
}
|
|
17705
|
-
if (!
|
|
18633
|
+
if (!fs25__default.default.existsSync(this.storePath)) {
|
|
17706
18634
|
const salt = crypto4__default.default.randomBytes(SALT_LEN);
|
|
17707
18635
|
this.masterKey = this.deriveKey(password, salt);
|
|
17708
18636
|
this.writeWithSalt({}, salt);
|
|
@@ -17716,7 +18644,7 @@ var Keystore = class {
|
|
|
17716
18644
|
}
|
|
17717
18645
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
17718
18646
|
unlockSync(password) {
|
|
17719
|
-
if (!
|
|
18647
|
+
if (!fs25__default.default.existsSync(this.storePath)) {
|
|
17720
18648
|
const salt = crypto4__default.default.randomBytes(SALT_LEN);
|
|
17721
18649
|
this.masterKey = this.deriveKey(password, salt);
|
|
17722
18650
|
this.writeWithSalt({}, salt);
|
|
@@ -17774,7 +18702,7 @@ var Keystore = class {
|
|
|
17774
18702
|
}
|
|
17775
18703
|
}
|
|
17776
18704
|
decryptFile(password, knownSalt) {
|
|
17777
|
-
if (!
|
|
18705
|
+
if (!fs25__default.default.existsSync(this.storePath)) return {};
|
|
17778
18706
|
try {
|
|
17779
18707
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
17780
18708
|
const useSalt = knownSalt ?? salt;
|
|
@@ -17796,8 +18724,8 @@ var Keystore = class {
|
|
|
17796
18724
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
17797
18725
|
const tag = cipher.getAuthTag();
|
|
17798
18726
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
17799
|
-
|
|
17800
|
-
|
|
18727
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
|
|
18728
|
+
fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
17801
18729
|
}
|
|
17802
18730
|
writeWithSalt(data, salt) {
|
|
17803
18731
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -17807,11 +18735,11 @@ var Keystore = class {
|
|
|
17807
18735
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
17808
18736
|
const tag = cipher.getAuthTag();
|
|
17809
18737
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
17810
|
-
|
|
17811
|
-
|
|
18738
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
|
|
18739
|
+
fs25__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
17812
18740
|
}
|
|
17813
18741
|
readRaw() {
|
|
17814
|
-
const buf =
|
|
18742
|
+
const buf = fs25__default.default.readFileSync(this.storePath);
|
|
17815
18743
|
let offset = 0;
|
|
17816
18744
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
17817
18745
|
offset += SALT_LEN;
|
|
@@ -17827,7 +18755,7 @@ var Keystore = class {
|
|
|
17827
18755
|
}
|
|
17828
18756
|
};
|
|
17829
18757
|
async function loadCascadeMd(workspacePath) {
|
|
17830
|
-
const filePath =
|
|
18758
|
+
const filePath = path26__default.default.join(workspacePath, "CASCADE.md");
|
|
17831
18759
|
try {
|
|
17832
18760
|
const raw = await fs5__default.default.readFile(filePath, "utf-8");
|
|
17833
18761
|
return parseCascadeMd(raw);
|
|
@@ -17858,7 +18786,7 @@ ${raw.trim()}`;
|
|
|
17858
18786
|
var MemoryStore = class _MemoryStore {
|
|
17859
18787
|
db;
|
|
17860
18788
|
constructor(dbPath) {
|
|
17861
|
-
|
|
18789
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
|
|
17862
18790
|
try {
|
|
17863
18791
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
17864
18792
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -18694,7 +19622,7 @@ Original error: ${err.message}`
|
|
|
18694
19622
|
}
|
|
18695
19623
|
};
|
|
18696
19624
|
function credentialsPath(globalDir2) {
|
|
18697
|
-
return
|
|
19625
|
+
return path26__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
18698
19626
|
}
|
|
18699
19627
|
function providerKey(p) {
|
|
18700
19628
|
if (p.type === "azure") {
|
|
@@ -18707,7 +19635,7 @@ function isPersistable(p) {
|
|
|
18707
19635
|
}
|
|
18708
19636
|
function loadGlobalCredentials(globalDir2) {
|
|
18709
19637
|
try {
|
|
18710
|
-
const raw =
|
|
19638
|
+
const raw = fs25__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
18711
19639
|
const parsed = JSON.parse(raw);
|
|
18712
19640
|
if (!Array.isArray(parsed.providers)) return [];
|
|
18713
19641
|
return parsed.providers.filter(
|
|
@@ -18719,13 +19647,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
18719
19647
|
}
|
|
18720
19648
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
18721
19649
|
const filePath = credentialsPath(globalDir2);
|
|
18722
|
-
|
|
19650
|
+
fs25__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
18723
19651
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
18724
19652
|
const tmp = `${filePath}.tmp`;
|
|
18725
|
-
|
|
18726
|
-
|
|
19653
|
+
fs25__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
19654
|
+
fs25__default.default.renameSync(tmp, filePath);
|
|
18727
19655
|
try {
|
|
18728
|
-
|
|
19656
|
+
fs25__default.default.chmodSync(filePath, 384);
|
|
18729
19657
|
} catch {
|
|
18730
19658
|
}
|
|
18731
19659
|
}
|
|
@@ -18760,7 +19688,7 @@ var ConfigManager = class {
|
|
|
18760
19688
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
18761
19689
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
18762
19690
|
this.workspacePath = workspacePath;
|
|
18763
|
-
this.globalDir = globalDirOverride ??
|
|
19691
|
+
this.globalDir = globalDirOverride ?? path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
18764
19692
|
}
|
|
18765
19693
|
async load() {
|
|
18766
19694
|
this.config = await this.loadConfig();
|
|
@@ -18774,8 +19702,8 @@ var ConfigManager = class {
|
|
|
18774
19702
|
this.ignore = new CascadeIgnore();
|
|
18775
19703
|
await this.ignore.load(this.workspacePath);
|
|
18776
19704
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
18777
|
-
this.keystore = new Keystore(
|
|
18778
|
-
this.store = new MemoryStore(
|
|
19705
|
+
this.keystore = new Keystore(path26__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
19706
|
+
this.store = new MemoryStore(path26__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
18779
19707
|
await this.injectEnvKeys();
|
|
18780
19708
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
18781
19709
|
await this.ensureDefaultIdentity();
|
|
@@ -18800,8 +19728,8 @@ var ConfigManager = class {
|
|
|
18800
19728
|
return this.workspacePath;
|
|
18801
19729
|
}
|
|
18802
19730
|
async save() {
|
|
18803
|
-
const configPath =
|
|
18804
|
-
await fs5__default.default.mkdir(
|
|
19731
|
+
const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
19732
|
+
await fs5__default.default.mkdir(path26__default.default.dirname(configPath), { recursive: true });
|
|
18805
19733
|
await fs5__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
18806
19734
|
try {
|
|
18807
19735
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
@@ -18837,7 +19765,7 @@ var ConfigManager = class {
|
|
|
18837
19765
|
return configProvider?.apiKey;
|
|
18838
19766
|
}
|
|
18839
19767
|
async loadConfig() {
|
|
18840
|
-
const configPath =
|
|
19768
|
+
const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
18841
19769
|
try {
|
|
18842
19770
|
const raw = await fs5__default.default.readFile(configPath, "utf-8");
|
|
18843
19771
|
return validateConfig(JSON.parse(raw));
|
|
@@ -19072,8 +20000,14 @@ var DashboardSocket = class {
|
|
|
19072
20000
|
emitCascadeEvent(ev) {
|
|
19073
20001
|
this.io.emit("cascade:event", ev);
|
|
19074
20002
|
}
|
|
19075
|
-
|
|
19076
|
-
|
|
20003
|
+
/**
|
|
20004
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
20005
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
20006
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
20007
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
20008
|
+
*/
|
|
20009
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
20010
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
19077
20011
|
this.io.emit("tier:status", payload);
|
|
19078
20012
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
19079
20013
|
}
|
|
@@ -19369,7 +20303,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
19369
20303
|
}
|
|
19370
20304
|
|
|
19371
20305
|
// src/dashboard/server.ts
|
|
19372
|
-
var __dirname$1 =
|
|
20306
|
+
var __dirname$1 = path26__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
19373
20307
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
19374
20308
|
var DashboardServer = class {
|
|
19375
20309
|
app;
|
|
@@ -19649,14 +20583,14 @@ var DashboardServer = class {
|
|
|
19649
20583
|
*/
|
|
19650
20584
|
persistConfig() {
|
|
19651
20585
|
try {
|
|
19652
|
-
const configPath =
|
|
19653
|
-
|
|
19654
|
-
|
|
20586
|
+
const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
20587
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(configPath), { recursive: true });
|
|
20588
|
+
fs25__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
19655
20589
|
} catch (err) {
|
|
19656
20590
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
19657
20591
|
}
|
|
19658
20592
|
try {
|
|
19659
|
-
saveGlobalCredentials(
|
|
20593
|
+
saveGlobalCredentials(path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
19660
20594
|
} catch (err) {
|
|
19661
20595
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
19662
20596
|
}
|
|
@@ -19671,15 +20605,15 @@ var DashboardServer = class {
|
|
|
19671
20605
|
resolveDashboardSecret() {
|
|
19672
20606
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
19673
20607
|
if (fromConfig) return fromConfig;
|
|
19674
|
-
const secretPath =
|
|
20608
|
+
const secretPath = path26__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
19675
20609
|
try {
|
|
19676
|
-
if (
|
|
19677
|
-
const existing =
|
|
20610
|
+
if (fs25__default.default.existsSync(secretPath)) {
|
|
20611
|
+
const existing = fs25__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
19678
20612
|
if (existing.length >= 16) return existing;
|
|
19679
20613
|
}
|
|
19680
20614
|
const generated = crypto4.randomUUID();
|
|
19681
|
-
|
|
19682
|
-
|
|
20615
|
+
fs25__default.default.mkdirSync(path26__default.default.dirname(secretPath), { recursive: true });
|
|
20616
|
+
fs25__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
19683
20617
|
if (this.config.dashboard.auth) {
|
|
19684
20618
|
console.warn(
|
|
19685
20619
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -19706,7 +20640,7 @@ var DashboardServer = class {
|
|
|
19706
20640
|
// ── Setup ─────────────────────────────────────
|
|
19707
20641
|
getGlobalStore() {
|
|
19708
20642
|
if (!this.globalStore) {
|
|
19709
|
-
const globalDbPath =
|
|
20643
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
19710
20644
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
19711
20645
|
}
|
|
19712
20646
|
return this.globalStore;
|
|
@@ -20086,12 +21020,12 @@ ${prompt}`;
|
|
|
20086
21020
|
}
|
|
20087
21021
|
}
|
|
20088
21022
|
watchRuntimeChanges() {
|
|
20089
|
-
const workspaceDbPath =
|
|
20090
|
-
const globalDbPath =
|
|
21023
|
+
const workspaceDbPath = path26__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
21024
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20091
21025
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
20092
21026
|
for (const watchPath of watchPaths) {
|
|
20093
|
-
if (!
|
|
20094
|
-
|
|
21027
|
+
if (!fs25__default.default.existsSync(watchPath)) continue;
|
|
21028
|
+
fs25__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
20095
21029
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
20096
21030
|
});
|
|
20097
21031
|
}
|
|
@@ -20213,7 +21147,7 @@ ${prompt}`;
|
|
|
20213
21147
|
const sessionId = req.params.id;
|
|
20214
21148
|
this.store.deleteSession(sessionId);
|
|
20215
21149
|
this.store.deleteRuntimeSession(sessionId);
|
|
20216
|
-
const globalDbPath =
|
|
21150
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20217
21151
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20218
21152
|
try {
|
|
20219
21153
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -20306,7 +21240,7 @@ ${prompt}`;
|
|
|
20306
21240
|
});
|
|
20307
21241
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
20308
21242
|
const body = req.body;
|
|
20309
|
-
const globalDbPath =
|
|
21243
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20310
21244
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
20311
21245
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20312
21246
|
try {
|
|
@@ -20329,7 +21263,7 @@ ${prompt}`;
|
|
|
20329
21263
|
});
|
|
20330
21264
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
20331
21265
|
this.store.deleteAllRuntimeNodes();
|
|
20332
|
-
const globalDbPath =
|
|
21266
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20333
21267
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20334
21268
|
try {
|
|
20335
21269
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -20415,12 +21349,12 @@ ${prompt}`;
|
|
|
20415
21349
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
20416
21350
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
20417
21351
|
try {
|
|
20418
|
-
const configPath =
|
|
20419
|
-
const existing =
|
|
21352
|
+
const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
21353
|
+
const existing = fs25__default.default.existsSync(configPath) ? JSON.parse(fs25__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
20420
21354
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
20421
21355
|
const tmp = configPath + ".tmp";
|
|
20422
|
-
|
|
20423
|
-
|
|
21356
|
+
fs25__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
21357
|
+
fs25__default.default.renameSync(tmp, configPath);
|
|
20424
21358
|
res.json({ ok: true });
|
|
20425
21359
|
} catch (err) {
|
|
20426
21360
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -20448,7 +21382,7 @@ ${prompt}`;
|
|
|
20448
21382
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
20449
21383
|
const scope = req.query["scope"] ?? "workspace";
|
|
20450
21384
|
if (scope === "global") {
|
|
20451
|
-
const globalDbPath =
|
|
21385
|
+
const globalDbPath = path26__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
20452
21386
|
const globalStore = new MemoryStore(globalDbPath);
|
|
20453
21387
|
try {
|
|
20454
21388
|
res.json({
|
|
@@ -20789,13 +21723,13 @@ ${prompt}`;
|
|
|
20789
21723
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
20790
21724
|
}
|
|
20791
21725
|
});
|
|
20792
|
-
const prodPath =
|
|
20793
|
-
const devPath =
|
|
20794
|
-
const webDistPath =
|
|
20795
|
-
if (
|
|
21726
|
+
const prodPath = path26__default.default.resolve(__dirname$1, "../web/dist");
|
|
21727
|
+
const devPath = path26__default.default.resolve(__dirname$1, "../../web/dist");
|
|
21728
|
+
const webDistPath = fs25__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
21729
|
+
if (fs25__default.default.existsSync(webDistPath)) {
|
|
20796
21730
|
this.app.use(express__default.default.static(webDistPath));
|
|
20797
21731
|
this.app.get("*", (_req, res) => {
|
|
20798
|
-
res.sendFile(
|
|
21732
|
+
res.sendFile(path26__default.default.join(webDistPath, "index.html"));
|
|
20799
21733
|
});
|
|
20800
21734
|
} else {
|
|
20801
21735
|
this.app.get("/", (_req, res) => {
|
|
@@ -20982,14 +21916,14 @@ function planRetrieval(ctx) {
|
|
|
20982
21916
|
}
|
|
20983
21917
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
20984
21918
|
function globalDir(homeDir = os6__default.default.homedir()) {
|
|
20985
|
-
return
|
|
21919
|
+
return path26__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
20986
21920
|
}
|
|
20987
21921
|
function cloudSessionPath(dir = globalDir()) {
|
|
20988
|
-
return
|
|
21922
|
+
return path26__default.default.join(dir, CLOUD_SESSION_FILE);
|
|
20989
21923
|
}
|
|
20990
21924
|
function loadCloudSession(dir = globalDir()) {
|
|
20991
21925
|
try {
|
|
20992
|
-
const raw =
|
|
21926
|
+
const raw = fs25__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
20993
21927
|
const parsed = JSON.parse(raw);
|
|
20994
21928
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
20995
21929
|
return parsed;
|
|
@@ -20998,17 +21932,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
20998
21932
|
}
|
|
20999
21933
|
}
|
|
21000
21934
|
function saveCloudSession(session, dir = globalDir()) {
|
|
21001
|
-
|
|
21935
|
+
fs25__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
21002
21936
|
const file = cloudSessionPath(dir);
|
|
21003
|
-
|
|
21937
|
+
fs25__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
21004
21938
|
try {
|
|
21005
|
-
|
|
21939
|
+
fs25__default.default.chmodSync(file, 384);
|
|
21006
21940
|
} catch {
|
|
21007
21941
|
}
|
|
21008
21942
|
}
|
|
21009
21943
|
function clearCloudSession(dir = globalDir()) {
|
|
21010
21944
|
try {
|
|
21011
|
-
|
|
21945
|
+
fs25__default.default.rmSync(cloudSessionPath(dir));
|
|
21012
21946
|
} catch {
|
|
21013
21947
|
}
|
|
21014
21948
|
}
|
|
@@ -21180,16 +22114,16 @@ var CloudClient = class _CloudClient {
|
|
|
21180
22114
|
return (await this.refresh()).accessToken;
|
|
21181
22115
|
}
|
|
21182
22116
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
21183
|
-
async authed(
|
|
21184
|
-
const call = (token) => fetch(this.url(
|
|
22117
|
+
async authed(path28, init = {}) {
|
|
22118
|
+
const call = (token) => fetch(this.url(path28), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
21185
22119
|
let res = await call(await this.accessToken());
|
|
21186
22120
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
21187
22121
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
21188
22122
|
const text = await res.text();
|
|
21189
22123
|
return text ? JSON.parse(text) : {};
|
|
21190
22124
|
}
|
|
21191
|
-
authedGet(
|
|
21192
|
-
return this.authed(
|
|
22125
|
+
authedGet(path28) {
|
|
22126
|
+
return this.authed(path28);
|
|
21193
22127
|
}
|
|
21194
22128
|
async fetchMe(token) {
|
|
21195
22129
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -21560,6 +22494,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
|
|
|
21560
22494
|
exports.DEFAULT_THEME = DEFAULT_THEME;
|
|
21561
22495
|
exports.DOCUMENT_MIME = DOCUMENT_MIME;
|
|
21562
22496
|
exports.DashboardServer = DashboardServer;
|
|
22497
|
+
exports.DependencyScheduler = DependencyScheduler;
|
|
21563
22498
|
exports.FileMcpOAuthStore = FileMcpOAuthStore;
|
|
21564
22499
|
exports.GITHUB_MODELS_API_VERSION = GITHUB_MODELS_API_VERSION;
|
|
21565
22500
|
exports.GITHUB_MODELS_CATALOG_URL = GITHUB_MODELS_CATALOG_URL;
|
|
@@ -21585,6 +22520,8 @@ exports.MemoryStore = MemoryStore;
|
|
|
21585
22520
|
exports.OLLAMA_BASE_URL = OLLAMA_BASE_URL;
|
|
21586
22521
|
exports.OpenAICompatibleEmbedder = OpenAICompatibleEmbedder;
|
|
21587
22522
|
exports.PROVIDER_DISPLAY_NAMES = PROVIDER_DISPLAY_NAMES;
|
|
22523
|
+
exports.RESUME_SCHEMA_VERSION = RESUME_SCHEMA_VERSION;
|
|
22524
|
+
exports.ResumeStore = ResumeStore;
|
|
21588
22525
|
exports.Retriever = Retriever;
|
|
21589
22526
|
exports.SqliteVectorStore = SqliteVectorStore;
|
|
21590
22527
|
exports.T1Administrator = T1Administrator;
|
|
@@ -21595,6 +22532,7 @@ exports.T3Worker = T3Worker;
|
|
|
21595
22532
|
exports.T3_MODEL_PRIORITY = T3_MODEL_PRIORITY;
|
|
21596
22533
|
exports.THEME_NAMES = THEME_NAMES;
|
|
21597
22534
|
exports.TOOL_NAMES = TOOL_NAMES;
|
|
22535
|
+
exports.TaskGraphValidationError = TaskGraphValidationError;
|
|
21598
22536
|
exports.TaskScheduler = TaskScheduler;
|
|
21599
22537
|
exports.Telemetry = Telemetry;
|
|
21600
22538
|
exports.ToolRegistry = ToolRegistry;
|
|
@@ -21613,6 +22551,9 @@ exports.chartToTableRows = chartToTableRows;
|
|
|
21613
22551
|
exports.chatCompleterFromProviders = chatCompleterFromProviders;
|
|
21614
22552
|
exports.chunkCode = chunkCode;
|
|
21615
22553
|
exports.chunkText = chunkText2;
|
|
22554
|
+
exports.compileSectionGraph = compileSectionGraph;
|
|
22555
|
+
exports.compileSubtaskGraph = compileSubtaskGraph;
|
|
22556
|
+
exports.compileTaskGraph = compileTaskGraph;
|
|
21616
22557
|
exports.completeMcpOAuth = completeMcpOAuth;
|
|
21617
22558
|
exports.connectMcpWithLoopbackOAuth = connectMcpWithLoopbackOAuth;
|
|
21618
22559
|
exports.createCascade = createCascade;
|
|
@@ -21624,7 +22565,9 @@ exports.discoverMcpTools = discoverMcpTools;
|
|
|
21624
22565
|
exports.distillSessionFacts = distillSessionFacts;
|
|
21625
22566
|
exports.embedderFromProviders = embedderFromProviders;
|
|
21626
22567
|
exports.encryptSyncBlob = encryptJSON;
|
|
22568
|
+
exports.evaluateAcceptance = evaluateAcceptance;
|
|
21627
22569
|
exports.extractCharts = extractCharts;
|
|
22570
|
+
exports.failures = failures;
|
|
21628
22571
|
exports.fileExt = fileExt;
|
|
21629
22572
|
exports.fileOAuthProvider = fileOAuthProvider;
|
|
21630
22573
|
exports.gatherSyncBundle = gatherSyncBundle;
|
|
@@ -21658,6 +22601,8 @@ exports.sniffImage = sniffImage;
|
|
|
21658
22601
|
exports.splitSlides = splitSlides;
|
|
21659
22602
|
exports.streamCascade = streamCascade;
|
|
21660
22603
|
exports.stripInline = stripInline;
|
|
22604
|
+
exports.summarizeCompleted = summarizeCompleted;
|
|
22605
|
+
exports.undecided = undecided;
|
|
21661
22606
|
exports.uniqueMcpServerName = uniqueMcpServerName;
|
|
21662
22607
|
//# sourceMappingURL=index.cjs.map
|
|
21663
22608
|
//# sourceMappingURL=index.cjs.map
|