cascade-ai 0.13.1 → 0.13.2
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 +6 -0
- package/dist/cli.cjs +117 -36
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +117 -36
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +117 -36
- package/dist/index.cjs +117 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +117 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,12 @@ Other AI CLIs run a single agent. Cascade runs a visible **organization** — an
|
|
|
63
63
|
|
|
64
64
|
## What's New
|
|
65
65
|
|
|
66
|
+
### v0.13.2 — desktop bugfix round
|
|
67
|
+
- **Live streaming, tool approvals, and file creation work again in the app.** The chat reply streams token-by-token on every route (not just Complex), a **tool-approval modal** now pops for dangerous actions (showing the escalation trail), and approved file writes actually land. Dangerous tools always escalate to you — T2/T1 advise but never final-approve.
|
|
68
|
+
- **New Cockpit controls.** A manual **tier override** (Auto / T1 / T2 / T3) pins a run's root tier; **click any node** to open a live detail panel (role, status, stream, peer messages); coordinating workers now draw an animated **peer edge** in the graph.
|
|
69
|
+
- **Smarter auto-routing.** Explicit multi-step build prompts floor to Complex so genuinely complex work engages the full T1→T2→T3 hierarchy instead of stalling at T2.
|
|
70
|
+
- Plus: the session list loads on connect, long model names stay inside the dropdown, "Check for updates" stays calm mid-release, and the landing page fits phones.
|
|
71
|
+
|
|
66
72
|
### v0.6 → v0.9.1 — the agentic releases
|
|
67
73
|
- **v0.9.1 — Workers recruit help.** A T3 worker that discovers its task should fan out can call `request_workers` to have its T2 spawn bounded sibling workers (no recursive 4th tier; depth-capped + budget-bounded).
|
|
68
74
|
- **v0.9.0 — Resumability, reflection, smarter local exec.** `/continue` resumes a budget-capped task from its partial state; opt-in **reflection** revises a worker's output against the goal; `t3Execution: auto` runs T3 waves sequentially on local/Ollama tiers and parallel on cloud.
|
package/dist/cli.cjs
CHANGED
|
@@ -109,7 +109,7 @@ var __export = (target, all) => {
|
|
|
109
109
|
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
110
|
var init_constants = __esm({
|
|
111
111
|
"src/constants.ts"() {
|
|
112
|
-
CASCADE_VERSION = "0.13.
|
|
112
|
+
CASCADE_VERSION = "0.13.2";
|
|
113
113
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
114
114
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
115
115
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3040,6 +3040,8 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
3040
3040
|
privacy: zod.z.object({
|
|
3041
3041
|
paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
|
|
3042
3042
|
}).optional(),
|
|
3043
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
3044
|
+
routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
3043
3045
|
/**
|
|
3044
3046
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
3045
3047
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -5025,6 +5027,13 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5025
5027
|
hierarchyContext = "";
|
|
5026
5028
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
5027
5029
|
signal;
|
|
5030
|
+
/**
|
|
5031
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
5032
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
5033
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
5034
|
+
* which would interleave, are not tagged.
|
|
5035
|
+
*/
|
|
5036
|
+
isPresenter = false;
|
|
5028
5037
|
constructor(role, id, parentId) {
|
|
5029
5038
|
super();
|
|
5030
5039
|
this.role = role;
|
|
@@ -5032,6 +5041,10 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5032
5041
|
this.parentId = parentId;
|
|
5033
5042
|
this.label = this.id;
|
|
5034
5043
|
}
|
|
5044
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
5045
|
+
setPresenter(on = true) {
|
|
5046
|
+
this.isPresenter = on;
|
|
5047
|
+
}
|
|
5035
5048
|
getStatus() {
|
|
5036
5049
|
return this.status;
|
|
5037
5050
|
}
|
|
@@ -5767,7 +5780,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5767
5780
|
"T3",
|
|
5768
5781
|
options,
|
|
5769
5782
|
(chunk) => {
|
|
5770
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
5783
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5771
5784
|
}
|
|
5772
5785
|
);
|
|
5773
5786
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -7228,6 +7241,7 @@ ${peerOutputs}` : "";
|
|
|
7228
7241
|
chunkEnd++;
|
|
7229
7242
|
}
|
|
7230
7243
|
i = chunkEnd;
|
|
7244
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
7231
7245
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
7232
7246
|
${currentSummary ? `
|
|
7233
7247
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -7237,6 +7251,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7237
7251
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
7238
7252
|
const messages = [{ role: "user", content: prompt }];
|
|
7239
7253
|
try {
|
|
7254
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
7240
7255
|
const result = await this.router.generate("T2", {
|
|
7241
7256
|
messages,
|
|
7242
7257
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -7244,7 +7259,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7244
7259
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
7245
7260
|
maxTokens: 500,
|
|
7246
7261
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7247
|
-
});
|
|
7262
|
+
}, streamFinal);
|
|
7248
7263
|
currentSummary = result.content;
|
|
7249
7264
|
} catch (err) {
|
|
7250
7265
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7296,14 +7311,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7296
7311
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7297
7312
|
});
|
|
7298
7313
|
const answer = result.content.trim().toUpperCase();
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
}
|
|
7302
|
-
if (answer.includes("NO")) {
|
|
7303
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
7304
|
-
}
|
|
7314
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7315
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
7305
7316
|
return null;
|
|
7306
7317
|
} catch {
|
|
7318
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
7307
7319
|
return null;
|
|
7308
7320
|
}
|
|
7309
7321
|
}
|
|
@@ -7985,7 +7997,7 @@ Instructions:
|
|
|
7985
7997
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
7986
7998
|
maxTokens: 8e3
|
|
7987
7999
|
}, (chunk) => {
|
|
7988
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
8000
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
7989
8001
|
});
|
|
7990
8002
|
return result.content;
|
|
7991
8003
|
}
|
|
@@ -8014,14 +8026,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8014
8026
|
temperature: 0
|
|
8015
8027
|
});
|
|
8016
8028
|
const answer = result.content.trim().toUpperCase();
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
}
|
|
8020
|
-
if (answer.includes("NO")) {
|
|
8021
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
8022
|
-
}
|
|
8029
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
8030
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
8023
8031
|
return null;
|
|
8024
8032
|
} catch {
|
|
8033
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
8025
8034
|
return null;
|
|
8026
8035
|
}
|
|
8027
8036
|
}
|
|
@@ -11117,6 +11126,21 @@ ${last.partialOutput}` : "");
|
|
|
11117
11126
|
const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
|
|
11118
11127
|
return inquiry && !producesArtifact;
|
|
11119
11128
|
}
|
|
11129
|
+
/**
|
|
11130
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
11131
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
11132
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
11133
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11134
|
+
* Simple/Moderate) don't get over-escalated.
|
|
11135
|
+
*/
|
|
11136
|
+
looksClearlyComplex(prompt) {
|
|
11137
|
+
const p = prompt.trim();
|
|
11138
|
+
if (p.length < 24) return false;
|
|
11139
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11140
|
+
const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
|
|
11141
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
11142
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
11143
|
+
}
|
|
11120
11144
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
11121
11145
|
static globCache = /* @__PURE__ */ new Map();
|
|
11122
11146
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -11200,10 +11224,16 @@ ${prompt}` : prompt;
|
|
|
11200
11224
|
let verdict;
|
|
11201
11225
|
if (match) {
|
|
11202
11226
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
11203
|
-
|
|
11227
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11228
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11229
|
+
verdict = "Complex";
|
|
11230
|
+
} else {
|
|
11231
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11232
|
+
}
|
|
11204
11233
|
} else {
|
|
11205
|
-
|
|
11206
|
-
|
|
11234
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
11235
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
11236
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
11207
11237
|
}
|
|
11208
11238
|
return verdict;
|
|
11209
11239
|
} catch {
|
|
@@ -11255,7 +11285,15 @@ ${prompt}` : prompt;
|
|
|
11255
11285
|
}
|
|
11256
11286
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
11257
11287
|
});
|
|
11258
|
-
const
|
|
11288
|
+
const forceTier = this.config.routing?.forceTier;
|
|
11289
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
11290
|
+
let complexity;
|
|
11291
|
+
if (forced) {
|
|
11292
|
+
complexity = forced;
|
|
11293
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
11294
|
+
} else {
|
|
11295
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
11296
|
+
}
|
|
11259
11297
|
this.telemetry.capture("cascade:session_start", {
|
|
11260
11298
|
complexity,
|
|
11261
11299
|
providerCount: this.config.providers.length,
|
|
@@ -11335,6 +11373,7 @@ ${prompt}` : prompt;
|
|
|
11335
11373
|
try {
|
|
11336
11374
|
if (complexity === "Simple") {
|
|
11337
11375
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
11376
|
+
t3.setPresenter(true);
|
|
11338
11377
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
11339
11378
|
if (identityPrompt) {
|
|
11340
11379
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -11359,6 +11398,7 @@ ${prompt}` : prompt;
|
|
|
11359
11398
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
11360
11399
|
} else if (complexity === "Moderate") {
|
|
11361
11400
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
11401
|
+
t2.setPresenter(true);
|
|
11362
11402
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
11363
11403
|
if (identityPrompt) {
|
|
11364
11404
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -11408,6 +11448,7 @@ ${prompt}` : prompt;
|
|
|
11408
11448
|
}
|
|
11409
11449
|
} else {
|
|
11410
11450
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
11451
|
+
t1.setPresenter(true);
|
|
11411
11452
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
11412
11453
|
if (identityPrompt) {
|
|
11413
11454
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -15408,7 +15449,8 @@ var DashboardSocket = class {
|
|
|
15408
15449
|
socket.on("cascade:run", (payload) => {
|
|
15409
15450
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
15410
15451
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
15411
|
-
|
|
15452
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
15453
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
15412
15454
|
}
|
|
15413
15455
|
});
|
|
15414
15456
|
});
|
|
@@ -15438,6 +15480,13 @@ var DashboardServer = class {
|
|
|
15438
15480
|
* of runs the session performed in this server's lifetime.
|
|
15439
15481
|
*/
|
|
15440
15482
|
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
15483
|
+
/**
|
|
15484
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
15485
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
15486
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
15487
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
15488
|
+
*/
|
|
15489
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
15441
15490
|
port;
|
|
15442
15491
|
host;
|
|
15443
15492
|
workspacePath;
|
|
@@ -15496,17 +15545,18 @@ var DashboardServer = class {
|
|
|
15496
15545
|
}
|
|
15497
15546
|
this.persistConfig();
|
|
15498
15547
|
});
|
|
15499
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
15548
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
15500
15549
|
const sessionId = requestedSessionId ?? crypto.randomUUID();
|
|
15501
15550
|
const abortController = new AbortController();
|
|
15502
15551
|
this.activeControllers.set(sessionId, abortController);
|
|
15503
15552
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
15504
15553
|
const title = this.persistRunStart(sessionId, prompt);
|
|
15505
|
-
|
|
15554
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
15555
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
15506
15556
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
15507
15557
|
this.activeSessions.set(sessionId, cascade);
|
|
15508
15558
|
cascade.on("stream:token", (e) => {
|
|
15509
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
15559
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
15510
15560
|
});
|
|
15511
15561
|
cascade.on("tier:status", (e) => {
|
|
15512
15562
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -15518,7 +15568,11 @@ var DashboardServer = class {
|
|
|
15518
15568
|
this.socket.emitPeerMessage(e);
|
|
15519
15569
|
});
|
|
15520
15570
|
try {
|
|
15521
|
-
const result = await cascade.run({
|
|
15571
|
+
const result = await cascade.run({
|
|
15572
|
+
prompt: runPrompt,
|
|
15573
|
+
signal: abortController.signal,
|
|
15574
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
15575
|
+
});
|
|
15522
15576
|
this.recordSessionTask(sessionId, result.taskId);
|
|
15523
15577
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15524
15578
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
@@ -15537,10 +15591,18 @@ var DashboardServer = class {
|
|
|
15537
15591
|
} finally {
|
|
15538
15592
|
this.activeSessions.delete(sessionId);
|
|
15539
15593
|
this.activeControllers.delete(sessionId);
|
|
15594
|
+
this.denyPendingApprovals(sessionId);
|
|
15540
15595
|
}
|
|
15541
15596
|
});
|
|
15542
15597
|
this.socket.onSessionHalt((sessionId) => {
|
|
15543
15598
|
this.activeControllers.get(sessionId)?.abort();
|
|
15599
|
+
this.denyPendingApprovals(sessionId);
|
|
15600
|
+
});
|
|
15601
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
15602
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
15603
|
+
if (!pending) return;
|
|
15604
|
+
this.pendingApprovals.delete(requestId);
|
|
15605
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
15544
15606
|
});
|
|
15545
15607
|
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15546
15608
|
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
@@ -15708,6 +15770,29 @@ var DashboardServer = class {
|
|
|
15708
15770
|
list.push(taskId);
|
|
15709
15771
|
this.sessionTaskIds.set(sessionId, list);
|
|
15710
15772
|
}
|
|
15773
|
+
/**
|
|
15774
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
15775
|
+
* user. The request itself was already pushed to the client via the
|
|
15776
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
15777
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
15778
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
15779
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
15780
|
+
* timeout denies it.
|
|
15781
|
+
*/
|
|
15782
|
+
makeApprovalCallback(sessionId) {
|
|
15783
|
+
return (request) => new Promise((resolve) => {
|
|
15784
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
15785
|
+
});
|
|
15786
|
+
}
|
|
15787
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
15788
|
+
denyPendingApprovals(sessionId) {
|
|
15789
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
15790
|
+
if (pending.sessionId === sessionId) {
|
|
15791
|
+
this.pendingApprovals.delete(id);
|
|
15792
|
+
pending.resolve({ approved: false, always: false });
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
}
|
|
15711
15796
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15712
15797
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15713
15798
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15896,15 +15981,6 @@ ${prompt}`;
|
|
|
15896
15981
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
15897
15982
|
res.json({ success: true, ...payload });
|
|
15898
15983
|
});
|
|
15899
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
15900
|
-
const body = req.body;
|
|
15901
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
15902
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
15903
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15904
|
-
this.socket.broadcast("session:approve", payload);
|
|
15905
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
15906
|
-
res.json({ success: true, ...payload });
|
|
15907
|
-
});
|
|
15908
15984
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
15909
15985
|
const body = req.body;
|
|
15910
15986
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -16155,7 +16231,7 @@ ${prompt}`;
|
|
|
16155
16231
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
16156
16232
|
this.activeSessions.set(sessionId, cascade);
|
|
16157
16233
|
cascade.on("stream:token", (e) => {
|
|
16158
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16234
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
16159
16235
|
});
|
|
16160
16236
|
cascade.on("tier:status", (e) => {
|
|
16161
16237
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -16167,7 +16243,11 @@ ${prompt}`;
|
|
|
16167
16243
|
this.socket.emitPeerMessage(e);
|
|
16168
16244
|
});
|
|
16169
16245
|
try {
|
|
16170
|
-
const result = await cascade.run({
|
|
16246
|
+
const result = await cascade.run({
|
|
16247
|
+
prompt: runPrompt,
|
|
16248
|
+
identityId: body.identityId,
|
|
16249
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16250
|
+
});
|
|
16171
16251
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16172
16252
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16173
16253
|
this.socket.broadcast("cost:update", {
|
|
@@ -16185,6 +16265,7 @@ ${prompt}`;
|
|
|
16185
16265
|
});
|
|
16186
16266
|
} finally {
|
|
16187
16267
|
this.activeSessions.delete(sessionId);
|
|
16268
|
+
this.denyPendingApprovals(sessionId);
|
|
16188
16269
|
}
|
|
16189
16270
|
})();
|
|
16190
16271
|
});
|