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/cli.cjs
CHANGED
|
@@ -9,8 +9,8 @@ var https = require('https');
|
|
|
9
9
|
var zlib = require('zlib');
|
|
10
10
|
var stream = require('stream');
|
|
11
11
|
var Database2 = require('better-sqlite3');
|
|
12
|
-
var
|
|
13
|
-
var
|
|
12
|
+
var path34 = require('path');
|
|
13
|
+
var fs31 = require('fs');
|
|
14
14
|
var os10 = require('os');
|
|
15
15
|
var crypto = require('crypto');
|
|
16
16
|
var ink = require('ink');
|
|
@@ -77,8 +77,8 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
77
77
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
78
78
|
var zlib__default = /*#__PURE__*/_interopDefault(zlib);
|
|
79
79
|
var Database2__default = /*#__PURE__*/_interopDefault(Database2);
|
|
80
|
-
var
|
|
81
|
-
var
|
|
80
|
+
var path34__default = /*#__PURE__*/_interopDefault(path34);
|
|
81
|
+
var fs31__default = /*#__PURE__*/_interopDefault(fs31);
|
|
82
82
|
var os10__default = /*#__PURE__*/_interopDefault(os10);
|
|
83
83
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
84
84
|
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
@@ -1810,7 +1810,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1810
1810
|
var init_constants = __esm({
|
|
1811
1811
|
"src/constants.ts"() {
|
|
1812
1812
|
init_pricing();
|
|
1813
|
-
CASCADE_VERSION = "0.
|
|
1813
|
+
CASCADE_VERSION = "0.69.0";
|
|
1814
1814
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1815
1815
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1816
1816
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3932,12 +3932,12 @@ var init_audit_logger = __esm({
|
|
|
3932
3932
|
constructor(workspacePath, debugMode = false) {
|
|
3933
3933
|
this.workspacePath = workspacePath;
|
|
3934
3934
|
this.debugMode = debugMode;
|
|
3935
|
-
const cascadeDir =
|
|
3936
|
-
if (!
|
|
3937
|
-
|
|
3935
|
+
const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
|
|
3936
|
+
if (!fs31__default.default.existsSync(cascadeDir)) {
|
|
3937
|
+
fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
3938
3938
|
}
|
|
3939
|
-
this.keyPath =
|
|
3940
|
-
this.dbPath =
|
|
3939
|
+
this.keyPath = path34__default.default.join(cascadeDir, "audit_log.key");
|
|
3940
|
+
this.dbPath = path34__default.default.join(cascadeDir, "audit_log.db");
|
|
3941
3941
|
this.initEncryptionKey();
|
|
3942
3942
|
this.db = new Database2__default.default(this.dbPath);
|
|
3943
3943
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3967,11 +3967,11 @@ var init_audit_logger = __esm({
|
|
|
3967
3967
|
return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
3968
3968
|
}
|
|
3969
3969
|
initEncryptionKey() {
|
|
3970
|
-
if (
|
|
3971
|
-
this.encryptionKey =
|
|
3970
|
+
if (fs31__default.default.existsSync(this.keyPath)) {
|
|
3971
|
+
this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
|
|
3972
3972
|
} else {
|
|
3973
3973
|
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
3974
|
-
|
|
3974
|
+
fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
3975
3975
|
}
|
|
3976
3976
|
}
|
|
3977
3977
|
encrypt(text) {
|
|
@@ -4049,9 +4049,9 @@ var init_audit_logger = __esm({
|
|
|
4049
4049
|
dumpDebugIfNeeded() {
|
|
4050
4050
|
if (!this.debugMode) return;
|
|
4051
4051
|
try {
|
|
4052
|
-
const dumpPath =
|
|
4052
|
+
const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
4053
4053
|
const entries = this.getAllLogs();
|
|
4054
|
-
|
|
4054
|
+
fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
4055
4055
|
} catch (err) {
|
|
4056
4056
|
console.error("Failed to dump debug audit logs", err);
|
|
4057
4057
|
}
|
|
@@ -4106,7 +4106,7 @@ var Keystore = class {
|
|
|
4106
4106
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
4107
4107
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
4108
4108
|
this.backend = "keytar";
|
|
4109
|
-
if (password &&
|
|
4109
|
+
if (password && fs31__default.default.existsSync(this.storePath)) {
|
|
4110
4110
|
try {
|
|
4111
4111
|
const fileEntries = this.decryptFile(password);
|
|
4112
4112
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -4125,7 +4125,7 @@ var Keystore = class {
|
|
|
4125
4125
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
4126
4126
|
);
|
|
4127
4127
|
}
|
|
4128
|
-
if (!
|
|
4128
|
+
if (!fs31__default.default.existsSync(this.storePath)) {
|
|
4129
4129
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
4130
4130
|
this.masterKey = this.deriveKey(password, salt);
|
|
4131
4131
|
this.writeWithSalt({}, salt);
|
|
@@ -4139,7 +4139,7 @@ var Keystore = class {
|
|
|
4139
4139
|
}
|
|
4140
4140
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
4141
4141
|
unlockSync(password) {
|
|
4142
|
-
if (!
|
|
4142
|
+
if (!fs31__default.default.existsSync(this.storePath)) {
|
|
4143
4143
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
4144
4144
|
this.masterKey = this.deriveKey(password, salt);
|
|
4145
4145
|
this.writeWithSalt({}, salt);
|
|
@@ -4197,7 +4197,7 @@ var Keystore = class {
|
|
|
4197
4197
|
}
|
|
4198
4198
|
}
|
|
4199
4199
|
decryptFile(password, knownSalt) {
|
|
4200
|
-
if (!
|
|
4200
|
+
if (!fs31__default.default.existsSync(this.storePath)) return {};
|
|
4201
4201
|
try {
|
|
4202
4202
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
4203
4203
|
const useSalt = knownSalt ?? salt;
|
|
@@ -4219,8 +4219,8 @@ var Keystore = class {
|
|
|
4219
4219
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4220
4220
|
const tag = cipher.getAuthTag();
|
|
4221
4221
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
4222
|
-
|
|
4223
|
-
|
|
4222
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
|
|
4223
|
+
fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4224
4224
|
}
|
|
4225
4225
|
writeWithSalt(data, salt) {
|
|
4226
4226
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -4230,11 +4230,11 @@ var Keystore = class {
|
|
|
4230
4230
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4231
4231
|
const tag = cipher.getAuthTag();
|
|
4232
4232
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
4233
|
-
|
|
4234
|
-
|
|
4233
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
|
|
4234
|
+
fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4235
4235
|
}
|
|
4236
4236
|
readRaw() {
|
|
4237
|
-
const buf =
|
|
4237
|
+
const buf = fs31__default.default.readFileSync(this.storePath);
|
|
4238
4238
|
let offset = 0;
|
|
4239
4239
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
4240
4240
|
offset += SALT_LEN;
|
|
@@ -4267,7 +4267,7 @@ var CascadeIgnore = class {
|
|
|
4267
4267
|
]);
|
|
4268
4268
|
}
|
|
4269
4269
|
async load(workspacePath) {
|
|
4270
|
-
const filePath =
|
|
4270
|
+
const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
|
|
4271
4271
|
try {
|
|
4272
4272
|
const content = await fs11__default.default.readFile(filePath, "utf-8");
|
|
4273
4273
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -4278,7 +4278,7 @@ var CascadeIgnore = class {
|
|
|
4278
4278
|
}
|
|
4279
4279
|
isIgnored(filePath, workspacePath) {
|
|
4280
4280
|
try {
|
|
4281
|
-
const relative = workspacePath ?
|
|
4281
|
+
const relative = workspacePath ? path34__default.default.relative(workspacePath, filePath) : filePath;
|
|
4282
4282
|
return this.ig.ignores(relative);
|
|
4283
4283
|
} catch {
|
|
4284
4284
|
return false;
|
|
@@ -4289,7 +4289,7 @@ var CascadeIgnore = class {
|
|
|
4289
4289
|
}
|
|
4290
4290
|
};
|
|
4291
4291
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
4292
|
-
const filePath =
|
|
4292
|
+
const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
|
|
4293
4293
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
4294
4294
|
# Syntax identical to .gitignore
|
|
4295
4295
|
|
|
@@ -4319,7 +4319,7 @@ Thumbs.db
|
|
|
4319
4319
|
await fs11__default.default.writeFile(filePath, content, "utf-8");
|
|
4320
4320
|
}
|
|
4321
4321
|
async function loadCascadeMd(workspacePath) {
|
|
4322
|
-
const filePath =
|
|
4322
|
+
const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
|
|
4323
4323
|
try {
|
|
4324
4324
|
const raw = await fs11__default.default.readFile(filePath, "utf-8");
|
|
4325
4325
|
return parseCascadeMd(raw);
|
|
@@ -4348,7 +4348,7 @@ ${raw.trim()}`;
|
|
|
4348
4348
|
return { raw, sections, systemPrompt };
|
|
4349
4349
|
}
|
|
4350
4350
|
async function createDefaultCascadeMd(workspacePath) {
|
|
4351
|
-
const filePath =
|
|
4351
|
+
const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
|
|
4352
4352
|
const content = `# Cascade Project Instructions
|
|
4353
4353
|
|
|
4354
4354
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -4383,7 +4383,7 @@ List any areas the agent should not touch.
|
|
|
4383
4383
|
var MemoryStore = class _MemoryStore {
|
|
4384
4384
|
db;
|
|
4385
4385
|
constructor(dbPath) {
|
|
4386
|
-
|
|
4386
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
|
|
4387
4387
|
try {
|
|
4388
4388
|
this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
|
|
4389
4389
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -5635,7 +5635,7 @@ function validateConfig(raw) {
|
|
|
5635
5635
|
// src/config/global-credentials.ts
|
|
5636
5636
|
init_constants();
|
|
5637
5637
|
function credentialsPath(globalDir2) {
|
|
5638
|
-
return
|
|
5638
|
+
return path34__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
5639
5639
|
}
|
|
5640
5640
|
function providerKey(p) {
|
|
5641
5641
|
if (p.type === "azure") {
|
|
@@ -5648,7 +5648,7 @@ function isPersistable(p) {
|
|
|
5648
5648
|
}
|
|
5649
5649
|
function loadGlobalCredentials(globalDir2) {
|
|
5650
5650
|
try {
|
|
5651
|
-
const raw =
|
|
5651
|
+
const raw = fs31__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
5652
5652
|
const parsed = JSON.parse(raw);
|
|
5653
5653
|
if (!Array.isArray(parsed.providers)) return [];
|
|
5654
5654
|
return parsed.providers.filter(
|
|
@@ -5660,13 +5660,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
5660
5660
|
}
|
|
5661
5661
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
5662
5662
|
const filePath = credentialsPath(globalDir2);
|
|
5663
|
-
|
|
5663
|
+
fs31__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
5664
5664
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
5665
5665
|
const tmp = `${filePath}.tmp`;
|
|
5666
|
-
|
|
5667
|
-
|
|
5666
|
+
fs31__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5667
|
+
fs31__default.default.renameSync(tmp, filePath);
|
|
5668
5668
|
try {
|
|
5669
|
-
|
|
5669
|
+
fs31__default.default.chmodSync(filePath, 384);
|
|
5670
5670
|
} catch {
|
|
5671
5671
|
}
|
|
5672
5672
|
}
|
|
@@ -5884,7 +5884,7 @@ var ConfigManager = class {
|
|
|
5884
5884
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
5885
5885
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
5886
5886
|
this.workspacePath = workspacePath;
|
|
5887
|
-
this.globalDir = globalDirOverride ??
|
|
5887
|
+
this.globalDir = globalDirOverride ?? path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
5888
5888
|
}
|
|
5889
5889
|
async load() {
|
|
5890
5890
|
this.config = await this.loadConfig();
|
|
@@ -5898,8 +5898,8 @@ var ConfigManager = class {
|
|
|
5898
5898
|
this.ignore = new CascadeIgnore();
|
|
5899
5899
|
await this.ignore.load(this.workspacePath);
|
|
5900
5900
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
5901
|
-
this.keystore = new Keystore(
|
|
5902
|
-
this.store = new MemoryStore(
|
|
5901
|
+
this.keystore = new Keystore(path34__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5902
|
+
this.store = new MemoryStore(path34__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
5903
5903
|
await this.injectEnvKeys();
|
|
5904
5904
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5905
5905
|
await this.ensureDefaultIdentity();
|
|
@@ -5924,8 +5924,8 @@ var ConfigManager = class {
|
|
|
5924
5924
|
return this.workspacePath;
|
|
5925
5925
|
}
|
|
5926
5926
|
async save() {
|
|
5927
|
-
const configPath =
|
|
5928
|
-
await fs11__default.default.mkdir(
|
|
5927
|
+
const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5928
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
|
|
5929
5929
|
await fs11__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
5930
5930
|
try {
|
|
5931
5931
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
@@ -5961,7 +5961,7 @@ var ConfigManager = class {
|
|
|
5961
5961
|
return configProvider?.apiKey;
|
|
5962
5962
|
}
|
|
5963
5963
|
async loadConfig() {
|
|
5964
|
-
const configPath =
|
|
5964
|
+
const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5965
5965
|
try {
|
|
5966
5966
|
const raw = await fs11__default.default.readFile(configPath, "utf-8");
|
|
5967
5967
|
return validateConfig(JSON.parse(raw));
|
|
@@ -6963,13 +6963,13 @@ var DeadModelStore = class {
|
|
|
6963
6963
|
function fileDeadModelPersistence(filePath) {
|
|
6964
6964
|
return {
|
|
6965
6965
|
load() {
|
|
6966
|
-
if (!
|
|
6967
|
-
const raw = JSON.parse(
|
|
6966
|
+
if (!fs31.existsSync(filePath)) return [];
|
|
6967
|
+
const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
|
|
6968
6968
|
return Array.isArray(raw) ? raw : [];
|
|
6969
6969
|
},
|
|
6970
6970
|
save(records) {
|
|
6971
|
-
|
|
6972
|
-
|
|
6971
|
+
fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
|
|
6972
|
+
fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6973
6973
|
}
|
|
6974
6974
|
};
|
|
6975
6975
|
}
|
|
@@ -7128,7 +7128,7 @@ init_pricing();
|
|
|
7128
7128
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
7129
7129
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
7130
7130
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
7131
|
-
var DEFAULT_CACHE_FILE =
|
|
7131
|
+
var DEFAULT_CACHE_FILE = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
7132
7132
|
function normalizeModelId2(id) {
|
|
7133
7133
|
let s = id.toLowerCase();
|
|
7134
7134
|
const slash = s.lastIndexOf("/");
|
|
@@ -7275,7 +7275,7 @@ var LiveDataProvider = class {
|
|
|
7275
7275
|
}
|
|
7276
7276
|
async saveCache() {
|
|
7277
7277
|
try {
|
|
7278
|
-
await fs11__default.default.mkdir(
|
|
7278
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
7279
7279
|
const cache = {
|
|
7280
7280
|
fetchedAt: this.fetchedAt,
|
|
7281
7281
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -8535,6 +8535,21 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8535
8535
|
* which node (Cockpit node panel / Why panel).
|
|
8536
8536
|
*/
|
|
8537
8537
|
servingModel;
|
|
8538
|
+
/**
|
|
8539
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
8540
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
8541
|
+
*
|
|
8542
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
8543
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
8544
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
8545
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
8546
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
8547
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
8548
|
+
* appears in the stream.
|
|
8549
|
+
*/
|
|
8550
|
+
graphNodeId;
|
|
8551
|
+
graphDependsOn;
|
|
8552
|
+
graphWave;
|
|
8538
8553
|
constructor(role, id, parentId) {
|
|
8539
8554
|
super();
|
|
8540
8555
|
this.role = role;
|
|
@@ -8546,6 +8561,33 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8546
8561
|
setPresenter(on = true) {
|
|
8547
8562
|
this.isPresenter = on;
|
|
8548
8563
|
}
|
|
8564
|
+
/**
|
|
8565
|
+
* Record where this tier sits in the compiled task graph.
|
|
8566
|
+
*
|
|
8567
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
8568
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
8569
|
+
* are set independently rather than all at once.
|
|
8570
|
+
*/
|
|
8571
|
+
setGraphPosition(position) {
|
|
8572
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
8573
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
8574
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
8575
|
+
}
|
|
8576
|
+
/**
|
|
8577
|
+
* The graph fields, for spreading into a status payload.
|
|
8578
|
+
*
|
|
8579
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
8580
|
+
* two different call sites with two different payload shapes; adding a field
|
|
8581
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
8582
|
+
* consumer than never arriving at all.
|
|
8583
|
+
*/
|
|
8584
|
+
graphFields() {
|
|
8585
|
+
return {
|
|
8586
|
+
nodeId: this.graphNodeId,
|
|
8587
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
8588
|
+
waveId: this.graphWave
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8549
8591
|
getStatus() {
|
|
8550
8592
|
return this.status;
|
|
8551
8593
|
}
|
|
@@ -8560,7 +8602,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8560
8602
|
status,
|
|
8561
8603
|
timestamp,
|
|
8562
8604
|
output,
|
|
8563
|
-
model: this.servingModel
|
|
8605
|
+
model: this.servingModel,
|
|
8606
|
+
...this.graphFields()
|
|
8564
8607
|
};
|
|
8565
8608
|
this.emit("status", event);
|
|
8566
8609
|
this.emit("tier:status", event);
|
|
@@ -8600,7 +8643,8 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
8600
8643
|
progressPct: update.progressPct,
|
|
8601
8644
|
timestamp,
|
|
8602
8645
|
output: update.output,
|
|
8603
|
-
model: this.servingModel
|
|
8646
|
+
model: this.servingModel,
|
|
8647
|
+
...this.graphFields()
|
|
8604
8648
|
});
|
|
8605
8649
|
}
|
|
8606
8650
|
buildMessage(type, to, payload) {
|
|
@@ -9068,6 +9112,79 @@ function describeProviderError(c, modelId) {
|
|
|
9068
9112
|
}
|
|
9069
9113
|
}
|
|
9070
9114
|
|
|
9115
|
+
// src/core/verification/acceptance.ts
|
|
9116
|
+
var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
|
|
9117
|
+
var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
|
|
9118
|
+
var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
|
|
9119
|
+
var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
|
|
9120
|
+
var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
|
|
9121
|
+
function targetFile(criterion, ownedFiles) {
|
|
9122
|
+
const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
|
|
9123
|
+
if (mentioned.length > 1) return null;
|
|
9124
|
+
if (mentioned.length === 1) {
|
|
9125
|
+
const candidate = mentioned[0];
|
|
9126
|
+
return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
|
|
9127
|
+
}
|
|
9128
|
+
return ownedFiles.length === 1 ? ownedFiles[0] : null;
|
|
9129
|
+
}
|
|
9130
|
+
async function evaluateAcceptance(criteria, ownedFiles, probe) {
|
|
9131
|
+
const results = [];
|
|
9132
|
+
for (const criterion of criteria) {
|
|
9133
|
+
const text = criterion.trim();
|
|
9134
|
+
if (!text) {
|
|
9135
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9136
|
+
continue;
|
|
9137
|
+
}
|
|
9138
|
+
if (NEGATION.test(text)) {
|
|
9139
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9140
|
+
continue;
|
|
9141
|
+
}
|
|
9142
|
+
const needle = text.match(QUOTED_NEEDLE)?.[1];
|
|
9143
|
+
const wantsExists = EXISTS.test(text);
|
|
9144
|
+
const wantsNonEmpty = NON_EMPTY.test(text);
|
|
9145
|
+
if (!needle && !wantsExists && !wantsNonEmpty) {
|
|
9146
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9147
|
+
continue;
|
|
9148
|
+
}
|
|
9149
|
+
const file = targetFile(text, ownedFiles);
|
|
9150
|
+
if (!file) {
|
|
9151
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9152
|
+
continue;
|
|
9153
|
+
}
|
|
9154
|
+
const stat = await probe.stat(file);
|
|
9155
|
+
if (!stat) {
|
|
9156
|
+
results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
|
|
9157
|
+
continue;
|
|
9158
|
+
}
|
|
9159
|
+
if (wantsNonEmpty && stat.size === 0) {
|
|
9160
|
+
results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
|
|
9161
|
+
continue;
|
|
9162
|
+
}
|
|
9163
|
+
if (needle) {
|
|
9164
|
+
const content = await probe.read(file);
|
|
9165
|
+
if (content === null) {
|
|
9166
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9167
|
+
continue;
|
|
9168
|
+
}
|
|
9169
|
+
const found = content.includes(needle);
|
|
9170
|
+
results.push({
|
|
9171
|
+
criterion,
|
|
9172
|
+
verdict: found ? "passed" : "failed",
|
|
9173
|
+
detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
|
|
9174
|
+
});
|
|
9175
|
+
continue;
|
|
9176
|
+
}
|
|
9177
|
+
results.push({ criterion, verdict: "passed", detail: `${file} exists` });
|
|
9178
|
+
}
|
|
9179
|
+
return results;
|
|
9180
|
+
}
|
|
9181
|
+
function undecided(results) {
|
|
9182
|
+
return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
|
|
9183
|
+
}
|
|
9184
|
+
function failures(results) {
|
|
9185
|
+
return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9071
9188
|
// src/core/tiers/t3-worker.ts
|
|
9072
9189
|
var CriticalToolError = class extends Error {
|
|
9073
9190
|
constructor(message, toolName) {
|
|
@@ -9384,7 +9501,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
9384
9501
|
}
|
|
9385
9502
|
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
9386
9503
|
if (!artifactCheck.ok) {
|
|
9387
|
-
correctionAttempts
|
|
9504
|
+
correctionAttempts++;
|
|
9388
9505
|
issues.push(...artifactCheck.issues);
|
|
9389
9506
|
output = await this.correctOutput(output, artifactCheck.issues);
|
|
9390
9507
|
const retryArtifactCheck = await this.verifyArtifacts(assignment);
|
|
@@ -9402,22 +9519,45 @@ Now execute your subtask using this context where relevant.`
|
|
|
9402
9519
|
this.tools.map((t) => t.name)
|
|
9403
9520
|
);
|
|
9404
9521
|
if (visualIssue) {
|
|
9405
|
-
correctionAttempts
|
|
9522
|
+
correctionAttempts++;
|
|
9406
9523
|
issues.push(visualIssue);
|
|
9407
9524
|
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
9408
9525
|
output = await this.correctOutput(output, [visualIssue]);
|
|
9409
9526
|
}
|
|
9527
|
+
const acceptanceResults = await this.checkAcceptance(assignment);
|
|
9528
|
+
const acceptanceFailures = failures(acceptanceResults);
|
|
9529
|
+
if (acceptanceFailures.length > 0) {
|
|
9530
|
+
correctionAttempts++;
|
|
9531
|
+
issues.push(...acceptanceFailures);
|
|
9532
|
+
this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
|
|
9533
|
+
output = await this.correctOutput(output, acceptanceFailures);
|
|
9534
|
+
const recheck = failures(await this.checkAcceptance(assignment));
|
|
9535
|
+
if (recheck.length > 0) {
|
|
9536
|
+
issues.push(...recheck);
|
|
9537
|
+
checksRun.push(...acceptanceResults.map((r) => r.criterion));
|
|
9538
|
+
failed.push(...recheck);
|
|
9539
|
+
this.setStatus("FAILED");
|
|
9540
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
|
|
9541
|
+
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9542
|
+
}
|
|
9543
|
+
}
|
|
9544
|
+
for (const result2 of acceptanceResults) {
|
|
9545
|
+
if (result2.verdict === "undecidable") continue;
|
|
9546
|
+
checksRun.push(result2.criterion);
|
|
9547
|
+
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
9548
|
+
}
|
|
9410
9549
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9411
|
-
const
|
|
9550
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
9551
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9412
9552
|
checksRun.push(...testResult.checksRun);
|
|
9413
9553
|
passed.push(...testResult.passed);
|
|
9414
9554
|
failed.push(...testResult.failed);
|
|
9415
9555
|
if (testResult.failed.length > 0) {
|
|
9416
|
-
correctionAttempts
|
|
9556
|
+
correctionAttempts++;
|
|
9417
9557
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
9418
9558
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
9419
9559
|
output = corrected;
|
|
9420
|
-
const retest = await this.selfTest(assignment, output);
|
|
9560
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9421
9561
|
passed.push(...retest.passed);
|
|
9422
9562
|
if (retest.failed.length > 0) {
|
|
9423
9563
|
failed.push(...retest.failed);
|
|
@@ -9721,8 +9861,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9721
9861
|
// Media generation can run for a minute; without this a cancelled run
|
|
9722
9862
|
// still pays for an image nobody will see.
|
|
9723
9863
|
...this.signal ? { signal: this.signal } : {},
|
|
9724
|
-
saveSnapshot: async (
|
|
9725
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
9864
|
+
saveSnapshot: async (path39, content) => {
|
|
9865
|
+
this.store?.addFileSnapshot(this.taskId, path39, content);
|
|
9726
9866
|
},
|
|
9727
9867
|
sendPeerSync: (to, syncType, content) => {
|
|
9728
9868
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -9909,7 +10049,7 @@ ${assignment.expectedOutput}`;
|
|
|
9909
10049
|
const { promisify: promisify4 } = await import('util');
|
|
9910
10050
|
const execAsync3 = promisify4(exec3);
|
|
9911
10051
|
for (const artifactPath of artifactPaths) {
|
|
9912
|
-
const absolutePath =
|
|
10052
|
+
const absolutePath = path34__default.default.resolve(process.cwd(), artifactPath);
|
|
9913
10053
|
try {
|
|
9914
10054
|
const stat = await fs11__default.default.stat(absolutePath);
|
|
9915
10055
|
if (!stat.isFile()) {
|
|
@@ -9944,7 +10084,7 @@ ${assignment.expectedOutput}`;
|
|
|
9944
10084
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
9945
10085
|
continue;
|
|
9946
10086
|
}
|
|
9947
|
-
const ext =
|
|
10087
|
+
const ext = path34__default.default.extname(absolutePath).toLowerCase();
|
|
9948
10088
|
try {
|
|
9949
10089
|
if (ext === ".ts" || ext === ".tsx") {
|
|
9950
10090
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -10030,14 +10170,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
10030
10170
|
}
|
|
10031
10171
|
return current;
|
|
10032
10172
|
}
|
|
10033
|
-
|
|
10173
|
+
/**
|
|
10174
|
+
* Run the acceptance criteria that can be settled by looking at the workspace.
|
|
10175
|
+
* Reads go through the same workspace-relative resolution verifyArtifacts uses.
|
|
10176
|
+
*/
|
|
10177
|
+
async checkAcceptance(assignment) {
|
|
10178
|
+
const criteria = assignment.acceptance ?? [];
|
|
10179
|
+
if (!criteria.length) return [];
|
|
10180
|
+
const resolve2 = (target) => path34__default.default.resolve(process.cwd(), target);
|
|
10181
|
+
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
10182
|
+
stat: async (target) => {
|
|
10183
|
+
try {
|
|
10184
|
+
const stat = await fs11__default.default.stat(resolve2(target));
|
|
10185
|
+
return stat.isFile() ? { size: stat.size } : null;
|
|
10186
|
+
} catch {
|
|
10187
|
+
return null;
|
|
10188
|
+
}
|
|
10189
|
+
},
|
|
10190
|
+
read: async (target) => {
|
|
10191
|
+
try {
|
|
10192
|
+
const content = await fs11__default.default.readFile(resolve2(target), "utf-8");
|
|
10193
|
+
return content.includes("\uFFFD") ? null : content;
|
|
10194
|
+
} catch {
|
|
10195
|
+
return null;
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10198
|
+
});
|
|
10199
|
+
}
|
|
10200
|
+
async selfTest(assignment, output, pendingAcceptance) {
|
|
10201
|
+
const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
|
|
10034
10202
|
const prompt = `Self-test this output against the assignment requirements.
|
|
10035
10203
|
|
|
10036
10204
|
Assignment: ${assignment.description}
|
|
10037
10205
|
Expected output: ${assignment.expectedOutput}
|
|
10038
10206
|
Constraints: ${assignment.constraints.join("; ")}
|
|
10039
|
-
${
|
|
10040
|
-
${
|
|
10207
|
+
${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
10208
|
+
${acceptance.map((a) => `- ${a}`).join("\n")}
|
|
10041
10209
|
` : ""}
|
|
10042
10210
|
Output to test:
|
|
10043
10211
|
${output}
|
|
@@ -10104,8 +10272,8 @@ ${output.slice(0, 4e3)}`;
|
|
|
10104
10272
|
} catch {
|
|
10105
10273
|
}
|
|
10106
10274
|
}
|
|
10107
|
-
async correctOutput(originalOutput,
|
|
10108
|
-
const correctionPrompt = `The following output failed these checks: ${
|
|
10275
|
+
async correctOutput(originalOutput, failures2) {
|
|
10276
|
+
const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
|
|
10109
10277
|
|
|
10110
10278
|
Original output:
|
|
10111
10279
|
${originalOutput}
|
|
@@ -10767,12 +10935,220 @@ function describeGenerationForPlanner(has) {
|
|
|
10767
10935
|
return lines.join("\n");
|
|
10768
10936
|
}
|
|
10769
10937
|
|
|
10938
|
+
// src/core/orchestration/task-graph.ts
|
|
10939
|
+
var TaskGraphValidationError = class extends Error {
|
|
10940
|
+
constructor(issues) {
|
|
10941
|
+
super(issues.map((issue) => issue.message).join("; "));
|
|
10942
|
+
this.issues = issues;
|
|
10943
|
+
this.name = "TaskGraphValidationError";
|
|
10944
|
+
}
|
|
10945
|
+
issues;
|
|
10946
|
+
};
|
|
10947
|
+
function normalizeDependencies(input) {
|
|
10948
|
+
if (!input?.length) return [];
|
|
10949
|
+
return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
|
|
10950
|
+
}
|
|
10951
|
+
function cycleNodes(nodes) {
|
|
10952
|
+
const adjacency = new Map(
|
|
10953
|
+
nodes.map((node) => [node.id, node.dependsOn])
|
|
10954
|
+
);
|
|
10955
|
+
const index = /* @__PURE__ */ new Map();
|
|
10956
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
10957
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
10958
|
+
const stack = [];
|
|
10959
|
+
const inCycle = /* @__PURE__ */ new Set();
|
|
10960
|
+
let counter = 0;
|
|
10961
|
+
const open = (id) => {
|
|
10962
|
+
index.set(id, counter);
|
|
10963
|
+
lowlink.set(id, counter);
|
|
10964
|
+
counter++;
|
|
10965
|
+
stack.push(id);
|
|
10966
|
+
onStack.add(id);
|
|
10967
|
+
};
|
|
10968
|
+
for (const root of nodes) {
|
|
10969
|
+
if (index.has(root.id)) continue;
|
|
10970
|
+
open(root.id);
|
|
10971
|
+
const frames = [{ id: root.id, edge: 0 }];
|
|
10972
|
+
while (frames.length > 0) {
|
|
10973
|
+
const frame = frames[frames.length - 1];
|
|
10974
|
+
const neighbours = adjacency.get(frame.id) ?? [];
|
|
10975
|
+
if (frame.edge < neighbours.length) {
|
|
10976
|
+
const next = neighbours[frame.edge];
|
|
10977
|
+
frame.edge++;
|
|
10978
|
+
if (!adjacency.has(next)) continue;
|
|
10979
|
+
if (!index.has(next)) {
|
|
10980
|
+
open(next);
|
|
10981
|
+
frames.push({ id: next, edge: 0 });
|
|
10982
|
+
} else if (onStack.has(next)) {
|
|
10983
|
+
lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
|
|
10984
|
+
}
|
|
10985
|
+
continue;
|
|
10986
|
+
}
|
|
10987
|
+
frames.pop();
|
|
10988
|
+
const parent = frames[frames.length - 1];
|
|
10989
|
+
if (parent) {
|
|
10990
|
+
lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
|
|
10991
|
+
}
|
|
10992
|
+
if (lowlink.get(frame.id) === index.get(frame.id)) {
|
|
10993
|
+
const component = [];
|
|
10994
|
+
for (; ; ) {
|
|
10995
|
+
const member = stack.pop();
|
|
10996
|
+
onStack.delete(member);
|
|
10997
|
+
component.push(member);
|
|
10998
|
+
if (member === frame.id) break;
|
|
10999
|
+
}
|
|
11000
|
+
const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
11001
|
+
if (cyclic) for (const member of component) inCycle.add(member);
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
return inCycle;
|
|
11006
|
+
}
|
|
11007
|
+
function throwIfStrict(mode, issues) {
|
|
11008
|
+
if (mode === "strict" && issues.length > 0) {
|
|
11009
|
+
throw new TaskGraphValidationError(issues);
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
function compileTaskGraph(inputNodes, options = {}) {
|
|
11013
|
+
const mode = options.mode ?? "strict";
|
|
11014
|
+
const issues = [];
|
|
11015
|
+
if (inputNodes.length === 0) {
|
|
11016
|
+
issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
|
|
11017
|
+
throwIfStrict(mode, issues);
|
|
11018
|
+
}
|
|
11019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11020
|
+
const nodes = [];
|
|
11021
|
+
for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
|
|
11022
|
+
const input = inputNodes[ordinal];
|
|
11023
|
+
const id = input.id?.trim();
|
|
11024
|
+
const title = input.title?.trim();
|
|
11025
|
+
if (!id || !title) {
|
|
11026
|
+
issues.push({
|
|
11027
|
+
code: "INVALID_NODE",
|
|
11028
|
+
nodeId: id || void 0,
|
|
11029
|
+
message: `Task graph node at index ${ordinal} requires a non-empty id and title`
|
|
11030
|
+
});
|
|
11031
|
+
continue;
|
|
11032
|
+
}
|
|
11033
|
+
if (seen.has(id)) {
|
|
11034
|
+
issues.push({
|
|
11035
|
+
code: "DUPLICATE_NODE_ID",
|
|
11036
|
+
nodeId: id,
|
|
11037
|
+
message: `Duplicate task graph node id: ${id}`
|
|
11038
|
+
});
|
|
11039
|
+
continue;
|
|
11040
|
+
}
|
|
11041
|
+
seen.add(id);
|
|
11042
|
+
nodes.push({
|
|
11043
|
+
id,
|
|
11044
|
+
title,
|
|
11045
|
+
kind: input.kind,
|
|
11046
|
+
dependsOn: normalizeDependencies(input.dependsOn),
|
|
11047
|
+
payload: input.payload,
|
|
11048
|
+
ordinal
|
|
11049
|
+
});
|
|
11050
|
+
}
|
|
11051
|
+
throwIfStrict(mode, issues);
|
|
11052
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
11053
|
+
const sanitized = nodes.map((node) => {
|
|
11054
|
+
const dependsOn = [];
|
|
11055
|
+
for (const dependencyId of node.dependsOn) {
|
|
11056
|
+
if (dependencyId === node.id) {
|
|
11057
|
+
issues.push({
|
|
11058
|
+
code: "SELF_DEPENDENCY",
|
|
11059
|
+
nodeId: node.id,
|
|
11060
|
+
dependencyId,
|
|
11061
|
+
message: `Task graph node ${node.id} cannot depend on itself`
|
|
11062
|
+
});
|
|
11063
|
+
continue;
|
|
11064
|
+
}
|
|
11065
|
+
if (!ids.has(dependencyId)) {
|
|
11066
|
+
issues.push({
|
|
11067
|
+
code: "UNKNOWN_DEPENDENCY",
|
|
11068
|
+
nodeId: node.id,
|
|
11069
|
+
dependencyId,
|
|
11070
|
+
message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
|
|
11071
|
+
});
|
|
11072
|
+
continue;
|
|
11073
|
+
}
|
|
11074
|
+
dependsOn.push(dependencyId);
|
|
11075
|
+
}
|
|
11076
|
+
return { ...node, dependsOn };
|
|
11077
|
+
});
|
|
11078
|
+
throwIfStrict(mode, issues);
|
|
11079
|
+
const cyclic = cycleNodes(sanitized);
|
|
11080
|
+
let repaired = sanitized;
|
|
11081
|
+
if (cyclic.size > 0) {
|
|
11082
|
+
const cycleIssues = [];
|
|
11083
|
+
repaired = sanitized.map((node) => {
|
|
11084
|
+
if (!cyclic.has(node.id)) return node;
|
|
11085
|
+
const dependsOn = node.dependsOn.filter((dependencyId) => {
|
|
11086
|
+
if (!cyclic.has(dependencyId)) return true;
|
|
11087
|
+
cycleIssues.push({
|
|
11088
|
+
code: "CYCLE_EDGE_REMOVED",
|
|
11089
|
+
nodeId: node.id,
|
|
11090
|
+
dependencyId,
|
|
11091
|
+
message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
|
|
11092
|
+
});
|
|
11093
|
+
return false;
|
|
11094
|
+
});
|
|
11095
|
+
return { ...node, dependsOn };
|
|
11096
|
+
});
|
|
11097
|
+
issues.push(...cycleIssues);
|
|
11098
|
+
throwIfStrict(mode, cycleIssues);
|
|
11099
|
+
}
|
|
11100
|
+
const remainingCycle = cycleNodes(repaired);
|
|
11101
|
+
if (remainingCycle.size > 0) {
|
|
11102
|
+
throw new TaskGraphValidationError([
|
|
11103
|
+
{
|
|
11104
|
+
code: "INVALID_NODE",
|
|
11105
|
+
message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
|
|
11106
|
+
}
|
|
11107
|
+
]);
|
|
11108
|
+
}
|
|
11109
|
+
return {
|
|
11110
|
+
graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
|
|
11111
|
+
issues
|
|
11112
|
+
};
|
|
11113
|
+
}
|
|
11114
|
+
|
|
11115
|
+
// src/core/orchestration/adapters.ts
|
|
11116
|
+
function compileSectionGraph(sections) {
|
|
11117
|
+
return compileTaskGraph(
|
|
11118
|
+
sections.map((section) => ({
|
|
11119
|
+
id: section.sectionId,
|
|
11120
|
+
title: section.sectionTitle,
|
|
11121
|
+
kind: "section",
|
|
11122
|
+
dependsOn: section.dependsOn,
|
|
11123
|
+
payload: section
|
|
11124
|
+
})),
|
|
11125
|
+
{ mode: "repair" }
|
|
11126
|
+
);
|
|
11127
|
+
}
|
|
11128
|
+
function compileSubtaskGraph(subtasks) {
|
|
11129
|
+
return compileTaskGraph(
|
|
11130
|
+
subtasks.map((subtask) => ({
|
|
11131
|
+
id: subtask.subtaskId,
|
|
11132
|
+
title: subtask.subtaskTitle,
|
|
11133
|
+
kind: "subtask",
|
|
11134
|
+
dependsOn: subtask.dependsOn,
|
|
11135
|
+
payload: subtask
|
|
11136
|
+
})),
|
|
11137
|
+
{ mode: "repair" }
|
|
11138
|
+
);
|
|
11139
|
+
}
|
|
11140
|
+
|
|
10770
11141
|
// src/core/tiers/t2-manager.ts
|
|
10771
11142
|
function buildT2SystemPrompt(has) {
|
|
10772
11143
|
const generation = describeGenerationForPlanner(has);
|
|
10773
11144
|
return [
|
|
10774
11145
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
10775
|
-
|
|
11146
|
+
// Must agree with the decomposition prompt in decomposeSection(), which is
|
|
11147
|
+
// the one carrying the actual instruction. It said 1-4 and "the FEWEST that
|
|
11148
|
+
// fully cover it"; this said 2-5. The model received both, so its floor was
|
|
11149
|
+
// simultaneously one and two, and the cheaper reading — pad to two — costs a
|
|
11150
|
+
// real worker call on every small section.
|
|
11151
|
+
"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.",
|
|
10776
11152
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
10777
11153
|
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
10778
11154
|
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
@@ -11106,6 +11482,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11106
11482
|
for (const a of assignments) {
|
|
11107
11483
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
11108
11484
|
if (this.store) worker.setStore(this.store, taskId);
|
|
11485
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
11109
11486
|
worker.setPeerBus(this.t3PeerBus);
|
|
11110
11487
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
11111
11488
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -11168,21 +11545,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11168
11545
|
* then reduce in-degrees of their dependents and repeat.
|
|
11169
11546
|
*/
|
|
11170
11547
|
async runWithDependencies(assignments, workerMap, taskId) {
|
|
11548
|
+
const compiled = compileSubtaskGraph(assignments);
|
|
11549
|
+
for (const issue of compiled.issues) {
|
|
11550
|
+
this.log(`\u26A0 Subtask graph: ${issue.message}`);
|
|
11551
|
+
}
|
|
11552
|
+
const sanitizedAssignments = compiled.graph.nodes.map((node) => {
|
|
11553
|
+
const assignment = node.payload;
|
|
11554
|
+
const original = assignment.dependsOn ?? [];
|
|
11555
|
+
const repaired = node.dependsOn;
|
|
11556
|
+
const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
|
|
11557
|
+
return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
|
|
11558
|
+
});
|
|
11171
11559
|
const adj = /* @__PURE__ */ new Map();
|
|
11172
11560
|
const inDegree = /* @__PURE__ */ new Map();
|
|
11173
11561
|
const resultMap = /* @__PURE__ */ new Map();
|
|
11174
|
-
for (const a of
|
|
11562
|
+
for (const a of sanitizedAssignments) {
|
|
11175
11563
|
if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
|
|
11176
11564
|
inDegree.set(a.subtaskId, 0);
|
|
11177
11565
|
}
|
|
11178
|
-
for (const a of
|
|
11179
|
-
const
|
|
11180
|
-
for (const dep of deps) {
|
|
11566
|
+
for (const a of sanitizedAssignments) {
|
|
11567
|
+
for (const dep of a.dependsOn ?? []) {
|
|
11181
11568
|
adj.get(dep).add(a.subtaskId);
|
|
11182
11569
|
inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
|
|
11183
11570
|
}
|
|
11184
11571
|
}
|
|
11185
|
-
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
11186
11572
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
11187
11573
|
let wave = 0;
|
|
11188
11574
|
let respawnBudget = 1;
|
|
@@ -11217,6 +11603,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11217
11603
|
runnableIds.push(fallbackId);
|
|
11218
11604
|
}
|
|
11219
11605
|
wave++;
|
|
11606
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
11220
11607
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
11221
11608
|
this.sendStatusUpdate({
|
|
11222
11609
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -11389,49 +11776,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11389
11776
|
}
|
|
11390
11777
|
return [...resultMap.values()];
|
|
11391
11778
|
}
|
|
11392
|
-
/**
|
|
11393
|
-
* Detects cyclic dependencies using Kahn's algorithm and breaks them
|
|
11394
|
-
* by removing back-edges. Returns a sanitized copy of assignments.
|
|
11395
|
-
*
|
|
11396
|
-
* A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
|
|
11397
|
-
* meaning t3 will start without waiting for t1, preventing deadlock.
|
|
11398
|
-
*/
|
|
11399
|
-
breakCycles(assignments, adj, inDegree) {
|
|
11400
|
-
const degree = new Map(inDegree);
|
|
11401
|
-
const queue = [];
|
|
11402
|
-
const visited = /* @__PURE__ */ new Set();
|
|
11403
|
-
for (const [id, d] of degree) {
|
|
11404
|
-
if (d === 0) queue.push(id);
|
|
11405
|
-
}
|
|
11406
|
-
while (queue.length > 0) {
|
|
11407
|
-
const id = queue.shift();
|
|
11408
|
-
visited.add(id);
|
|
11409
|
-
for (const dep of adj.get(id) ?? []) {
|
|
11410
|
-
const newDeg = (degree.get(dep) ?? 1) - 1;
|
|
11411
|
-
degree.set(dep, newDeg);
|
|
11412
|
-
if (newDeg === 0) queue.push(dep);
|
|
11413
|
-
}
|
|
11414
|
-
}
|
|
11415
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
11416
|
-
if (cycleNodes.length === 0) return assignments;
|
|
11417
|
-
this.log(
|
|
11418
|
-
`\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
|
|
11419
|
-
);
|
|
11420
|
-
return assignments.map((a) => {
|
|
11421
|
-
if (!cycleNodes.includes(a.subtaskId)) return a;
|
|
11422
|
-
const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
11423
|
-
if (safeDeps.length !== (a.dependsOn ?? []).length) {
|
|
11424
|
-
this.log(
|
|
11425
|
-
` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
|
|
11426
|
-
);
|
|
11427
|
-
for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
11428
|
-
inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
|
|
11429
|
-
adj.get(removed)?.delete(a.subtaskId);
|
|
11430
|
-
}
|
|
11431
|
-
}
|
|
11432
|
-
return { ...a, dependsOn: safeDeps };
|
|
11433
|
-
});
|
|
11434
|
-
}
|
|
11435
11779
|
async retryT3(assignment, taskId) {
|
|
11436
11780
|
this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
|
|
11437
11781
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
@@ -11570,6 +11914,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11570
11914
|
// src/core/tiers/t1-administrator.ts
|
|
11571
11915
|
init_constants();
|
|
11572
11916
|
|
|
11917
|
+
// src/core/orchestration/scheduler.ts
|
|
11918
|
+
var DependencyScheduler = class {
|
|
11919
|
+
constructor(graph, hooks, options = {}) {
|
|
11920
|
+
this.graph = graph;
|
|
11921
|
+
this.hooks = hooks;
|
|
11922
|
+
this.options = options;
|
|
11923
|
+
}
|
|
11924
|
+
graph;
|
|
11925
|
+
hooks;
|
|
11926
|
+
options;
|
|
11927
|
+
async run() {
|
|
11928
|
+
const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
|
|
11929
|
+
const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
|
|
11930
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
11931
|
+
for (const node of nodes) dependents.set(node.id, []);
|
|
11932
|
+
for (const node of nodes) {
|
|
11933
|
+
for (const dependencyId of node.dependsOn) {
|
|
11934
|
+
dependents.get(dependencyId)?.push(node.id);
|
|
11935
|
+
}
|
|
11936
|
+
}
|
|
11937
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
11938
|
+
const settled = /* @__PURE__ */ new Set();
|
|
11939
|
+
const results = /* @__PURE__ */ new Map();
|
|
11940
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
11941
|
+
const waves = [];
|
|
11942
|
+
let wave = 0;
|
|
11943
|
+
const blockDescendants = async (failedIds) => {
|
|
11944
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
11945
|
+
const queue = [];
|
|
11946
|
+
for (const rootId of failedIds) {
|
|
11947
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
11948
|
+
}
|
|
11949
|
+
while (queue.length > 0) {
|
|
11950
|
+
const { id, root } = queue.shift();
|
|
11951
|
+
if (settled.has(id)) continue;
|
|
11952
|
+
const known = rootsFor.get(id);
|
|
11953
|
+
if (known) {
|
|
11954
|
+
if (!known.has(root)) known.add(root);
|
|
11955
|
+
continue;
|
|
11956
|
+
}
|
|
11957
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
11958
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
11959
|
+
}
|
|
11960
|
+
for (const node of nodes) {
|
|
11961
|
+
const roots = rootsFor.get(node.id);
|
|
11962
|
+
if (!roots || settled.has(node.id)) continue;
|
|
11963
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
11964
|
+
const blockedBy = causes.map((n) => n.id);
|
|
11965
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
11966
|
+
const record = {
|
|
11967
|
+
blockedBy,
|
|
11968
|
+
blockedByTitles,
|
|
11969
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
11970
|
+
};
|
|
11971
|
+
blocked.set(node.id, record);
|
|
11972
|
+
settled.add(node.id);
|
|
11973
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
11974
|
+
}
|
|
11975
|
+
};
|
|
11976
|
+
while (settled.size < nodes.length) {
|
|
11977
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
11978
|
+
if (ready.length === 0) {
|
|
11979
|
+
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
11980
|
+
}
|
|
11981
|
+
waves.push(ready.map((node) => node.id));
|
|
11982
|
+
await this.hooks.onWaveStart?.(ready, wave);
|
|
11983
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
11984
|
+
const runOne = async (node) => {
|
|
11985
|
+
const result = await this.hooks.execute(node);
|
|
11986
|
+
results.set(node.id, result);
|
|
11987
|
+
settled.add(node.id);
|
|
11988
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
11989
|
+
outcomes.set(node.id, outcome);
|
|
11990
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
11991
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
11992
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
11993
|
+
}
|
|
11994
|
+
}
|
|
11995
|
+
await this.hooks.onNodeComplete?.(node, result);
|
|
11996
|
+
};
|
|
11997
|
+
if ((this.options.mode ?? "parallel") === "sequential") {
|
|
11998
|
+
for (const node of ready) await runOne(node);
|
|
11999
|
+
} else {
|
|
12000
|
+
await Promise.all(ready.map(runOne));
|
|
12001
|
+
}
|
|
12002
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
12003
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
12004
|
+
await blockDescendants(failedThisWave);
|
|
12005
|
+
}
|
|
12006
|
+
wave++;
|
|
12007
|
+
}
|
|
12008
|
+
return { results, waves, blocked };
|
|
12009
|
+
}
|
|
12010
|
+
};
|
|
12011
|
+
|
|
11573
12012
|
// src/utils/json-extract.ts
|
|
11574
12013
|
function extractFirstJsonObject(input) {
|
|
11575
12014
|
if (!input) return null;
|
|
@@ -11641,7 +12080,7 @@ function buildT1SystemPrompt(has) {
|
|
|
11641
12080
|
Your responsibilities:
|
|
11642
12081
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
11643
12082
|
2. Decompose the task into logical sections (one per T2 Manager)
|
|
11644
|
-
3. For each section, define
|
|
12083
|
+
3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
|
|
11645
12084
|
4. Return a structured plan as JSON
|
|
11646
12085
|
|
|
11647
12086
|
CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
|
|
@@ -11667,6 +12106,9 @@ QUALITY RULES:
|
|
|
11667
12106
|
|
|
11668
12107
|
${generation}` : ""}`;
|
|
11669
12108
|
}
|
|
12109
|
+
function producedOutput(result) {
|
|
12110
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
12111
|
+
}
|
|
11670
12112
|
var T1Administrator = class extends BaseTier {
|
|
11671
12113
|
router;
|
|
11672
12114
|
toolRegistry;
|
|
@@ -11801,7 +12243,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
11801
12243
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
11802
12244
|
let pass = 1;
|
|
11803
12245
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
11804
|
-
const okCount = (rs) => rs.filter(
|
|
12246
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
11805
12247
|
while (pass <= maxReplanPasses) {
|
|
11806
12248
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
11807
12249
|
if (reviewResult.approved) {
|
|
@@ -11932,7 +12374,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
11932
12374
|
/** Structured, grounded summary of what's already done — used to keep
|
|
11933
12375
|
* corrective replan passes from re-emitting completed sections. */
|
|
11934
12376
|
summarizeCompletedSections(results) {
|
|
11935
|
-
const done = results.filter(
|
|
12377
|
+
const done = results.filter(producedOutput);
|
|
11936
12378
|
if (done.length === 0) return "";
|
|
11937
12379
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
11938
12380
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -12056,7 +12498,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12056
12498
|
}
|
|
12057
12499
|
}
|
|
12058
12500
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
12059
|
-
if (plan.sections.length < min)
|
|
12501
|
+
if (plan.sections.length < min || plan.sections.length > max) {
|
|
12502
|
+
this.log(
|
|
12503
|
+
`\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.`
|
|
12504
|
+
);
|
|
12505
|
+
}
|
|
12060
12506
|
}
|
|
12061
12507
|
async dispatchT2Managers(sections) {
|
|
12062
12508
|
for (const section of sections) {
|
|
@@ -12073,6 +12519,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12073
12519
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
12074
12520
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
12075
12521
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
12522
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
12076
12523
|
if (this.store) {
|
|
12077
12524
|
manager.setStore(this.store);
|
|
12078
12525
|
}
|
|
@@ -12168,118 +12615,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12168
12615
|
return t2Results;
|
|
12169
12616
|
}
|
|
12170
12617
|
/**
|
|
12171
|
-
* Runs T2 managers respecting dependsOn declarations
|
|
12618
|
+
* Runs T2 managers respecting dependsOn declarations.
|
|
12619
|
+
*
|
|
12620
|
+
* Graph construction, validation and cycle repair now live in
|
|
12621
|
+
* compileSectionGraph (orchestration/adapters.ts), and wave execution in
|
|
12622
|
+
* DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
|
|
12623
|
+
* ordered by one implementation rather than two that drifted.
|
|
12624
|
+
*
|
|
12625
|
+
* The hand-rolled Kahn pass this replaces treated every node it could not
|
|
12626
|
+
* reach as part of a cycle. That set also contains everything DOWNSTREAM of a
|
|
12627
|
+
* cycle, whose in-degree can never fall to zero while its dependency is stuck,
|
|
12628
|
+
* so breaking a cycle here also cut the dependencies of innocent later
|
|
12629
|
+
* sections and started them immediately — before the sections they consume.
|
|
12630
|
+
* compileTaskGraph uses strongly connected components, so only real cycle
|
|
12631
|
+
* members are touched.
|
|
12172
12632
|
*/
|
|
12173
12633
|
async runT2sWithDependencies(sections, managers, taskId) {
|
|
12174
|
-
const
|
|
12175
|
-
const
|
|
12176
|
-
|
|
12177
|
-
const allKeys = new Set(sections.map((s) => s.sectionId));
|
|
12178
|
-
for (const s of sections) {
|
|
12179
|
-
if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
|
|
12180
|
-
inDegree.set(s.sectionId, 0);
|
|
12181
|
-
s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
|
|
12182
|
-
}
|
|
12183
|
-
for (const s of sections) {
|
|
12184
|
-
for (const dep of s.dependsOn ?? []) {
|
|
12185
|
-
adj.get(dep).add(s.sectionId);
|
|
12186
|
-
inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
|
|
12187
|
-
}
|
|
12188
|
-
}
|
|
12189
|
-
const queue = [];
|
|
12190
|
-
const degree = new Map(inDegree);
|
|
12191
|
-
for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
|
|
12192
|
-
const visited = /* @__PURE__ */ new Set();
|
|
12193
|
-
while (queue.length > 0) {
|
|
12194
|
-
const u = queue.shift();
|
|
12195
|
-
visited.add(u);
|
|
12196
|
-
for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
|
|
12197
|
-
const newDeg = (degree.get(v) ?? 1) - 1;
|
|
12198
|
-
degree.set(v, newDeg);
|
|
12199
|
-
if (newDeg === 0) queue.push(v);
|
|
12200
|
-
}
|
|
12201
|
-
}
|
|
12202
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
12203
|
-
if (cycleNodes.length > 0) {
|
|
12204
|
-
this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
|
|
12205
|
-
for (const s of sections) {
|
|
12206
|
-
if (cycleNodes.includes(s.sectionId)) {
|
|
12207
|
-
const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
12208
|
-
for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
12209
|
-
inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
|
|
12210
|
-
adj.get(removed)?.delete(s.sectionId);
|
|
12211
|
-
}
|
|
12212
|
-
s.dependsOn = safeDeps;
|
|
12213
|
-
}
|
|
12214
|
-
}
|
|
12634
|
+
const compiled = compileSectionGraph(sections);
|
|
12635
|
+
for (const issue of compiled.issues) {
|
|
12636
|
+
this.log(`\u26A0 Section graph: ${issue.message}`);
|
|
12215
12637
|
}
|
|
12216
|
-
const
|
|
12638
|
+
const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
|
|
12639
|
+
for (const section of sections) {
|
|
12640
|
+
const node = byId.get(section.sectionId);
|
|
12641
|
+
if (node) section.dependsOn = [...node.dependsOn];
|
|
12642
|
+
}
|
|
12643
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
12644
|
+
const totalSections = compiled.graph.nodes.length;
|
|
12217
12645
|
let completedSections = 0;
|
|
12218
|
-
const
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12646
|
+
const scheduler = new DependencyScheduler(
|
|
12647
|
+
compiled.graph,
|
|
12648
|
+
{
|
|
12649
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
12650
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
12651
|
+
// Stamped before the wave executes so the first status event a node
|
|
12652
|
+
// emits already carries it.
|
|
12653
|
+
onWaveStart: (nodes, wave) => {
|
|
12654
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
12655
|
+
},
|
|
12656
|
+
execute: async (node) => {
|
|
12657
|
+
const section = node.payload;
|
|
12658
|
+
const manager = managers[node.ordinal];
|
|
12659
|
+
const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
|
|
12660
|
+
this.sendStatusUpdate({
|
|
12661
|
+
progressPct,
|
|
12662
|
+
currentAction: `T2 working on: ${section.sectionTitle}`,
|
|
12663
|
+
status: "IN_PROGRESS"
|
|
12664
|
+
});
|
|
12665
|
+
this.throwIfCancelled();
|
|
12666
|
+
try {
|
|
12667
|
+
const result = await manager.execute(section, taskId, this.signal);
|
|
12668
|
+
manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
|
|
12669
|
+
if (result.status === "ESCALATED") {
|
|
12670
|
+
this.escalations.push({
|
|
12671
|
+
raisedBy: `T2_${section.sectionId}`,
|
|
12672
|
+
sectionId: section.sectionId,
|
|
12673
|
+
attempted: result.issues,
|
|
12674
|
+
blocker: result.issues.join("; "),
|
|
12675
|
+
needs: "Human review required"
|
|
12676
|
+
});
|
|
12677
|
+
}
|
|
12678
|
+
return result;
|
|
12679
|
+
} catch (err) {
|
|
12680
|
+
return {
|
|
12245
12681
|
sectionId: section.sectionId,
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12682
|
+
sectionTitle: section.sectionTitle,
|
|
12683
|
+
status: "FAILED",
|
|
12684
|
+
t3Results: [],
|
|
12685
|
+
sectionSummary: "",
|
|
12686
|
+
issues: [err instanceof Error ? err.message : String(err)]
|
|
12687
|
+
};
|
|
12250
12688
|
}
|
|
12251
|
-
}
|
|
12252
|
-
|
|
12689
|
+
},
|
|
12690
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
12691
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
12692
|
+
// the user more than letting it try.
|
|
12693
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
12694
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
12695
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
12696
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
12697
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
12698
|
+
// should have been skipped would run and be billed.
|
|
12699
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
12700
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
12701
|
+
const section = node.payload;
|
|
12702
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
12703
|
+
resultMap.set(node.id, {
|
|
12253
12704
|
sectionId: section.sectionId,
|
|
12254
12705
|
sectionTitle: section.sectionTitle,
|
|
12255
|
-
|
|
12706
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
12707
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
12708
|
+
// that did (the upstream section named below).
|
|
12709
|
+
status: "BLOCKED",
|
|
12256
12710
|
t3Results: [],
|
|
12257
12711
|
sectionSummary: "",
|
|
12258
|
-
issues: [
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12712
|
+
issues: [
|
|
12713
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
12714
|
+
],
|
|
12715
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
12716
|
+
});
|
|
12717
|
+
completedSections++;
|
|
12718
|
+
this.sendStatusUpdate({
|
|
12719
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
12720
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
12721
|
+
status: "IN_PROGRESS"
|
|
12722
|
+
});
|
|
12723
|
+
this.emit("tier:status", {
|
|
12724
|
+
tierId: section.sectionId,
|
|
12725
|
+
// A blocked section has no tier instance — it was never
|
|
12726
|
+
// constructed — so its runtime id and its graph id are the same
|
|
12727
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
12728
|
+
// stream addressable in the graph's id space, which is what
|
|
12729
|
+
// dependsOn edges point at.
|
|
12730
|
+
nodeId: section.sectionId,
|
|
12731
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
12732
|
+
role: "T2",
|
|
12733
|
+
label: section.sectionTitle,
|
|
12734
|
+
status: "BLOCKED",
|
|
12735
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12736
|
+
output: blockedInfo.reason
|
|
12737
|
+
});
|
|
12738
|
+
this.emit("section:complete", {
|
|
12739
|
+
id: node.id,
|
|
12740
|
+
title: section.sectionTitle,
|
|
12741
|
+
status: "BLOCKED",
|
|
12742
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12743
|
+
output: ""
|
|
12744
|
+
});
|
|
12745
|
+
},
|
|
12746
|
+
onNodeComplete: (node, result) => {
|
|
12747
|
+
resultMap.set(node.id, result);
|
|
12748
|
+
completedSections++;
|
|
12749
|
+
this.emit("section:complete", {
|
|
12750
|
+
id: node.id,
|
|
12751
|
+
title: result.sectionTitle,
|
|
12752
|
+
status: result.status,
|
|
12753
|
+
output: result.sectionSummary
|
|
12754
|
+
});
|
|
12270
12755
|
}
|
|
12271
|
-
}
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12756
|
+
},
|
|
12757
|
+
{
|
|
12758
|
+
// Cross-section concurrency respects the same t3Execution mode as the
|
|
12759
|
+
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
12760
|
+
// serialized T3 workers within a single section while every section's
|
|
12761
|
+
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
12762
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
12763
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
12764
|
+
// output. Running it after the dependency failed produces a second
|
|
12765
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
12766
|
+
onUpstreamFailure: "block"
|
|
12276
12767
|
}
|
|
12277
|
-
|
|
12278
|
-
await
|
|
12768
|
+
);
|
|
12769
|
+
await scheduler.run();
|
|
12279
12770
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
12280
12771
|
}
|
|
12281
12772
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
12282
|
-
const completedSections = t2Results.filter(
|
|
12773
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
12283
12774
|
if (!completedSections.length) {
|
|
12284
12775
|
const allIssues = [
|
|
12285
12776
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -12299,7 +12790,6 @@ ${r.sectionSummary}
|
|
|
12299
12790
|
Outputs:
|
|
12300
12791
|
${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
|
|
12301
12792
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
12302
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
12303
12793
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
12304
12794
|
|
|
12305
12795
|
Original request: ${originalPrompt}
|
|
@@ -12794,6 +13284,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
12794
13284
|
};
|
|
12795
13285
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
12796
13286
|
var MAX_VIDEO_SECONDS = 8;
|
|
13287
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
12797
13288
|
var GenerateVideoTool = class extends BaseTool {
|
|
12798
13289
|
constructor(deps) {
|
|
12799
13290
|
super();
|
|
@@ -12810,7 +13301,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12810
13301
|
type: "object",
|
|
12811
13302
|
properties: {
|
|
12812
13303
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
12813
|
-
seconds: { type: "number", description: `Clip length,
|
|
13304
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
12814
13305
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
12815
13306
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
12816
13307
|
},
|
|
@@ -12822,7 +13313,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12822
13313
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
12823
13314
|
if (!r.ok) return r.error;
|
|
12824
13315
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
12825
|
-
const seconds = Math.max(
|
|
13316
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
12826
13317
|
const asset = await generateVideo(
|
|
12827
13318
|
r.selected.capability,
|
|
12828
13319
|
r.cfg,
|
|
@@ -12839,7 +13330,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12839
13330
|
const location = await this.deps.sink(asset);
|
|
12840
13331
|
const unit = r.selected.cost;
|
|
12841
13332
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
12842
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
13333
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
12843
13334
|
return `Video generated and saved to ${location}.
|
|
12844
13335
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
12845
13336
|
}
|
|
@@ -12908,16 +13399,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
12908
13399
|
if (typeof input !== "string" || input.length === 0) {
|
|
12909
13400
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12910
13401
|
}
|
|
12911
|
-
const root =
|
|
12912
|
-
const abs =
|
|
12913
|
-
const rel =
|
|
12914
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
13402
|
+
const root = path34__default.default.resolve(workspaceRoot);
|
|
13403
|
+
const abs = path34__default.default.isAbsolute(input) ? path34__default.default.resolve(input) : path34__default.default.resolve(root, input);
|
|
13404
|
+
const rel = path34__default.default.relative(root, abs);
|
|
13405
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34__default.default.isAbsolute(rel)) {
|
|
12915
13406
|
throw new WorkspaceSandboxError(input, root);
|
|
12916
13407
|
}
|
|
12917
13408
|
try {
|
|
12918
|
-
const real =
|
|
12919
|
-
const realRel =
|
|
12920
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
13409
|
+
const real = fs31__default.default.realpathSync(abs);
|
|
13410
|
+
const realRel = path34__default.default.relative(root, real);
|
|
13411
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34__default.default.isAbsolute(realRel))) {
|
|
12921
13412
|
throw new WorkspaceSandboxError(input, root);
|
|
12922
13413
|
}
|
|
12923
13414
|
} catch (e) {
|
|
@@ -13570,7 +14061,7 @@ var GenerateDocumentTool = class extends BaseTool {
|
|
|
13570
14061
|
}
|
|
13571
14062
|
}
|
|
13572
14063
|
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
13573
|
-
await fs11__default.default.mkdir(
|
|
14064
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
|
|
13574
14065
|
await fs11__default.default.writeFile(absPath, bytes);
|
|
13575
14066
|
const charts = countCharts(content);
|
|
13576
14067
|
return [
|
|
@@ -13791,7 +14282,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
13791
14282
|
} catch {
|
|
13792
14283
|
}
|
|
13793
14284
|
}
|
|
13794
|
-
await fs11__default.default.mkdir(
|
|
14285
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
|
|
13795
14286
|
await fs11__default.default.writeFile(absPath, content, "utf-8");
|
|
13796
14287
|
return `Written ${content.length} characters to ${filePath}`;
|
|
13797
14288
|
}
|
|
@@ -14289,7 +14780,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
14289
14780
|
};
|
|
14290
14781
|
async function fileToImageAttachment(filePath) {
|
|
14291
14782
|
const data = await fs11__default.default.readFile(filePath);
|
|
14292
|
-
const ext =
|
|
14783
|
+
const ext = path34__default.default.extname(filePath).toLowerCase();
|
|
14293
14784
|
const mimeMap = {
|
|
14294
14785
|
".jpg": "image/jpeg",
|
|
14295
14786
|
".jpeg": "image/jpeg",
|
|
@@ -14323,14 +14814,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
14323
14814
|
const filePath = input["path"];
|
|
14324
14815
|
const content = input["content"];
|
|
14325
14816
|
const title = input["title"];
|
|
14326
|
-
const dir =
|
|
14327
|
-
if (!
|
|
14328
|
-
|
|
14817
|
+
const dir = path34__default.default.dirname(filePath);
|
|
14818
|
+
if (!fs31__default.default.existsSync(dir)) {
|
|
14819
|
+
fs31__default.default.mkdirSync(dir, { recursive: true });
|
|
14329
14820
|
}
|
|
14330
14821
|
return new Promise((resolve2, reject) => {
|
|
14331
14822
|
try {
|
|
14332
14823
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
14333
|
-
const stream =
|
|
14824
|
+
const stream = fs31__default.default.createWriteStream(filePath);
|
|
14334
14825
|
doc.pipe(stream);
|
|
14335
14826
|
if (title) {
|
|
14336
14827
|
doc.info["Title"] = title;
|
|
@@ -14408,22 +14899,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
14408
14899
|
}
|
|
14409
14900
|
cmdPrefix = NODE_CMD;
|
|
14410
14901
|
}
|
|
14411
|
-
const tmpDir =
|
|
14412
|
-
if (!
|
|
14413
|
-
|
|
14902
|
+
const tmpDir = path34__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
14903
|
+
if (!fs31__default.default.existsSync(tmpDir)) {
|
|
14904
|
+
fs31__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
14414
14905
|
}
|
|
14415
14906
|
const extension = language === "python" ? "py" : "js";
|
|
14416
14907
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
14417
|
-
const filePath =
|
|
14418
|
-
|
|
14908
|
+
const filePath = path34__default.default.join(tmpDir, fileName);
|
|
14909
|
+
fs31__default.default.writeFileSync(filePath, code, "utf-8");
|
|
14419
14910
|
const execArgs = [filePath, ...args];
|
|
14420
14911
|
return new Promise((resolve2) => {
|
|
14421
14912
|
const startMs = Date.now();
|
|
14422
14913
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
14423
14914
|
const duration = Date.now() - startMs;
|
|
14424
14915
|
try {
|
|
14425
|
-
if (
|
|
14426
|
-
|
|
14916
|
+
if (fs31__default.default.existsSync(filePath)) {
|
|
14917
|
+
fs31__default.default.unlinkSync(filePath);
|
|
14427
14918
|
}
|
|
14428
14919
|
} catch (cleanupErr) {
|
|
14429
14920
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -14735,7 +15226,7 @@ var GlobTool = class extends BaseTool {
|
|
|
14735
15226
|
};
|
|
14736
15227
|
async execute(input, _options) {
|
|
14737
15228
|
const pattern = input["pattern"];
|
|
14738
|
-
const searchPath = input["path"] ?
|
|
15229
|
+
const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
14739
15230
|
const matches = await glob.glob(pattern, {
|
|
14740
15231
|
cwd: searchPath,
|
|
14741
15232
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -14748,7 +15239,7 @@ var GlobTool = class extends BaseTool {
|
|
|
14748
15239
|
const withMtime = await Promise.all(
|
|
14749
15240
|
matches.map(async (rel) => {
|
|
14750
15241
|
try {
|
|
14751
|
-
const stat = await fs11__default.default.stat(
|
|
15242
|
+
const stat = await fs11__default.default.stat(path34__default.default.join(searchPath, rel));
|
|
14752
15243
|
return { rel, mtime: stat.mtimeMs };
|
|
14753
15244
|
} catch {
|
|
14754
15245
|
return { rel, mtime: 0 };
|
|
@@ -14797,7 +15288,7 @@ var GrepTool = class extends BaseTool {
|
|
|
14797
15288
|
};
|
|
14798
15289
|
async execute(input, _options) {
|
|
14799
15290
|
const pattern = input["pattern"];
|
|
14800
|
-
const searchPath = input["path"] ?
|
|
15291
|
+
const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
14801
15292
|
const globPattern = input["glob"];
|
|
14802
15293
|
const outputMode = input["output_mode"] ?? "content";
|
|
14803
15294
|
const context = input["context"] ?? 0;
|
|
@@ -14851,12 +15342,12 @@ var GrepTool = class extends BaseTool {
|
|
|
14851
15342
|
nodir: true
|
|
14852
15343
|
});
|
|
14853
15344
|
} catch {
|
|
14854
|
-
files = [
|
|
15345
|
+
files = [path34__default.default.relative(searchPath, searchPath) || "."];
|
|
14855
15346
|
}
|
|
14856
15347
|
const results = [];
|
|
14857
15348
|
let totalCount = 0;
|
|
14858
15349
|
for (const rel of files) {
|
|
14859
|
-
const abs =
|
|
15350
|
+
const abs = path34__default.default.join(searchPath, rel);
|
|
14860
15351
|
let content;
|
|
14861
15352
|
try {
|
|
14862
15353
|
content = await fs11__default.default.readFile(abs, "utf-8");
|
|
@@ -15243,10 +15734,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
15243
15734
|
}
|
|
15244
15735
|
isIgnored(filePath) {
|
|
15245
15736
|
if (!filePath) return false;
|
|
15246
|
-
const abs =
|
|
15247
|
-
const rel =
|
|
15248
|
-
if (!rel || rel.startsWith("..") ||
|
|
15249
|
-
const posixRel = rel.split(
|
|
15737
|
+
const abs = path34__default.default.resolve(this.workspaceRoot, filePath);
|
|
15738
|
+
const rel = path34__default.default.relative(this.workspaceRoot, abs);
|
|
15739
|
+
if (!rel || rel.startsWith("..") || path34__default.default.isAbsolute(rel)) return true;
|
|
15740
|
+
const posixRel = rel.split(path34__default.default.sep).join("/");
|
|
15250
15741
|
return this.ignoreMatcher.ignores(posixRel);
|
|
15251
15742
|
}
|
|
15252
15743
|
};
|
|
@@ -15480,22 +15971,22 @@ var FileMcpOAuthStore = class {
|
|
|
15480
15971
|
filePath;
|
|
15481
15972
|
load() {
|
|
15482
15973
|
try {
|
|
15483
|
-
return JSON.parse(
|
|
15974
|
+
return JSON.parse(fs31__default.default.readFileSync(this.filePath, "utf-8"));
|
|
15484
15975
|
} catch {
|
|
15485
15976
|
return void 0;
|
|
15486
15977
|
}
|
|
15487
15978
|
}
|
|
15488
15979
|
save(state) {
|
|
15489
|
-
|
|
15490
|
-
|
|
15980
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
15981
|
+
fs31__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
15491
15982
|
try {
|
|
15492
|
-
|
|
15983
|
+
fs31__default.default.chmodSync(this.filePath, 384);
|
|
15493
15984
|
} catch {
|
|
15494
15985
|
}
|
|
15495
15986
|
}
|
|
15496
15987
|
clear() {
|
|
15497
15988
|
try {
|
|
15498
|
-
|
|
15989
|
+
fs31__default.default.rmSync(this.filePath);
|
|
15499
15990
|
} catch {
|
|
15500
15991
|
}
|
|
15501
15992
|
}
|
|
@@ -15958,6 +16449,14 @@ function heuristicAnalyze(prompt) {
|
|
|
15958
16449
|
const estimatedTokens = wordCount * 5;
|
|
15959
16450
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
15960
16451
|
}
|
|
16452
|
+
function hashPrompt(prompt) {
|
|
16453
|
+
let hash = 2166136261;
|
|
16454
|
+
for (let index = 0; index < prompt.length; index++) {
|
|
16455
|
+
hash ^= prompt.charCodeAt(index);
|
|
16456
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
16457
|
+
}
|
|
16458
|
+
return `${prompt.length}:${hash.toString(36)}`;
|
|
16459
|
+
}
|
|
15961
16460
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
15962
16461
|
var TASK_TYPE_TAGS = {
|
|
15963
16462
|
code: ["code", "instruction"],
|
|
@@ -15972,7 +16471,24 @@ var TaskAnalyzer = class {
|
|
|
15972
16471
|
feedback;
|
|
15973
16472
|
bias;
|
|
15974
16473
|
lastProfile = null;
|
|
15975
|
-
|
|
16474
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
16475
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
16476
|
+
/**
|
|
16477
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
16478
|
+
* type they were chosen for.
|
|
16479
|
+
*
|
|
16480
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
16481
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
16482
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
16483
|
+
* model is good at creative writing, from a rating that never said so.
|
|
16484
|
+
*
|
|
16485
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
16486
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
16487
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
16488
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
16489
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
16490
|
+
*/
|
|
16491
|
+
lastCompletedRun;
|
|
15976
16492
|
constructor(tracker, bias = "balanced") {
|
|
15977
16493
|
this.tracker = tracker;
|
|
15978
16494
|
this.bias = bias;
|
|
@@ -16001,7 +16517,7 @@ var TaskAnalyzer = class {
|
|
|
16001
16517
|
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
16002
16518
|
*/
|
|
16003
16519
|
async analyze(prompt) {
|
|
16004
|
-
const cacheKey = prompt
|
|
16520
|
+
const cacheKey = hashPrompt(prompt);
|
|
16005
16521
|
const cached = analysisCache.get(cacheKey);
|
|
16006
16522
|
if (cached) {
|
|
16007
16523
|
this.lastProfile = cached;
|
|
@@ -16019,11 +16535,15 @@ var TaskAnalyzer = class {
|
|
|
16019
16535
|
*/
|
|
16020
16536
|
async selectModel(prompt, tier, selector, opts) {
|
|
16021
16537
|
const profile = await this.analyze(prompt);
|
|
16538
|
+
const recordSelection = (model) => {
|
|
16539
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
16540
|
+
return model;
|
|
16541
|
+
};
|
|
16022
16542
|
if (profile.requiresVision) {
|
|
16023
|
-
return selector.selectVisionModel();
|
|
16543
|
+
return recordSelection(selector.selectVisionModel());
|
|
16024
16544
|
}
|
|
16025
16545
|
let candidates2 = selector.getCandidatesForTier(tier);
|
|
16026
|
-
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
16546
|
+
if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
16027
16547
|
if (opts?.requiresToolUse) {
|
|
16028
16548
|
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
16029
16549
|
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
@@ -16033,9 +16553,7 @@ var TaskAnalyzer = class {
|
|
|
16033
16553
|
score: this.scoreModel(m, profile)
|
|
16034
16554
|
}));
|
|
16035
16555
|
scored.sort((a, b) => b.score - a.score);
|
|
16036
|
-
|
|
16037
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
16038
|
-
return best;
|
|
16556
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
16039
16557
|
}
|
|
16040
16558
|
/**
|
|
16041
16559
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -16044,25 +16562,38 @@ var TaskAnalyzer = class {
|
|
|
16044
16562
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
16045
16563
|
if (!this.tracker || !this.lastProfile) return;
|
|
16046
16564
|
const taskType = this.lastProfile.type;
|
|
16047
|
-
for (const [tier, model] of this.
|
|
16565
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
16048
16566
|
const cost = costByTier[tier] ?? 0;
|
|
16049
16567
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
16050
16568
|
}
|
|
16051
|
-
this.
|
|
16569
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
16570
|
+
this.currentRunSelections.clear();
|
|
16052
16571
|
void this.tracker.save();
|
|
16053
16572
|
}
|
|
16054
16573
|
/**
|
|
16055
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
16574
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
16056
16575
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
16057
|
-
*
|
|
16576
|
+
*
|
|
16577
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
16578
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
16579
|
+
* empty.
|
|
16580
|
+
*
|
|
16581
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
16582
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
16583
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
16584
|
+
* that reports false, which is what "rate this run" should mean.
|
|
16058
16585
|
*/
|
|
16059
16586
|
recordExplicitRating(rating) {
|
|
16060
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16587
|
+
const snapshot = this.lastCompletedRun;
|
|
16588
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
16589
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
16590
|
+
for (const [, model] of snapshot.selections) {
|
|
16591
|
+
if (ratedModels.has(model.id)) continue;
|
|
16592
|
+
ratedModels.add(model.id);
|
|
16593
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
16594
|
+
}
|
|
16595
|
+
this.lastCompletedRun = void 0;
|
|
16596
|
+
return true;
|
|
16066
16597
|
}
|
|
16067
16598
|
scoreModel(model, profile) {
|
|
16068
16599
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -16103,7 +16634,7 @@ var TaskAnalyzer = class {
|
|
|
16103
16634
|
|
|
16104
16635
|
// src/core/router/model-performance-tracker.ts
|
|
16105
16636
|
init_pricing();
|
|
16106
|
-
var DEFAULT_STATS_FILE =
|
|
16637
|
+
var DEFAULT_STATS_FILE = path34__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
|
|
16107
16638
|
var ModelPerformanceTracker = class {
|
|
16108
16639
|
stats = /* @__PURE__ */ new Map();
|
|
16109
16640
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -16143,7 +16674,7 @@ var ModelPerformanceTracker = class {
|
|
|
16143
16674
|
async save() {
|
|
16144
16675
|
if (this.readOnly) return;
|
|
16145
16676
|
try {
|
|
16146
|
-
await fs11__default.default.mkdir(
|
|
16677
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(this.statsFile), { recursive: true });
|
|
16147
16678
|
const modelsObj = {};
|
|
16148
16679
|
const featuresObj = {};
|
|
16149
16680
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -16691,7 +17222,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
16691
17222
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
16692
17223
|
async loadPersistedTools() {
|
|
16693
17224
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
16694
|
-
const file =
|
|
17225
|
+
const file = path34__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
16695
17226
|
try {
|
|
16696
17227
|
const raw = await fs11__default.default.readFile(file, "utf-8");
|
|
16697
17228
|
const specs = JSON.parse(raw);
|
|
@@ -16715,8 +17246,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
16715
17246
|
}
|
|
16716
17247
|
async persist() {
|
|
16717
17248
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
16718
|
-
const dir =
|
|
16719
|
-
const file =
|
|
17249
|
+
const dir = path34__default.default.join(this.workspacePath, ".cascade");
|
|
17250
|
+
const file = path34__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
16720
17251
|
try {
|
|
16721
17252
|
await fs11__default.default.mkdir(dir, { recursive: true });
|
|
16722
17253
|
await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -16736,12 +17267,12 @@ var WorldStateDB = class {
|
|
|
16736
17267
|
constructor(workspacePath, debugMode = false) {
|
|
16737
17268
|
this.workspacePath = workspacePath;
|
|
16738
17269
|
this.debugMode = debugMode;
|
|
16739
|
-
const cascadeDir =
|
|
16740
|
-
if (!
|
|
16741
|
-
|
|
17270
|
+
const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
|
|
17271
|
+
if (!fs31__default.default.existsSync(cascadeDir)) {
|
|
17272
|
+
fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
16742
17273
|
}
|
|
16743
|
-
this.keyPath =
|
|
16744
|
-
this.dbPath =
|
|
17274
|
+
this.keyPath = path34__default.default.join(cascadeDir, "world_state.key");
|
|
17275
|
+
this.dbPath = path34__default.default.join(cascadeDir, "world_state.db");
|
|
16745
17276
|
this.initEncryptionKey();
|
|
16746
17277
|
this.db = new Database2__default.default(this.dbPath);
|
|
16747
17278
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -16798,11 +17329,11 @@ var WorldStateDB = class {
|
|
|
16798
17329
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
16799
17330
|
}
|
|
16800
17331
|
initEncryptionKey() {
|
|
16801
|
-
if (
|
|
16802
|
-
this.encryptionKey =
|
|
17332
|
+
if (fs31__default.default.existsSync(this.keyPath)) {
|
|
17333
|
+
this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
|
|
16803
17334
|
} else {
|
|
16804
17335
|
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
16805
|
-
|
|
17336
|
+
fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
16806
17337
|
}
|
|
16807
17338
|
}
|
|
16808
17339
|
encrypt(text) {
|
|
@@ -17088,9 +17619,9 @@ var WorldStateDB = class {
|
|
|
17088
17619
|
dumpDebugIfNeeded() {
|
|
17089
17620
|
if (!this.debugMode) return;
|
|
17090
17621
|
try {
|
|
17091
|
-
const dumpPath =
|
|
17622
|
+
const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
17092
17623
|
const entries = this.getAllEntries();
|
|
17093
|
-
|
|
17624
|
+
fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
17094
17625
|
} catch (err) {
|
|
17095
17626
|
console.error("Failed to dump debug world state", err);
|
|
17096
17627
|
}
|
|
@@ -17099,6 +17630,212 @@ var WorldStateDB = class {
|
|
|
17099
17630
|
this.db.close();
|
|
17100
17631
|
}
|
|
17101
17632
|
};
|
|
17633
|
+
var RESUME_SCHEMA_VERSION = 1;
|
|
17634
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
17635
|
+
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17636
|
+
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
17637
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
17638
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
17639
|
+
function safeId(taskId) {
|
|
17640
|
+
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
17641
|
+
return cleaned.slice(0, 100) || "run";
|
|
17642
|
+
}
|
|
17643
|
+
function isCheckpoint(value) {
|
|
17644
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17645
|
+
const c = value;
|
|
17646
|
+
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);
|
|
17647
|
+
}
|
|
17648
|
+
var ResumeStore = class {
|
|
17649
|
+
dir;
|
|
17650
|
+
maxCheckpoints;
|
|
17651
|
+
maxAgeMs;
|
|
17652
|
+
now;
|
|
17653
|
+
claimTimeoutMs;
|
|
17654
|
+
constructor(options) {
|
|
17655
|
+
this.dir = options.dir;
|
|
17656
|
+
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
17657
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
17658
|
+
this.now = options.now ?? Date.now;
|
|
17659
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
17660
|
+
}
|
|
17661
|
+
/**
|
|
17662
|
+
* Persist a checkpoint and prune.
|
|
17663
|
+
*
|
|
17664
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
17665
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
17666
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
17667
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
17668
|
+
* look alike to them.
|
|
17669
|
+
*
|
|
17670
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
17671
|
+
*/
|
|
17672
|
+
async save(checkpoint) {
|
|
17673
|
+
try {
|
|
17674
|
+
const timestamp = this.now();
|
|
17675
|
+
const record = {
|
|
17676
|
+
...checkpoint,
|
|
17677
|
+
version: RESUME_SCHEMA_VERSION,
|
|
17678
|
+
createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
|
|
17679
|
+
};
|
|
17680
|
+
await fs31.promises.mkdir(this.dir, { recursive: true });
|
|
17681
|
+
const finalPath = path34__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
|
|
17682
|
+
const tempPath = `${finalPath}.${process.pid}.tmp`;
|
|
17683
|
+
await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
17684
|
+
await fs31.promises.rename(tempPath, finalPath);
|
|
17685
|
+
await this.prune();
|
|
17686
|
+
return true;
|
|
17687
|
+
} catch {
|
|
17688
|
+
return false;
|
|
17689
|
+
}
|
|
17690
|
+
}
|
|
17691
|
+
/** Valid, unexpired checkpoints, newest first. */
|
|
17692
|
+
async list() {
|
|
17693
|
+
let entries;
|
|
17694
|
+
try {
|
|
17695
|
+
entries = await fs31.promises.readdir(this.dir);
|
|
17696
|
+
} catch {
|
|
17697
|
+
return [];
|
|
17698
|
+
}
|
|
17699
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17700
|
+
const candidates2 = 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);
|
|
17701
|
+
const checkpoints = [];
|
|
17702
|
+
for (const candidate of candidates2) {
|
|
17703
|
+
try {
|
|
17704
|
+
const raw = await fs31.promises.readFile(path34__default.default.join(this.dir, candidate.name), "utf-8");
|
|
17705
|
+
const parsed = JSON.parse(raw);
|
|
17706
|
+
if (isCheckpoint(parsed)) checkpoints.push(parsed);
|
|
17707
|
+
} catch {
|
|
17708
|
+
continue;
|
|
17709
|
+
}
|
|
17710
|
+
}
|
|
17711
|
+
return checkpoints;
|
|
17712
|
+
}
|
|
17713
|
+
/** The most recent resumable run, or null. */
|
|
17714
|
+
async latest() {
|
|
17715
|
+
return (await this.list())[0] ?? null;
|
|
17716
|
+
}
|
|
17717
|
+
/**
|
|
17718
|
+
* Claim a checkpoint for a resume attempt.
|
|
17719
|
+
*
|
|
17720
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
17721
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
17722
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
17723
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
17724
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
17725
|
+
* wrong trade for a recovery mechanism.
|
|
17726
|
+
*
|
|
17727
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
17728
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
17729
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
17730
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
17731
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
17732
|
+
*/
|
|
17733
|
+
async claim(taskId) {
|
|
17734
|
+
const all = await this.list();
|
|
17735
|
+
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
17736
|
+
if (!target) return null;
|
|
17737
|
+
try {
|
|
17738
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17739
|
+
const wanted = safeId(target.taskId);
|
|
17740
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
17741
|
+
if (!source) return null;
|
|
17742
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
17743
|
+
await fs31.promises.rename(path34__default.default.join(this.dir, source), path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
17744
|
+
return { checkpoint: target, claimId };
|
|
17745
|
+
} catch {
|
|
17746
|
+
return null;
|
|
17747
|
+
}
|
|
17748
|
+
}
|
|
17749
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
17750
|
+
async release(claimId) {
|
|
17751
|
+
try {
|
|
17752
|
+
await fs31.promises.rename(
|
|
17753
|
+
path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
17754
|
+
path34__default.default.join(this.dir, `${claimId}.json`)
|
|
17755
|
+
);
|
|
17756
|
+
} catch {
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
17760
|
+
async settle(claimId) {
|
|
17761
|
+
try {
|
|
17762
|
+
await fs31.promises.rm(path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
17763
|
+
} catch {
|
|
17764
|
+
}
|
|
17765
|
+
}
|
|
17766
|
+
/**
|
|
17767
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
17768
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
17769
|
+
*/
|
|
17770
|
+
async consume(taskId) {
|
|
17771
|
+
const claimed = await this.claim(taskId);
|
|
17772
|
+
if (!claimed) return null;
|
|
17773
|
+
await this.settle(claimed.claimId);
|
|
17774
|
+
return claimed.checkpoint;
|
|
17775
|
+
}
|
|
17776
|
+
/**
|
|
17777
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
17778
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
17779
|
+
* trading one loss window for another.
|
|
17780
|
+
*/
|
|
17781
|
+
async reclaimStale() {
|
|
17782
|
+
let reclaimed = 0;
|
|
17783
|
+
try {
|
|
17784
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17785
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
17786
|
+
for (const name of entries) {
|
|
17787
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
17788
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
17789
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
17790
|
+
await fs31.promises.rename(
|
|
17791
|
+
path34__default.default.join(this.dir, name),
|
|
17792
|
+
path34__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
17793
|
+
);
|
|
17794
|
+
reclaimed++;
|
|
17795
|
+
}
|
|
17796
|
+
} catch {
|
|
17797
|
+
}
|
|
17798
|
+
return reclaimed;
|
|
17799
|
+
}
|
|
17800
|
+
/** Remove every checkpoint for a task id. */
|
|
17801
|
+
async delete(taskId) {
|
|
17802
|
+
try {
|
|
17803
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17804
|
+
const wanted = safeId(taskId);
|
|
17805
|
+
await Promise.all(entries.map(async (name) => {
|
|
17806
|
+
const match = FILE_PATTERN.exec(name);
|
|
17807
|
+
if (match?.[2] === wanted) {
|
|
17808
|
+
await fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true });
|
|
17809
|
+
}
|
|
17810
|
+
}));
|
|
17811
|
+
} catch {
|
|
17812
|
+
}
|
|
17813
|
+
}
|
|
17814
|
+
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
17815
|
+
async prune() {
|
|
17816
|
+
try {
|
|
17817
|
+
const entries = await fs31.promises.readdir(this.dir);
|
|
17818
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17819
|
+
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);
|
|
17820
|
+
const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
|
|
17821
|
+
const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
|
|
17822
|
+
await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
|
|
17823
|
+
(name) => fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true })
|
|
17824
|
+
));
|
|
17825
|
+
} catch {
|
|
17826
|
+
}
|
|
17827
|
+
}
|
|
17828
|
+
};
|
|
17829
|
+
function summarizeCompleted(completed, perNodeChars = 500) {
|
|
17830
|
+
if (!completed.length) return "";
|
|
17831
|
+
const lines = completed.map((node) => {
|
|
17832
|
+
const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
|
|
17833
|
+
return `- [${node.status}] ${node.title}
|
|
17834
|
+
${body.replace(/\n/g, "\n ")}`;
|
|
17835
|
+
});
|
|
17836
|
+
return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
|
|
17837
|
+
${lines.join("\n")}`;
|
|
17838
|
+
}
|
|
17102
17839
|
var ignore3 = _ignoreModule__namespace.default ?? _ignoreModule__namespace;
|
|
17103
17840
|
var PrivacyPaths = class {
|
|
17104
17841
|
localOnly;
|
|
@@ -17460,7 +18197,7 @@ var WorkspaceIndex = class {
|
|
|
17460
18197
|
isIgnored;
|
|
17461
18198
|
chunker;
|
|
17462
18199
|
constructor(opts) {
|
|
17463
|
-
this.root =
|
|
18200
|
+
this.root = path34__default.default.resolve(opts.root);
|
|
17464
18201
|
this.db = opts.db;
|
|
17465
18202
|
this.namespace = opts.namespace ?? "code";
|
|
17466
18203
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -17492,14 +18229,14 @@ var WorkspaceIndex = class {
|
|
|
17492
18229
|
return;
|
|
17493
18230
|
}
|
|
17494
18231
|
for (const entry of entries) {
|
|
17495
|
-
const abs =
|
|
18232
|
+
const abs = path34__default.default.join(dir, entry.name);
|
|
17496
18233
|
if (entry.isDirectory()) {
|
|
17497
18234
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
17498
18235
|
await walk(abs);
|
|
17499
18236
|
} else if (entry.isFile()) {
|
|
17500
|
-
const ext =
|
|
18237
|
+
const ext = path34__default.default.extname(entry.name).slice(1).toLowerCase();
|
|
17501
18238
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
17502
|
-
out.push(
|
|
18239
|
+
out.push(path34__default.default.relative(this.root, abs));
|
|
17503
18240
|
}
|
|
17504
18241
|
}
|
|
17505
18242
|
};
|
|
@@ -17529,7 +18266,7 @@ var WorkspaceIndex = class {
|
|
|
17529
18266
|
const entries = [];
|
|
17530
18267
|
for (const rel of rels) {
|
|
17531
18268
|
try {
|
|
17532
|
-
const abs =
|
|
18269
|
+
const abs = path34__default.default.join(this.root, rel);
|
|
17533
18270
|
const stat = await fs11__default.default.stat(abs);
|
|
17534
18271
|
if (stat.size > this.maxFileBytes) continue;
|
|
17535
18272
|
const buf = await fs11__default.default.readFile(abs);
|
|
@@ -18067,8 +18804,35 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
18067
18804
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
18068
18805
|
decisionLog = [];
|
|
18069
18806
|
initialized = false;
|
|
18070
|
-
/**
|
|
18807
|
+
/**
|
|
18808
|
+
* Last interrupted task — powers /continue (resumeRun). Kept in memory as a
|
|
18809
|
+
* fast path for the same-process case; the durable copy lives in `resumeStore`
|
|
18810
|
+
* so a crash or an exit no longer throws away everything already finished.
|
|
18811
|
+
*/
|
|
18071
18812
|
lastInterruptedRun;
|
|
18813
|
+
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
18814
|
+
resumeStore;
|
|
18815
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
18816
|
+
claimedResumeId;
|
|
18817
|
+
/**
|
|
18818
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
18819
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
18820
|
+
*/
|
|
18821
|
+
resumedFrom;
|
|
18822
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
18823
|
+
wroteCheckpointThisRun = false;
|
|
18824
|
+
/**
|
|
18825
|
+
* Whether the run reached the end of its normal path.
|
|
18826
|
+
*
|
|
18827
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
18828
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
18829
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
18830
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
18831
|
+
* the original claim and the finished sections with it.
|
|
18832
|
+
*/
|
|
18833
|
+
completedNormally = false;
|
|
18834
|
+
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
18835
|
+
currentRunCompleted = [];
|
|
18072
18836
|
initPromise;
|
|
18073
18837
|
store;
|
|
18074
18838
|
audit;
|
|
@@ -18101,10 +18865,16 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
18101
18865
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
18102
18866
|
try {
|
|
18103
18867
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
18104
|
-
fileDeadModelPersistence(
|
|
18868
|
+
fileDeadModelPersistence(path34__default.default.join(workspacePath, ".cascade", "dead-models.json"))
|
|
18105
18869
|
));
|
|
18106
18870
|
} catch {
|
|
18107
18871
|
}
|
|
18872
|
+
try {
|
|
18873
|
+
this.resumeStore = new ResumeStore({
|
|
18874
|
+
dir: path34__default.default.join(workspacePath, ".cascade", "resume")
|
|
18875
|
+
});
|
|
18876
|
+
} catch {
|
|
18877
|
+
}
|
|
18108
18878
|
this.multimodal = new MultimodalRegistry(
|
|
18109
18879
|
(this.config.providers ?? []).map((p) => p.type)
|
|
18110
18880
|
);
|
|
@@ -18189,8 +18959,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
18189
18959
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
18190
18960
|
return;
|
|
18191
18961
|
}
|
|
18192
|
-
const dbPath = ci.dbPath ||
|
|
18193
|
-
|
|
18962
|
+
const dbPath = ci.dbPath || path34__default.default.join(this.workspacePath, ".cascade", "code-index.db");
|
|
18963
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
|
|
18194
18964
|
const db = new Database2__default.default(dbPath);
|
|
18195
18965
|
db.pragma("journal_mode = WAL");
|
|
18196
18966
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -18570,10 +19340,57 @@ ${input}` }],
|
|
|
18570
19340
|
setFeedbackSource(source) {
|
|
18571
19341
|
this.router.setFeedbackSource(source);
|
|
18572
19342
|
}
|
|
18573
|
-
/**
|
|
19343
|
+
/**
|
|
19344
|
+
* Write a durable checkpoint for an interrupted run.
|
|
19345
|
+
*
|
|
19346
|
+
* Best-effort throughout: a run is already ending badly on every path that
|
|
19347
|
+
* calls this, and failing to record a resume file must not make that ending
|
|
19348
|
+
* worse. Nothing here is allowed to throw.
|
|
19349
|
+
*/
|
|
19350
|
+
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
19351
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
19352
|
+
const merged = /* @__PURE__ */ new Map();
|
|
19353
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
19354
|
+
for (const node of this.currentRunCompleted) {
|
|
19355
|
+
const existing = merged.get(node.id);
|
|
19356
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
19357
|
+
merged.set(node.id, node);
|
|
19358
|
+
}
|
|
19359
|
+
const completed = [...merged.values()];
|
|
19360
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
19361
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
19362
|
+
if (!completed.length && !carriedPartial) return false;
|
|
19363
|
+
try {
|
|
19364
|
+
const saved = await this.resumeStore?.save({
|
|
19365
|
+
taskId,
|
|
19366
|
+
prompt: canonicalPrompt,
|
|
19367
|
+
reason,
|
|
19368
|
+
...detail ? { detail } : {},
|
|
19369
|
+
partialOutput: carriedPartial,
|
|
19370
|
+
completed
|
|
19371
|
+
});
|
|
19372
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
19373
|
+
return saved === true;
|
|
19374
|
+
} catch {
|
|
19375
|
+
return false;
|
|
19376
|
+
}
|
|
19377
|
+
}
|
|
19378
|
+
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
18574
19379
|
hasResumableRun() {
|
|
18575
19380
|
return this.lastInterruptedRun != null;
|
|
18576
19381
|
}
|
|
19382
|
+
/**
|
|
19383
|
+
* True when a checkpoint from ANY process is resumable. Async because the
|
|
19384
|
+
* durable check touches disk; `hasResumableRun` remains the sync in-memory
|
|
19385
|
+
* answer for callers already on a hot path.
|
|
19386
|
+
*/
|
|
19387
|
+
async hasDurableResume() {
|
|
19388
|
+
return await this.resumeStore?.latest() != null;
|
|
19389
|
+
}
|
|
19390
|
+
/** Interrupted runs available to resume, newest first. */
|
|
19391
|
+
async listResumableRuns() {
|
|
19392
|
+
return await this.resumeStore?.list() ?? [];
|
|
19393
|
+
}
|
|
18577
19394
|
/**
|
|
18578
19395
|
* Raise the per-run token budget for a resume and return the continuation
|
|
18579
19396
|
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
@@ -18584,24 +19401,105 @@ ${input}` }],
|
|
|
18584
19401
|
const last = this.lastInterruptedRun;
|
|
18585
19402
|
if (!last) return null;
|
|
18586
19403
|
this.lastInterruptedRun = void 0;
|
|
18587
|
-
|
|
19404
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19405
|
+
return this.buildResumePrompt(last.prompt, last.partialOutput, []);
|
|
19406
|
+
}
|
|
19407
|
+
/**
|
|
19408
|
+
* Durable counterpart to prepareResume: recovers a run interrupted in ANY
|
|
19409
|
+
* process, including one that crashed before it could return. Consumes the
|
|
19410
|
+
* checkpoint, so the same finished work is never restored into two runs.
|
|
19411
|
+
*/
|
|
19412
|
+
async prepareDurableResume(opts = {}) {
|
|
19413
|
+
await this.resumeStore?.reclaimStale();
|
|
19414
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
19415
|
+
if (!claimed) return null;
|
|
19416
|
+
try {
|
|
19417
|
+
this.lastInterruptedRun = void 0;
|
|
19418
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19419
|
+
const prompt = this.buildResumePrompt(
|
|
19420
|
+
claimed.checkpoint.prompt,
|
|
19421
|
+
claimed.checkpoint.partialOutput,
|
|
19422
|
+
claimed.checkpoint.completed
|
|
19423
|
+
);
|
|
19424
|
+
this.resumedFrom = {
|
|
19425
|
+
prompt: claimed.checkpoint.prompt,
|
|
19426
|
+
completed: claimed.checkpoint.completed,
|
|
19427
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
19428
|
+
};
|
|
19429
|
+
this.claimedResumeId = claimed.claimId;
|
|
19430
|
+
return prompt;
|
|
19431
|
+
} catch (err) {
|
|
19432
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
19433
|
+
throw err;
|
|
19434
|
+
}
|
|
19435
|
+
}
|
|
19436
|
+
/**
|
|
19437
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
19438
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
19439
|
+
* run loop; safe to call when nothing is claimed.
|
|
19440
|
+
*/
|
|
19441
|
+
async settleClaimedResume(succeeded) {
|
|
19442
|
+
const claimId = this.claimedResumeId;
|
|
19443
|
+
if (!claimId) return;
|
|
19444
|
+
this.claimedResumeId = void 0;
|
|
19445
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
19446
|
+
await this.resumeStore?.settle(claimId);
|
|
19447
|
+
} else {
|
|
19448
|
+
await this.resumeStore?.release(claimId);
|
|
19449
|
+
}
|
|
19450
|
+
}
|
|
19451
|
+
/**
|
|
19452
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
19453
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
19454
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
19455
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
19456
|
+
*/
|
|
19457
|
+
async releaseClaimedResume() {
|
|
19458
|
+
const claimId = this.claimedResumeId;
|
|
19459
|
+
if (!claimId) return;
|
|
19460
|
+
this.claimedResumeId = void 0;
|
|
19461
|
+
await this.resumeStore?.release(claimId);
|
|
19462
|
+
}
|
|
19463
|
+
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
19464
|
+
raiseBudgetForResume(maxTokens) {
|
|
19465
|
+
const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
18588
19466
|
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
18589
19467
|
this.router.setMaxTokensPerRun(raised);
|
|
18590
|
-
|
|
18591
|
-
|
|
18592
|
-
|
|
18593
|
-
|
|
19468
|
+
}
|
|
19469
|
+
/**
|
|
19470
|
+
* The resume prompt. Completed sections are named explicitly rather than
|
|
19471
|
+
* gestured at: "do not recreate the files" is an instruction a planner can
|
|
19472
|
+
* only guess at, whereas a list of what is already done, and what it produced,
|
|
19473
|
+
* is something it can act on. That difference is the entire reason node
|
|
19474
|
+
* results are kept — a re-plan that cannot see the finished work will plan the
|
|
19475
|
+
* finished work again, and the user pays for it a second time.
|
|
19476
|
+
*/
|
|
19477
|
+
buildResumePrompt(prompt, partialOutput, completed) {
|
|
19478
|
+
const done = summarizeCompleted(completed);
|
|
19479
|
+
return [
|
|
19480
|
+
"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.",
|
|
19481
|
+
"",
|
|
19482
|
+
`Original task: ${prompt}`,
|
|
19483
|
+
done ? `
|
|
19484
|
+
${done}` : "",
|
|
19485
|
+
partialOutput ? `
|
|
18594
19486
|
Partial result so far:
|
|
18595
|
-
${
|
|
19487
|
+
${partialOutput}` : ""
|
|
19488
|
+
].filter(Boolean).join("\n");
|
|
18596
19489
|
}
|
|
18597
19490
|
/**
|
|
18598
19491
|
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
18599
19492
|
* Returns null when there is nothing to resume.
|
|
18600
19493
|
*/
|
|
18601
19494
|
async resumeRun(opts = {}) {
|
|
18602
|
-
const prompt = this.prepareResume(opts);
|
|
19495
|
+
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
18603
19496
|
if (!prompt) return null;
|
|
18604
|
-
|
|
19497
|
+
try {
|
|
19498
|
+
return await this.run({ prompt });
|
|
19499
|
+
} catch (err) {
|
|
19500
|
+
await this.releaseClaimedResume();
|
|
19501
|
+
throw err;
|
|
19502
|
+
}
|
|
18605
19503
|
}
|
|
18606
19504
|
getWorkspacePath() {
|
|
18607
19505
|
return this.workspacePath;
|
|
@@ -18970,6 +19868,10 @@ ${prompt}` : prompt;
|
|
|
18970
19868
|
const startMs = Date.now();
|
|
18971
19869
|
const taskId = crypto.randomUUID();
|
|
18972
19870
|
this.decisionLog = [];
|
|
19871
|
+
this.currentRunCompleted = [];
|
|
19872
|
+
this.wroteCheckpointThisRun = false;
|
|
19873
|
+
this.completedNormally = false;
|
|
19874
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
18973
19875
|
if (options.fastAnswer) {
|
|
18974
19876
|
return this.runFastAnswer(options, startMs, taskId);
|
|
18975
19877
|
}
|
|
@@ -19084,6 +19986,11 @@ ${prompt}` : prompt;
|
|
|
19084
19986
|
});
|
|
19085
19987
|
tier.on("log", (e) => this.emit("log", e));
|
|
19086
19988
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
19989
|
+
tier.on("section:complete", (e) => {
|
|
19990
|
+
if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
|
|
19991
|
+
this.currentRunCompleted.push(e);
|
|
19992
|
+
this.emit("section:complete", e);
|
|
19993
|
+
});
|
|
19087
19994
|
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
19088
19995
|
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
19089
19996
|
tier.on("tool:approval-request", async (request) => {
|
|
@@ -19230,6 +20137,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19230
20137
|
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
19231
20138
|
const trip = runBreaker.reason();
|
|
19232
20139
|
if (trip) {
|
|
20140
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
|
|
19233
20141
|
finalOutput = [
|
|
19234
20142
|
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
19235
20143
|
"",
|
|
@@ -19242,6 +20150,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19242
20150
|
${finalOutput}` : ""
|
|
19243
20151
|
].join("\n").trimEnd();
|
|
19244
20152
|
}
|
|
20153
|
+
this.completedNormally = trip == null;
|
|
19245
20154
|
} catch (err) {
|
|
19246
20155
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
19247
20156
|
this.emit("run:cancelled", {
|
|
@@ -19249,6 +20158,13 @@ ${finalOutput}` : ""
|
|
|
19249
20158
|
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
19250
20159
|
partialOutput: finalOutput || ""
|
|
19251
20160
|
});
|
|
20161
|
+
await this.checkpointRun(
|
|
20162
|
+
taskId,
|
|
20163
|
+
options.prompt,
|
|
20164
|
+
finalOutput || "",
|
|
20165
|
+
"cancelled",
|
|
20166
|
+
err instanceof Error ? err.message : "Task cancelled"
|
|
20167
|
+
);
|
|
19252
20168
|
runError = null;
|
|
19253
20169
|
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
19254
20170
|
this.emit("run:budget-exceeded", {
|
|
@@ -19257,9 +20173,17 @@ ${finalOutput}` : ""
|
|
|
19257
20173
|
partialOutput: finalOutput || ""
|
|
19258
20174
|
});
|
|
19259
20175
|
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
20176
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
|
|
19260
20177
|
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
19261
20178
|
runError = null;
|
|
19262
20179
|
} else {
|
|
20180
|
+
await this.checkpointRun(
|
|
20181
|
+
taskId,
|
|
20182
|
+
options.prompt,
|
|
20183
|
+
finalOutput || "",
|
|
20184
|
+
"error",
|
|
20185
|
+
err instanceof Error ? err.message : String(err)
|
|
20186
|
+
);
|
|
19263
20187
|
runError = err;
|
|
19264
20188
|
throw err;
|
|
19265
20189
|
}
|
|
@@ -19268,6 +20192,10 @@ ${finalOutput}` : ""
|
|
|
19268
20192
|
escalator.cancelAllPending();
|
|
19269
20193
|
} catch {
|
|
19270
20194
|
}
|
|
20195
|
+
try {
|
|
20196
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
20197
|
+
} catch {
|
|
20198
|
+
}
|
|
19271
20199
|
try {
|
|
19272
20200
|
this.releasePendingEscalations();
|
|
19273
20201
|
} catch {
|
|
@@ -19471,14 +20399,14 @@ ${finalOutput}` : ""
|
|
|
19471
20399
|
init_constants();
|
|
19472
20400
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
19473
20401
|
function globalDir(homeDir = os10__default.default.homedir()) {
|
|
19474
|
-
return
|
|
20402
|
+
return path34__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
19475
20403
|
}
|
|
19476
20404
|
function cloudSessionPath(dir = globalDir()) {
|
|
19477
|
-
return
|
|
20405
|
+
return path34__default.default.join(dir, CLOUD_SESSION_FILE);
|
|
19478
20406
|
}
|
|
19479
20407
|
function loadCloudSession(dir = globalDir()) {
|
|
19480
20408
|
try {
|
|
19481
|
-
const raw =
|
|
20409
|
+
const raw = fs31__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
19482
20410
|
const parsed = JSON.parse(raw);
|
|
19483
20411
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
19484
20412
|
return parsed;
|
|
@@ -19487,17 +20415,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
19487
20415
|
}
|
|
19488
20416
|
}
|
|
19489
20417
|
function saveCloudSession(session, dir = globalDir()) {
|
|
19490
|
-
|
|
20418
|
+
fs31__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19491
20419
|
const file = cloudSessionPath(dir);
|
|
19492
|
-
|
|
20420
|
+
fs31__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
19493
20421
|
try {
|
|
19494
|
-
|
|
20422
|
+
fs31__default.default.chmodSync(file, 384);
|
|
19495
20423
|
} catch {
|
|
19496
20424
|
}
|
|
19497
20425
|
}
|
|
19498
20426
|
function clearCloudSession(dir = globalDir()) {
|
|
19499
20427
|
try {
|
|
19500
|
-
|
|
20428
|
+
fs31__default.default.rmSync(cloudSessionPath(dir));
|
|
19501
20429
|
} catch {
|
|
19502
20430
|
}
|
|
19503
20431
|
}
|
|
@@ -19669,16 +20597,16 @@ var CloudClient = class _CloudClient {
|
|
|
19669
20597
|
return (await this.refresh()).accessToken;
|
|
19670
20598
|
}
|
|
19671
20599
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
19672
|
-
async authed(
|
|
19673
|
-
const call = (token) => fetch(this.url(
|
|
20600
|
+
async authed(path39, init = {}) {
|
|
20601
|
+
const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
19674
20602
|
let res = await call(await this.accessToken());
|
|
19675
20603
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
19676
20604
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
19677
20605
|
const text = await res.text();
|
|
19678
20606
|
return text ? JSON.parse(text) : {};
|
|
19679
20607
|
}
|
|
19680
|
-
authedGet(
|
|
19681
|
-
return this.authed(
|
|
20608
|
+
authedGet(path39) {
|
|
20609
|
+
return this.authed(path39);
|
|
19682
20610
|
}
|
|
19683
20611
|
async fetchMe(token) {
|
|
19684
20612
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -20458,7 +21386,8 @@ function AgentTreeInternal({
|
|
|
20458
21386
|
] }),
|
|
20459
21387
|
node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20460
21388
|
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
|
|
20461
|
-
node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" })
|
|
21389
|
+
node.status === "ESCALATED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.warning, children: " \u25B2" }),
|
|
21390
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20462
21391
|
] }, `t2-${node.id}-${i}`);
|
|
20463
21392
|
}
|
|
20464
21393
|
if (row.type === "t3") {
|
|
@@ -20483,7 +21412,8 @@ function AgentTreeInternal({
|
|
|
20483
21412
|
/* @__PURE__ */ jsxRuntime.jsx(Spinner__default.default, { type: "dots" })
|
|
20484
21413
|
] }),
|
|
20485
21414
|
node.status === "COMPLETED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20486
|
-
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" })
|
|
21415
|
+
node.status === "FAILED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.error, children: " \u2718" }),
|
|
21416
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20487
21417
|
] }, `t3-${node.id}-${i}`);
|
|
20488
21418
|
}
|
|
20489
21419
|
return null;
|
|
@@ -21453,9 +22383,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21453
22383
|
}
|
|
21454
22384
|
} catch {
|
|
21455
22385
|
}
|
|
21456
|
-
const configPath =
|
|
22386
|
+
const configPath = path34__default.default.join(workspacePath, ".cascade", "config.json");
|
|
21457
22387
|
try {
|
|
21458
|
-
await fs11__default.default.mkdir(
|
|
22388
|
+
await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
|
|
21459
22389
|
await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
21460
22390
|
} catch (err) {
|
|
21461
22391
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -21482,7 +22412,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21482
22412
|
}, []);
|
|
21483
22413
|
const recordNodeEvent = React2.useCallback((event) => {
|
|
21484
22414
|
const existing = treeNodesRef.current.get(event.tierId);
|
|
21485
|
-
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, children: [] };
|
|
22415
|
+
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, nodeId: event.nodeId ?? existing?.nodeId, dependsOn: event.dependsOn ?? existing?.dependsOn, waveId: event.waveId ?? existing?.waveId, blockedBy: event.blockedBy ?? existing?.blockedBy, children: [] };
|
|
21486
22416
|
treeNodesRef.current.set(event.tierId, node);
|
|
21487
22417
|
const store = storeRef.current;
|
|
21488
22418
|
if (store) {
|
|
@@ -21507,9 +22437,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21507
22437
|
if (msg.includes("non-text parts")) return;
|
|
21508
22438
|
originalLog(...args);
|
|
21509
22439
|
};
|
|
21510
|
-
const store = new MemoryStore(
|
|
22440
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
21511
22441
|
storeRef.current = store;
|
|
21512
|
-
globalStoreRef.current = new MemoryStore(
|
|
22442
|
+
globalStoreRef.current = new MemoryStore(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
21513
22443
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
21514
22444
|
setIdentities(identityRows);
|
|
21515
22445
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -21577,7 +22507,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21577
22507
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
21578
22508
|
onExport: async (fmt) => {
|
|
21579
22509
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
21580
|
-
const exportPath =
|
|
22510
|
+
const exportPath = path34__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
21581
22511
|
if (fmt === "json") {
|
|
21582
22512
|
await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
21583
22513
|
} else {
|
|
@@ -21819,11 +22749,21 @@ ${lastUser.content}`;
|
|
|
21819
22749
|
onContinue: async (args) => {
|
|
21820
22750
|
const cascade = cascadeRef.current;
|
|
21821
22751
|
if (!cascade) return "Not ready yet.";
|
|
21822
|
-
if (
|
|
22752
|
+
if (state.isExecuting) {
|
|
22753
|
+
return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
|
|
22754
|
+
}
|
|
21823
22755
|
const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
|
|
21824
|
-
const
|
|
21825
|
-
|
|
21826
|
-
|
|
22756
|
+
const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
|
|
22757
|
+
const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
|
|
22758
|
+
if (!prompt) {
|
|
22759
|
+
return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
|
|
22760
|
+
}
|
|
22761
|
+
try {
|
|
22762
|
+
await handleSubmit(prompt);
|
|
22763
|
+
} catch (err) {
|
|
22764
|
+
await cascade.releaseClaimedResume();
|
|
22765
|
+
throw err;
|
|
22766
|
+
}
|
|
21827
22767
|
return "";
|
|
21828
22768
|
}
|
|
21829
22769
|
});
|
|
@@ -22475,7 +23415,7 @@ function stringifySlashOutput(val) {
|
|
|
22475
23415
|
}
|
|
22476
23416
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
22477
23417
|
if (!query) return "Usage: /search <query>";
|
|
22478
|
-
const dbPath =
|
|
23418
|
+
const dbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
22479
23419
|
try {
|
|
22480
23420
|
await fs11__default.default.access(dbPath);
|
|
22481
23421
|
} catch {
|
|
@@ -22509,7 +23449,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
22509
23449
|
async function diagnoseRuntime(config, workspacePath) {
|
|
22510
23450
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
22511
23451
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
22512
|
-
const store = new MemoryStore(
|
|
23452
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
22513
23453
|
try {
|
|
22514
23454
|
const sessions2 = store.listSessions(void 0, 3);
|
|
22515
23455
|
return [
|
|
@@ -22528,7 +23468,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
22528
23468
|
}
|
|
22529
23469
|
async function showRecentLogs(args, workspacePath) {
|
|
22530
23470
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
22531
|
-
const store = new MemoryStore(
|
|
23471
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
22532
23472
|
try {
|
|
22533
23473
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
22534
23474
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -22540,7 +23480,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
22540
23480
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
22541
23481
|
const sessionId = args[0];
|
|
22542
23482
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
22543
|
-
const store = new MemoryStore(
|
|
23483
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
22544
23484
|
try {
|
|
22545
23485
|
const session = store.getSession(sessionId);
|
|
22546
23486
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -22968,9 +23908,9 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
22968
23908
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
22969
23909
|
};
|
|
22970
23910
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
22971
|
-
const configDir =
|
|
23911
|
+
const configDir = path34__default.default.join(workspacePath, ".cascade");
|
|
22972
23912
|
await fs11__default.default.mkdir(configDir, { recursive: true });
|
|
22973
|
-
const configPath =
|
|
23913
|
+
const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22974
23914
|
await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
22975
23915
|
savedConfigRef.current = config;
|
|
22976
23916
|
dispatchRef.current({ type: "GO_DONE" });
|
|
@@ -23312,14 +24252,14 @@ function printTelemetryBanner() {
|
|
|
23312
24252
|
async function initCommand(workspacePath = process.cwd()) {
|
|
23313
24253
|
const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
23314
24254
|
try {
|
|
23315
|
-
const configDir =
|
|
24255
|
+
const configDir = path34__default.default.join(workspacePath, ".cascade");
|
|
23316
24256
|
await fs11__default.default.mkdir(configDir, { recursive: true });
|
|
23317
|
-
const mdPath =
|
|
24257
|
+
const mdPath = path34__default.default.join(workspacePath, "CASCADE.md");
|
|
23318
24258
|
if (!await fileExists(mdPath)) {
|
|
23319
24259
|
await createDefaultCascadeMd(workspacePath);
|
|
23320
24260
|
spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
|
|
23321
24261
|
}
|
|
23322
|
-
const ignorePath =
|
|
24262
|
+
const ignorePath = path34__default.default.join(workspacePath, ".cascadeignore");
|
|
23323
24263
|
if (!await fileExists(ignorePath)) {
|
|
23324
24264
|
await createDefaultIgnoreFile(workspacePath);
|
|
23325
24265
|
spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
|
|
@@ -23328,7 +24268,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
23328
24268
|
console.log();
|
|
23329
24269
|
console.log(chalk7__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
23330
24270
|
console.log();
|
|
23331
|
-
const configPath =
|
|
24271
|
+
const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
23332
24272
|
if (await fileExists(configPath)) {
|
|
23333
24273
|
console.log(chalk7__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
23334
24274
|
console.log();
|
|
@@ -23386,7 +24326,7 @@ function fromEnv(env) {
|
|
|
23386
24326
|
return out;
|
|
23387
24327
|
}
|
|
23388
24328
|
async function fromClaudeCode(home) {
|
|
23389
|
-
const file =
|
|
24329
|
+
const file = path34__default.default.join(home, ".claude", ".credentials.json");
|
|
23390
24330
|
const data = await readJson(file);
|
|
23391
24331
|
if (!data) return [];
|
|
23392
24332
|
const oauth = data["claudeAiOauth"];
|
|
@@ -23409,7 +24349,7 @@ async function fromClaudeCode(home) {
|
|
|
23409
24349
|
return [];
|
|
23410
24350
|
}
|
|
23411
24351
|
async function fromCodex(home) {
|
|
23412
|
-
const file =
|
|
24352
|
+
const file = path34__default.default.join(home, ".codex", "auth.json");
|
|
23413
24353
|
const data = await readJson(file);
|
|
23414
24354
|
if (!data) return [];
|
|
23415
24355
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -23432,7 +24372,7 @@ async function fromCodex(home) {
|
|
|
23432
24372
|
return [];
|
|
23433
24373
|
}
|
|
23434
24374
|
async function fromGemini(home) {
|
|
23435
|
-
const file =
|
|
24375
|
+
const file = path34__default.default.join(home, ".gemini", "oauth_creds.json");
|
|
23436
24376
|
const data = await readJson(file);
|
|
23437
24377
|
const accessToken = str(data?.["access_token"]);
|
|
23438
24378
|
if (!accessToken) return [];
|
|
@@ -23448,7 +24388,7 @@ async function fromGemini(home) {
|
|
|
23448
24388
|
}
|
|
23449
24389
|
async function fromCopilot(home) {
|
|
23450
24390
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
23451
|
-
const file =
|
|
24391
|
+
const file = path34__default.default.join(home, ".config", "github-copilot", name);
|
|
23452
24392
|
const data = await readJson(file);
|
|
23453
24393
|
if (!data) continue;
|
|
23454
24394
|
for (const value of Object.values(data)) {
|
|
@@ -23502,7 +24442,7 @@ async function doctorCommand() {
|
|
|
23502
24442
|
checks.push({
|
|
23503
24443
|
label: "Cascade config",
|
|
23504
24444
|
ok: true,
|
|
23505
|
-
detail: `Loaded ${
|
|
24445
|
+
detail: `Loaded ${path34__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
23506
24446
|
});
|
|
23507
24447
|
const providers = [
|
|
23508
24448
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -23585,17 +24525,17 @@ async function doctorCommand() {
|
|
|
23585
24525
|
const detail = c.detail ? chalk7__default.default.gray(` \u2014 ${c.detail}`) : "";
|
|
23586
24526
|
console.log(`${icon} ${label}${detail}`);
|
|
23587
24527
|
}
|
|
23588
|
-
const
|
|
24528
|
+
const failures2 = checks.filter((c) => !c.ok);
|
|
23589
24529
|
console.log();
|
|
23590
|
-
if (
|
|
24530
|
+
if (failures2.length === 0) {
|
|
23591
24531
|
console.log(chalk7__default.default.green(" All checks passed!\n"));
|
|
23592
24532
|
} else {
|
|
23593
|
-
const critical =
|
|
24533
|
+
const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
|
|
23594
24534
|
if (critical.length) {
|
|
23595
24535
|
console.log(chalk7__default.default.yellow(` ${critical.length} issue(s) need attention.
|
|
23596
24536
|
`));
|
|
23597
24537
|
} else {
|
|
23598
|
-
console.log(chalk7__default.default.gray(` ${
|
|
24538
|
+
console.log(chalk7__default.default.gray(` ${failures2.length} optional item(s) not configured.
|
|
23599
24539
|
`));
|
|
23600
24540
|
}
|
|
23601
24541
|
}
|
|
@@ -23613,7 +24553,7 @@ async function checkEndpoint(url) {
|
|
|
23613
24553
|
}
|
|
23614
24554
|
}
|
|
23615
24555
|
async function indexCommand(dirPath) {
|
|
23616
|
-
const workspace =
|
|
24556
|
+
const workspace = path34__default.default.resolve(dirPath || process.cwd());
|
|
23617
24557
|
const cm = new ConfigManager(workspace);
|
|
23618
24558
|
await cm.load();
|
|
23619
24559
|
const config = cm.getConfig();
|
|
@@ -23624,8 +24564,8 @@ async function indexCommand(dirPath) {
|
|
|
23624
24564
|
process.exitCode = 1;
|
|
23625
24565
|
return;
|
|
23626
24566
|
}
|
|
23627
|
-
const dbPath = config.codeIndex?.dbPath ||
|
|
23628
|
-
|
|
24567
|
+
const dbPath = config.codeIndex?.dbPath || path34__default.default.join(workspace, ".cascade", "code-index.db");
|
|
24568
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
|
|
23629
24569
|
const db = new Database2__default.default(dbPath);
|
|
23630
24570
|
db.pragma("journal_mode = WAL");
|
|
23631
24571
|
const complete = chatCompleterFromProviders(config.providers);
|
|
@@ -24114,7 +25054,7 @@ function openBrowser2(url) {
|
|
|
24114
25054
|
}
|
|
24115
25055
|
function storePathFor(name) {
|
|
24116
25056
|
const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
|
|
24117
|
-
return
|
|
25057
|
+
return path34__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
|
|
24118
25058
|
}
|
|
24119
25059
|
function defaultName(url) {
|
|
24120
25060
|
try {
|
|
@@ -24316,8 +25256,14 @@ var DashboardSocket = class {
|
|
|
24316
25256
|
emitCascadeEvent(ev) {
|
|
24317
25257
|
this.io.emit("cascade:event", ev);
|
|
24318
25258
|
}
|
|
24319
|
-
|
|
24320
|
-
|
|
25259
|
+
/**
|
|
25260
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
25261
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
25262
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
25263
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
25264
|
+
*/
|
|
25265
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
25266
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
24321
25267
|
this.io.emit("tier:status", payload);
|
|
24322
25268
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
24323
25269
|
}
|
|
@@ -24616,7 +25562,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
24616
25562
|
}
|
|
24617
25563
|
|
|
24618
25564
|
// src/dashboard/server.ts
|
|
24619
|
-
var __dirname$1 =
|
|
25565
|
+
var __dirname$1 = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
24620
25566
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
24621
25567
|
var DashboardServer = class {
|
|
24622
25568
|
app;
|
|
@@ -24896,14 +25842,14 @@ var DashboardServer = class {
|
|
|
24896
25842
|
*/
|
|
24897
25843
|
persistConfig() {
|
|
24898
25844
|
try {
|
|
24899
|
-
const configPath =
|
|
24900
|
-
|
|
24901
|
-
|
|
25845
|
+
const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
25846
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(configPath), { recursive: true });
|
|
25847
|
+
fs31__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
24902
25848
|
} catch (err) {
|
|
24903
25849
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
24904
25850
|
}
|
|
24905
25851
|
try {
|
|
24906
|
-
saveGlobalCredentials(
|
|
25852
|
+
saveGlobalCredentials(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
24907
25853
|
} catch (err) {
|
|
24908
25854
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
24909
25855
|
}
|
|
@@ -24918,15 +25864,15 @@ var DashboardServer = class {
|
|
|
24918
25864
|
resolveDashboardSecret() {
|
|
24919
25865
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
24920
25866
|
if (fromConfig) return fromConfig;
|
|
24921
|
-
const secretPath =
|
|
25867
|
+
const secretPath = path34__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
24922
25868
|
try {
|
|
24923
|
-
if (
|
|
24924
|
-
const existing =
|
|
25869
|
+
if (fs31__default.default.existsSync(secretPath)) {
|
|
25870
|
+
const existing = fs31__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
24925
25871
|
if (existing.length >= 16) return existing;
|
|
24926
25872
|
}
|
|
24927
25873
|
const generated = crypto.randomUUID();
|
|
24928
|
-
|
|
24929
|
-
|
|
25874
|
+
fs31__default.default.mkdirSync(path34__default.default.dirname(secretPath), { recursive: true });
|
|
25875
|
+
fs31__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
24930
25876
|
if (this.config.dashboard.auth) {
|
|
24931
25877
|
console.warn(
|
|
24932
25878
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -24953,7 +25899,7 @@ var DashboardServer = class {
|
|
|
24953
25899
|
// ── Setup ─────────────────────────────────────
|
|
24954
25900
|
getGlobalStore() {
|
|
24955
25901
|
if (!this.globalStore) {
|
|
24956
|
-
const globalDbPath =
|
|
25902
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24957
25903
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
24958
25904
|
}
|
|
24959
25905
|
return this.globalStore;
|
|
@@ -25333,12 +26279,12 @@ ${prompt}`;
|
|
|
25333
26279
|
}
|
|
25334
26280
|
}
|
|
25335
26281
|
watchRuntimeChanges() {
|
|
25336
|
-
const workspaceDbPath =
|
|
25337
|
-
const globalDbPath =
|
|
26282
|
+
const workspaceDbPath = path34__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
26283
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25338
26284
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
25339
26285
|
for (const watchPath of watchPaths) {
|
|
25340
|
-
if (!
|
|
25341
|
-
|
|
26286
|
+
if (!fs31__default.default.existsSync(watchPath)) continue;
|
|
26287
|
+
fs31__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
25342
26288
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
25343
26289
|
});
|
|
25344
26290
|
}
|
|
@@ -25460,7 +26406,7 @@ ${prompt}`;
|
|
|
25460
26406
|
const sessionId = req.params.id;
|
|
25461
26407
|
this.store.deleteSession(sessionId);
|
|
25462
26408
|
this.store.deleteRuntimeSession(sessionId);
|
|
25463
|
-
const globalDbPath =
|
|
26409
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25464
26410
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25465
26411
|
try {
|
|
25466
26412
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -25553,7 +26499,7 @@ ${prompt}`;
|
|
|
25553
26499
|
});
|
|
25554
26500
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
25555
26501
|
const body = req.body;
|
|
25556
|
-
const globalDbPath =
|
|
26502
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25557
26503
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
25558
26504
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25559
26505
|
try {
|
|
@@ -25576,7 +26522,7 @@ ${prompt}`;
|
|
|
25576
26522
|
});
|
|
25577
26523
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
25578
26524
|
this.store.deleteAllRuntimeNodes();
|
|
25579
|
-
const globalDbPath =
|
|
26525
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25580
26526
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25581
26527
|
try {
|
|
25582
26528
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -25662,12 +26608,12 @@ ${prompt}`;
|
|
|
25662
26608
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
25663
26609
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
25664
26610
|
try {
|
|
25665
|
-
const configPath =
|
|
25666
|
-
const existing =
|
|
26611
|
+
const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
26612
|
+
const existing = fs31__default.default.existsSync(configPath) ? JSON.parse(fs31__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
25667
26613
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
25668
26614
|
const tmp = configPath + ".tmp";
|
|
25669
|
-
|
|
25670
|
-
|
|
26615
|
+
fs31__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
26616
|
+
fs31__default.default.renameSync(tmp, configPath);
|
|
25671
26617
|
res.json({ ok: true });
|
|
25672
26618
|
} catch (err) {
|
|
25673
26619
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -25695,7 +26641,7 @@ ${prompt}`;
|
|
|
25695
26641
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
25696
26642
|
const scope = req.query["scope"] ?? "workspace";
|
|
25697
26643
|
if (scope === "global") {
|
|
25698
|
-
const globalDbPath =
|
|
26644
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25699
26645
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25700
26646
|
try {
|
|
25701
26647
|
res.json({
|
|
@@ -26036,13 +26982,13 @@ ${prompt}`;
|
|
|
26036
26982
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
26037
26983
|
}
|
|
26038
26984
|
});
|
|
26039
|
-
const prodPath =
|
|
26040
|
-
const devPath =
|
|
26041
|
-
const webDistPath =
|
|
26042
|
-
if (
|
|
26985
|
+
const prodPath = path34__default.default.resolve(__dirname$1, "../web/dist");
|
|
26986
|
+
const devPath = path34__default.default.resolve(__dirname$1, "../../web/dist");
|
|
26987
|
+
const webDistPath = fs31__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
26988
|
+
if (fs31__default.default.existsSync(webDistPath)) {
|
|
26043
26989
|
this.app.use(express__default.default.static(webDistPath));
|
|
26044
26990
|
this.app.get("*", (_req, res) => {
|
|
26045
|
-
res.sendFile(
|
|
26991
|
+
res.sendFile(path34__default.default.join(webDistPath, "index.html"));
|
|
26046
26992
|
});
|
|
26047
26993
|
} else {
|
|
26048
26994
|
this.app.get("/", (_req, res) => {
|
|
@@ -26063,7 +27009,7 @@ init_constants();
|
|
|
26063
27009
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
26064
27010
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
26065
27011
|
const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
26066
|
-
const store = new MemoryStore(
|
|
27012
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
26067
27013
|
const server = new DashboardServer(config, store, workspacePath);
|
|
26068
27014
|
server.watchRuntimeChanges();
|
|
26069
27015
|
const gThis = globalThis;
|
|
@@ -26095,7 +27041,7 @@ init_constants();
|
|
|
26095
27041
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
26096
27042
|
const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
|
|
26097
27043
|
identity.command("list").description("List all available identities").action(() => {
|
|
26098
|
-
const store = new MemoryStore(
|
|
27044
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
26099
27045
|
try {
|
|
26100
27046
|
const identities = store.listIdentities();
|
|
26101
27047
|
console.log(chalk7__default.default.bold("\n Identities:"));
|
|
@@ -26115,7 +27061,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
26115
27061
|
}
|
|
26116
27062
|
});
|
|
26117
27063
|
identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
|
|
26118
|
-
const store = new MemoryStore(
|
|
27064
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
26119
27065
|
try {
|
|
26120
27066
|
if (options.default) {
|
|
26121
27067
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -26141,7 +27087,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
26141
27087
|
}
|
|
26142
27088
|
});
|
|
26143
27089
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
26144
|
-
const store = new MemoryStore(
|
|
27090
|
+
const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
26145
27091
|
try {
|
|
26146
27092
|
const identities = store.listIdentities();
|
|
26147
27093
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -26290,8 +27236,8 @@ async function exportCommand(options = {}) {
|
|
|
26290
27236
|
let store;
|
|
26291
27237
|
try {
|
|
26292
27238
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
26293
|
-
const workspaceDbPath =
|
|
26294
|
-
const globalDbPath =
|
|
27239
|
+
const workspaceDbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
27240
|
+
const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
26295
27241
|
let dbPath = globalDbPath;
|
|
26296
27242
|
try {
|
|
26297
27243
|
await fs11__default.default.access(workspaceDbPath);
|
|
@@ -26333,7 +27279,7 @@ async function exportCommand(options = {}) {
|
|
|
26333
27279
|
const ext = format === "json" ? ".json" : ".md";
|
|
26334
27280
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
26335
27281
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
26336
|
-
const outPath = options.output ?
|
|
27282
|
+
const outPath = options.output ? path34__default.default.resolve(options.output) : path34__default.default.join(process.cwd(), defaultFile);
|
|
26337
27283
|
await fs11__default.default.writeFile(outPath, content, "utf-8");
|
|
26338
27284
|
spin.succeed(chalk7__default.default.green(`Exported to ${chalk7__default.default.white(outPath)}`));
|
|
26339
27285
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
@@ -26561,11 +27507,11 @@ async function statsCommand() {
|
|
|
26561
27507
|
dotenv__default.default.config();
|
|
26562
27508
|
function warnIfBuildIsStale() {
|
|
26563
27509
|
try {
|
|
26564
|
-
const here =
|
|
27510
|
+
const here = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
26565
27511
|
for (const rel of ["..", "../.."]) {
|
|
26566
|
-
const pkgPath =
|
|
26567
|
-
if (!
|
|
26568
|
-
const pkg = JSON.parse(
|
|
27512
|
+
const pkgPath = path34__default.default.join(here, rel, "package.json");
|
|
27513
|
+
if (!fs31__default.default.existsSync(pkgPath)) continue;
|
|
27514
|
+
const pkg = JSON.parse(fs31__default.default.readFileSync(pkgPath, "utf-8"));
|
|
26569
27515
|
if (pkg.name !== "cascade-ai") continue;
|
|
26570
27516
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
26571
27517
|
console.error(
|