cascade-ai 0.16.0 → 0.17.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/dist/cli.cjs +429 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +428 -4
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +430 -69
- package/dist/index.cjs +427 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +427 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -43,6 +43,7 @@ var bcrypt = require('bcryptjs');
|
|
|
43
43
|
var socket_io = require('socket.io');
|
|
44
44
|
var parser = require('socket.io-msgpack-parser');
|
|
45
45
|
var jwt = require('jsonwebtoken');
|
|
46
|
+
var cron = require('node-cron');
|
|
46
47
|
|
|
47
48
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
48
49
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -93,6 +94,7 @@ var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
|
93
94
|
var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
|
|
94
95
|
var parser__default = /*#__PURE__*/_interopDefault(parser);
|
|
95
96
|
var jwt__default = /*#__PURE__*/_interopDefault(jwt);
|
|
97
|
+
var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
96
98
|
|
|
97
99
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
98
100
|
var __defProp = Object.defineProperty;
|
|
@@ -109,7 +111,7 @@ var __export = (target, all) => {
|
|
|
109
111
|
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
110
112
|
var init_constants = __esm({
|
|
111
113
|
"src/constants.ts"() {
|
|
112
|
-
CASCADE_VERSION = "0.
|
|
114
|
+
CASCADE_VERSION = "0.17.0";
|
|
113
115
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
114
116
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
115
117
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -16087,6 +16089,25 @@ var DashboardSocket = class {
|
|
|
16087
16089
|
});
|
|
16088
16090
|
});
|
|
16089
16091
|
}
|
|
16092
|
+
/**
|
|
16093
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
16094
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
16095
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
16096
|
+
*/
|
|
16097
|
+
onPlanDecision(callback) {
|
|
16098
|
+
this.io.on("connection", (socket) => {
|
|
16099
|
+
socket.on("plan:decision", (payload) => {
|
|
16100
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
16101
|
+
callback({
|
|
16102
|
+
sessionId: payload.sessionId,
|
|
16103
|
+
approved: payload.approved,
|
|
16104
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
16105
|
+
editedPlan: payload.editedPlan
|
|
16106
|
+
});
|
|
16107
|
+
}
|
|
16108
|
+
});
|
|
16109
|
+
});
|
|
16110
|
+
}
|
|
16090
16111
|
onSessionSteer(callback) {
|
|
16091
16112
|
this.io.on("connection", (socket) => {
|
|
16092
16113
|
socket.on("session:steer", (payload) => {
|
|
@@ -16131,6 +16152,122 @@ var DashboardSocket = class {
|
|
|
16131
16152
|
|
|
16132
16153
|
// src/dashboard/server.ts
|
|
16133
16154
|
init_constants();
|
|
16155
|
+
var TaskScheduler = class {
|
|
16156
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
16157
|
+
store;
|
|
16158
|
+
runner;
|
|
16159
|
+
constructor(store, runner) {
|
|
16160
|
+
this.store = store;
|
|
16161
|
+
this.runner = runner;
|
|
16162
|
+
}
|
|
16163
|
+
start() {
|
|
16164
|
+
const tasks = this.store.listScheduledTasks();
|
|
16165
|
+
for (const task of tasks) {
|
|
16166
|
+
if (task.enabled) this.schedule(task);
|
|
16167
|
+
}
|
|
16168
|
+
}
|
|
16169
|
+
stop() {
|
|
16170
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
16171
|
+
this.cronJobs.clear();
|
|
16172
|
+
}
|
|
16173
|
+
schedule(task) {
|
|
16174
|
+
if (!cron__default.default.validate(task.cronExpression)) {
|
|
16175
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
16176
|
+
}
|
|
16177
|
+
const existing = this.cronJobs.get(task.id);
|
|
16178
|
+
if (existing) {
|
|
16179
|
+
try {
|
|
16180
|
+
existing.stop();
|
|
16181
|
+
} catch {
|
|
16182
|
+
}
|
|
16183
|
+
}
|
|
16184
|
+
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
16185
|
+
try {
|
|
16186
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
16187
|
+
this.store.saveScheduledTask(task);
|
|
16188
|
+
await this.runner(task);
|
|
16189
|
+
} catch (err) {
|
|
16190
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
16191
|
+
}
|
|
16192
|
+
}, { timezone: "UTC" });
|
|
16193
|
+
this.cronJobs.set(task.id, job);
|
|
16194
|
+
}
|
|
16195
|
+
unschedule(taskId) {
|
|
16196
|
+
this.cronJobs.get(taskId)?.stop();
|
|
16197
|
+
this.cronJobs.delete(taskId);
|
|
16198
|
+
}
|
|
16199
|
+
add(task) {
|
|
16200
|
+
this.store.saveScheduledTask(task);
|
|
16201
|
+
if (task.enabled) this.schedule(task);
|
|
16202
|
+
}
|
|
16203
|
+
remove(taskId) {
|
|
16204
|
+
this.unschedule(taskId);
|
|
16205
|
+
this.store.deleteScheduledTask(taskId);
|
|
16206
|
+
}
|
|
16207
|
+
list() {
|
|
16208
|
+
return this.store.listScheduledTasks();
|
|
16209
|
+
}
|
|
16210
|
+
isRunning(taskId) {
|
|
16211
|
+
return this.cronJobs.has(taskId);
|
|
16212
|
+
}
|
|
16213
|
+
static validateCron(expression) {
|
|
16214
|
+
return cron__default.default.validate(expression);
|
|
16215
|
+
}
|
|
16216
|
+
};
|
|
16217
|
+
|
|
16218
|
+
// src/dashboard/cost-stats.ts
|
|
16219
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
16220
|
+
function utcDateKey(d) {
|
|
16221
|
+
return d.toISOString().slice(0, 10);
|
|
16222
|
+
}
|
|
16223
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
16224
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
16225
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
16226
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
16227
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
16228
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
16229
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
16230
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
16231
|
+
}
|
|
16232
|
+
let totalCostUsd = 0;
|
|
16233
|
+
let totalTokens = 0;
|
|
16234
|
+
let totalRuns = 0;
|
|
16235
|
+
for (const s of sessions) {
|
|
16236
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
16237
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
16238
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
16239
|
+
totalCostUsd += cost;
|
|
16240
|
+
totalTokens += tokens;
|
|
16241
|
+
totalRuns += runs;
|
|
16242
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
16243
|
+
if (!Number.isNaN(when.getTime())) {
|
|
16244
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
16245
|
+
if (bucket) {
|
|
16246
|
+
bucket.costUsd += cost;
|
|
16247
|
+
bucket.tokens += tokens;
|
|
16248
|
+
bucket.runs += runs;
|
|
16249
|
+
}
|
|
16250
|
+
}
|
|
16251
|
+
}
|
|
16252
|
+
const topSessions = [...sessions].filter((s) => (s.metadata?.totalCostUsd ?? 0) > 0).sort((a, b) => (b.metadata?.totalCostUsd ?? 0) - (a.metadata?.totalCostUsd ?? 0)).slice(0, topN).map((s) => ({
|
|
16253
|
+
sessionId: s.id,
|
|
16254
|
+
title: s.title,
|
|
16255
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
16256
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
16257
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
16258
|
+
updatedAt: s.updatedAt
|
|
16259
|
+
}));
|
|
16260
|
+
return {
|
|
16261
|
+
totalCostUsd,
|
|
16262
|
+
totalTokens,
|
|
16263
|
+
totalSessions: sessions.length,
|
|
16264
|
+
totalRuns,
|
|
16265
|
+
perDay: [...buckets.values()],
|
|
16266
|
+
topSessions
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
|
|
16270
|
+
// src/dashboard/server.ts
|
|
16134
16271
|
var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
16135
16272
|
var DashboardServer = class {
|
|
16136
16273
|
app;
|
|
@@ -16156,6 +16293,14 @@ var DashboardServer = class {
|
|
|
16156
16293
|
* map is how that answer reaches the run that's blocked on it.
|
|
16157
16294
|
*/
|
|
16158
16295
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
16296
|
+
/**
|
|
16297
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
16298
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
16299
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
16300
|
+
*/
|
|
16301
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
16302
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
16303
|
+
scheduler;
|
|
16159
16304
|
port;
|
|
16160
16305
|
host;
|
|
16161
16306
|
workspacePath;
|
|
@@ -16173,6 +16318,7 @@ var DashboardServer = class {
|
|
|
16173
16318
|
secret: this.dashboardSecret,
|
|
16174
16319
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
16175
16320
|
});
|
|
16321
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
16176
16322
|
this.setupMiddleware();
|
|
16177
16323
|
this.setupRoutes();
|
|
16178
16324
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -16236,6 +16382,9 @@ var DashboardServer = class {
|
|
|
16236
16382
|
cascade.on("peer:message", (e) => {
|
|
16237
16383
|
this.socket.emitPeerMessage(e);
|
|
16238
16384
|
});
|
|
16385
|
+
cascade.on("plan:approval-required", (e) => {
|
|
16386
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
16387
|
+
});
|
|
16239
16388
|
try {
|
|
16240
16389
|
const result = await cascade.run({
|
|
16241
16390
|
prompt: runPrompt,
|
|
@@ -16243,7 +16392,8 @@ var DashboardServer = class {
|
|
|
16243
16392
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16244
16393
|
});
|
|
16245
16394
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16246
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16395
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16396
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16247
16397
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
16248
16398
|
this.socket.broadcast("cost:update", {
|
|
16249
16399
|
sessionId,
|
|
@@ -16253,6 +16403,7 @@ var DashboardServer = class {
|
|
|
16253
16403
|
this.throttledBroadcast("workspace");
|
|
16254
16404
|
} catch (err) {
|
|
16255
16405
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16406
|
+
this.captureWhy(sessionId, cascade);
|
|
16256
16407
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
16257
16408
|
sessionId,
|
|
16258
16409
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16263,9 +16414,17 @@ var DashboardServer = class {
|
|
|
16263
16414
|
this.denyPendingApprovals(sessionId);
|
|
16264
16415
|
}
|
|
16265
16416
|
});
|
|
16417
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
16418
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
16419
|
+
approved,
|
|
16420
|
+
note,
|
|
16421
|
+
editedPlan
|
|
16422
|
+
);
|
|
16423
|
+
});
|
|
16266
16424
|
this.socket.onSessionHalt((sessionId) => {
|
|
16267
16425
|
this.activeControllers.get(sessionId)?.abort();
|
|
16268
16426
|
this.denyPendingApprovals(sessionId);
|
|
16427
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
16269
16428
|
});
|
|
16270
16429
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16271
16430
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -16298,12 +16457,21 @@ var DashboardServer = class {
|
|
|
16298
16457
|
resolve();
|
|
16299
16458
|
});
|
|
16300
16459
|
});
|
|
16460
|
+
try {
|
|
16461
|
+
this.scheduler.start();
|
|
16462
|
+
} catch (err) {
|
|
16463
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
16464
|
+
}
|
|
16301
16465
|
}
|
|
16302
16466
|
async stop() {
|
|
16303
16467
|
if (this.broadcastTimer) {
|
|
16304
16468
|
clearTimeout(this.broadcastTimer);
|
|
16305
16469
|
this.broadcastTimer = null;
|
|
16306
16470
|
}
|
|
16471
|
+
try {
|
|
16472
|
+
this.scheduler.stop();
|
|
16473
|
+
} catch {
|
|
16474
|
+
}
|
|
16307
16475
|
this.socket.close();
|
|
16308
16476
|
try {
|
|
16309
16477
|
this.globalStore?.close();
|
|
@@ -16424,16 +16592,60 @@ var DashboardServer = class {
|
|
|
16424
16592
|
return title;
|
|
16425
16593
|
}
|
|
16426
16594
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
16427
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
16595
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
16428
16596
|
try {
|
|
16429
16597
|
if (reply && reply.trim()) {
|
|
16430
16598
|
this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16431
16599
|
}
|
|
16600
|
+
if (result) {
|
|
16601
|
+
const session = this.store.getSession(sessionId);
|
|
16602
|
+
if (session) {
|
|
16603
|
+
this.store.updateSession(sessionId, {
|
|
16604
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16605
|
+
metadata: {
|
|
16606
|
+
...session.metadata,
|
|
16607
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
16608
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
16609
|
+
taskCount: session.metadata.taskCount + 1
|
|
16610
|
+
}
|
|
16611
|
+
});
|
|
16612
|
+
}
|
|
16613
|
+
}
|
|
16432
16614
|
} catch (err) {
|
|
16433
16615
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
16434
16616
|
}
|
|
16435
16617
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
16436
16618
|
}
|
|
16619
|
+
/**
|
|
16620
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
16621
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
16622
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
16623
|
+
*/
|
|
16624
|
+
captureWhy(sessionId, cascade, result) {
|
|
16625
|
+
try {
|
|
16626
|
+
const stats = cascade.getRouter().getStats();
|
|
16627
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
16628
|
+
const report = {
|
|
16629
|
+
sessionId,
|
|
16630
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16631
|
+
decisions: cascade.getDecisionLog(),
|
|
16632
|
+
savedUsd: savings.savedUsd,
|
|
16633
|
+
savedPct: savings.savedPct,
|
|
16634
|
+
totalCostUsd: stats.totalCostUsd,
|
|
16635
|
+
totalTokens: stats.totalTokens,
|
|
16636
|
+
costByTier: stats.costByTier,
|
|
16637
|
+
durationMs: result?.durationMs
|
|
16638
|
+
};
|
|
16639
|
+
this.whyBySession.set(sessionId, report);
|
|
16640
|
+
if (this.whyBySession.size > 50) {
|
|
16641
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
16642
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
16643
|
+
}
|
|
16644
|
+
this.socket.broadcast("run:why", report);
|
|
16645
|
+
} catch (err) {
|
|
16646
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16437
16649
|
/**
|
|
16438
16650
|
* Route steering text into running Cascade instances. Targets the given
|
|
16439
16651
|
* session, or every active session when none is specified (the desktop
|
|
@@ -16463,6 +16675,52 @@ var DashboardServer = class {
|
|
|
16463
16675
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16464
16676
|
});
|
|
16465
16677
|
}
|
|
16678
|
+
/**
|
|
16679
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
16680
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
16681
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
16682
|
+
* client so an open desktop sees the run appear live.
|
|
16683
|
+
*/
|
|
16684
|
+
async runScheduledTask(task) {
|
|
16685
|
+
const sessionId = crypto.randomUUID();
|
|
16686
|
+
const prompt = task.prompt;
|
|
16687
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
16688
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
16689
|
+
this.activeSessions.set(sessionId, cascade);
|
|
16690
|
+
cascade.on("tier:status", (e) => {
|
|
16691
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
16692
|
+
});
|
|
16693
|
+
cascade.on("peer:message", (e) => {
|
|
16694
|
+
this.socket.emitPeerMessage(e);
|
|
16695
|
+
});
|
|
16696
|
+
try {
|
|
16697
|
+
const result = await cascade.run({
|
|
16698
|
+
prompt,
|
|
16699
|
+
identityId: task.identityId,
|
|
16700
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
16701
|
+
});
|
|
16702
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
16703
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16704
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16705
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
16706
|
+
this.socket.broadcast("cost:update", {
|
|
16707
|
+
sessionId,
|
|
16708
|
+
totalTokens: result.usage.totalTokens,
|
|
16709
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
16710
|
+
});
|
|
16711
|
+
this.throttledBroadcast("workspace");
|
|
16712
|
+
} catch (err) {
|
|
16713
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16714
|
+
this.captureWhy(sessionId, cascade);
|
|
16715
|
+
this.socket.broadcast("session:error", {
|
|
16716
|
+
sessionId,
|
|
16717
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16718
|
+
});
|
|
16719
|
+
throw err;
|
|
16720
|
+
} finally {
|
|
16721
|
+
this.activeSessions.delete(sessionId);
|
|
16722
|
+
}
|
|
16723
|
+
}
|
|
16466
16724
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16467
16725
|
denyPendingApprovals(sessionId) {
|
|
16468
16726
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -16975,6 +17233,9 @@ ${prompt}`;
|
|
|
16975
17233
|
cascade.on("peer:message", (e) => {
|
|
16976
17234
|
this.socket.emitPeerMessage(e);
|
|
16977
17235
|
});
|
|
17236
|
+
cascade.on("plan:approval-required", (e) => {
|
|
17237
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
17238
|
+
});
|
|
16978
17239
|
try {
|
|
16979
17240
|
const result = await cascade.run({
|
|
16980
17241
|
prompt: runPrompt,
|
|
@@ -16982,7 +17243,8 @@ ${prompt}`;
|
|
|
16982
17243
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16983
17244
|
});
|
|
16984
17245
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16985
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
17246
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17247
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16986
17248
|
this.socket.broadcast("cost:update", {
|
|
16987
17249
|
sessionId,
|
|
16988
17250
|
totalTokens: result.usage.totalTokens,
|
|
@@ -16992,6 +17254,7 @@ ${prompt}`;
|
|
|
16992
17254
|
this.throttledBroadcast("workspace");
|
|
16993
17255
|
} catch (err) {
|
|
16994
17256
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17257
|
+
this.captureWhy(sessionId, cascade);
|
|
16995
17258
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
16996
17259
|
sessionId,
|
|
16997
17260
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -17013,6 +17276,168 @@ ${prompt}`;
|
|
|
17013
17276
|
}))
|
|
17014
17277
|
});
|
|
17015
17278
|
});
|
|
17279
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
17280
|
+
const report = this.whyBySession.get(req.params.id);
|
|
17281
|
+
if (!report) {
|
|
17282
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
17283
|
+
return;
|
|
17284
|
+
}
|
|
17285
|
+
res.json(report);
|
|
17286
|
+
});
|
|
17287
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
17288
|
+
const sessionId = req.params.id;
|
|
17289
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17290
|
+
const before = /* @__PURE__ */ new Map();
|
|
17291
|
+
for (const taskId of taskIds) {
|
|
17292
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
17293
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
17294
|
+
}
|
|
17295
|
+
}
|
|
17296
|
+
const { readFile } = await import('fs/promises');
|
|
17297
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
17298
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
17299
|
+
let after = "";
|
|
17300
|
+
let missing = false;
|
|
17301
|
+
try {
|
|
17302
|
+
const buf = await readFile(filePath);
|
|
17303
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
17304
|
+
} catch {
|
|
17305
|
+
missing = true;
|
|
17306
|
+
}
|
|
17307
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
17308
|
+
}));
|
|
17309
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
17310
|
+
});
|
|
17311
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
17312
|
+
const sessionId = req.params.id;
|
|
17313
|
+
const body = req.body;
|
|
17314
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
17315
|
+
res.status(400).json({ error: "filePath is required" });
|
|
17316
|
+
return;
|
|
17317
|
+
}
|
|
17318
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17319
|
+
let content;
|
|
17320
|
+
for (const taskId of taskIds) {
|
|
17321
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
17322
|
+
if (snap) {
|
|
17323
|
+
content = snap.content;
|
|
17324
|
+
break;
|
|
17325
|
+
}
|
|
17326
|
+
}
|
|
17327
|
+
if (content === void 0) {
|
|
17328
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
17329
|
+
return;
|
|
17330
|
+
}
|
|
17331
|
+
try {
|
|
17332
|
+
const { writeFile } = await import('fs/promises');
|
|
17333
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
17334
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
17335
|
+
} catch (err) {
|
|
17336
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17337
|
+
}
|
|
17338
|
+
});
|
|
17339
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
17340
|
+
try {
|
|
17341
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
17342
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
17343
|
+
res.json({
|
|
17344
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
17345
|
+
budget: {
|
|
17346
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
17347
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
17348
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
17349
|
+
}
|
|
17350
|
+
});
|
|
17351
|
+
} catch (err) {
|
|
17352
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17353
|
+
}
|
|
17354
|
+
});
|
|
17355
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
17356
|
+
try {
|
|
17357
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
17358
|
+
} catch (err) {
|
|
17359
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17360
|
+
}
|
|
17361
|
+
});
|
|
17362
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
17363
|
+
const body = req.body;
|
|
17364
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
17365
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
17366
|
+
return;
|
|
17367
|
+
}
|
|
17368
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
17369
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17370
|
+
return;
|
|
17371
|
+
}
|
|
17372
|
+
const task = {
|
|
17373
|
+
id: crypto.randomUUID(),
|
|
17374
|
+
name: body.name.trim(),
|
|
17375
|
+
cronExpression: body.cronExpression.trim(),
|
|
17376
|
+
prompt: body.prompt.trim(),
|
|
17377
|
+
workspacePath: this.workspacePath,
|
|
17378
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17379
|
+
enabled: body.enabled !== false
|
|
17380
|
+
};
|
|
17381
|
+
try {
|
|
17382
|
+
this.scheduler.add(task);
|
|
17383
|
+
res.json(task);
|
|
17384
|
+
} catch (err) {
|
|
17385
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17386
|
+
}
|
|
17387
|
+
});
|
|
17388
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17389
|
+
const id = req.params.id;
|
|
17390
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
17391
|
+
if (!existing) {
|
|
17392
|
+
res.status(404).json({ error: "Not found" });
|
|
17393
|
+
return;
|
|
17394
|
+
}
|
|
17395
|
+
const body = req.body;
|
|
17396
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
17397
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17398
|
+
return;
|
|
17399
|
+
}
|
|
17400
|
+
const updated = {
|
|
17401
|
+
...existing,
|
|
17402
|
+
name: body.name?.trim() || existing.name,
|
|
17403
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
17404
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
17405
|
+
enabled: body.enabled ?? existing.enabled
|
|
17406
|
+
};
|
|
17407
|
+
try {
|
|
17408
|
+
this.scheduler.add(updated);
|
|
17409
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
17410
|
+
res.json(updated);
|
|
17411
|
+
} catch (err) {
|
|
17412
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17413
|
+
}
|
|
17414
|
+
});
|
|
17415
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17416
|
+
try {
|
|
17417
|
+
this.scheduler.remove(req.params.id);
|
|
17418
|
+
res.json({ ok: true });
|
|
17419
|
+
} catch (err) {
|
|
17420
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17421
|
+
}
|
|
17422
|
+
});
|
|
17423
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
17424
|
+
try {
|
|
17425
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
17426
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
17427
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
17428
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
17429
|
+
try {
|
|
17430
|
+
const all = logger.getAllLogs();
|
|
17431
|
+
const total = all.length;
|
|
17432
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
17433
|
+
res.json({ total, offset, entries });
|
|
17434
|
+
} finally {
|
|
17435
|
+
logger.close();
|
|
17436
|
+
}
|
|
17437
|
+
} catch (err) {
|
|
17438
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17439
|
+
}
|
|
17440
|
+
});
|
|
17016
17441
|
const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
|
|
17017
17442
|
const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
|
|
17018
17443
|
const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;
|