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.js
CHANGED
|
@@ -7,8 +7,8 @@ import https from 'https';
|
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
9
|
import Database2 from 'better-sqlite3';
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path34, { dirname } from 'path';
|
|
11
|
+
import fs31, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
|
|
12
12
|
import os10 from 'os';
|
|
13
13
|
import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
|
|
14
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
@@ -1757,7 +1757,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1757
1757
|
var init_constants = __esm({
|
|
1758
1758
|
"src/constants.ts"() {
|
|
1759
1759
|
init_pricing();
|
|
1760
|
-
CASCADE_VERSION = "0.
|
|
1760
|
+
CASCADE_VERSION = "0.69.0";
|
|
1761
1761
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1762
1762
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1763
1763
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3879,12 +3879,12 @@ var init_audit_logger = __esm({
|
|
|
3879
3879
|
constructor(workspacePath, debugMode = false) {
|
|
3880
3880
|
this.workspacePath = workspacePath;
|
|
3881
3881
|
this.debugMode = debugMode;
|
|
3882
|
-
const cascadeDir =
|
|
3883
|
-
if (!
|
|
3884
|
-
|
|
3882
|
+
const cascadeDir = path34.join(workspacePath, ".cascade");
|
|
3883
|
+
if (!fs31.existsSync(cascadeDir)) {
|
|
3884
|
+
fs31.mkdirSync(cascadeDir, { recursive: true });
|
|
3885
3885
|
}
|
|
3886
|
-
this.keyPath =
|
|
3887
|
-
this.dbPath =
|
|
3886
|
+
this.keyPath = path34.join(cascadeDir, "audit_log.key");
|
|
3887
|
+
this.dbPath = path34.join(cascadeDir, "audit_log.db");
|
|
3888
3888
|
this.initEncryptionKey();
|
|
3889
3889
|
this.db = new Database2(this.dbPath);
|
|
3890
3890
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3914,11 +3914,11 @@ var init_audit_logger = __esm({
|
|
|
3914
3914
|
return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
3915
3915
|
}
|
|
3916
3916
|
initEncryptionKey() {
|
|
3917
|
-
if (
|
|
3918
|
-
this.encryptionKey =
|
|
3917
|
+
if (fs31.existsSync(this.keyPath)) {
|
|
3918
|
+
this.encryptionKey = fs31.readFileSync(this.keyPath);
|
|
3919
3919
|
} else {
|
|
3920
3920
|
this.encryptionKey = crypto.randomBytes(32);
|
|
3921
|
-
|
|
3921
|
+
fs31.writeFileSync(this.keyPath, this.encryptionKey);
|
|
3922
3922
|
}
|
|
3923
3923
|
}
|
|
3924
3924
|
encrypt(text) {
|
|
@@ -3996,9 +3996,9 @@ var init_audit_logger = __esm({
|
|
|
3996
3996
|
dumpDebugIfNeeded() {
|
|
3997
3997
|
if (!this.debugMode) return;
|
|
3998
3998
|
try {
|
|
3999
|
-
const dumpPath =
|
|
3999
|
+
const dumpPath = path34.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
|
|
4000
4000
|
const entries = this.getAllLogs();
|
|
4001
|
-
|
|
4001
|
+
fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
4002
4002
|
} catch (err) {
|
|
4003
4003
|
console.error("Failed to dump debug audit logs", err);
|
|
4004
4004
|
}
|
|
@@ -4053,7 +4053,7 @@ var Keystore = class {
|
|
|
4053
4053
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
4054
4054
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
4055
4055
|
this.backend = "keytar";
|
|
4056
|
-
if (password &&
|
|
4056
|
+
if (password && fs31.existsSync(this.storePath)) {
|
|
4057
4057
|
try {
|
|
4058
4058
|
const fileEntries = this.decryptFile(password);
|
|
4059
4059
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -4072,7 +4072,7 @@ var Keystore = class {
|
|
|
4072
4072
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
4073
4073
|
);
|
|
4074
4074
|
}
|
|
4075
|
-
if (!
|
|
4075
|
+
if (!fs31.existsSync(this.storePath)) {
|
|
4076
4076
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
4077
4077
|
this.masterKey = this.deriveKey(password, salt);
|
|
4078
4078
|
this.writeWithSalt({}, salt);
|
|
@@ -4086,7 +4086,7 @@ var Keystore = class {
|
|
|
4086
4086
|
}
|
|
4087
4087
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
4088
4088
|
unlockSync(password) {
|
|
4089
|
-
if (!
|
|
4089
|
+
if (!fs31.existsSync(this.storePath)) {
|
|
4090
4090
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
4091
4091
|
this.masterKey = this.deriveKey(password, salt);
|
|
4092
4092
|
this.writeWithSalt({}, salt);
|
|
@@ -4144,7 +4144,7 @@ var Keystore = class {
|
|
|
4144
4144
|
}
|
|
4145
4145
|
}
|
|
4146
4146
|
decryptFile(password, knownSalt) {
|
|
4147
|
-
if (!
|
|
4147
|
+
if (!fs31.existsSync(this.storePath)) return {};
|
|
4148
4148
|
try {
|
|
4149
4149
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
4150
4150
|
const useSalt = knownSalt ?? salt;
|
|
@@ -4166,8 +4166,8 @@ var Keystore = class {
|
|
|
4166
4166
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4167
4167
|
const tag = cipher.getAuthTag();
|
|
4168
4168
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
4169
|
-
|
|
4170
|
-
|
|
4169
|
+
fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
|
|
4170
|
+
fs31.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4171
4171
|
}
|
|
4172
4172
|
writeWithSalt(data, salt) {
|
|
4173
4173
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -4177,11 +4177,11 @@ var Keystore = class {
|
|
|
4177
4177
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4178
4178
|
const tag = cipher.getAuthTag();
|
|
4179
4179
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
4180
|
-
|
|
4181
|
-
|
|
4180
|
+
fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
|
|
4181
|
+
fs31.writeFileSync(this.storePath, out, { mode: 384 });
|
|
4182
4182
|
}
|
|
4183
4183
|
readRaw() {
|
|
4184
|
-
const buf =
|
|
4184
|
+
const buf = fs31.readFileSync(this.storePath);
|
|
4185
4185
|
let offset = 0;
|
|
4186
4186
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
4187
4187
|
offset += SALT_LEN;
|
|
@@ -4214,7 +4214,7 @@ var CascadeIgnore = class {
|
|
|
4214
4214
|
]);
|
|
4215
4215
|
}
|
|
4216
4216
|
async load(workspacePath) {
|
|
4217
|
-
const filePath =
|
|
4217
|
+
const filePath = path34.join(workspacePath, ".cascadeignore");
|
|
4218
4218
|
try {
|
|
4219
4219
|
const content = await fs11.readFile(filePath, "utf-8");
|
|
4220
4220
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -4225,7 +4225,7 @@ var CascadeIgnore = class {
|
|
|
4225
4225
|
}
|
|
4226
4226
|
isIgnored(filePath, workspacePath) {
|
|
4227
4227
|
try {
|
|
4228
|
-
const relative = workspacePath ?
|
|
4228
|
+
const relative = workspacePath ? path34.relative(workspacePath, filePath) : filePath;
|
|
4229
4229
|
return this.ig.ignores(relative);
|
|
4230
4230
|
} catch {
|
|
4231
4231
|
return false;
|
|
@@ -4236,7 +4236,7 @@ var CascadeIgnore = class {
|
|
|
4236
4236
|
}
|
|
4237
4237
|
};
|
|
4238
4238
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
4239
|
-
const filePath =
|
|
4239
|
+
const filePath = path34.join(workspacePath, ".cascadeignore");
|
|
4240
4240
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
4241
4241
|
# Syntax identical to .gitignore
|
|
4242
4242
|
|
|
@@ -4266,7 +4266,7 @@ Thumbs.db
|
|
|
4266
4266
|
await fs11.writeFile(filePath, content, "utf-8");
|
|
4267
4267
|
}
|
|
4268
4268
|
async function loadCascadeMd(workspacePath) {
|
|
4269
|
-
const filePath =
|
|
4269
|
+
const filePath = path34.join(workspacePath, "CASCADE.md");
|
|
4270
4270
|
try {
|
|
4271
4271
|
const raw = await fs11.readFile(filePath, "utf-8");
|
|
4272
4272
|
return parseCascadeMd(raw);
|
|
@@ -4295,7 +4295,7 @@ ${raw.trim()}`;
|
|
|
4295
4295
|
return { raw, sections, systemPrompt };
|
|
4296
4296
|
}
|
|
4297
4297
|
async function createDefaultCascadeMd(workspacePath) {
|
|
4298
|
-
const filePath =
|
|
4298
|
+
const filePath = path34.join(workspacePath, "CASCADE.md");
|
|
4299
4299
|
const content = `# Cascade Project Instructions
|
|
4300
4300
|
|
|
4301
4301
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -4330,7 +4330,7 @@ List any areas the agent should not touch.
|
|
|
4330
4330
|
var MemoryStore = class _MemoryStore {
|
|
4331
4331
|
db;
|
|
4332
4332
|
constructor(dbPath) {
|
|
4333
|
-
|
|
4333
|
+
fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
|
|
4334
4334
|
try {
|
|
4335
4335
|
this.db = new Database2(dbPath, { timeout: 5e3 });
|
|
4336
4336
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -5582,7 +5582,7 @@ function validateConfig(raw) {
|
|
|
5582
5582
|
// src/config/global-credentials.ts
|
|
5583
5583
|
init_constants();
|
|
5584
5584
|
function credentialsPath(globalDir2) {
|
|
5585
|
-
return
|
|
5585
|
+
return path34.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
5586
5586
|
}
|
|
5587
5587
|
function providerKey(p) {
|
|
5588
5588
|
if (p.type === "azure") {
|
|
@@ -5595,7 +5595,7 @@ function isPersistable(p) {
|
|
|
5595
5595
|
}
|
|
5596
5596
|
function loadGlobalCredentials(globalDir2) {
|
|
5597
5597
|
try {
|
|
5598
|
-
const raw =
|
|
5598
|
+
const raw = fs31.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
5599
5599
|
const parsed = JSON.parse(raw);
|
|
5600
5600
|
if (!Array.isArray(parsed.providers)) return [];
|
|
5601
5601
|
return parsed.providers.filter(
|
|
@@ -5607,13 +5607,13 @@ function loadGlobalCredentials(globalDir2) {
|
|
|
5607
5607
|
}
|
|
5608
5608
|
function saveGlobalCredentials(globalDir2, providers) {
|
|
5609
5609
|
const filePath = credentialsPath(globalDir2);
|
|
5610
|
-
|
|
5610
|
+
fs31.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
5611
5611
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
5612
5612
|
const tmp = `${filePath}.tmp`;
|
|
5613
|
-
|
|
5614
|
-
|
|
5613
|
+
fs31.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5614
|
+
fs31.renameSync(tmp, filePath);
|
|
5615
5615
|
try {
|
|
5616
|
-
|
|
5616
|
+
fs31.chmodSync(filePath, 384);
|
|
5617
5617
|
} catch {
|
|
5618
5618
|
}
|
|
5619
5619
|
}
|
|
@@ -5831,7 +5831,7 @@ var ConfigManager = class {
|
|
|
5831
5831
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
5832
5832
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
5833
5833
|
this.workspacePath = workspacePath;
|
|
5834
|
-
this.globalDir = globalDirOverride ??
|
|
5834
|
+
this.globalDir = globalDirOverride ?? path34.join(os10.homedir(), GLOBAL_CONFIG_DIR);
|
|
5835
5835
|
}
|
|
5836
5836
|
async load() {
|
|
5837
5837
|
this.config = await this.loadConfig();
|
|
@@ -5845,8 +5845,8 @@ var ConfigManager = class {
|
|
|
5845
5845
|
this.ignore = new CascadeIgnore();
|
|
5846
5846
|
await this.ignore.load(this.workspacePath);
|
|
5847
5847
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
5848
|
-
this.keystore = new Keystore(
|
|
5849
|
-
this.store = new MemoryStore(
|
|
5848
|
+
this.keystore = new Keystore(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
5849
|
+
this.store = new MemoryStore(path34.join(this.workspacePath, CASCADE_DB_FILE));
|
|
5850
5850
|
await this.injectEnvKeys();
|
|
5851
5851
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
5852
5852
|
await this.ensureDefaultIdentity();
|
|
@@ -5871,8 +5871,8 @@ var ConfigManager = class {
|
|
|
5871
5871
|
return this.workspacePath;
|
|
5872
5872
|
}
|
|
5873
5873
|
async save() {
|
|
5874
|
-
const configPath =
|
|
5875
|
-
await fs11.mkdir(
|
|
5874
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5875
|
+
await fs11.mkdir(path34.dirname(configPath), { recursive: true });
|
|
5876
5876
|
await fs11.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
5877
5877
|
try {
|
|
5878
5878
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
@@ -5908,7 +5908,7 @@ var ConfigManager = class {
|
|
|
5908
5908
|
return configProvider?.apiKey;
|
|
5909
5909
|
}
|
|
5910
5910
|
async loadConfig() {
|
|
5911
|
-
const configPath =
|
|
5911
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
5912
5912
|
try {
|
|
5913
5913
|
const raw = await fs11.readFile(configPath, "utf-8");
|
|
5914
5914
|
return validateConfig(JSON.parse(raw));
|
|
@@ -7075,7 +7075,7 @@ init_pricing();
|
|
|
7075
7075
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
7076
7076
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
7077
7077
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
7078
|
-
var DEFAULT_CACHE_FILE =
|
|
7078
|
+
var DEFAULT_CACHE_FILE = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
7079
7079
|
function normalizeModelId2(id) {
|
|
7080
7080
|
let s = id.toLowerCase();
|
|
7081
7081
|
const slash = s.lastIndexOf("/");
|
|
@@ -7222,7 +7222,7 @@ var LiveDataProvider = class {
|
|
|
7222
7222
|
}
|
|
7223
7223
|
async saveCache() {
|
|
7224
7224
|
try {
|
|
7225
|
-
await fs11.mkdir(
|
|
7225
|
+
await fs11.mkdir(path34.dirname(this.opts.cacheFile), { recursive: true });
|
|
7226
7226
|
const cache = {
|
|
7227
7227
|
fetchedAt: this.fetchedAt,
|
|
7228
7228
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -8482,6 +8482,21 @@ var BaseTier = class extends EventEmitter {
|
|
|
8482
8482
|
* which node (Cockpit node panel / Why panel).
|
|
8483
8483
|
*/
|
|
8484
8484
|
servingModel;
|
|
8485
|
+
/**
|
|
8486
|
+
* This tier's identity IN THE TASK GRAPH — the section or subtask id the
|
|
8487
|
+
* planner assigned — plus where it sat in the dependency order.
|
|
8488
|
+
*
|
|
8489
|
+
* `id` and `graphNodeId` are deliberately different id spaces, and both are
|
|
8490
|
+
* needed. `id` identifies this tier INSTANCE ("T2_a1b2c3d4", minted per
|
|
8491
|
+
* construction); `graphNodeId` identifies the WORK ("s1"), which is what
|
|
8492
|
+
* dependency edges point at. Without the distinction a graph view cannot be
|
|
8493
|
+
* drawn at all: `dependsOn` lists section ids, while every running tier
|
|
8494
|
+
* reports a generated tier id, so every edge would name a node that never
|
|
8495
|
+
* appears in the stream.
|
|
8496
|
+
*/
|
|
8497
|
+
graphNodeId;
|
|
8498
|
+
graphDependsOn;
|
|
8499
|
+
graphWave;
|
|
8485
8500
|
constructor(role, id, parentId) {
|
|
8486
8501
|
super();
|
|
8487
8502
|
this.role = role;
|
|
@@ -8493,6 +8508,33 @@ var BaseTier = class extends EventEmitter {
|
|
|
8493
8508
|
setPresenter(on = true) {
|
|
8494
8509
|
this.isPresenter = on;
|
|
8495
8510
|
}
|
|
8511
|
+
/**
|
|
8512
|
+
* Record where this tier sits in the compiled task graph.
|
|
8513
|
+
*
|
|
8514
|
+
* Called by whichever tier scheduled this one: the id and edges are known at
|
|
8515
|
+
* construction, the wave only once the scheduler reaches it, so the fields
|
|
8516
|
+
* are set independently rather than all at once.
|
|
8517
|
+
*/
|
|
8518
|
+
setGraphPosition(position) {
|
|
8519
|
+
if (position.nodeId !== void 0) this.graphNodeId = position.nodeId;
|
|
8520
|
+
if (position.dependsOn !== void 0) this.graphDependsOn = [...position.dependsOn];
|
|
8521
|
+
if (position.wave !== void 0) this.graphWave = position.wave;
|
|
8522
|
+
}
|
|
8523
|
+
/**
|
|
8524
|
+
* The graph fields, for spreading into a status payload.
|
|
8525
|
+
*
|
|
8526
|
+
* One helper rather than two literals because `tier:status` is emitted from
|
|
8527
|
+
* two different call sites with two different payload shapes; adding a field
|
|
8528
|
+
* to only one of them makes it arrive intermittently, which is worse for a
|
|
8529
|
+
* consumer than never arriving at all.
|
|
8530
|
+
*/
|
|
8531
|
+
graphFields() {
|
|
8532
|
+
return {
|
|
8533
|
+
nodeId: this.graphNodeId,
|
|
8534
|
+
dependsOn: this.graphDependsOn ? [...this.graphDependsOn] : void 0,
|
|
8535
|
+
waveId: this.graphWave
|
|
8536
|
+
};
|
|
8537
|
+
}
|
|
8496
8538
|
getStatus() {
|
|
8497
8539
|
return this.status;
|
|
8498
8540
|
}
|
|
@@ -8507,7 +8549,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
8507
8549
|
status,
|
|
8508
8550
|
timestamp,
|
|
8509
8551
|
output,
|
|
8510
|
-
model: this.servingModel
|
|
8552
|
+
model: this.servingModel,
|
|
8553
|
+
...this.graphFields()
|
|
8511
8554
|
};
|
|
8512
8555
|
this.emit("status", event);
|
|
8513
8556
|
this.emit("tier:status", event);
|
|
@@ -8547,7 +8590,8 @@ var BaseTier = class extends EventEmitter {
|
|
|
8547
8590
|
progressPct: update.progressPct,
|
|
8548
8591
|
timestamp,
|
|
8549
8592
|
output: update.output,
|
|
8550
|
-
model: this.servingModel
|
|
8593
|
+
model: this.servingModel,
|
|
8594
|
+
...this.graphFields()
|
|
8551
8595
|
});
|
|
8552
8596
|
}
|
|
8553
8597
|
buildMessage(type, to, payload) {
|
|
@@ -9015,6 +9059,79 @@ function describeProviderError(c, modelId) {
|
|
|
9015
9059
|
}
|
|
9016
9060
|
}
|
|
9017
9061
|
|
|
9062
|
+
// src/core/verification/acceptance.ts
|
|
9063
|
+
var PATH_TOKEN = /[\w./\\-]{0,200}\w\.[A-Za-z0-9]{1,8}\b/g;
|
|
9064
|
+
var QUOTED_NEEDLE = /(?:contains?|includes?|mentions?)\s+["'`]([^"'`]+)["'`]/i;
|
|
9065
|
+
var EXISTS = /\b(?:exists?|is\s+created|is\s+present|was\s+created|be\s+created)\b/i;
|
|
9066
|
+
var NON_EMPTY = /\b(?:non-?empty|not\s+empty)\b/i;
|
|
9067
|
+
var NEGATION = /\b(?:not|no|never|without|absent|missing|does\s*n[o']t)\b/i;
|
|
9068
|
+
function targetFile(criterion, ownedFiles) {
|
|
9069
|
+
const mentioned = [...new Set(criterion.match(PATH_TOKEN) ?? [])];
|
|
9070
|
+
if (mentioned.length > 1) return null;
|
|
9071
|
+
if (mentioned.length === 1) {
|
|
9072
|
+
const candidate = mentioned[0];
|
|
9073
|
+
return ownedFiles.find((file) => file === candidate || file.endsWith(`/${candidate}`)) ?? candidate;
|
|
9074
|
+
}
|
|
9075
|
+
return ownedFiles.length === 1 ? ownedFiles[0] : null;
|
|
9076
|
+
}
|
|
9077
|
+
async function evaluateAcceptance(criteria, ownedFiles, probe) {
|
|
9078
|
+
const results = [];
|
|
9079
|
+
for (const criterion of criteria) {
|
|
9080
|
+
const text = criterion.trim();
|
|
9081
|
+
if (!text) {
|
|
9082
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9083
|
+
continue;
|
|
9084
|
+
}
|
|
9085
|
+
if (NEGATION.test(text)) {
|
|
9086
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9087
|
+
continue;
|
|
9088
|
+
}
|
|
9089
|
+
const needle = text.match(QUOTED_NEEDLE)?.[1];
|
|
9090
|
+
const wantsExists = EXISTS.test(text);
|
|
9091
|
+
const wantsNonEmpty = NON_EMPTY.test(text);
|
|
9092
|
+
if (!needle && !wantsExists && !wantsNonEmpty) {
|
|
9093
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9094
|
+
continue;
|
|
9095
|
+
}
|
|
9096
|
+
const file = targetFile(text, ownedFiles);
|
|
9097
|
+
if (!file) {
|
|
9098
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9099
|
+
continue;
|
|
9100
|
+
}
|
|
9101
|
+
const stat = await probe.stat(file);
|
|
9102
|
+
if (!stat) {
|
|
9103
|
+
results.push({ criterion, verdict: "failed", detail: `${file} does not exist` });
|
|
9104
|
+
continue;
|
|
9105
|
+
}
|
|
9106
|
+
if (wantsNonEmpty && stat.size === 0) {
|
|
9107
|
+
results.push({ criterion, verdict: "failed", detail: `${file} is empty` });
|
|
9108
|
+
continue;
|
|
9109
|
+
}
|
|
9110
|
+
if (needle) {
|
|
9111
|
+
const content = await probe.read(file);
|
|
9112
|
+
if (content === null) {
|
|
9113
|
+
results.push({ criterion, verdict: "undecidable" });
|
|
9114
|
+
continue;
|
|
9115
|
+
}
|
|
9116
|
+
const found = content.includes(needle);
|
|
9117
|
+
results.push({
|
|
9118
|
+
criterion,
|
|
9119
|
+
verdict: found ? "passed" : "failed",
|
|
9120
|
+
detail: found ? `${file} contains ${JSON.stringify(needle)}` : `${file} does not contain ${JSON.stringify(needle)}`
|
|
9121
|
+
});
|
|
9122
|
+
continue;
|
|
9123
|
+
}
|
|
9124
|
+
results.push({ criterion, verdict: "passed", detail: `${file} exists` });
|
|
9125
|
+
}
|
|
9126
|
+
return results;
|
|
9127
|
+
}
|
|
9128
|
+
function undecided(results) {
|
|
9129
|
+
return results.filter((r) => r.verdict === "undecidable").map((r) => r.criterion);
|
|
9130
|
+
}
|
|
9131
|
+
function failures(results) {
|
|
9132
|
+
return results.filter((r) => r.verdict === "failed").map((r) => `Acceptance not met \u2014 ${r.criterion}${r.detail ? ` (${r.detail})` : ""}`);
|
|
9133
|
+
}
|
|
9134
|
+
|
|
9018
9135
|
// src/core/tiers/t3-worker.ts
|
|
9019
9136
|
var CriticalToolError = class extends Error {
|
|
9020
9137
|
constructor(message, toolName) {
|
|
@@ -9331,7 +9448,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
9331
9448
|
}
|
|
9332
9449
|
const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
|
|
9333
9450
|
if (!artifactCheck.ok) {
|
|
9334
|
-
correctionAttempts
|
|
9451
|
+
correctionAttempts++;
|
|
9335
9452
|
issues.push(...artifactCheck.issues);
|
|
9336
9453
|
output = await this.correctOutput(output, artifactCheck.issues);
|
|
9337
9454
|
const retryArtifactCheck = await this.verifyArtifacts(assignment);
|
|
@@ -9349,22 +9466,45 @@ Now execute your subtask using this context where relevant.`
|
|
|
9349
9466
|
this.tools.map((t) => t.name)
|
|
9350
9467
|
);
|
|
9351
9468
|
if (visualIssue) {
|
|
9352
|
-
correctionAttempts
|
|
9469
|
+
correctionAttempts++;
|
|
9353
9470
|
issues.push(visualIssue);
|
|
9354
9471
|
this.sendStatusUpdate({ progressPct: 68, currentAction: "Verifying requested visuals", status: "IN_PROGRESS" });
|
|
9355
9472
|
output = await this.correctOutput(output, [visualIssue]);
|
|
9356
9473
|
}
|
|
9474
|
+
const acceptanceResults = await this.checkAcceptance(assignment);
|
|
9475
|
+
const acceptanceFailures = failures(acceptanceResults);
|
|
9476
|
+
if (acceptanceFailures.length > 0) {
|
|
9477
|
+
correctionAttempts++;
|
|
9478
|
+
issues.push(...acceptanceFailures);
|
|
9479
|
+
this.sendStatusUpdate({ progressPct: 69, currentAction: "Acceptance checks failed \u2014 correcting", status: "IN_PROGRESS" });
|
|
9480
|
+
output = await this.correctOutput(output, acceptanceFailures);
|
|
9481
|
+
const recheck = failures(await this.checkAcceptance(assignment));
|
|
9482
|
+
if (recheck.length > 0) {
|
|
9483
|
+
issues.push(...recheck);
|
|
9484
|
+
checksRun.push(...acceptanceResults.map((r) => r.criterion));
|
|
9485
|
+
failed.push(...recheck);
|
|
9486
|
+
this.setStatus("FAILED");
|
|
9487
|
+
this.peerBus?.publish(this.id, assignment.subtaskId, output, "ESCALATED");
|
|
9488
|
+
return this.buildResult("ESCALATED", output, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
9489
|
+
}
|
|
9490
|
+
}
|
|
9491
|
+
for (const result2 of acceptanceResults) {
|
|
9492
|
+
if (result2.verdict === "undecidable") continue;
|
|
9493
|
+
checksRun.push(result2.criterion);
|
|
9494
|
+
if (result2.verdict === "passed") passed.push(result2.criterion);
|
|
9495
|
+
}
|
|
9357
9496
|
this.sendStatusUpdate({ progressPct: 70, currentAction: "Self-testing output", status: "IN_PROGRESS" });
|
|
9358
|
-
const
|
|
9497
|
+
const pendingAcceptance = undecided(acceptanceResults);
|
|
9498
|
+
const testResult = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9359
9499
|
checksRun.push(...testResult.checksRun);
|
|
9360
9500
|
passed.push(...testResult.passed);
|
|
9361
9501
|
failed.push(...testResult.failed);
|
|
9362
9502
|
if (testResult.failed.length > 0) {
|
|
9363
|
-
correctionAttempts
|
|
9503
|
+
correctionAttempts++;
|
|
9364
9504
|
issues.push(`Initial check failed: ${testResult.failed.join(", ")}`);
|
|
9365
9505
|
const corrected = await this.correctOutput(output, testResult.failed);
|
|
9366
9506
|
output = corrected;
|
|
9367
|
-
const retest = await this.selfTest(assignment, output);
|
|
9507
|
+
const retest = await this.selfTest(assignment, output, pendingAcceptance);
|
|
9368
9508
|
passed.push(...retest.passed);
|
|
9369
9509
|
if (retest.failed.length > 0) {
|
|
9370
9510
|
failed.push(...retest.failed);
|
|
@@ -9668,8 +9808,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9668
9808
|
// Media generation can run for a minute; without this a cancelled run
|
|
9669
9809
|
// still pays for an image nobody will see.
|
|
9670
9810
|
...this.signal ? { signal: this.signal } : {},
|
|
9671
|
-
saveSnapshot: async (
|
|
9672
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
9811
|
+
saveSnapshot: async (path39, content) => {
|
|
9812
|
+
this.store?.addFileSnapshot(this.taskId, path39, content);
|
|
9673
9813
|
},
|
|
9674
9814
|
sendPeerSync: (to, syncType, content) => {
|
|
9675
9815
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -9856,7 +9996,7 @@ ${assignment.expectedOutput}`;
|
|
|
9856
9996
|
const { promisify: promisify4 } = await import('util');
|
|
9857
9997
|
const execAsync3 = promisify4(exec3);
|
|
9858
9998
|
for (const artifactPath of artifactPaths) {
|
|
9859
|
-
const absolutePath =
|
|
9999
|
+
const absolutePath = path34.resolve(process.cwd(), artifactPath);
|
|
9860
10000
|
try {
|
|
9861
10001
|
const stat = await fs11.stat(absolutePath);
|
|
9862
10002
|
if (!stat.isFile()) {
|
|
@@ -9891,7 +10031,7 @@ ${assignment.expectedOutput}`;
|
|
|
9891
10031
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
9892
10032
|
continue;
|
|
9893
10033
|
}
|
|
9894
|
-
const ext =
|
|
10034
|
+
const ext = path34.extname(absolutePath).toLowerCase();
|
|
9895
10035
|
try {
|
|
9896
10036
|
if (ext === ".ts" || ext === ".tsx") {
|
|
9897
10037
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -9977,14 +10117,42 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9977
10117
|
}
|
|
9978
10118
|
return current;
|
|
9979
10119
|
}
|
|
9980
|
-
|
|
10120
|
+
/**
|
|
10121
|
+
* Run the acceptance criteria that can be settled by looking at the workspace.
|
|
10122
|
+
* Reads go through the same workspace-relative resolution verifyArtifacts uses.
|
|
10123
|
+
*/
|
|
10124
|
+
async checkAcceptance(assignment) {
|
|
10125
|
+
const criteria = assignment.acceptance ?? [];
|
|
10126
|
+
if (!criteria.length) return [];
|
|
10127
|
+
const resolve2 = (target) => path34.resolve(process.cwd(), target);
|
|
10128
|
+
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
10129
|
+
stat: async (target) => {
|
|
10130
|
+
try {
|
|
10131
|
+
const stat = await fs11.stat(resolve2(target));
|
|
10132
|
+
return stat.isFile() ? { size: stat.size } : null;
|
|
10133
|
+
} catch {
|
|
10134
|
+
return null;
|
|
10135
|
+
}
|
|
10136
|
+
},
|
|
10137
|
+
read: async (target) => {
|
|
10138
|
+
try {
|
|
10139
|
+
const content = await fs11.readFile(resolve2(target), "utf-8");
|
|
10140
|
+
return content.includes("\uFFFD") ? null : content;
|
|
10141
|
+
} catch {
|
|
10142
|
+
return null;
|
|
10143
|
+
}
|
|
10144
|
+
}
|
|
10145
|
+
});
|
|
10146
|
+
}
|
|
10147
|
+
async selfTest(assignment, output, pendingAcceptance) {
|
|
10148
|
+
const acceptance = pendingAcceptance ?? assignment.acceptance ?? [];
|
|
9981
10149
|
const prompt = `Self-test this output against the assignment requirements.
|
|
9982
10150
|
|
|
9983
10151
|
Assignment: ${assignment.description}
|
|
9984
10152
|
Expected output: ${assignment.expectedOutput}
|
|
9985
10153
|
Constraints: ${assignment.constraints.join("; ")}
|
|
9986
|
-
${
|
|
9987
|
-
${
|
|
10154
|
+
${acceptance.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
|
|
10155
|
+
${acceptance.map((a) => `- ${a}`).join("\n")}
|
|
9988
10156
|
` : ""}
|
|
9989
10157
|
Output to test:
|
|
9990
10158
|
${output}
|
|
@@ -10051,8 +10219,8 @@ ${output.slice(0, 4e3)}`;
|
|
|
10051
10219
|
} catch {
|
|
10052
10220
|
}
|
|
10053
10221
|
}
|
|
10054
|
-
async correctOutput(originalOutput,
|
|
10055
|
-
const correctionPrompt = `The following output failed these checks: ${
|
|
10222
|
+
async correctOutput(originalOutput, failures2) {
|
|
10223
|
+
const correctionPrompt = `The following output failed these checks: ${failures2.join(", ")}.
|
|
10056
10224
|
|
|
10057
10225
|
Original output:
|
|
10058
10226
|
${originalOutput}
|
|
@@ -10714,12 +10882,220 @@ function describeGenerationForPlanner(has) {
|
|
|
10714
10882
|
return lines.join("\n");
|
|
10715
10883
|
}
|
|
10716
10884
|
|
|
10885
|
+
// src/core/orchestration/task-graph.ts
|
|
10886
|
+
var TaskGraphValidationError = class extends Error {
|
|
10887
|
+
constructor(issues) {
|
|
10888
|
+
super(issues.map((issue) => issue.message).join("; "));
|
|
10889
|
+
this.issues = issues;
|
|
10890
|
+
this.name = "TaskGraphValidationError";
|
|
10891
|
+
}
|
|
10892
|
+
issues;
|
|
10893
|
+
};
|
|
10894
|
+
function normalizeDependencies(input) {
|
|
10895
|
+
if (!input?.length) return [];
|
|
10896
|
+
return [...new Set(input.map((dependency) => dependency.trim()).filter(Boolean))];
|
|
10897
|
+
}
|
|
10898
|
+
function cycleNodes(nodes) {
|
|
10899
|
+
const adjacency = new Map(
|
|
10900
|
+
nodes.map((node) => [node.id, node.dependsOn])
|
|
10901
|
+
);
|
|
10902
|
+
const index = /* @__PURE__ */ new Map();
|
|
10903
|
+
const lowlink = /* @__PURE__ */ new Map();
|
|
10904
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
10905
|
+
const stack = [];
|
|
10906
|
+
const inCycle = /* @__PURE__ */ new Set();
|
|
10907
|
+
let counter = 0;
|
|
10908
|
+
const open = (id) => {
|
|
10909
|
+
index.set(id, counter);
|
|
10910
|
+
lowlink.set(id, counter);
|
|
10911
|
+
counter++;
|
|
10912
|
+
stack.push(id);
|
|
10913
|
+
onStack.add(id);
|
|
10914
|
+
};
|
|
10915
|
+
for (const root of nodes) {
|
|
10916
|
+
if (index.has(root.id)) continue;
|
|
10917
|
+
open(root.id);
|
|
10918
|
+
const frames = [{ id: root.id, edge: 0 }];
|
|
10919
|
+
while (frames.length > 0) {
|
|
10920
|
+
const frame = frames[frames.length - 1];
|
|
10921
|
+
const neighbours = adjacency.get(frame.id) ?? [];
|
|
10922
|
+
if (frame.edge < neighbours.length) {
|
|
10923
|
+
const next = neighbours[frame.edge];
|
|
10924
|
+
frame.edge++;
|
|
10925
|
+
if (!adjacency.has(next)) continue;
|
|
10926
|
+
if (!index.has(next)) {
|
|
10927
|
+
open(next);
|
|
10928
|
+
frames.push({ id: next, edge: 0 });
|
|
10929
|
+
} else if (onStack.has(next)) {
|
|
10930
|
+
lowlink.set(frame.id, Math.min(lowlink.get(frame.id), index.get(next)));
|
|
10931
|
+
}
|
|
10932
|
+
continue;
|
|
10933
|
+
}
|
|
10934
|
+
frames.pop();
|
|
10935
|
+
const parent = frames[frames.length - 1];
|
|
10936
|
+
if (parent) {
|
|
10937
|
+
lowlink.set(parent.id, Math.min(lowlink.get(parent.id), lowlink.get(frame.id)));
|
|
10938
|
+
}
|
|
10939
|
+
if (lowlink.get(frame.id) === index.get(frame.id)) {
|
|
10940
|
+
const component = [];
|
|
10941
|
+
for (; ; ) {
|
|
10942
|
+
const member = stack.pop();
|
|
10943
|
+
onStack.delete(member);
|
|
10944
|
+
component.push(member);
|
|
10945
|
+
if (member === frame.id) break;
|
|
10946
|
+
}
|
|
10947
|
+
const cyclic = component.length > 1 || (adjacency.get(component[0]) ?? []).includes(component[0]);
|
|
10948
|
+
if (cyclic) for (const member of component) inCycle.add(member);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
return inCycle;
|
|
10953
|
+
}
|
|
10954
|
+
function throwIfStrict(mode, issues) {
|
|
10955
|
+
if (mode === "strict" && issues.length > 0) {
|
|
10956
|
+
throw new TaskGraphValidationError(issues);
|
|
10957
|
+
}
|
|
10958
|
+
}
|
|
10959
|
+
function compileTaskGraph(inputNodes, options = {}) {
|
|
10960
|
+
const mode = options.mode ?? "strict";
|
|
10961
|
+
const issues = [];
|
|
10962
|
+
if (inputNodes.length === 0) {
|
|
10963
|
+
issues.push({ code: "INVALID_NODE", message: "Task graph requires at least one node" });
|
|
10964
|
+
throwIfStrict(mode, issues);
|
|
10965
|
+
}
|
|
10966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10967
|
+
const nodes = [];
|
|
10968
|
+
for (let ordinal = 0; ordinal < inputNodes.length; ordinal++) {
|
|
10969
|
+
const input = inputNodes[ordinal];
|
|
10970
|
+
const id = input.id?.trim();
|
|
10971
|
+
const title = input.title?.trim();
|
|
10972
|
+
if (!id || !title) {
|
|
10973
|
+
issues.push({
|
|
10974
|
+
code: "INVALID_NODE",
|
|
10975
|
+
nodeId: id || void 0,
|
|
10976
|
+
message: `Task graph node at index ${ordinal} requires a non-empty id and title`
|
|
10977
|
+
});
|
|
10978
|
+
continue;
|
|
10979
|
+
}
|
|
10980
|
+
if (seen.has(id)) {
|
|
10981
|
+
issues.push({
|
|
10982
|
+
code: "DUPLICATE_NODE_ID",
|
|
10983
|
+
nodeId: id,
|
|
10984
|
+
message: `Duplicate task graph node id: ${id}`
|
|
10985
|
+
});
|
|
10986
|
+
continue;
|
|
10987
|
+
}
|
|
10988
|
+
seen.add(id);
|
|
10989
|
+
nodes.push({
|
|
10990
|
+
id,
|
|
10991
|
+
title,
|
|
10992
|
+
kind: input.kind,
|
|
10993
|
+
dependsOn: normalizeDependencies(input.dependsOn),
|
|
10994
|
+
payload: input.payload,
|
|
10995
|
+
ordinal
|
|
10996
|
+
});
|
|
10997
|
+
}
|
|
10998
|
+
throwIfStrict(mode, issues);
|
|
10999
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
11000
|
+
const sanitized = nodes.map((node) => {
|
|
11001
|
+
const dependsOn = [];
|
|
11002
|
+
for (const dependencyId of node.dependsOn) {
|
|
11003
|
+
if (dependencyId === node.id) {
|
|
11004
|
+
issues.push({
|
|
11005
|
+
code: "SELF_DEPENDENCY",
|
|
11006
|
+
nodeId: node.id,
|
|
11007
|
+
dependencyId,
|
|
11008
|
+
message: `Task graph node ${node.id} cannot depend on itself`
|
|
11009
|
+
});
|
|
11010
|
+
continue;
|
|
11011
|
+
}
|
|
11012
|
+
if (!ids.has(dependencyId)) {
|
|
11013
|
+
issues.push({
|
|
11014
|
+
code: "UNKNOWN_DEPENDENCY",
|
|
11015
|
+
nodeId: node.id,
|
|
11016
|
+
dependencyId,
|
|
11017
|
+
message: `Task graph node ${node.id} depends on unknown node ${dependencyId}`
|
|
11018
|
+
});
|
|
11019
|
+
continue;
|
|
11020
|
+
}
|
|
11021
|
+
dependsOn.push(dependencyId);
|
|
11022
|
+
}
|
|
11023
|
+
return { ...node, dependsOn };
|
|
11024
|
+
});
|
|
11025
|
+
throwIfStrict(mode, issues);
|
|
11026
|
+
const cyclic = cycleNodes(sanitized);
|
|
11027
|
+
let repaired = sanitized;
|
|
11028
|
+
if (cyclic.size > 0) {
|
|
11029
|
+
const cycleIssues = [];
|
|
11030
|
+
repaired = sanitized.map((node) => {
|
|
11031
|
+
if (!cyclic.has(node.id)) return node;
|
|
11032
|
+
const dependsOn = node.dependsOn.filter((dependencyId) => {
|
|
11033
|
+
if (!cyclic.has(dependencyId)) return true;
|
|
11034
|
+
cycleIssues.push({
|
|
11035
|
+
code: "CYCLE_EDGE_REMOVED",
|
|
11036
|
+
nodeId: node.id,
|
|
11037
|
+
dependencyId,
|
|
11038
|
+
message: `Removed cyclic dependency ${dependencyId} -> ${node.id}`
|
|
11039
|
+
});
|
|
11040
|
+
return false;
|
|
11041
|
+
});
|
|
11042
|
+
return { ...node, dependsOn };
|
|
11043
|
+
});
|
|
11044
|
+
issues.push(...cycleIssues);
|
|
11045
|
+
throwIfStrict(mode, cycleIssues);
|
|
11046
|
+
}
|
|
11047
|
+
const remainingCycle = cycleNodes(repaired);
|
|
11048
|
+
if (remainingCycle.size > 0) {
|
|
11049
|
+
throw new TaskGraphValidationError([
|
|
11050
|
+
{
|
|
11051
|
+
code: "INVALID_NODE",
|
|
11052
|
+
message: `Task graph remained cyclic after repair: ${[...remainingCycle].join(", ")}`
|
|
11053
|
+
}
|
|
11054
|
+
]);
|
|
11055
|
+
}
|
|
11056
|
+
return {
|
|
11057
|
+
graph: { nodes: repaired.sort((a, b) => a.ordinal - b.ordinal) },
|
|
11058
|
+
issues
|
|
11059
|
+
};
|
|
11060
|
+
}
|
|
11061
|
+
|
|
11062
|
+
// src/core/orchestration/adapters.ts
|
|
11063
|
+
function compileSectionGraph(sections) {
|
|
11064
|
+
return compileTaskGraph(
|
|
11065
|
+
sections.map((section) => ({
|
|
11066
|
+
id: section.sectionId,
|
|
11067
|
+
title: section.sectionTitle,
|
|
11068
|
+
kind: "section",
|
|
11069
|
+
dependsOn: section.dependsOn,
|
|
11070
|
+
payload: section
|
|
11071
|
+
})),
|
|
11072
|
+
{ mode: "repair" }
|
|
11073
|
+
);
|
|
11074
|
+
}
|
|
11075
|
+
function compileSubtaskGraph(subtasks) {
|
|
11076
|
+
return compileTaskGraph(
|
|
11077
|
+
subtasks.map((subtask) => ({
|
|
11078
|
+
id: subtask.subtaskId,
|
|
11079
|
+
title: subtask.subtaskTitle,
|
|
11080
|
+
kind: "subtask",
|
|
11081
|
+
dependsOn: subtask.dependsOn,
|
|
11082
|
+
payload: subtask
|
|
11083
|
+
})),
|
|
11084
|
+
{ mode: "repair" }
|
|
11085
|
+
);
|
|
11086
|
+
}
|
|
11087
|
+
|
|
10717
11088
|
// src/core/tiers/t2-manager.ts
|
|
10718
11089
|
function buildT2SystemPrompt(has) {
|
|
10719
11090
|
const generation = describeGenerationForPlanner(has);
|
|
10720
11091
|
return [
|
|
10721
11092
|
"You are a T2 Manager agent in the Cascade AI system.",
|
|
10722
|
-
|
|
11093
|
+
// Must agree with the decomposition prompt in decomposeSection(), which is
|
|
11094
|
+
// the one carrying the actual instruction. It said 1-4 and "the FEWEST that
|
|
11095
|
+
// fully cover it"; this said 2-5. The model received both, so its floor was
|
|
11096
|
+
// simultaneously one and two, and the cheaper reading — pad to two — costs a
|
|
11097
|
+
// real worker call on every small section.
|
|
11098
|
+
"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.",
|
|
10723
11099
|
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
10724
11100
|
has("peer_message") && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
10725
11101
|
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.",
|
|
@@ -11053,6 +11429,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11053
11429
|
for (const a of assignments) {
|
|
11054
11430
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
11055
11431
|
if (this.store) worker.setStore(this.store, taskId);
|
|
11432
|
+
worker.setGraphPosition({ nodeId: a.subtaskId, dependsOn: a.dependsOn ?? [] });
|
|
11056
11433
|
worker.setPeerBus(this.t3PeerBus);
|
|
11057
11434
|
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
11058
11435
|
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
@@ -11115,21 +11492,30 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11115
11492
|
* then reduce in-degrees of their dependents and repeat.
|
|
11116
11493
|
*/
|
|
11117
11494
|
async runWithDependencies(assignments, workerMap, taskId) {
|
|
11495
|
+
const compiled = compileSubtaskGraph(assignments);
|
|
11496
|
+
for (const issue of compiled.issues) {
|
|
11497
|
+
this.log(`\u26A0 Subtask graph: ${issue.message}`);
|
|
11498
|
+
}
|
|
11499
|
+
const sanitizedAssignments = compiled.graph.nodes.map((node) => {
|
|
11500
|
+
const assignment = node.payload;
|
|
11501
|
+
const original = assignment.dependsOn ?? [];
|
|
11502
|
+
const repaired = node.dependsOn;
|
|
11503
|
+
const unchanged = repaired.length === original.length && repaired.every((dependencyId, index) => dependencyId === original[index]);
|
|
11504
|
+
return unchanged ? assignment : { ...assignment, dependsOn: [...repaired] };
|
|
11505
|
+
});
|
|
11118
11506
|
const adj = /* @__PURE__ */ new Map();
|
|
11119
11507
|
const inDegree = /* @__PURE__ */ new Map();
|
|
11120
11508
|
const resultMap = /* @__PURE__ */ new Map();
|
|
11121
|
-
for (const a of
|
|
11509
|
+
for (const a of sanitizedAssignments) {
|
|
11122
11510
|
if (!adj.has(a.subtaskId)) adj.set(a.subtaskId, /* @__PURE__ */ new Set());
|
|
11123
11511
|
inDegree.set(a.subtaskId, 0);
|
|
11124
11512
|
}
|
|
11125
|
-
for (const a of
|
|
11126
|
-
const
|
|
11127
|
-
for (const dep of deps) {
|
|
11513
|
+
for (const a of sanitizedAssignments) {
|
|
11514
|
+
for (const dep of a.dependsOn ?? []) {
|
|
11128
11515
|
adj.get(dep).add(a.subtaskId);
|
|
11129
11516
|
inDegree.set(a.subtaskId, (inDegree.get(a.subtaskId) ?? 0) + 1);
|
|
11130
11517
|
}
|
|
11131
11518
|
}
|
|
11132
|
-
const sanitizedAssignments = this.breakCycles(assignments, adj, inDegree);
|
|
11133
11519
|
let remaining = new Set(sanitizedAssignments.map((a) => a.subtaskId));
|
|
11134
11520
|
let wave = 0;
|
|
11135
11521
|
let respawnBudget = 1;
|
|
@@ -11164,6 +11550,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11164
11550
|
runnableIds.push(fallbackId);
|
|
11165
11551
|
}
|
|
11166
11552
|
wave++;
|
|
11553
|
+
for (const id of runnableIds) workerMap.get(id)?.setGraphPosition({ wave });
|
|
11167
11554
|
this.log(`Wave ${wave}: running ${runnableIds.length} subtask(s) in parallel`);
|
|
11168
11555
|
this.sendStatusUpdate({
|
|
11169
11556
|
progressPct: 20 + Math.min(wave * 10, 60),
|
|
@@ -11336,49 +11723,6 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11336
11723
|
}
|
|
11337
11724
|
return [...resultMap.values()];
|
|
11338
11725
|
}
|
|
11339
|
-
/**
|
|
11340
|
-
* Detects cyclic dependencies using Kahn's algorithm and breaks them
|
|
11341
|
-
* by removing back-edges. Returns a sanitized copy of assignments.
|
|
11342
|
-
*
|
|
11343
|
-
* A cycle like t1→t2→t3→t1 is broken at the last edge (t3→t1),
|
|
11344
|
-
* meaning t3 will start without waiting for t1, preventing deadlock.
|
|
11345
|
-
*/
|
|
11346
|
-
breakCycles(assignments, adj, inDegree) {
|
|
11347
|
-
const degree = new Map(inDegree);
|
|
11348
|
-
const queue = [];
|
|
11349
|
-
const visited = /* @__PURE__ */ new Set();
|
|
11350
|
-
for (const [id, d] of degree) {
|
|
11351
|
-
if (d === 0) queue.push(id);
|
|
11352
|
-
}
|
|
11353
|
-
while (queue.length > 0) {
|
|
11354
|
-
const id = queue.shift();
|
|
11355
|
-
visited.add(id);
|
|
11356
|
-
for (const dep of adj.get(id) ?? []) {
|
|
11357
|
-
const newDeg = (degree.get(dep) ?? 1) - 1;
|
|
11358
|
-
degree.set(dep, newDeg);
|
|
11359
|
-
if (newDeg === 0) queue.push(dep);
|
|
11360
|
-
}
|
|
11361
|
-
}
|
|
11362
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
11363
|
-
if (cycleNodes.length === 0) return assignments;
|
|
11364
|
-
this.log(
|
|
11365
|
-
`\u26A0 Circular dependency detected among subtasks: [${cycleNodes.join(", ")}]. Breaking cycles \u2014 affected tasks will run without their cyclic dependencies.`
|
|
11366
|
-
);
|
|
11367
|
-
return assignments.map((a) => {
|
|
11368
|
-
if (!cycleNodes.includes(a.subtaskId)) return a;
|
|
11369
|
-
const safeDeps = (a.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
11370
|
-
if (safeDeps.length !== (a.dependsOn ?? []).length) {
|
|
11371
|
-
this.log(
|
|
11372
|
-
` \u2192 Breaking cycle: removed ${(a.dependsOn ?? []).filter((d) => cycleNodes.includes(d)).join(", ")} from "${a.subtaskTitle}" dependsOn`
|
|
11373
|
-
);
|
|
11374
|
-
for (const removed of (a.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
11375
|
-
inDegree.set(a.subtaskId, Math.max(0, (inDegree.get(a.subtaskId) ?? 1) - 1));
|
|
11376
|
-
adj.get(removed)?.delete(a.subtaskId);
|
|
11377
|
-
}
|
|
11378
|
-
}
|
|
11379
|
-
return { ...a, dependsOn: safeDeps };
|
|
11380
|
-
});
|
|
11381
|
-
}
|
|
11382
11726
|
async retryT3(assignment, taskId) {
|
|
11383
11727
|
this.log(`Retrying T3 for subtask: ${assignment.subtaskTitle}`);
|
|
11384
11728
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
@@ -11517,6 +11861,101 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11517
11861
|
// src/core/tiers/t1-administrator.ts
|
|
11518
11862
|
init_constants();
|
|
11519
11863
|
|
|
11864
|
+
// src/core/orchestration/scheduler.ts
|
|
11865
|
+
var DependencyScheduler = class {
|
|
11866
|
+
constructor(graph, hooks, options = {}) {
|
|
11867
|
+
this.graph = graph;
|
|
11868
|
+
this.hooks = hooks;
|
|
11869
|
+
this.options = options;
|
|
11870
|
+
}
|
|
11871
|
+
graph;
|
|
11872
|
+
hooks;
|
|
11873
|
+
options;
|
|
11874
|
+
async run() {
|
|
11875
|
+
const nodes = [...this.graph.nodes].sort((a, b) => a.ordinal - b.ordinal);
|
|
11876
|
+
const inDegree = new Map(nodes.map((node) => [node.id, node.dependsOn.length]));
|
|
11877
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
11878
|
+
for (const node of nodes) dependents.set(node.id, []);
|
|
11879
|
+
for (const node of nodes) {
|
|
11880
|
+
for (const dependencyId of node.dependsOn) {
|
|
11881
|
+
dependents.get(dependencyId)?.push(node.id);
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
const policy = this.options.onUpstreamFailure ?? "continue";
|
|
11885
|
+
const settled = /* @__PURE__ */ new Set();
|
|
11886
|
+
const results = /* @__PURE__ */ new Map();
|
|
11887
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
11888
|
+
const waves = [];
|
|
11889
|
+
let wave = 0;
|
|
11890
|
+
const blockDescendants = async (failedIds) => {
|
|
11891
|
+
const rootsFor = /* @__PURE__ */ new Map();
|
|
11892
|
+
const queue = [];
|
|
11893
|
+
for (const rootId of failedIds) {
|
|
11894
|
+
for (const childId of dependents.get(rootId) ?? []) queue.push({ id: childId, root: rootId });
|
|
11895
|
+
}
|
|
11896
|
+
while (queue.length > 0) {
|
|
11897
|
+
const { id, root } = queue.shift();
|
|
11898
|
+
if (settled.has(id)) continue;
|
|
11899
|
+
const known = rootsFor.get(id);
|
|
11900
|
+
if (known) {
|
|
11901
|
+
if (!known.has(root)) known.add(root);
|
|
11902
|
+
continue;
|
|
11903
|
+
}
|
|
11904
|
+
rootsFor.set(id, /* @__PURE__ */ new Set([root]));
|
|
11905
|
+
for (const childId of dependents.get(id) ?? []) queue.push({ id: childId, root });
|
|
11906
|
+
}
|
|
11907
|
+
for (const node of nodes) {
|
|
11908
|
+
const roots = rootsFor.get(node.id);
|
|
11909
|
+
if (!roots || settled.has(node.id)) continue;
|
|
11910
|
+
const causes = nodes.filter((n) => roots.has(n.id));
|
|
11911
|
+
const blockedBy = causes.map((n) => n.id);
|
|
11912
|
+
const blockedByTitles = causes.map((n) => n.title || n.id);
|
|
11913
|
+
const record = {
|
|
11914
|
+
blockedBy,
|
|
11915
|
+
blockedByTitles,
|
|
11916
|
+
reason: blockedByTitles.length === 1 ? `Required upstream work "${blockedByTitles[0]}" failed` : `Required upstream work ${blockedByTitles.map((t) => `"${t}"`).join(", ")} failed`
|
|
11917
|
+
};
|
|
11918
|
+
blocked.set(node.id, record);
|
|
11919
|
+
settled.add(node.id);
|
|
11920
|
+
await this.hooks.onNodeBlocked?.(node, record);
|
|
11921
|
+
}
|
|
11922
|
+
};
|
|
11923
|
+
while (settled.size < nodes.length) {
|
|
11924
|
+
const ready = nodes.filter((node) => !settled.has(node.id) && (inDegree.get(node.id) ?? 0) === 0);
|
|
11925
|
+
if (ready.length === 0) {
|
|
11926
|
+
throw new Error("Dependency scheduler received a cyclic or incomplete task graph");
|
|
11927
|
+
}
|
|
11928
|
+
waves.push(ready.map((node) => node.id));
|
|
11929
|
+
await this.hooks.onWaveStart?.(ready, wave);
|
|
11930
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
11931
|
+
const runOne = async (node) => {
|
|
11932
|
+
const result = await this.hooks.execute(node);
|
|
11933
|
+
results.set(node.id, result);
|
|
11934
|
+
settled.add(node.id);
|
|
11935
|
+
const outcome = this.hooks.classify?.(node, result) ?? "succeeded";
|
|
11936
|
+
outcomes.set(node.id, outcome);
|
|
11937
|
+
if (outcome === "failed" && policy === "block") ; else {
|
|
11938
|
+
for (const dependentId of dependents.get(node.id) ?? []) {
|
|
11939
|
+
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
11940
|
+
}
|
|
11941
|
+
}
|
|
11942
|
+
await this.hooks.onNodeComplete?.(node, result);
|
|
11943
|
+
};
|
|
11944
|
+
if ((this.options.mode ?? "parallel") === "sequential") {
|
|
11945
|
+
for (const node of ready) await runOne(node);
|
|
11946
|
+
} else {
|
|
11947
|
+
await Promise.all(ready.map(runOne));
|
|
11948
|
+
}
|
|
11949
|
+
const failedThisWave = ready.filter((node) => outcomes.get(node.id) === "failed").map((node) => node.id);
|
|
11950
|
+
if (policy === "block" && failedThisWave.length > 0) {
|
|
11951
|
+
await blockDescendants(failedThisWave);
|
|
11952
|
+
}
|
|
11953
|
+
wave++;
|
|
11954
|
+
}
|
|
11955
|
+
return { results, waves, blocked };
|
|
11956
|
+
}
|
|
11957
|
+
};
|
|
11958
|
+
|
|
11520
11959
|
// src/utils/json-extract.ts
|
|
11521
11960
|
function extractFirstJsonObject(input) {
|
|
11522
11961
|
if (!input) return null;
|
|
@@ -11588,7 +12027,7 @@ function buildT1SystemPrompt(has) {
|
|
|
11588
12027
|
Your responsibilities:
|
|
11589
12028
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
11590
12029
|
2. Decompose the task into logical sections (one per T2 Manager)
|
|
11591
|
-
3. For each section, define
|
|
12030
|
+
3. For each section, define 1-4 subtasks for T3 Workers \u2014 the fewest that fully cover it
|
|
11592
12031
|
4. Return a structured plan as JSON
|
|
11593
12032
|
|
|
11594
12033
|
CRITICAL PATH RULE: If the user specifies a target directory (e.g. "inside python_exclusive",
|
|
@@ -11614,6 +12053,9 @@ QUALITY RULES:
|
|
|
11614
12053
|
|
|
11615
12054
|
${generation}` : ""}`;
|
|
11616
12055
|
}
|
|
12056
|
+
function producedOutput(result) {
|
|
12057
|
+
return result.status === "COMPLETED" || result.status === "PARTIAL";
|
|
12058
|
+
}
|
|
11617
12059
|
var T1Administrator = class extends BaseTier {
|
|
11618
12060
|
router;
|
|
11619
12061
|
toolRegistry;
|
|
@@ -11748,7 +12190,7 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
11748
12190
|
let allT2Results = await this.dispatchT2Managers(plan.sections);
|
|
11749
12191
|
let pass = 1;
|
|
11750
12192
|
const maxReplanPasses = this.config.maxReplanPasses ?? 2;
|
|
11751
|
-
const okCount = (rs) => rs.filter(
|
|
12193
|
+
const okCount = (rs) => rs.filter(producedOutput).length;
|
|
11752
12194
|
while (pass <= maxReplanPasses) {
|
|
11753
12195
|
const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
|
|
11754
12196
|
if (reviewResult.approved) {
|
|
@@ -11879,7 +12321,7 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
11879
12321
|
/** Structured, grounded summary of what's already done — used to keep
|
|
11880
12322
|
* corrective replan passes from re-emitting completed sections. */
|
|
11881
12323
|
summarizeCompletedSections(results) {
|
|
11882
|
-
const done = results.filter(
|
|
12324
|
+
const done = results.filter(producedOutput);
|
|
11883
12325
|
if (done.length === 0) return "";
|
|
11884
12326
|
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
11885
12327
|
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
@@ -12003,7 +12445,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12003
12445
|
}
|
|
12004
12446
|
}
|
|
12005
12447
|
const [min, max] = COMPLEXITY_T2_COUNT[plan.complexity] ?? [1, 8];
|
|
12006
|
-
if (plan.sections.length < min)
|
|
12448
|
+
if (plan.sections.length < min || plan.sections.length > max) {
|
|
12449
|
+
this.log(
|
|
12450
|
+
`\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.`
|
|
12451
|
+
);
|
|
12452
|
+
}
|
|
12007
12453
|
}
|
|
12008
12454
|
async dispatchT2Managers(sections) {
|
|
12009
12455
|
for (const section of sections) {
|
|
@@ -12020,6 +12466,7 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12020
12466
|
if (this.runBreaker) manager.setRunBreaker(this.runBreaker);
|
|
12021
12467
|
if (this.escalationCallback) manager.setEscalationCallback(this.escalationCallback);
|
|
12022
12468
|
manager.setHierarchyContext(`You are a T2 Manager for the section "${section.sectionTitle}". You are part of a COMPLEX task overseen by T1 Administrator.`);
|
|
12469
|
+
manager.setGraphPosition({ nodeId: section.sectionId, dependsOn: section.dependsOn ?? [] });
|
|
12023
12470
|
if (this.store) {
|
|
12024
12471
|
manager.setStore(this.store);
|
|
12025
12472
|
}
|
|
@@ -12115,118 +12562,162 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
12115
12562
|
return t2Results;
|
|
12116
12563
|
}
|
|
12117
12564
|
/**
|
|
12118
|
-
* Runs T2 managers respecting dependsOn declarations
|
|
12565
|
+
* Runs T2 managers respecting dependsOn declarations.
|
|
12566
|
+
*
|
|
12567
|
+
* Graph construction, validation and cycle repair now live in
|
|
12568
|
+
* compileSectionGraph (orchestration/adapters.ts), and wave execution in
|
|
12569
|
+
* DependencyScheduler — the same pair T2 uses for its subtasks, so a plan is
|
|
12570
|
+
* ordered by one implementation rather than two that drifted.
|
|
12571
|
+
*
|
|
12572
|
+
* The hand-rolled Kahn pass this replaces treated every node it could not
|
|
12573
|
+
* reach as part of a cycle. That set also contains everything DOWNSTREAM of a
|
|
12574
|
+
* cycle, whose in-degree can never fall to zero while its dependency is stuck,
|
|
12575
|
+
* so breaking a cycle here also cut the dependencies of innocent later
|
|
12576
|
+
* sections and started them immediately — before the sections they consume.
|
|
12577
|
+
* compileTaskGraph uses strongly connected components, so only real cycle
|
|
12578
|
+
* members are touched.
|
|
12119
12579
|
*/
|
|
12120
12580
|
async runT2sWithDependencies(sections, managers, taskId) {
|
|
12121
|
-
const
|
|
12122
|
-
const
|
|
12123
|
-
|
|
12124
|
-
const allKeys = new Set(sections.map((s) => s.sectionId));
|
|
12125
|
-
for (const s of sections) {
|
|
12126
|
-
if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
|
|
12127
|
-
inDegree.set(s.sectionId, 0);
|
|
12128
|
-
s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
|
|
12129
|
-
}
|
|
12130
|
-
for (const s of sections) {
|
|
12131
|
-
for (const dep of s.dependsOn ?? []) {
|
|
12132
|
-
adj.get(dep).add(s.sectionId);
|
|
12133
|
-
inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
|
|
12134
|
-
}
|
|
12135
|
-
}
|
|
12136
|
-
const queue = [];
|
|
12137
|
-
const degree = new Map(inDegree);
|
|
12138
|
-
for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
|
|
12139
|
-
const visited = /* @__PURE__ */ new Set();
|
|
12140
|
-
while (queue.length > 0) {
|
|
12141
|
-
const u = queue.shift();
|
|
12142
|
-
visited.add(u);
|
|
12143
|
-
for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
|
|
12144
|
-
const newDeg = (degree.get(v) ?? 1) - 1;
|
|
12145
|
-
degree.set(v, newDeg);
|
|
12146
|
-
if (newDeg === 0) queue.push(v);
|
|
12147
|
-
}
|
|
12148
|
-
}
|
|
12149
|
-
const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
|
|
12150
|
-
if (cycleNodes.length > 0) {
|
|
12151
|
-
this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
|
|
12152
|
-
for (const s of sections) {
|
|
12153
|
-
if (cycleNodes.includes(s.sectionId)) {
|
|
12154
|
-
const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
|
|
12155
|
-
for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
|
|
12156
|
-
inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
|
|
12157
|
-
adj.get(removed)?.delete(s.sectionId);
|
|
12158
|
-
}
|
|
12159
|
-
s.dependsOn = safeDeps;
|
|
12160
|
-
}
|
|
12161
|
-
}
|
|
12581
|
+
const compiled = compileSectionGraph(sections);
|
|
12582
|
+
for (const issue of compiled.issues) {
|
|
12583
|
+
this.log(`\u26A0 Section graph: ${issue.message}`);
|
|
12162
12584
|
}
|
|
12163
|
-
const
|
|
12585
|
+
const byId = new Map(compiled.graph.nodes.map((node) => [node.id, node]));
|
|
12586
|
+
for (const section of sections) {
|
|
12587
|
+
const node = byId.get(section.sectionId);
|
|
12588
|
+
if (node) section.dependsOn = [...node.dependsOn];
|
|
12589
|
+
}
|
|
12590
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
12591
|
+
const totalSections = compiled.graph.nodes.length;
|
|
12164
12592
|
let completedSections = 0;
|
|
12165
|
-
const
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12593
|
+
const scheduler = new DependencyScheduler(
|
|
12594
|
+
compiled.graph,
|
|
12595
|
+
{
|
|
12596
|
+
// The wave is the scheduler's own counter, and it is the only place the
|
|
12597
|
+
// parallelism is visible: nodes sharing a wave ran at the same time.
|
|
12598
|
+
// Stamped before the wave executes so the first status event a node
|
|
12599
|
+
// emits already carries it.
|
|
12600
|
+
onWaveStart: (nodes, wave) => {
|
|
12601
|
+
for (const node of nodes) managers[node.ordinal]?.setGraphPosition({ wave });
|
|
12602
|
+
},
|
|
12603
|
+
execute: async (node) => {
|
|
12604
|
+
const section = node.payload;
|
|
12605
|
+
const manager = managers[node.ordinal];
|
|
12606
|
+
const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
|
|
12607
|
+
this.sendStatusUpdate({
|
|
12608
|
+
progressPct,
|
|
12609
|
+
currentAction: `T2 working on: ${section.sectionTitle}`,
|
|
12610
|
+
status: "IN_PROGRESS"
|
|
12611
|
+
});
|
|
12612
|
+
this.throwIfCancelled();
|
|
12613
|
+
try {
|
|
12614
|
+
const result = await manager.execute(section, taskId, this.signal);
|
|
12615
|
+
manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
|
|
12616
|
+
if (result.status === "ESCALATED") {
|
|
12617
|
+
this.escalations.push({
|
|
12618
|
+
raisedBy: `T2_${section.sectionId}`,
|
|
12619
|
+
sectionId: section.sectionId,
|
|
12620
|
+
attempted: result.issues,
|
|
12621
|
+
blocker: result.issues.join("; "),
|
|
12622
|
+
needs: "Human review required"
|
|
12623
|
+
});
|
|
12624
|
+
}
|
|
12625
|
+
return result;
|
|
12626
|
+
} catch (err) {
|
|
12627
|
+
return {
|
|
12192
12628
|
sectionId: section.sectionId,
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12629
|
+
sectionTitle: section.sectionTitle,
|
|
12630
|
+
status: "FAILED",
|
|
12631
|
+
t3Results: [],
|
|
12632
|
+
sectionSummary: "",
|
|
12633
|
+
issues: [err instanceof Error ? err.message : String(err)]
|
|
12634
|
+
};
|
|
12197
12635
|
}
|
|
12198
|
-
}
|
|
12199
|
-
|
|
12636
|
+
},
|
|
12637
|
+
// PARTIAL is deliberately not a failure: a degraded-but-real section
|
|
12638
|
+
// can still feed the one after it, and cancelling that work would cost
|
|
12639
|
+
// the user more than letting it try.
|
|
12640
|
+
// BLOCKED cannot reach here today — the scheduler settles blocked nodes
|
|
12641
|
+
// through onNodeBlocked and never executes them, so classify only ever
|
|
12642
|
+
// sees results that ran. It is listed anyway because the failure mode if
|
|
12643
|
+
// that ever changes is silent and expensive: a blocked section would
|
|
12644
|
+
// classify as 'succeeded' and release its own dependents, so work that
|
|
12645
|
+
// should have been skipped would run and be billed.
|
|
12646
|
+
classify: (_node, result) => result.status === "FAILED" || result.status === "BLOCKED" ? "failed" : "succeeded",
|
|
12647
|
+
onNodeBlocked: (node, blockedInfo) => {
|
|
12648
|
+
const section = node.payload;
|
|
12649
|
+
this.log(`\u292B Skipped "${section.sectionTitle}" \u2014 ${blockedInfo.reason}`);
|
|
12650
|
+
resultMap.set(node.id, {
|
|
12200
12651
|
sectionId: section.sectionId,
|
|
12201
12652
|
sectionTitle: section.sectionTitle,
|
|
12202
|
-
|
|
12653
|
+
// BLOCKED, not FAILED. This section was never attempted — saying it
|
|
12654
|
+
// failed reports a breakage that did not happen, and hides the one
|
|
12655
|
+
// that did (the upstream section named below).
|
|
12656
|
+
status: "BLOCKED",
|
|
12203
12657
|
t3Results: [],
|
|
12204
12658
|
sectionSummary: "",
|
|
12205
|
-
issues: [
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12659
|
+
issues: [
|
|
12660
|
+
`${blockedInfo.reason}. This section was not attempted, so no tokens were spent on it.`
|
|
12661
|
+
],
|
|
12662
|
+
blockedBy: { ids: [...blockedInfo.blockedBy], titles: [...blockedInfo.blockedByTitles] }
|
|
12663
|
+
});
|
|
12664
|
+
completedSections++;
|
|
12665
|
+
this.sendStatusUpdate({
|
|
12666
|
+
progressPct: 10 + Math.floor(completedSections / totalSections * 85),
|
|
12667
|
+
currentAction: `Skipped: ${section.sectionTitle} (blocked by an upstream failure)`,
|
|
12668
|
+
status: "IN_PROGRESS"
|
|
12669
|
+
});
|
|
12670
|
+
this.emit("tier:status", {
|
|
12671
|
+
tierId: section.sectionId,
|
|
12672
|
+
// A blocked section has no tier instance — it was never
|
|
12673
|
+
// constructed — so its runtime id and its graph id are the same
|
|
12674
|
+
// thing here. Emitting nodeId anyway keeps every node in the
|
|
12675
|
+
// stream addressable in the graph's id space, which is what
|
|
12676
|
+
// dependsOn edges point at.
|
|
12677
|
+
nodeId: section.sectionId,
|
|
12678
|
+
dependsOn: [...section.dependsOn ?? []],
|
|
12679
|
+
role: "T2",
|
|
12680
|
+
label: section.sectionTitle,
|
|
12681
|
+
status: "BLOCKED",
|
|
12682
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12683
|
+
output: blockedInfo.reason
|
|
12684
|
+
});
|
|
12685
|
+
this.emit("section:complete", {
|
|
12686
|
+
id: node.id,
|
|
12687
|
+
title: section.sectionTitle,
|
|
12688
|
+
status: "BLOCKED",
|
|
12689
|
+
blockedBy: [...blockedInfo.blockedByTitles],
|
|
12690
|
+
output: ""
|
|
12691
|
+
});
|
|
12692
|
+
},
|
|
12693
|
+
onNodeComplete: (node, result) => {
|
|
12694
|
+
resultMap.set(node.id, result);
|
|
12695
|
+
completedSections++;
|
|
12696
|
+
this.emit("section:complete", {
|
|
12697
|
+
id: node.id,
|
|
12698
|
+
title: result.sectionTitle,
|
|
12699
|
+
status: result.status,
|
|
12700
|
+
output: result.sectionSummary
|
|
12701
|
+
});
|
|
12217
12702
|
}
|
|
12218
|
-
}
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12703
|
+
},
|
|
12704
|
+
{
|
|
12705
|
+
// Cross-section concurrency respects the same t3Execution mode as the
|
|
12706
|
+
// intra-section T3 wave (t2-manager.ts) — 'sequential' otherwise only
|
|
12707
|
+
// serialized T3 workers within a single section while every section's
|
|
12708
|
+
// T2Manager (and its T3 workers) still ran in parallel here.
|
|
12709
|
+
mode: this.router.getT3ExecutionMode?.() === "sequential" ? "sequential" : "parallel",
|
|
12710
|
+
// A section that declared `dependsOn` said it needs that section's
|
|
12711
|
+
// output. Running it after the dependency failed produces a second
|
|
12712
|
+
// failure, billed, whose cause is the first one — so don't.
|
|
12713
|
+
onUpstreamFailure: "block"
|
|
12223
12714
|
}
|
|
12224
|
-
|
|
12225
|
-
await
|
|
12715
|
+
);
|
|
12716
|
+
await scheduler.run();
|
|
12226
12717
|
return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
|
|
12227
12718
|
}
|
|
12228
12719
|
async compileFinalOutput(originalPrompt, plan, t2Results) {
|
|
12229
|
-
const completedSections = t2Results.filter(
|
|
12720
|
+
const completedSections = t2Results.filter(producedOutput);
|
|
12230
12721
|
if (!completedSections.length) {
|
|
12231
12722
|
const allIssues = [
|
|
12232
12723
|
...t2Results.flatMap((r) => r.issues ?? []),
|
|
@@ -12246,7 +12737,6 @@ ${r.sectionSummary}
|
|
|
12246
12737
|
Outputs:
|
|
12247
12738
|
${r.t3Results.filter((t) => t.status === "COMPLETED").map((t) => `\u2022 ${typeof t.output === "string" ? t.output : JSON.stringify(t.output)}`).join("\n")}`).join("\n\n---\n\n");
|
|
12248
12739
|
const openIssues = t2Results.flatMap((r) => r.issues).filter(Boolean);
|
|
12249
|
-
t2Results.filter((r) => r.status === "FAILED" || r.status === "PARTIAL");
|
|
12250
12740
|
const compilePrompt = `Compile a final, coherent response to the user's original request.
|
|
12251
12741
|
|
|
12252
12742
|
Original request: ${originalPrompt}
|
|
@@ -12741,6 +13231,7 @@ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
|
12741
13231
|
};
|
|
12742
13232
|
var DEFAULT_VIDEO_SECONDS = 5;
|
|
12743
13233
|
var MAX_VIDEO_SECONDS = 8;
|
|
13234
|
+
var MIN_VIDEO_SECONDS = 4;
|
|
12744
13235
|
var GenerateVideoTool = class extends BaseTool {
|
|
12745
13236
|
constructor(deps) {
|
|
12746
13237
|
super();
|
|
@@ -12757,7 +13248,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12757
13248
|
type: "object",
|
|
12758
13249
|
properties: {
|
|
12759
13250
|
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
12760
|
-
seconds: { type: "number", description: `Clip length,
|
|
13251
|
+
seconds: { type: "number", description: `Clip length, ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
12761
13252
|
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
12762
13253
|
model: { type: "string", description: "Optional: force a specific video model." }
|
|
12763
13254
|
},
|
|
@@ -12769,7 +13260,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12769
13260
|
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
12770
13261
|
if (!r.ok) return r.error;
|
|
12771
13262
|
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
12772
|
-
const seconds = Math.max(
|
|
13263
|
+
const seconds = Math.max(MIN_VIDEO_SECONDS, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
12773
13264
|
const asset = await generateVideo(
|
|
12774
13265
|
r.selected.capability,
|
|
12775
13266
|
r.cfg,
|
|
@@ -12786,7 +13277,7 @@ var GenerateVideoTool = class extends BaseTool {
|
|
|
12786
13277
|
const location = await this.deps.sink(asset);
|
|
12787
13278
|
const unit = r.selected.cost;
|
|
12788
13279
|
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
12789
|
-
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (
|
|
13280
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (allowed ${MIN_VIDEO_SECONDS}-${MAX_VIDEO_SECONDS}).` : "";
|
|
12790
13281
|
return `Video generated and saved to ${location}.
|
|
12791
13282
|
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
12792
13283
|
}
|
|
@@ -12855,16 +13346,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
12855
13346
|
if (typeof input !== "string" || input.length === 0) {
|
|
12856
13347
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
12857
13348
|
}
|
|
12858
|
-
const root =
|
|
12859
|
-
const abs =
|
|
12860
|
-
const rel =
|
|
12861
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
13349
|
+
const root = path34.resolve(workspaceRoot);
|
|
13350
|
+
const abs = path34.isAbsolute(input) ? path34.resolve(input) : path34.resolve(root, input);
|
|
13351
|
+
const rel = path34.relative(root, abs);
|
|
13352
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
12862
13353
|
throw new WorkspaceSandboxError(input, root);
|
|
12863
13354
|
}
|
|
12864
13355
|
try {
|
|
12865
|
-
const real =
|
|
12866
|
-
const realRel =
|
|
12867
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
13356
|
+
const real = fs31.realpathSync(abs);
|
|
13357
|
+
const realRel = path34.relative(root, real);
|
|
13358
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34.isAbsolute(realRel))) {
|
|
12868
13359
|
throw new WorkspaceSandboxError(input, root);
|
|
12869
13360
|
}
|
|
12870
13361
|
} catch (e) {
|
|
@@ -13517,7 +14008,7 @@ var GenerateDocumentTool = class extends BaseTool {
|
|
|
13517
14008
|
}
|
|
13518
14009
|
}
|
|
13519
14010
|
const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
|
|
13520
|
-
await fs11.mkdir(
|
|
14011
|
+
await fs11.mkdir(path34.dirname(absPath), { recursive: true });
|
|
13521
14012
|
await fs11.writeFile(absPath, bytes);
|
|
13522
14013
|
const charts = countCharts(content);
|
|
13523
14014
|
return [
|
|
@@ -13738,7 +14229,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
13738
14229
|
} catch {
|
|
13739
14230
|
}
|
|
13740
14231
|
}
|
|
13741
|
-
await fs11.mkdir(
|
|
14232
|
+
await fs11.mkdir(path34.dirname(absPath), { recursive: true });
|
|
13742
14233
|
await fs11.writeFile(absPath, content, "utf-8");
|
|
13743
14234
|
return `Written ${content.length} characters to ${filePath}`;
|
|
13744
14235
|
}
|
|
@@ -14236,7 +14727,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
14236
14727
|
};
|
|
14237
14728
|
async function fileToImageAttachment(filePath) {
|
|
14238
14729
|
const data = await fs11.readFile(filePath);
|
|
14239
|
-
const ext =
|
|
14730
|
+
const ext = path34.extname(filePath).toLowerCase();
|
|
14240
14731
|
const mimeMap = {
|
|
14241
14732
|
".jpg": "image/jpeg",
|
|
14242
14733
|
".jpeg": "image/jpeg",
|
|
@@ -14270,14 +14761,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
14270
14761
|
const filePath = input["path"];
|
|
14271
14762
|
const content = input["content"];
|
|
14272
14763
|
const title = input["title"];
|
|
14273
|
-
const dir =
|
|
14274
|
-
if (!
|
|
14275
|
-
|
|
14764
|
+
const dir = path34.dirname(filePath);
|
|
14765
|
+
if (!fs31.existsSync(dir)) {
|
|
14766
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
14276
14767
|
}
|
|
14277
14768
|
return new Promise((resolve2, reject) => {
|
|
14278
14769
|
try {
|
|
14279
14770
|
const doc = new PDFDocument({ margin: 50 });
|
|
14280
|
-
const stream =
|
|
14771
|
+
const stream = fs31.createWriteStream(filePath);
|
|
14281
14772
|
doc.pipe(stream);
|
|
14282
14773
|
if (title) {
|
|
14283
14774
|
doc.info["Title"] = title;
|
|
@@ -14355,22 +14846,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
14355
14846
|
}
|
|
14356
14847
|
cmdPrefix = NODE_CMD;
|
|
14357
14848
|
}
|
|
14358
|
-
const tmpDir =
|
|
14359
|
-
if (!
|
|
14360
|
-
|
|
14849
|
+
const tmpDir = path34.join(this.workspaceRoot, ".cascade", "tmp");
|
|
14850
|
+
if (!fs31.existsSync(tmpDir)) {
|
|
14851
|
+
fs31.mkdirSync(tmpDir, { recursive: true });
|
|
14361
14852
|
}
|
|
14362
14853
|
const extension = language === "python" ? "py" : "js";
|
|
14363
14854
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
14364
|
-
const filePath =
|
|
14365
|
-
|
|
14855
|
+
const filePath = path34.join(tmpDir, fileName);
|
|
14856
|
+
fs31.writeFileSync(filePath, code, "utf-8");
|
|
14366
14857
|
const execArgs = [filePath, ...args];
|
|
14367
14858
|
return new Promise((resolve2) => {
|
|
14368
14859
|
const startMs = Date.now();
|
|
14369
14860
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
14370
14861
|
const duration = Date.now() - startMs;
|
|
14371
14862
|
try {
|
|
14372
|
-
if (
|
|
14373
|
-
|
|
14863
|
+
if (fs31.existsSync(filePath)) {
|
|
14864
|
+
fs31.unlinkSync(filePath);
|
|
14374
14865
|
}
|
|
14375
14866
|
} catch (cleanupErr) {
|
|
14376
14867
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -14682,7 +15173,7 @@ var GlobTool = class extends BaseTool {
|
|
|
14682
15173
|
};
|
|
14683
15174
|
async execute(input, _options) {
|
|
14684
15175
|
const pattern = input["pattern"];
|
|
14685
|
-
const searchPath = input["path"] ?
|
|
15176
|
+
const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
14686
15177
|
const matches = await glob(pattern, {
|
|
14687
15178
|
cwd: searchPath,
|
|
14688
15179
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -14695,7 +15186,7 @@ var GlobTool = class extends BaseTool {
|
|
|
14695
15186
|
const withMtime = await Promise.all(
|
|
14696
15187
|
matches.map(async (rel) => {
|
|
14697
15188
|
try {
|
|
14698
|
-
const stat = await fs11.stat(
|
|
15189
|
+
const stat = await fs11.stat(path34.join(searchPath, rel));
|
|
14699
15190
|
return { rel, mtime: stat.mtimeMs };
|
|
14700
15191
|
} catch {
|
|
14701
15192
|
return { rel, mtime: 0 };
|
|
@@ -14744,7 +15235,7 @@ var GrepTool = class extends BaseTool {
|
|
|
14744
15235
|
};
|
|
14745
15236
|
async execute(input, _options) {
|
|
14746
15237
|
const pattern = input["pattern"];
|
|
14747
|
-
const searchPath = input["path"] ?
|
|
15238
|
+
const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
14748
15239
|
const globPattern = input["glob"];
|
|
14749
15240
|
const outputMode = input["output_mode"] ?? "content";
|
|
14750
15241
|
const context = input["context"] ?? 0;
|
|
@@ -14798,12 +15289,12 @@ var GrepTool = class extends BaseTool {
|
|
|
14798
15289
|
nodir: true
|
|
14799
15290
|
});
|
|
14800
15291
|
} catch {
|
|
14801
|
-
files = [
|
|
15292
|
+
files = [path34.relative(searchPath, searchPath) || "."];
|
|
14802
15293
|
}
|
|
14803
15294
|
const results = [];
|
|
14804
15295
|
let totalCount = 0;
|
|
14805
15296
|
for (const rel of files) {
|
|
14806
|
-
const abs =
|
|
15297
|
+
const abs = path34.join(searchPath, rel);
|
|
14807
15298
|
let content;
|
|
14808
15299
|
try {
|
|
14809
15300
|
content = await fs11.readFile(abs, "utf-8");
|
|
@@ -15190,10 +15681,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
15190
15681
|
}
|
|
15191
15682
|
isIgnored(filePath) {
|
|
15192
15683
|
if (!filePath) return false;
|
|
15193
|
-
const abs =
|
|
15194
|
-
const rel =
|
|
15195
|
-
if (!rel || rel.startsWith("..") ||
|
|
15196
|
-
const posixRel = rel.split(
|
|
15684
|
+
const abs = path34.resolve(this.workspaceRoot, filePath);
|
|
15685
|
+
const rel = path34.relative(this.workspaceRoot, abs);
|
|
15686
|
+
if (!rel || rel.startsWith("..") || path34.isAbsolute(rel)) return true;
|
|
15687
|
+
const posixRel = rel.split(path34.sep).join("/");
|
|
15197
15688
|
return this.ignoreMatcher.ignores(posixRel);
|
|
15198
15689
|
}
|
|
15199
15690
|
};
|
|
@@ -15427,22 +15918,22 @@ var FileMcpOAuthStore = class {
|
|
|
15427
15918
|
filePath;
|
|
15428
15919
|
load() {
|
|
15429
15920
|
try {
|
|
15430
|
-
return JSON.parse(
|
|
15921
|
+
return JSON.parse(fs31.readFileSync(this.filePath, "utf-8"));
|
|
15431
15922
|
} catch {
|
|
15432
15923
|
return void 0;
|
|
15433
15924
|
}
|
|
15434
15925
|
}
|
|
15435
15926
|
save(state) {
|
|
15436
|
-
|
|
15437
|
-
|
|
15927
|
+
fs31.mkdirSync(path34.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
15928
|
+
fs31.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
15438
15929
|
try {
|
|
15439
|
-
|
|
15930
|
+
fs31.chmodSync(this.filePath, 384);
|
|
15440
15931
|
} catch {
|
|
15441
15932
|
}
|
|
15442
15933
|
}
|
|
15443
15934
|
clear() {
|
|
15444
15935
|
try {
|
|
15445
|
-
|
|
15936
|
+
fs31.rmSync(this.filePath);
|
|
15446
15937
|
} catch {
|
|
15447
15938
|
}
|
|
15448
15939
|
}
|
|
@@ -15905,6 +16396,14 @@ function heuristicAnalyze(prompt) {
|
|
|
15905
16396
|
const estimatedTokens = wordCount * 5;
|
|
15906
16397
|
return { type, complexity, requiresReasoning, requiresVision, estimatedTokens, confidence };
|
|
15907
16398
|
}
|
|
16399
|
+
function hashPrompt(prompt) {
|
|
16400
|
+
let hash = 2166136261;
|
|
16401
|
+
for (let index = 0; index < prompt.length; index++) {
|
|
16402
|
+
hash ^= prompt.charCodeAt(index);
|
|
16403
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
16404
|
+
}
|
|
16405
|
+
return `${prompt.length}:${hash.toString(36)}`;
|
|
16406
|
+
}
|
|
15908
16407
|
var analysisCache = /* @__PURE__ */ new Map();
|
|
15909
16408
|
var TASK_TYPE_TAGS = {
|
|
15910
16409
|
code: ["code", "instruction"],
|
|
@@ -15919,7 +16418,24 @@ var TaskAnalyzer = class {
|
|
|
15919
16418
|
feedback;
|
|
15920
16419
|
bias;
|
|
15921
16420
|
lastProfile = null;
|
|
15922
|
-
|
|
16421
|
+
/** Models chosen by the run currently in flight. Cleared when it completes. */
|
|
16422
|
+
currentRunSelections = /* @__PURE__ */ new Map();
|
|
16423
|
+
/**
|
|
16424
|
+
* Immutable snapshot of the last COMPLETED run: its selections AND the task
|
|
16425
|
+
* type they were chosen for.
|
|
16426
|
+
*
|
|
16427
|
+
* The task type has to travel WITH the selections. `lastProfile` is overwritten
|
|
16428
|
+
* by the next analyze(), so if run B started before the user rated run A, A's
|
|
16429
|
+
* models were credited under B's task type — teaching the router that a coding
|
|
16430
|
+
* model is good at creative writing, from a rating that never said so.
|
|
16431
|
+
*
|
|
16432
|
+
* Explicit ratings arrive after the run finishes — rateLastRun() is by nature
|
|
16433
|
+
* a reaction to a finished answer — but the run finalizer calls
|
|
16434
|
+
* recordRunOutcome() first, and that used to clear the only map there was. So
|
|
16435
|
+
* every explicit rating iterated an empty map, recorded nothing, and returned
|
|
16436
|
+
* false: the 3x-weighted user signal never reached the tracker at all.
|
|
16437
|
+
*/
|
|
16438
|
+
lastCompletedRun;
|
|
15923
16439
|
constructor(tracker, bias = "balanced") {
|
|
15924
16440
|
this.tracker = tracker;
|
|
15925
16441
|
this.bias = bias;
|
|
@@ -15948,7 +16464,7 @@ var TaskAnalyzer = class {
|
|
|
15948
16464
|
* Low confidence prompts fall back to a conservative mixed/moderate profile.
|
|
15949
16465
|
*/
|
|
15950
16466
|
async analyze(prompt) {
|
|
15951
|
-
const cacheKey = prompt
|
|
16467
|
+
const cacheKey = hashPrompt(prompt);
|
|
15952
16468
|
const cached = analysisCache.get(cacheKey);
|
|
15953
16469
|
if (cached) {
|
|
15954
16470
|
this.lastProfile = cached;
|
|
@@ -15966,11 +16482,15 @@ var TaskAnalyzer = class {
|
|
|
15966
16482
|
*/
|
|
15967
16483
|
async selectModel(prompt, tier, selector, opts) {
|
|
15968
16484
|
const profile = await this.analyze(prompt);
|
|
16485
|
+
const recordSelection = (model) => {
|
|
16486
|
+
if (model) this.currentRunSelections.set(tier, model);
|
|
16487
|
+
return model;
|
|
16488
|
+
};
|
|
15969
16489
|
if (profile.requiresVision) {
|
|
15970
|
-
return selector.selectVisionModel();
|
|
16490
|
+
return recordSelection(selector.selectVisionModel());
|
|
15971
16491
|
}
|
|
15972
16492
|
let candidates2 = selector.getCandidatesForTier(tier);
|
|
15973
|
-
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
16493
|
+
if (candidates2.length === 0) return recordSelection(selector.selectForTier(tier));
|
|
15974
16494
|
if (opts?.requiresToolUse) {
|
|
15975
16495
|
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
15976
16496
|
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
@@ -15980,9 +16500,7 @@ var TaskAnalyzer = class {
|
|
|
15980
16500
|
score: this.scoreModel(m, profile)
|
|
15981
16501
|
}));
|
|
15982
16502
|
scored.sort((a, b) => b.score - a.score);
|
|
15983
|
-
|
|
15984
|
-
if (best) this.lastSelectedModels.set(tier, best);
|
|
15985
|
-
return best;
|
|
16503
|
+
return recordSelection(scored[0]?.model ?? selector.selectForTier(tier));
|
|
15986
16504
|
}
|
|
15987
16505
|
/**
|
|
15988
16506
|
* Record the outcome of a completed run across all tiers that were selected
|
|
@@ -15991,25 +16509,38 @@ var TaskAnalyzer = class {
|
|
|
15991
16509
|
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
15992
16510
|
if (!this.tracker || !this.lastProfile) return;
|
|
15993
16511
|
const taskType = this.lastProfile.type;
|
|
15994
|
-
for (const [tier, model] of this.
|
|
16512
|
+
for (const [tier, model] of this.currentRunSelections) {
|
|
15995
16513
|
const cost = costByTier[tier] ?? 0;
|
|
15996
16514
|
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
15997
16515
|
}
|
|
15998
|
-
this.
|
|
16516
|
+
this.lastCompletedRun = { selections: new Map(this.currentRunSelections), taskType };
|
|
16517
|
+
this.currentRunSelections.clear();
|
|
15999
16518
|
void this.tracker.save();
|
|
16000
16519
|
}
|
|
16001
16520
|
/**
|
|
16002
|
-
* Record an explicit user rating (good/bad) for the last run's
|
|
16521
|
+
* Record an explicit user rating (good/bad) for the last COMPLETED run's models.
|
|
16003
16522
|
* Explicit ratings carry 3× the weight of auto-detected outcomes.
|
|
16004
|
-
*
|
|
16523
|
+
*
|
|
16524
|
+
* Reads the completed-run snapshot, not the in-flight map: by the time a user
|
|
16525
|
+
* can rate an answer, the finalizer has already run and the in-flight map is
|
|
16526
|
+
* empty.
|
|
16527
|
+
*
|
|
16528
|
+
* The snapshot is CONSUMED. recordExplicit() weights a rating 3x by recording
|
|
16529
|
+
* three samples, so a double submit would inject six — a stutter on the
|
|
16530
|
+
* button would count as two opinions. Consuming makes a second call a no-op
|
|
16531
|
+
* that reports false, which is what "rate this run" should mean.
|
|
16005
16532
|
*/
|
|
16006
16533
|
recordExplicitRating(rating) {
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16534
|
+
const snapshot = this.lastCompletedRun;
|
|
16535
|
+
if (!this.tracker || !snapshot || snapshot.selections.size === 0) return false;
|
|
16536
|
+
const ratedModels = /* @__PURE__ */ new Set();
|
|
16537
|
+
for (const [, model] of snapshot.selections) {
|
|
16538
|
+
if (ratedModels.has(model.id)) continue;
|
|
16539
|
+
ratedModels.add(model.id);
|
|
16540
|
+
this.tracker.recordExplicit(model.id, snapshot.taskType, rating, 0);
|
|
16541
|
+
}
|
|
16542
|
+
this.lastCompletedRun = void 0;
|
|
16543
|
+
return true;
|
|
16013
16544
|
}
|
|
16014
16545
|
scoreModel(model, profile) {
|
|
16015
16546
|
const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
|
|
@@ -16050,7 +16581,7 @@ var TaskAnalyzer = class {
|
|
|
16050
16581
|
|
|
16051
16582
|
// src/core/router/model-performance-tracker.ts
|
|
16052
16583
|
init_pricing();
|
|
16053
|
-
var DEFAULT_STATS_FILE =
|
|
16584
|
+
var DEFAULT_STATS_FILE = path34.join(os10.homedir(), ".cascade", "model-perf.json");
|
|
16054
16585
|
var ModelPerformanceTracker = class {
|
|
16055
16586
|
stats = /* @__PURE__ */ new Map();
|
|
16056
16587
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -16090,7 +16621,7 @@ var ModelPerformanceTracker = class {
|
|
|
16090
16621
|
async save() {
|
|
16091
16622
|
if (this.readOnly) return;
|
|
16092
16623
|
try {
|
|
16093
|
-
await fs11.mkdir(
|
|
16624
|
+
await fs11.mkdir(path34.dirname(this.statsFile), { recursive: true });
|
|
16094
16625
|
const modelsObj = {};
|
|
16095
16626
|
const featuresObj = {};
|
|
16096
16627
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -16638,7 +17169,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
16638
17169
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
16639
17170
|
async loadPersistedTools() {
|
|
16640
17171
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
16641
|
-
const file =
|
|
17172
|
+
const file = path34.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
16642
17173
|
try {
|
|
16643
17174
|
const raw = await fs11.readFile(file, "utf-8");
|
|
16644
17175
|
const specs = JSON.parse(raw);
|
|
@@ -16662,8 +17193,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
16662
17193
|
}
|
|
16663
17194
|
async persist() {
|
|
16664
17195
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
16665
|
-
const dir =
|
|
16666
|
-
const file =
|
|
17196
|
+
const dir = path34.join(this.workspacePath, ".cascade");
|
|
17197
|
+
const file = path34.join(dir, DYNAMIC_TOOLS_FILE);
|
|
16667
17198
|
try {
|
|
16668
17199
|
await fs11.mkdir(dir, { recursive: true });
|
|
16669
17200
|
await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -16683,12 +17214,12 @@ var WorldStateDB = class {
|
|
|
16683
17214
|
constructor(workspacePath, debugMode = false) {
|
|
16684
17215
|
this.workspacePath = workspacePath;
|
|
16685
17216
|
this.debugMode = debugMode;
|
|
16686
|
-
const cascadeDir =
|
|
16687
|
-
if (!
|
|
16688
|
-
|
|
17217
|
+
const cascadeDir = path34.join(workspacePath, ".cascade");
|
|
17218
|
+
if (!fs31.existsSync(cascadeDir)) {
|
|
17219
|
+
fs31.mkdirSync(cascadeDir, { recursive: true });
|
|
16689
17220
|
}
|
|
16690
|
-
this.keyPath =
|
|
16691
|
-
this.dbPath =
|
|
17221
|
+
this.keyPath = path34.join(cascadeDir, "world_state.key");
|
|
17222
|
+
this.dbPath = path34.join(cascadeDir, "world_state.db");
|
|
16692
17223
|
this.initEncryptionKey();
|
|
16693
17224
|
this.db = new Database2(this.dbPath);
|
|
16694
17225
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -16745,11 +17276,11 @@ var WorldStateDB = class {
|
|
|
16745
17276
|
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
16746
17277
|
}
|
|
16747
17278
|
initEncryptionKey() {
|
|
16748
|
-
if (
|
|
16749
|
-
this.encryptionKey =
|
|
17279
|
+
if (fs31.existsSync(this.keyPath)) {
|
|
17280
|
+
this.encryptionKey = fs31.readFileSync(this.keyPath);
|
|
16750
17281
|
} else {
|
|
16751
17282
|
this.encryptionKey = crypto.randomBytes(32);
|
|
16752
|
-
|
|
17283
|
+
fs31.writeFileSync(this.keyPath, this.encryptionKey);
|
|
16753
17284
|
}
|
|
16754
17285
|
}
|
|
16755
17286
|
encrypt(text) {
|
|
@@ -17035,9 +17566,9 @@ var WorldStateDB = class {
|
|
|
17035
17566
|
dumpDebugIfNeeded() {
|
|
17036
17567
|
if (!this.debugMode) return;
|
|
17037
17568
|
try {
|
|
17038
|
-
const dumpPath =
|
|
17569
|
+
const dumpPath = path34.join(os10.tmpdir(), "cascade_world_state_debug.json");
|
|
17039
17570
|
const entries = this.getAllEntries();
|
|
17040
|
-
|
|
17571
|
+
fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
17041
17572
|
} catch (err) {
|
|
17042
17573
|
console.error("Failed to dump debug world state", err);
|
|
17043
17574
|
}
|
|
@@ -17046,6 +17577,212 @@ var WorldStateDB = class {
|
|
|
17046
17577
|
this.db.close();
|
|
17047
17578
|
}
|
|
17048
17579
|
};
|
|
17580
|
+
var RESUME_SCHEMA_VERSION = 1;
|
|
17581
|
+
var DEFAULT_MAX_CHECKPOINTS = 5;
|
|
17582
|
+
var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17583
|
+
var FILE_PATTERN = /^(\d+)-(.+)\.json$/;
|
|
17584
|
+
var CLAIM_SUFFIX = ".claimed";
|
|
17585
|
+
var DEFAULT_CLAIM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
17586
|
+
function safeId(taskId) {
|
|
17587
|
+
const cleaned = taskId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
17588
|
+
return cleaned.slice(0, 100) || "run";
|
|
17589
|
+
}
|
|
17590
|
+
function isCheckpoint(value) {
|
|
17591
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17592
|
+
const c = value;
|
|
17593
|
+
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);
|
|
17594
|
+
}
|
|
17595
|
+
var ResumeStore = class {
|
|
17596
|
+
dir;
|
|
17597
|
+
maxCheckpoints;
|
|
17598
|
+
maxAgeMs;
|
|
17599
|
+
now;
|
|
17600
|
+
claimTimeoutMs;
|
|
17601
|
+
constructor(options) {
|
|
17602
|
+
this.dir = options.dir;
|
|
17603
|
+
this.maxCheckpoints = options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS;
|
|
17604
|
+
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
17605
|
+
this.now = options.now ?? Date.now;
|
|
17606
|
+
this.claimTimeoutMs = options.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS;
|
|
17607
|
+
}
|
|
17608
|
+
/**
|
|
17609
|
+
* Persist a checkpoint and prune.
|
|
17610
|
+
*
|
|
17611
|
+
* Never throws — failing to save a resume file must not turn a recoverable
|
|
17612
|
+
* stop into a crash — but DOES report whether the atomic rename landed.
|
|
17613
|
+
* Callers decide whether an original claim is safe to discard based on that
|
|
17614
|
+
* answer, so "I swallowed the error" and "the checkpoint is on disk" must not
|
|
17615
|
+
* look alike to them.
|
|
17616
|
+
*
|
|
17617
|
+
* @returns true only when the checkpoint is durably on disk.
|
|
17618
|
+
*/
|
|
17619
|
+
async save(checkpoint) {
|
|
17620
|
+
try {
|
|
17621
|
+
const timestamp = this.now();
|
|
17622
|
+
const record = {
|
|
17623
|
+
...checkpoint,
|
|
17624
|
+
version: RESUME_SCHEMA_VERSION,
|
|
17625
|
+
createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
|
|
17626
|
+
};
|
|
17627
|
+
await promises.mkdir(this.dir, { recursive: true });
|
|
17628
|
+
const finalPath = path34.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
|
|
17629
|
+
const tempPath = `${finalPath}.${process.pid}.tmp`;
|
|
17630
|
+
await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
|
|
17631
|
+
await promises.rename(tempPath, finalPath);
|
|
17632
|
+
await this.prune();
|
|
17633
|
+
return true;
|
|
17634
|
+
} catch {
|
|
17635
|
+
return false;
|
|
17636
|
+
}
|
|
17637
|
+
}
|
|
17638
|
+
/** Valid, unexpired checkpoints, newest first. */
|
|
17639
|
+
async list() {
|
|
17640
|
+
let entries;
|
|
17641
|
+
try {
|
|
17642
|
+
entries = await promises.readdir(this.dir);
|
|
17643
|
+
} catch {
|
|
17644
|
+
return [];
|
|
17645
|
+
}
|
|
17646
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17647
|
+
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);
|
|
17648
|
+
const checkpoints = [];
|
|
17649
|
+
for (const candidate of candidates2) {
|
|
17650
|
+
try {
|
|
17651
|
+
const raw = await promises.readFile(path34.join(this.dir, candidate.name), "utf-8");
|
|
17652
|
+
const parsed = JSON.parse(raw);
|
|
17653
|
+
if (isCheckpoint(parsed)) checkpoints.push(parsed);
|
|
17654
|
+
} catch {
|
|
17655
|
+
continue;
|
|
17656
|
+
}
|
|
17657
|
+
}
|
|
17658
|
+
return checkpoints;
|
|
17659
|
+
}
|
|
17660
|
+
/** The most recent resumable run, or null. */
|
|
17661
|
+
async latest() {
|
|
17662
|
+
return (await this.list())[0] ?? null;
|
|
17663
|
+
}
|
|
17664
|
+
/**
|
|
17665
|
+
* Claim a checkpoint for a resume attempt.
|
|
17666
|
+
*
|
|
17667
|
+
* This used to DELETE the file and return it, which opened a window with no
|
|
17668
|
+
* recovery record at all: between preparing the continuation and the resumed
|
|
17669
|
+
* run writing its own checkpoint, a crash lost the only copy — and a crash is
|
|
17670
|
+
* precisely what this store exists to survive. Deleting on read optimised for
|
|
17671
|
+
* "never resume twice" at the cost of "never lose the work", which is the
|
|
17672
|
+
* wrong trade for a recovery mechanism.
|
|
17673
|
+
*
|
|
17674
|
+
* Instead the file is RENAMED to `.claimed`, which still hides it from
|
|
17675
|
+
* `list()` (so a concurrent resume cannot take the same work) but keeps it on
|
|
17676
|
+
* disk. The caller then either `release`s it — restoring it for another
|
|
17677
|
+
* attempt — or `settle`s it once the resumed run is safely under way. A claim
|
|
17678
|
+
* abandoned by a dead process is reclaimed after `claimTimeoutMs`.
|
|
17679
|
+
*/
|
|
17680
|
+
async claim(taskId) {
|
|
17681
|
+
const all = await this.list();
|
|
17682
|
+
const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
|
|
17683
|
+
if (!target) return null;
|
|
17684
|
+
try {
|
|
17685
|
+
const entries = await promises.readdir(this.dir);
|
|
17686
|
+
const wanted = safeId(target.taskId);
|
|
17687
|
+
const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
|
|
17688
|
+
if (!source) return null;
|
|
17689
|
+
const claimId = `${this.now()}-${safeId(target.taskId)}`;
|
|
17690
|
+
await promises.rename(path34.join(this.dir, source), path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
|
|
17691
|
+
return { checkpoint: target, claimId };
|
|
17692
|
+
} catch {
|
|
17693
|
+
return null;
|
|
17694
|
+
}
|
|
17695
|
+
}
|
|
17696
|
+
/** Give a claim back, so the work stays recoverable after a failed start. */
|
|
17697
|
+
async release(claimId) {
|
|
17698
|
+
try {
|
|
17699
|
+
await promises.rename(
|
|
17700
|
+
path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
|
|
17701
|
+
path34.join(this.dir, `${claimId}.json`)
|
|
17702
|
+
);
|
|
17703
|
+
} catch {
|
|
17704
|
+
}
|
|
17705
|
+
}
|
|
17706
|
+
/** Discard a claim for good — the resumed run has taken ownership. */
|
|
17707
|
+
async settle(claimId) {
|
|
17708
|
+
try {
|
|
17709
|
+
await promises.rm(path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
|
|
17710
|
+
} catch {
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
17713
|
+
/**
|
|
17714
|
+
* Claim-and-discard in one step. Kept for callers that genuinely want the old
|
|
17715
|
+
* semantics; prefer claim/settle so a crash mid-resume stays recoverable.
|
|
17716
|
+
*/
|
|
17717
|
+
async consume(taskId) {
|
|
17718
|
+
const claimed = await this.claim(taskId);
|
|
17719
|
+
if (!claimed) return null;
|
|
17720
|
+
await this.settle(claimed.claimId);
|
|
17721
|
+
return claimed.checkpoint;
|
|
17722
|
+
}
|
|
17723
|
+
/**
|
|
17724
|
+
* Return claims abandoned by a process that died mid-resume. Without this a
|
|
17725
|
+
* crash between claim and settle would strand the checkpoint invisibly —
|
|
17726
|
+
* trading one loss window for another.
|
|
17727
|
+
*/
|
|
17728
|
+
async reclaimStale() {
|
|
17729
|
+
let reclaimed = 0;
|
|
17730
|
+
try {
|
|
17731
|
+
const entries = await promises.readdir(this.dir);
|
|
17732
|
+
const cutoff = this.now() - this.claimTimeoutMs;
|
|
17733
|
+
for (const name of entries) {
|
|
17734
|
+
if (!name.endsWith(CLAIM_SUFFIX)) continue;
|
|
17735
|
+
const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
|
|
17736
|
+
if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
|
|
17737
|
+
await promises.rename(
|
|
17738
|
+
path34.join(this.dir, name),
|
|
17739
|
+
path34.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
|
|
17740
|
+
);
|
|
17741
|
+
reclaimed++;
|
|
17742
|
+
}
|
|
17743
|
+
} catch {
|
|
17744
|
+
}
|
|
17745
|
+
return reclaimed;
|
|
17746
|
+
}
|
|
17747
|
+
/** Remove every checkpoint for a task id. */
|
|
17748
|
+
async delete(taskId) {
|
|
17749
|
+
try {
|
|
17750
|
+
const entries = await promises.readdir(this.dir);
|
|
17751
|
+
const wanted = safeId(taskId);
|
|
17752
|
+
await Promise.all(entries.map(async (name) => {
|
|
17753
|
+
const match = FILE_PATTERN.exec(name);
|
|
17754
|
+
if (match?.[2] === wanted) {
|
|
17755
|
+
await promises.rm(path34.join(this.dir, name), { force: true });
|
|
17756
|
+
}
|
|
17757
|
+
}));
|
|
17758
|
+
} catch {
|
|
17759
|
+
}
|
|
17760
|
+
}
|
|
17761
|
+
/** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
|
|
17762
|
+
async prune() {
|
|
17763
|
+
try {
|
|
17764
|
+
const entries = await promises.readdir(this.dir);
|
|
17765
|
+
const cutoff = this.now() - this.maxAgeMs;
|
|
17766
|
+
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);
|
|
17767
|
+
const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
|
|
17768
|
+
const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
|
|
17769
|
+
await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
|
|
17770
|
+
(name) => promises.rm(path34.join(this.dir, name), { force: true })
|
|
17771
|
+
));
|
|
17772
|
+
} catch {
|
|
17773
|
+
}
|
|
17774
|
+
}
|
|
17775
|
+
};
|
|
17776
|
+
function summarizeCompleted(completed, perNodeChars = 500) {
|
|
17777
|
+
if (!completed.length) return "";
|
|
17778
|
+
const lines = completed.map((node) => {
|
|
17779
|
+
const body = node.output.length > perNodeChars ? `${node.output.slice(0, perNodeChars)}\u2026` : node.output;
|
|
17780
|
+
return `- [${node.status}] ${node.title}
|
|
17781
|
+
${body.replace(/\n/g, "\n ")}`;
|
|
17782
|
+
});
|
|
17783
|
+
return `ALREADY COMPLETED in a previous attempt \u2014 do NOT redo these:
|
|
17784
|
+
${lines.join("\n")}`;
|
|
17785
|
+
}
|
|
17049
17786
|
var ignore3 = _ignoreModule.default ?? _ignoreModule;
|
|
17050
17787
|
var PrivacyPaths = class {
|
|
17051
17788
|
localOnly;
|
|
@@ -17407,7 +18144,7 @@ var WorkspaceIndex = class {
|
|
|
17407
18144
|
isIgnored;
|
|
17408
18145
|
chunker;
|
|
17409
18146
|
constructor(opts) {
|
|
17410
|
-
this.root =
|
|
18147
|
+
this.root = path34.resolve(opts.root);
|
|
17411
18148
|
this.db = opts.db;
|
|
17412
18149
|
this.namespace = opts.namespace ?? "code";
|
|
17413
18150
|
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
@@ -17439,14 +18176,14 @@ var WorkspaceIndex = class {
|
|
|
17439
18176
|
return;
|
|
17440
18177
|
}
|
|
17441
18178
|
for (const entry of entries) {
|
|
17442
|
-
const abs =
|
|
18179
|
+
const abs = path34.join(dir, entry.name);
|
|
17443
18180
|
if (entry.isDirectory()) {
|
|
17444
18181
|
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
17445
18182
|
await walk(abs);
|
|
17446
18183
|
} else if (entry.isFile()) {
|
|
17447
|
-
const ext =
|
|
18184
|
+
const ext = path34.extname(entry.name).slice(1).toLowerCase();
|
|
17448
18185
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
17449
|
-
out.push(
|
|
18186
|
+
out.push(path34.relative(this.root, abs));
|
|
17450
18187
|
}
|
|
17451
18188
|
}
|
|
17452
18189
|
};
|
|
@@ -17476,7 +18213,7 @@ var WorkspaceIndex = class {
|
|
|
17476
18213
|
const entries = [];
|
|
17477
18214
|
for (const rel of rels) {
|
|
17478
18215
|
try {
|
|
17479
|
-
const abs =
|
|
18216
|
+
const abs = path34.join(this.root, rel);
|
|
17480
18217
|
const stat = await fs11.stat(abs);
|
|
17481
18218
|
if (stat.size > this.maxFileBytes) continue;
|
|
17482
18219
|
const buf = await fs11.readFile(abs);
|
|
@@ -18014,8 +18751,35 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
18014
18751
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
18015
18752
|
decisionLog = [];
|
|
18016
18753
|
initialized = false;
|
|
18017
|
-
/**
|
|
18754
|
+
/**
|
|
18755
|
+
* Last interrupted task — powers /continue (resumeRun). Kept in memory as a
|
|
18756
|
+
* fast path for the same-process case; the durable copy lives in `resumeStore`
|
|
18757
|
+
* so a crash or an exit no longer throws away everything already finished.
|
|
18758
|
+
*/
|
|
18018
18759
|
lastInterruptedRun;
|
|
18760
|
+
/** Durable resume checkpoints. See core/orchestration/resume-store.ts. */
|
|
18761
|
+
resumeStore;
|
|
18762
|
+
/** Claim held by an in-flight resume, settled once the run owns the work. */
|
|
18763
|
+
claimedResumeId;
|
|
18764
|
+
/**
|
|
18765
|
+
* Facts inherited from the checkpoint this run is resuming, so its own
|
|
18766
|
+
* checkpoint is cumulative rather than only covering this attempt.
|
|
18767
|
+
*/
|
|
18768
|
+
resumedFrom;
|
|
18769
|
+
/** Whether THIS run persisted a checkpoint of its own. Drives settle-vs-release. */
|
|
18770
|
+
wroteCheckpointThisRun = false;
|
|
18771
|
+
/**
|
|
18772
|
+
* Whether the run reached the end of its normal path.
|
|
18773
|
+
*
|
|
18774
|
+
* NOT the same as `runError == null`: the cancellation and budget handlers
|
|
18775
|
+
* both null `runError` deliberately (an intentional stop is not a telemetry
|
|
18776
|
+
* error) and the breaker path never sets it at all. Treating that as success
|
|
18777
|
+
* meant an immediate cancel — no output, no replacement checkpoint — deleted
|
|
18778
|
+
* the original claim and the finished sections with it.
|
|
18779
|
+
*/
|
|
18780
|
+
completedNormally = false;
|
|
18781
|
+
/** Section results of the CURRENT run, so an interruption can checkpoint them. */
|
|
18782
|
+
currentRunCompleted = [];
|
|
18019
18783
|
initPromise;
|
|
18020
18784
|
store;
|
|
18021
18785
|
audit;
|
|
@@ -18048,10 +18812,16 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
18048
18812
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
18049
18813
|
try {
|
|
18050
18814
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
18051
|
-
fileDeadModelPersistence(
|
|
18815
|
+
fileDeadModelPersistence(path34.join(workspacePath, ".cascade", "dead-models.json"))
|
|
18052
18816
|
));
|
|
18053
18817
|
} catch {
|
|
18054
18818
|
}
|
|
18819
|
+
try {
|
|
18820
|
+
this.resumeStore = new ResumeStore({
|
|
18821
|
+
dir: path34.join(workspacePath, ".cascade", "resume")
|
|
18822
|
+
});
|
|
18823
|
+
} catch {
|
|
18824
|
+
}
|
|
18055
18825
|
this.multimodal = new MultimodalRegistry(
|
|
18056
18826
|
(this.config.providers ?? []).map((p) => p.type)
|
|
18057
18827
|
);
|
|
@@ -18136,8 +18906,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
18136
18906
|
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
18137
18907
|
return;
|
|
18138
18908
|
}
|
|
18139
|
-
const dbPath = ci.dbPath ||
|
|
18140
|
-
|
|
18909
|
+
const dbPath = ci.dbPath || path34.join(this.workspacePath, ".cascade", "code-index.db");
|
|
18910
|
+
fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
|
|
18141
18911
|
const db = new Database2(dbPath);
|
|
18142
18912
|
db.pragma("journal_mode = WAL");
|
|
18143
18913
|
const complete = chatCompleterFromProviders(this.config.providers);
|
|
@@ -18517,10 +19287,57 @@ ${input}` }],
|
|
|
18517
19287
|
setFeedbackSource(source) {
|
|
18518
19288
|
this.router.setFeedbackSource(source);
|
|
18519
19289
|
}
|
|
18520
|
-
/**
|
|
19290
|
+
/**
|
|
19291
|
+
* Write a durable checkpoint for an interrupted run.
|
|
19292
|
+
*
|
|
19293
|
+
* Best-effort throughout: a run is already ending badly on every path that
|
|
19294
|
+
* calls this, and failing to record a resume file must not make that ending
|
|
19295
|
+
* worse. Nothing here is allowed to throw.
|
|
19296
|
+
*/
|
|
19297
|
+
async checkpointRun(taskId, prompt, partialOutput, reason, detail) {
|
|
19298
|
+
const inherited = this.resumedFrom?.completed ?? [];
|
|
19299
|
+
const merged = /* @__PURE__ */ new Map();
|
|
19300
|
+
for (const node of inherited) merged.set(node.id, node);
|
|
19301
|
+
for (const node of this.currentRunCompleted) {
|
|
19302
|
+
const existing = merged.get(node.id);
|
|
19303
|
+
if (existing?.status === "COMPLETED" && node.status !== "COMPLETED") continue;
|
|
19304
|
+
merged.set(node.id, node);
|
|
19305
|
+
}
|
|
19306
|
+
const completed = [...merged.values()];
|
|
19307
|
+
const canonicalPrompt = this.resumedFrom?.prompt ?? prompt;
|
|
19308
|
+
const carriedPartial = partialOutput || (this.resumedFrom?.partialOutput ?? "");
|
|
19309
|
+
if (!completed.length && !carriedPartial) return false;
|
|
19310
|
+
try {
|
|
19311
|
+
const saved = await this.resumeStore?.save({
|
|
19312
|
+
taskId,
|
|
19313
|
+
prompt: canonicalPrompt,
|
|
19314
|
+
reason,
|
|
19315
|
+
...detail ? { detail } : {},
|
|
19316
|
+
partialOutput: carriedPartial,
|
|
19317
|
+
completed
|
|
19318
|
+
});
|
|
19319
|
+
this.wroteCheckpointThisRun = saved === true;
|
|
19320
|
+
return saved === true;
|
|
19321
|
+
} catch {
|
|
19322
|
+
return false;
|
|
19323
|
+
}
|
|
19324
|
+
}
|
|
19325
|
+
/** True when a task can be resumed via /continue (this process or a previous one). */
|
|
18521
19326
|
hasResumableRun() {
|
|
18522
19327
|
return this.lastInterruptedRun != null;
|
|
18523
19328
|
}
|
|
19329
|
+
/**
|
|
19330
|
+
* True when a checkpoint from ANY process is resumable. Async because the
|
|
19331
|
+
* durable check touches disk; `hasResumableRun` remains the sync in-memory
|
|
19332
|
+
* answer for callers already on a hot path.
|
|
19333
|
+
*/
|
|
19334
|
+
async hasDurableResume() {
|
|
19335
|
+
return await this.resumeStore?.latest() != null;
|
|
19336
|
+
}
|
|
19337
|
+
/** Interrupted runs available to resume, newest first. */
|
|
19338
|
+
async listResumableRuns() {
|
|
19339
|
+
return await this.resumeStore?.list() ?? [];
|
|
19340
|
+
}
|
|
18524
19341
|
/**
|
|
18525
19342
|
* Raise the per-run token budget for a resume and return the continuation
|
|
18526
19343
|
* prompt (or null when nothing is resumable). Consumes the interrupted-run
|
|
@@ -18531,24 +19348,105 @@ ${input}` }],
|
|
|
18531
19348
|
const last = this.lastInterruptedRun;
|
|
18532
19349
|
if (!last) return null;
|
|
18533
19350
|
this.lastInterruptedRun = void 0;
|
|
18534
|
-
|
|
19351
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19352
|
+
return this.buildResumePrompt(last.prompt, last.partialOutput, []);
|
|
19353
|
+
}
|
|
19354
|
+
/**
|
|
19355
|
+
* Durable counterpart to prepareResume: recovers a run interrupted in ANY
|
|
19356
|
+
* process, including one that crashed before it could return. Consumes the
|
|
19357
|
+
* checkpoint, so the same finished work is never restored into two runs.
|
|
19358
|
+
*/
|
|
19359
|
+
async prepareDurableResume(opts = {}) {
|
|
19360
|
+
await this.resumeStore?.reclaimStale();
|
|
19361
|
+
const claimed = await this.resumeStore?.claim(opts.taskId);
|
|
19362
|
+
if (!claimed) return null;
|
|
19363
|
+
try {
|
|
19364
|
+
this.lastInterruptedRun = void 0;
|
|
19365
|
+
this.raiseBudgetForResume(opts.maxTokens);
|
|
19366
|
+
const prompt = this.buildResumePrompt(
|
|
19367
|
+
claimed.checkpoint.prompt,
|
|
19368
|
+
claimed.checkpoint.partialOutput,
|
|
19369
|
+
claimed.checkpoint.completed
|
|
19370
|
+
);
|
|
19371
|
+
this.resumedFrom = {
|
|
19372
|
+
prompt: claimed.checkpoint.prompt,
|
|
19373
|
+
completed: claimed.checkpoint.completed,
|
|
19374
|
+
partialOutput: claimed.checkpoint.partialOutput
|
|
19375
|
+
};
|
|
19376
|
+
this.claimedResumeId = claimed.claimId;
|
|
19377
|
+
return prompt;
|
|
19378
|
+
} catch (err) {
|
|
19379
|
+
await this.resumeStore?.release(claimed.claimId);
|
|
19380
|
+
throw err;
|
|
19381
|
+
}
|
|
19382
|
+
}
|
|
19383
|
+
/**
|
|
19384
|
+
* Discard the claimed checkpoint once the resumed run has taken ownership of
|
|
19385
|
+
* the work (it has produced its own checkpoint, or finished). Called by the
|
|
19386
|
+
* run loop; safe to call when nothing is claimed.
|
|
19387
|
+
*/
|
|
19388
|
+
async settleClaimedResume(succeeded) {
|
|
19389
|
+
const claimId = this.claimedResumeId;
|
|
19390
|
+
if (!claimId) return;
|
|
19391
|
+
this.claimedResumeId = void 0;
|
|
19392
|
+
if (succeeded || this.wroteCheckpointThisRun) {
|
|
19393
|
+
await this.resumeStore?.settle(claimId);
|
|
19394
|
+
} else {
|
|
19395
|
+
await this.resumeStore?.release(claimId);
|
|
19396
|
+
}
|
|
19397
|
+
}
|
|
19398
|
+
/**
|
|
19399
|
+
* Hand back a claim taken by prepareDurableResume() when the caller never
|
|
19400
|
+
* gets as far as run(). The REPL prepares the continuation and submits it
|
|
19401
|
+
* separately, so a failure in between would otherwise strand the checkpoint
|
|
19402
|
+
* for the whole lease timeout before reclaimStale() could recover it.
|
|
19403
|
+
*/
|
|
19404
|
+
async releaseClaimedResume() {
|
|
19405
|
+
const claimId = this.claimedResumeId;
|
|
19406
|
+
if (!claimId) return;
|
|
19407
|
+
this.claimedResumeId = void 0;
|
|
19408
|
+
await this.resumeStore?.release(claimId);
|
|
19409
|
+
}
|
|
19410
|
+
/** A resume gets a bigger budget — the previous attempt exhausted the old one. */
|
|
19411
|
+
raiseBudgetForResume(maxTokens) {
|
|
19412
|
+
const raised = maxTokens ?? Math.round((this.config.budget?.maxTokensPerRun ?? 2e5) * 2);
|
|
18535
19413
|
this.config = { ...this.config, budget: { ...this.config.budget, maxTokensPerRun: raised } };
|
|
18536
19414
|
this.router.setMaxTokensPerRun(raised);
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
19415
|
+
}
|
|
19416
|
+
/**
|
|
19417
|
+
* The resume prompt. Completed sections are named explicitly rather than
|
|
19418
|
+
* gestured at: "do not recreate the files" is an instruction a planner can
|
|
19419
|
+
* only guess at, whereas a list of what is already done, and what it produced,
|
|
19420
|
+
* is something it can act on. That difference is the entire reason node
|
|
19421
|
+
* results are kept — a re-plan that cannot see the finished work will plan the
|
|
19422
|
+
* finished work again, and the user pays for it a second time.
|
|
19423
|
+
*/
|
|
19424
|
+
buildResumePrompt(prompt, partialOutput, completed) {
|
|
19425
|
+
const done = summarizeCompleted(completed);
|
|
19426
|
+
return [
|
|
19427
|
+
"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.",
|
|
19428
|
+
"",
|
|
19429
|
+
`Original task: ${prompt}`,
|
|
19430
|
+
done ? `
|
|
19431
|
+
${done}` : "",
|
|
19432
|
+
partialOutput ? `
|
|
18541
19433
|
Partial result so far:
|
|
18542
|
-
${
|
|
19434
|
+
${partialOutput}` : ""
|
|
19435
|
+
].filter(Boolean).join("\n");
|
|
18543
19436
|
}
|
|
18544
19437
|
/**
|
|
18545
19438
|
* Resume the last budget-capped task with a raised budget (SDK/headless).
|
|
18546
19439
|
* Returns null when there is nothing to resume.
|
|
18547
19440
|
*/
|
|
18548
19441
|
async resumeRun(opts = {}) {
|
|
18549
|
-
const prompt = this.prepareResume(opts);
|
|
19442
|
+
const prompt = await this.prepareDurableResume(opts) ?? this.prepareResume(opts);
|
|
18550
19443
|
if (!prompt) return null;
|
|
18551
|
-
|
|
19444
|
+
try {
|
|
19445
|
+
return await this.run({ prompt });
|
|
19446
|
+
} catch (err) {
|
|
19447
|
+
await this.releaseClaimedResume();
|
|
19448
|
+
throw err;
|
|
19449
|
+
}
|
|
18552
19450
|
}
|
|
18553
19451
|
getWorkspacePath() {
|
|
18554
19452
|
return this.workspacePath;
|
|
@@ -18917,6 +19815,10 @@ ${prompt}` : prompt;
|
|
|
18917
19815
|
const startMs = Date.now();
|
|
18918
19816
|
const taskId = randomUUID();
|
|
18919
19817
|
this.decisionLog = [];
|
|
19818
|
+
this.currentRunCompleted = [];
|
|
19819
|
+
this.wroteCheckpointThisRun = false;
|
|
19820
|
+
this.completedNormally = false;
|
|
19821
|
+
if (!this.claimedResumeId) this.resumedFrom = void 0;
|
|
18920
19822
|
if (options.fastAnswer) {
|
|
18921
19823
|
return this.runFastAnswer(options, startMs, taskId);
|
|
18922
19824
|
}
|
|
@@ -19031,6 +19933,11 @@ ${prompt}` : prompt;
|
|
|
19031
19933
|
});
|
|
19032
19934
|
tier.on("log", (e) => this.emit("log", e));
|
|
19033
19935
|
tier.on("tier:status", (e) => this.emit("tier:status", e));
|
|
19936
|
+
tier.on("section:complete", (e) => {
|
|
19937
|
+
if (e.status !== "COMPLETED" && e.status !== "PARTIAL") return;
|
|
19938
|
+
this.currentRunCompleted.push(e);
|
|
19939
|
+
this.emit("section:complete", e);
|
|
19940
|
+
});
|
|
19034
19941
|
tier.on("tool:call", (e) => this.emit("tool:call", e));
|
|
19035
19942
|
tier.on("tool:result", (e) => this.emit("tool:result", e));
|
|
19036
19943
|
tier.on("tool:approval-request", async (request) => {
|
|
@@ -19177,6 +20084,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19177
20084
|
if (budgetInfo) throw new CascadeRouter.BudgetExceededError(budgetInfo.reason);
|
|
19178
20085
|
const trip = runBreaker.reason();
|
|
19179
20086
|
if (trip) {
|
|
20087
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "breaker", trip.message);
|
|
19180
20088
|
finalOutput = [
|
|
19181
20089
|
`\u26A0 **Run stopped early \u2014 ${trip.modelId} was failing every request.**`,
|
|
19182
20090
|
"",
|
|
@@ -19189,6 +20097,7 @@ _(incomplete: ${reasons.join("; ")})_` : partial;
|
|
|
19189
20097
|
${finalOutput}` : ""
|
|
19190
20098
|
].join("\n").trimEnd();
|
|
19191
20099
|
}
|
|
20100
|
+
this.completedNormally = trip == null;
|
|
19192
20101
|
} catch (err) {
|
|
19193
20102
|
if (err instanceof CascadeCancelledError || err instanceof Error && err.name === "AbortError" || options.signal?.aborted) {
|
|
19194
20103
|
this.emit("run:cancelled", {
|
|
@@ -19196,6 +20105,13 @@ ${finalOutput}` : ""
|
|
|
19196
20105
|
reason: err instanceof Error ? err.message : "Task cancelled",
|
|
19197
20106
|
partialOutput: finalOutput || ""
|
|
19198
20107
|
});
|
|
20108
|
+
await this.checkpointRun(
|
|
20109
|
+
taskId,
|
|
20110
|
+
options.prompt,
|
|
20111
|
+
finalOutput || "",
|
|
20112
|
+
"cancelled",
|
|
20113
|
+
err instanceof Error ? err.message : "Task cancelled"
|
|
20114
|
+
);
|
|
19199
20115
|
runError = null;
|
|
19200
20116
|
} else if (err instanceof Error && err.name === "BudgetExceededError") {
|
|
19201
20117
|
this.emit("run:budget-exceeded", {
|
|
@@ -19204,9 +20120,17 @@ ${finalOutput}` : ""
|
|
|
19204
20120
|
partialOutput: finalOutput || ""
|
|
19205
20121
|
});
|
|
19206
20122
|
this.lastInterruptedRun = { prompt: options.prompt, partialOutput: finalOutput || "", taskId };
|
|
20123
|
+
await this.checkpointRun(taskId, options.prompt, finalOutput || "", "budget", err.message);
|
|
19207
20124
|
if (!finalOutput) finalOutput = `\u26A0 Stopped to avoid runaway cost: ${err.message}`;
|
|
19208
20125
|
runError = null;
|
|
19209
20126
|
} else {
|
|
20127
|
+
await this.checkpointRun(
|
|
20128
|
+
taskId,
|
|
20129
|
+
options.prompt,
|
|
20130
|
+
finalOutput || "",
|
|
20131
|
+
"error",
|
|
20132
|
+
err instanceof Error ? err.message : String(err)
|
|
20133
|
+
);
|
|
19210
20134
|
runError = err;
|
|
19211
20135
|
throw err;
|
|
19212
20136
|
}
|
|
@@ -19215,6 +20139,10 @@ ${finalOutput}` : ""
|
|
|
19215
20139
|
escalator.cancelAllPending();
|
|
19216
20140
|
} catch {
|
|
19217
20141
|
}
|
|
20142
|
+
try {
|
|
20143
|
+
await this.settleClaimedResume(this.completedNormally);
|
|
20144
|
+
} catch {
|
|
20145
|
+
}
|
|
19218
20146
|
try {
|
|
19219
20147
|
this.releasePendingEscalations();
|
|
19220
20148
|
} catch {
|
|
@@ -19418,14 +20346,14 @@ ${finalOutput}` : ""
|
|
|
19418
20346
|
init_constants();
|
|
19419
20347
|
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
19420
20348
|
function globalDir(homeDir = os10.homedir()) {
|
|
19421
|
-
return
|
|
20349
|
+
return path34.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
19422
20350
|
}
|
|
19423
20351
|
function cloudSessionPath(dir = globalDir()) {
|
|
19424
|
-
return
|
|
20352
|
+
return path34.join(dir, CLOUD_SESSION_FILE);
|
|
19425
20353
|
}
|
|
19426
20354
|
function loadCloudSession(dir = globalDir()) {
|
|
19427
20355
|
try {
|
|
19428
|
-
const raw =
|
|
20356
|
+
const raw = fs31.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
19429
20357
|
const parsed = JSON.parse(raw);
|
|
19430
20358
|
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
19431
20359
|
return parsed;
|
|
@@ -19434,17 +20362,17 @@ function loadCloudSession(dir = globalDir()) {
|
|
|
19434
20362
|
}
|
|
19435
20363
|
}
|
|
19436
20364
|
function saveCloudSession(session, dir = globalDir()) {
|
|
19437
|
-
|
|
20365
|
+
fs31.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19438
20366
|
const file = cloudSessionPath(dir);
|
|
19439
|
-
|
|
20367
|
+
fs31.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
19440
20368
|
try {
|
|
19441
|
-
|
|
20369
|
+
fs31.chmodSync(file, 384);
|
|
19442
20370
|
} catch {
|
|
19443
20371
|
}
|
|
19444
20372
|
}
|
|
19445
20373
|
function clearCloudSession(dir = globalDir()) {
|
|
19446
20374
|
try {
|
|
19447
|
-
|
|
20375
|
+
fs31.rmSync(cloudSessionPath(dir));
|
|
19448
20376
|
} catch {
|
|
19449
20377
|
}
|
|
19450
20378
|
}
|
|
@@ -19616,16 +20544,16 @@ var CloudClient = class _CloudClient {
|
|
|
19616
20544
|
return (await this.refresh()).accessToken;
|
|
19617
20545
|
}
|
|
19618
20546
|
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
19619
|
-
async authed(
|
|
19620
|
-
const call = (token) => fetch(this.url(
|
|
20547
|
+
async authed(path39, init = {}) {
|
|
20548
|
+
const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
19621
20549
|
let res = await call(await this.accessToken());
|
|
19622
20550
|
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
19623
20551
|
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
19624
20552
|
const text = await res.text();
|
|
19625
20553
|
return text ? JSON.parse(text) : {};
|
|
19626
20554
|
}
|
|
19627
|
-
authedGet(
|
|
19628
|
-
return this.authed(
|
|
20555
|
+
authedGet(path39) {
|
|
20556
|
+
return this.authed(path39);
|
|
19629
20557
|
}
|
|
19630
20558
|
async fetchMe(token) {
|
|
19631
20559
|
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -20405,7 +21333,8 @@ function AgentTreeInternal({
|
|
|
20405
21333
|
] }),
|
|
20406
21334
|
node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20407
21335
|
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
|
|
20408
|
-
node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" })
|
|
21336
|
+
node.status === "ESCALATED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.warning, children: " \u25B2" }),
|
|
21337
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20409
21338
|
] }, `t2-${node.id}-${i}`);
|
|
20410
21339
|
}
|
|
20411
21340
|
if (row.type === "t3") {
|
|
@@ -20430,7 +21359,8 @@ function AgentTreeInternal({
|
|
|
20430
21359
|
/* @__PURE__ */ jsx(Spinner, { type: "dots" })
|
|
20431
21360
|
] }),
|
|
20432
21361
|
node.status === "COMPLETED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.success, children: " \u2714" }),
|
|
20433
|
-
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" })
|
|
21362
|
+
node.status === "FAILED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.error, children: " \u2718" }),
|
|
21363
|
+
node.status === "BLOCKED" && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: " \u292B skipped" })
|
|
20434
21364
|
] }, `t3-${node.id}-${i}`);
|
|
20435
21365
|
}
|
|
20436
21366
|
return null;
|
|
@@ -21400,9 +22330,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21400
22330
|
}
|
|
21401
22331
|
} catch {
|
|
21402
22332
|
}
|
|
21403
|
-
const configPath =
|
|
22333
|
+
const configPath = path34.join(workspacePath, ".cascade", "config.json");
|
|
21404
22334
|
try {
|
|
21405
|
-
await fs11.mkdir(
|
|
22335
|
+
await fs11.mkdir(path34.dirname(configPath), { recursive: true });
|
|
21406
22336
|
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
21407
22337
|
} catch (err) {
|
|
21408
22338
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -21429,7 +22359,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21429
22359
|
}, []);
|
|
21430
22360
|
const recordNodeEvent = useCallback((event) => {
|
|
21431
22361
|
const existing = treeNodesRef.current.get(event.tierId);
|
|
21432
|
-
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, children: [] };
|
|
22362
|
+
const node = { id: event.tierId, role: event.role ?? existing?.role ?? "T3", label: event.label ?? existing?.label ?? event.tierId, status: event.status ?? existing?.status ?? "IDLE", currentAction: event.currentAction ?? existing?.currentAction, progressPct: event.progressPct ?? existing?.progressPct, parentId: event.parentId ?? existing?.parentId, nodeId: event.nodeId ?? existing?.nodeId, dependsOn: event.dependsOn ?? existing?.dependsOn, waveId: event.waveId ?? existing?.waveId, blockedBy: event.blockedBy ?? existing?.blockedBy, children: [] };
|
|
21433
22363
|
treeNodesRef.current.set(event.tierId, node);
|
|
21434
22364
|
const store = storeRef.current;
|
|
21435
22365
|
if (store) {
|
|
@@ -21454,9 +22384,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21454
22384
|
if (msg.includes("non-text parts")) return;
|
|
21455
22385
|
originalLog(...args);
|
|
21456
22386
|
};
|
|
21457
|
-
const store = new MemoryStore(
|
|
22387
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
21458
22388
|
storeRef.current = store;
|
|
21459
|
-
globalStoreRef.current = new MemoryStore(
|
|
22389
|
+
globalStoreRef.current = new MemoryStore(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
21460
22390
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
21461
22391
|
setIdentities(identityRows);
|
|
21462
22392
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -21524,7 +22454,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
21524
22454
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
21525
22455
|
onExport: async (fmt) => {
|
|
21526
22456
|
const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
21527
|
-
const exportPath =
|
|
22457
|
+
const exportPath = path34.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
|
|
21528
22458
|
if (fmt === "json") {
|
|
21529
22459
|
await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
21530
22460
|
} else {
|
|
@@ -21766,11 +22696,21 @@ ${lastUser.content}`;
|
|
|
21766
22696
|
onContinue: async (args) => {
|
|
21767
22697
|
const cascade = cascadeRef.current;
|
|
21768
22698
|
if (!cascade) return "Not ready yet.";
|
|
21769
|
-
if (
|
|
22699
|
+
if (state.isExecuting) {
|
|
22700
|
+
return "A task is already running \u2014 wait for it to finish (or press Esc to stop it) before using /continue.";
|
|
22701
|
+
}
|
|
21770
22702
|
const n = args[0] ? parseInt(args[0].replace(/[^0-9]/g, ""), 10) : NaN;
|
|
21771
|
-
const
|
|
21772
|
-
|
|
21773
|
-
|
|
22703
|
+
const opts = Number.isFinite(n) && n > 0 ? { maxTokens: n } : {};
|
|
22704
|
+
const prompt = await cascade.prepareDurableResume(opts) ?? cascade.prepareResume(opts);
|
|
22705
|
+
if (!prompt) {
|
|
22706
|
+
return "Nothing to continue \u2014 no interrupted task was found for this workspace.";
|
|
22707
|
+
}
|
|
22708
|
+
try {
|
|
22709
|
+
await handleSubmit(prompt);
|
|
22710
|
+
} catch (err) {
|
|
22711
|
+
await cascade.releaseClaimedResume();
|
|
22712
|
+
throw err;
|
|
22713
|
+
}
|
|
21774
22714
|
return "";
|
|
21775
22715
|
}
|
|
21776
22716
|
});
|
|
@@ -22422,7 +23362,7 @@ function stringifySlashOutput(val) {
|
|
|
22422
23362
|
}
|
|
22423
23363
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
22424
23364
|
if (!query) return "Usage: /search <query>";
|
|
22425
|
-
const dbPath =
|
|
23365
|
+
const dbPath = path34.join(workspacePath, CASCADE_DB_FILE);
|
|
22426
23366
|
try {
|
|
22427
23367
|
await fs11.access(dbPath);
|
|
22428
23368
|
} catch {
|
|
@@ -22456,7 +23396,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
22456
23396
|
async function diagnoseRuntime(config, workspacePath) {
|
|
22457
23397
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
22458
23398
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
22459
|
-
const store = new MemoryStore(
|
|
23399
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
22460
23400
|
try {
|
|
22461
23401
|
const sessions2 = store.listSessions(void 0, 3);
|
|
22462
23402
|
return [
|
|
@@ -22475,7 +23415,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
22475
23415
|
}
|
|
22476
23416
|
async function showRecentLogs(args, workspacePath) {
|
|
22477
23417
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
22478
|
-
const store = new MemoryStore(
|
|
23418
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
22479
23419
|
try {
|
|
22480
23420
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
22481
23421
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -22487,7 +23427,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
22487
23427
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
22488
23428
|
const sessionId = args[0];
|
|
22489
23429
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
22490
|
-
const store = new MemoryStore(
|
|
23430
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
22491
23431
|
try {
|
|
22492
23432
|
const session = store.getSession(sessionId);
|
|
22493
23433
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -22915,9 +23855,9 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
22915
23855
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
22916
23856
|
};
|
|
22917
23857
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
22918
|
-
const configDir =
|
|
23858
|
+
const configDir = path34.join(workspacePath, ".cascade");
|
|
22919
23859
|
await fs11.mkdir(configDir, { recursive: true });
|
|
22920
|
-
const configPath =
|
|
23860
|
+
const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
22921
23861
|
await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
22922
23862
|
savedConfigRef.current = config;
|
|
22923
23863
|
dispatchRef.current({ type: "GO_DONE" });
|
|
@@ -23259,14 +24199,14 @@ function printTelemetryBanner() {
|
|
|
23259
24199
|
async function initCommand(workspacePath = process.cwd()) {
|
|
23260
24200
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
23261
24201
|
try {
|
|
23262
|
-
const configDir =
|
|
24202
|
+
const configDir = path34.join(workspacePath, ".cascade");
|
|
23263
24203
|
await fs11.mkdir(configDir, { recursive: true });
|
|
23264
|
-
const mdPath =
|
|
24204
|
+
const mdPath = path34.join(workspacePath, "CASCADE.md");
|
|
23265
24205
|
if (!await fileExists(mdPath)) {
|
|
23266
24206
|
await createDefaultCascadeMd(workspacePath);
|
|
23267
24207
|
spin.succeed(chalk7.green("Created CASCADE.md"));
|
|
23268
24208
|
}
|
|
23269
|
-
const ignorePath =
|
|
24209
|
+
const ignorePath = path34.join(workspacePath, ".cascadeignore");
|
|
23270
24210
|
if (!await fileExists(ignorePath)) {
|
|
23271
24211
|
await createDefaultIgnoreFile(workspacePath);
|
|
23272
24212
|
spin.succeed(chalk7.green("Created .cascadeignore"));
|
|
@@ -23275,7 +24215,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
23275
24215
|
console.log();
|
|
23276
24216
|
console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
23277
24217
|
console.log();
|
|
23278
|
-
const configPath =
|
|
24218
|
+
const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
23279
24219
|
if (await fileExists(configPath)) {
|
|
23280
24220
|
console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
23281
24221
|
console.log();
|
|
@@ -23333,7 +24273,7 @@ function fromEnv(env) {
|
|
|
23333
24273
|
return out;
|
|
23334
24274
|
}
|
|
23335
24275
|
async function fromClaudeCode(home) {
|
|
23336
|
-
const file =
|
|
24276
|
+
const file = path34.join(home, ".claude", ".credentials.json");
|
|
23337
24277
|
const data = await readJson(file);
|
|
23338
24278
|
if (!data) return [];
|
|
23339
24279
|
const oauth = data["claudeAiOauth"];
|
|
@@ -23356,7 +24296,7 @@ async function fromClaudeCode(home) {
|
|
|
23356
24296
|
return [];
|
|
23357
24297
|
}
|
|
23358
24298
|
async function fromCodex(home) {
|
|
23359
|
-
const file =
|
|
24299
|
+
const file = path34.join(home, ".codex", "auth.json");
|
|
23360
24300
|
const data = await readJson(file);
|
|
23361
24301
|
if (!data) return [];
|
|
23362
24302
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -23379,7 +24319,7 @@ async function fromCodex(home) {
|
|
|
23379
24319
|
return [];
|
|
23380
24320
|
}
|
|
23381
24321
|
async function fromGemini(home) {
|
|
23382
|
-
const file =
|
|
24322
|
+
const file = path34.join(home, ".gemini", "oauth_creds.json");
|
|
23383
24323
|
const data = await readJson(file);
|
|
23384
24324
|
const accessToken = str(data?.["access_token"]);
|
|
23385
24325
|
if (!accessToken) return [];
|
|
@@ -23395,7 +24335,7 @@ async function fromGemini(home) {
|
|
|
23395
24335
|
}
|
|
23396
24336
|
async function fromCopilot(home) {
|
|
23397
24337
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
23398
|
-
const file =
|
|
24338
|
+
const file = path34.join(home, ".config", "github-copilot", name);
|
|
23399
24339
|
const data = await readJson(file);
|
|
23400
24340
|
if (!data) continue;
|
|
23401
24341
|
for (const value of Object.values(data)) {
|
|
@@ -23449,7 +24389,7 @@ async function doctorCommand() {
|
|
|
23449
24389
|
checks.push({
|
|
23450
24390
|
label: "Cascade config",
|
|
23451
24391
|
ok: true,
|
|
23452
|
-
detail: `Loaded ${
|
|
24392
|
+
detail: `Loaded ${path34.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
23453
24393
|
});
|
|
23454
24394
|
const providers = [
|
|
23455
24395
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -23532,17 +24472,17 @@ async function doctorCommand() {
|
|
|
23532
24472
|
const detail = c.detail ? chalk7.gray(` \u2014 ${c.detail}`) : "";
|
|
23533
24473
|
console.log(`${icon} ${label}${detail}`);
|
|
23534
24474
|
}
|
|
23535
|
-
const
|
|
24475
|
+
const failures2 = checks.filter((c) => !c.ok);
|
|
23536
24476
|
console.log();
|
|
23537
|
-
if (
|
|
24477
|
+
if (failures2.length === 0) {
|
|
23538
24478
|
console.log(chalk7.green(" All checks passed!\n"));
|
|
23539
24479
|
} else {
|
|
23540
|
-
const critical =
|
|
24480
|
+
const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
|
|
23541
24481
|
if (critical.length) {
|
|
23542
24482
|
console.log(chalk7.yellow(` ${critical.length} issue(s) need attention.
|
|
23543
24483
|
`));
|
|
23544
24484
|
} else {
|
|
23545
|
-
console.log(chalk7.gray(` ${
|
|
24485
|
+
console.log(chalk7.gray(` ${failures2.length} optional item(s) not configured.
|
|
23546
24486
|
`));
|
|
23547
24487
|
}
|
|
23548
24488
|
}
|
|
@@ -23560,7 +24500,7 @@ async function checkEndpoint(url) {
|
|
|
23560
24500
|
}
|
|
23561
24501
|
}
|
|
23562
24502
|
async function indexCommand(dirPath) {
|
|
23563
|
-
const workspace =
|
|
24503
|
+
const workspace = path34.resolve(dirPath || process.cwd());
|
|
23564
24504
|
const cm = new ConfigManager(workspace);
|
|
23565
24505
|
await cm.load();
|
|
23566
24506
|
const config = cm.getConfig();
|
|
@@ -23571,8 +24511,8 @@ async function indexCommand(dirPath) {
|
|
|
23571
24511
|
process.exitCode = 1;
|
|
23572
24512
|
return;
|
|
23573
24513
|
}
|
|
23574
|
-
const dbPath = config.codeIndex?.dbPath ||
|
|
23575
|
-
|
|
24514
|
+
const dbPath = config.codeIndex?.dbPath || path34.join(workspace, ".cascade", "code-index.db");
|
|
24515
|
+
fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
|
|
23576
24516
|
const db = new Database2(dbPath);
|
|
23577
24517
|
db.pragma("journal_mode = WAL");
|
|
23578
24518
|
const complete = chatCompleterFromProviders(config.providers);
|
|
@@ -24061,7 +25001,7 @@ function openBrowser2(url) {
|
|
|
24061
25001
|
}
|
|
24062
25002
|
function storePathFor(name) {
|
|
24063
25003
|
const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
|
|
24064
|
-
return
|
|
25004
|
+
return path34.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
|
|
24065
25005
|
}
|
|
24066
25006
|
function defaultName(url) {
|
|
24067
25007
|
try {
|
|
@@ -24263,8 +25203,14 @@ var DashboardSocket = class {
|
|
|
24263
25203
|
emitCascadeEvent(ev) {
|
|
24264
25204
|
this.io.emit("cascade:event", ev);
|
|
24265
25205
|
}
|
|
24266
|
-
|
|
24267
|
-
|
|
25206
|
+
/**
|
|
25207
|
+
* `extra` exists because this signature is positional: every field the tier
|
|
25208
|
+
* emits that is not named here is silently dropped on this path, while the
|
|
25209
|
+
* other two forwarders spread the whole event through. That asymmetry is how
|
|
25210
|
+
* a field can reach the web dashboard from one code path and not another.
|
|
25211
|
+
*/
|
|
25212
|
+
emitTierStatus(tierId, role, status, sessionId, action, extra) {
|
|
25213
|
+
const payload = { tierId, role, status, action, timestamp: (/* @__PURE__ */ new Date()).toISOString(), sessionId, ...extra };
|
|
24268
25214
|
this.io.emit("tier:status", payload);
|
|
24269
25215
|
this.io.to(`session:${sessionId}`).emit("tier:status", payload);
|
|
24270
25216
|
}
|
|
@@ -24563,7 +25509,7 @@ function aggregateCostStats(sessions2, opts = {}) {
|
|
|
24563
25509
|
}
|
|
24564
25510
|
|
|
24565
25511
|
// src/dashboard/server.ts
|
|
24566
|
-
var __dirname$1 =
|
|
25512
|
+
var __dirname$1 = path34.dirname(fileURLToPath(import.meta.url));
|
|
24567
25513
|
var ORPHANED_ESCALATION_GRACE_MS = 3e4;
|
|
24568
25514
|
var DashboardServer = class {
|
|
24569
25515
|
app;
|
|
@@ -24843,14 +25789,14 @@ var DashboardServer = class {
|
|
|
24843
25789
|
*/
|
|
24844
25790
|
persistConfig() {
|
|
24845
25791
|
try {
|
|
24846
|
-
const configPath =
|
|
24847
|
-
|
|
24848
|
-
|
|
25792
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
25793
|
+
fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
|
|
25794
|
+
fs31.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
24849
25795
|
} catch (err) {
|
|
24850
25796
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
24851
25797
|
}
|
|
24852
25798
|
try {
|
|
24853
|
-
saveGlobalCredentials(
|
|
25799
|
+
saveGlobalCredentials(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
24854
25800
|
} catch (err) {
|
|
24855
25801
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
24856
25802
|
}
|
|
@@ -24865,15 +25811,15 @@ var DashboardServer = class {
|
|
|
24865
25811
|
resolveDashboardSecret() {
|
|
24866
25812
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
24867
25813
|
if (fromConfig) return fromConfig;
|
|
24868
|
-
const secretPath =
|
|
25814
|
+
const secretPath = path34.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
24869
25815
|
try {
|
|
24870
|
-
if (
|
|
24871
|
-
const existing =
|
|
25816
|
+
if (fs31.existsSync(secretPath)) {
|
|
25817
|
+
const existing = fs31.readFileSync(secretPath, "utf-8").trim();
|
|
24872
25818
|
if (existing.length >= 16) return existing;
|
|
24873
25819
|
}
|
|
24874
25820
|
const generated = randomUUID();
|
|
24875
|
-
|
|
24876
|
-
|
|
25821
|
+
fs31.mkdirSync(path34.dirname(secretPath), { recursive: true });
|
|
25822
|
+
fs31.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
24877
25823
|
if (this.config.dashboard.auth) {
|
|
24878
25824
|
console.warn(
|
|
24879
25825
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -24900,7 +25846,7 @@ var DashboardServer = class {
|
|
|
24900
25846
|
// ── Setup ─────────────────────────────────────
|
|
24901
25847
|
getGlobalStore() {
|
|
24902
25848
|
if (!this.globalStore) {
|
|
24903
|
-
const globalDbPath =
|
|
25849
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
24904
25850
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
24905
25851
|
}
|
|
24906
25852
|
return this.globalStore;
|
|
@@ -25280,12 +26226,12 @@ ${prompt}`;
|
|
|
25280
26226
|
}
|
|
25281
26227
|
}
|
|
25282
26228
|
watchRuntimeChanges() {
|
|
25283
|
-
const workspaceDbPath =
|
|
25284
|
-
const globalDbPath =
|
|
26229
|
+
const workspaceDbPath = path34.join(this.workspacePath, CASCADE_DB_FILE);
|
|
26230
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25285
26231
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
25286
26232
|
for (const watchPath of watchPaths) {
|
|
25287
|
-
if (!
|
|
25288
|
-
|
|
26233
|
+
if (!fs31.existsSync(watchPath)) continue;
|
|
26234
|
+
fs31.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
25289
26235
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
25290
26236
|
});
|
|
25291
26237
|
}
|
|
@@ -25407,7 +26353,7 @@ ${prompt}`;
|
|
|
25407
26353
|
const sessionId = req.params.id;
|
|
25408
26354
|
this.store.deleteSession(sessionId);
|
|
25409
26355
|
this.store.deleteRuntimeSession(sessionId);
|
|
25410
|
-
const globalDbPath =
|
|
26356
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25411
26357
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25412
26358
|
try {
|
|
25413
26359
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -25500,7 +26446,7 @@ ${prompt}`;
|
|
|
25500
26446
|
});
|
|
25501
26447
|
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
25502
26448
|
const body = req.body;
|
|
25503
|
-
const globalDbPath =
|
|
26449
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25504
26450
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
25505
26451
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25506
26452
|
try {
|
|
@@ -25523,7 +26469,7 @@ ${prompt}`;
|
|
|
25523
26469
|
});
|
|
25524
26470
|
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
25525
26471
|
this.store.deleteAllRuntimeNodes();
|
|
25526
|
-
const globalDbPath =
|
|
26472
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25527
26473
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25528
26474
|
try {
|
|
25529
26475
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -25609,12 +26555,12 @@ ${prompt}`;
|
|
|
25609
26555
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
25610
26556
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
25611
26557
|
try {
|
|
25612
|
-
const configPath =
|
|
25613
|
-
const existing =
|
|
26558
|
+
const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
26559
|
+
const existing = fs31.existsSync(configPath) ? JSON.parse(fs31.readFileSync(configPath, "utf-8")) : {};
|
|
25614
26560
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
25615
26561
|
const tmp = configPath + ".tmp";
|
|
25616
|
-
|
|
25617
|
-
|
|
26562
|
+
fs31.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
26563
|
+
fs31.renameSync(tmp, configPath);
|
|
25618
26564
|
res.json({ ok: true });
|
|
25619
26565
|
} catch (err) {
|
|
25620
26566
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -25642,7 +26588,7 @@ ${prompt}`;
|
|
|
25642
26588
|
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
25643
26589
|
const scope = req.query["scope"] ?? "workspace";
|
|
25644
26590
|
if (scope === "global") {
|
|
25645
|
-
const globalDbPath =
|
|
26591
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
25646
26592
|
const globalStore = new MemoryStore(globalDbPath);
|
|
25647
26593
|
try {
|
|
25648
26594
|
res.json({
|
|
@@ -25983,13 +26929,13 @@ ${prompt}`;
|
|
|
25983
26929
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
25984
26930
|
}
|
|
25985
26931
|
});
|
|
25986
|
-
const prodPath =
|
|
25987
|
-
const devPath =
|
|
25988
|
-
const webDistPath =
|
|
25989
|
-
if (
|
|
26932
|
+
const prodPath = path34.resolve(__dirname$1, "../web/dist");
|
|
26933
|
+
const devPath = path34.resolve(__dirname$1, "../../web/dist");
|
|
26934
|
+
const webDistPath = fs31.existsSync(prodPath) ? prodPath : devPath;
|
|
26935
|
+
if (fs31.existsSync(webDistPath)) {
|
|
25990
26936
|
this.app.use(express.static(webDistPath));
|
|
25991
26937
|
this.app.get("*", (_req, res) => {
|
|
25992
|
-
res.sendFile(
|
|
26938
|
+
res.sendFile(path34.join(webDistPath, "index.html"));
|
|
25993
26939
|
});
|
|
25994
26940
|
} else {
|
|
25995
26941
|
this.app.get("/", (_req, res) => {
|
|
@@ -26010,7 +26956,7 @@ init_constants();
|
|
|
26010
26956
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
26011
26957
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
26012
26958
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
26013
|
-
const store = new MemoryStore(
|
|
26959
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
26014
26960
|
const server = new DashboardServer(config, store, workspacePath);
|
|
26015
26961
|
server.watchRuntimeChanges();
|
|
26016
26962
|
const gThis = globalThis;
|
|
@@ -26042,7 +26988,7 @@ init_constants();
|
|
|
26042
26988
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
26043
26989
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
26044
26990
|
identity.command("list").description("List all available identities").action(() => {
|
|
26045
|
-
const store = new MemoryStore(
|
|
26991
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
26046
26992
|
try {
|
|
26047
26993
|
const identities = store.listIdentities();
|
|
26048
26994
|
console.log(chalk7.bold("\n Identities:"));
|
|
@@ -26062,7 +27008,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
26062
27008
|
}
|
|
26063
27009
|
});
|
|
26064
27010
|
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) => {
|
|
26065
|
-
const store = new MemoryStore(
|
|
27011
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
26066
27012
|
try {
|
|
26067
27013
|
if (options.default) {
|
|
26068
27014
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -26088,7 +27034,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
26088
27034
|
}
|
|
26089
27035
|
});
|
|
26090
27036
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
26091
|
-
const store = new MemoryStore(
|
|
27037
|
+
const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
|
|
26092
27038
|
try {
|
|
26093
27039
|
const identities = store.listIdentities();
|
|
26094
27040
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -26237,8 +27183,8 @@ async function exportCommand(options = {}) {
|
|
|
26237
27183
|
let store;
|
|
26238
27184
|
try {
|
|
26239
27185
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
26240
|
-
const workspaceDbPath =
|
|
26241
|
-
const globalDbPath =
|
|
27186
|
+
const workspaceDbPath = path34.join(workspacePath, CASCADE_DB_FILE);
|
|
27187
|
+
const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
26242
27188
|
let dbPath = globalDbPath;
|
|
26243
27189
|
try {
|
|
26244
27190
|
await fs11.access(workspaceDbPath);
|
|
@@ -26280,7 +27226,7 @@ async function exportCommand(options = {}) {
|
|
|
26280
27226
|
const ext = format === "json" ? ".json" : ".md";
|
|
26281
27227
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
26282
27228
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
26283
|
-
const outPath = options.output ?
|
|
27229
|
+
const outPath = options.output ? path34.resolve(options.output) : path34.join(process.cwd(), defaultFile);
|
|
26284
27230
|
await fs11.writeFile(outPath, content, "utf-8");
|
|
26285
27231
|
spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
|
|
26286
27232
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
@@ -26508,11 +27454,11 @@ async function statsCommand() {
|
|
|
26508
27454
|
dotenv.config();
|
|
26509
27455
|
function warnIfBuildIsStale() {
|
|
26510
27456
|
try {
|
|
26511
|
-
const here =
|
|
27457
|
+
const here = path34.dirname(fileURLToPath(import.meta.url));
|
|
26512
27458
|
for (const rel of ["..", "../.."]) {
|
|
26513
|
-
const pkgPath =
|
|
26514
|
-
if (!
|
|
26515
|
-
const pkg = JSON.parse(
|
|
27459
|
+
const pkgPath = path34.join(here, rel, "package.json");
|
|
27460
|
+
if (!fs31.existsSync(pkgPath)) continue;
|
|
27461
|
+
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
|
|
26516
27462
|
if (pkg.name !== "cascade-ai") continue;
|
|
26517
27463
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
26518
27464
|
console.error(
|