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 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.1";
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
- if (answer.includes("YES")) {
7300
- return { requestId: req.id, approved: true, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: consistent with section goal" };
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
- if (answer.includes("YES")) {
8018
- return { requestId: req.id, approved: true, always: true, decidedBy: "T1", reasoning: "T1 evaluated: consistent with overall task goal" };
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
- this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
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
- verdict = prompt.trim().split(/\s+/).length <= 12 ? "Simple" : "Moderate";
11206
- this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length`);
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 complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
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
- callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId);
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
- const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
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({ prompt: runPrompt, signal: abortController.signal });
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({ prompt: runPrompt, identityId: body.identityId });
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
  });