cascade-ai 0.77.0 → 0.78.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 +268 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +268 -4
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +269 -4
- package/dist/index.cjs +269 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -1
- package/dist/index.d.ts +252 -1
- package/dist/index.js +269 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1796,7 +1796,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1796
1796
|
var init_constants = __esm({
|
|
1797
1797
|
"src/constants.ts"() {
|
|
1798
1798
|
init_pricing();
|
|
1799
|
-
CASCADE_VERSION = "0.
|
|
1799
|
+
CASCADE_VERSION = "0.78.0";
|
|
1800
1800
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1801
1801
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1802
1802
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -15487,6 +15487,20 @@ var ToolsConfigSchema = z.object({
|
|
|
15487
15487
|
shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
|
|
15488
15488
|
requireApprovalFor: z.array(z.string()).default([]),
|
|
15489
15489
|
browserEnabled: z.boolean().default(false),
|
|
15490
|
+
/**
|
|
15491
|
+
* Let a run ACT on the page open in the desktop's built-in browser — click,
|
|
15492
|
+
* type, submit — not just read it.
|
|
15493
|
+
*
|
|
15494
|
+
* Distinct from `browserEnabled`, which turns on the headless Playwright
|
|
15495
|
+
* tool. That one drives a throwaway browser with an empty cookie jar; this
|
|
15496
|
+
* one drives the session the user is signed into, which is a different order
|
|
15497
|
+
* of consequence and so gets its own switch rather than riding on that one.
|
|
15498
|
+
*
|
|
15499
|
+
* Off by default. Turning it on does not hand anything over silently: the
|
|
15500
|
+
* tool still goes through the approval chain, and the browser panel carries a
|
|
15501
|
+
* stop control for the run that is acting.
|
|
15502
|
+
*/
|
|
15503
|
+
agentBrowserControl: z.boolean().default(false),
|
|
15490
15504
|
mcpServers: z.array(McpServerConfigSchema).optional(),
|
|
15491
15505
|
mcpTrusted: z.array(z.string()).optional(),
|
|
15492
15506
|
/** Web search backends — at least one should be configured for best results */
|
|
@@ -21411,6 +21425,23 @@ Now execute your subtask using this context where relevant.`
|
|
|
21411
21425
|
this.setStatus("FAILED", finalOutput);
|
|
21412
21426
|
this.peerBus?.publish(this.id, assignment.subtaskId, finalOutput, "FAILED");
|
|
21413
21427
|
return this.buildResult("ESCALATED", finalOutput, { checksRun, passed, failed }, issues, correctionAttempts);
|
|
21428
|
+
} finally {
|
|
21429
|
+
this.releaseBrowserControl();
|
|
21430
|
+
}
|
|
21431
|
+
}
|
|
21432
|
+
/**
|
|
21433
|
+
* End this worker's hold on the shared browser.
|
|
21434
|
+
*
|
|
21435
|
+
* Duck-typed rather than imported: `browser_control` is registered only by a
|
|
21436
|
+
* host that HAS a browser (the desktop), so in the CLI and hosted runs there
|
|
21437
|
+
* is no such tool and this is a no-op. Importing the class to instanceof-check
|
|
21438
|
+
* it would couple the worker to a tool that usually is not there.
|
|
21439
|
+
*/
|
|
21440
|
+
releaseBrowserControl() {
|
|
21441
|
+
try {
|
|
21442
|
+
const tool = this.toolRegistry.getTool?.("browser_control");
|
|
21443
|
+
tool?.releaseActor?.(this.id);
|
|
21444
|
+
} catch {
|
|
21414
21445
|
}
|
|
21415
21446
|
}
|
|
21416
21447
|
sendToPeer(toId, content) {
|
|
@@ -31214,6 +31245,127 @@ var CurrentPageTool = class extends BaseTool {
|
|
|
31214
31245
|
}
|
|
31215
31246
|
};
|
|
31216
31247
|
|
|
31248
|
+
// src/tools/browser-control.ts
|
|
31249
|
+
var BrowserControlTool = class extends BaseTool {
|
|
31250
|
+
name = "browser_control";
|
|
31251
|
+
description = "Act on the web page the user has open in the built-in browser: navigate, click an element, type into a field, press a key, or wait for something to appear. This drives the REAL browser the user is signed into and can see, so actions have real consequences \u2014 prefer read_current_page when you only need to look. CSS selectors are matched against the live page; call extract_text first if you need to find one. There is one browser: if another part of this run is using it, this call waits its turn, so a slow reply means queued, not stuck.";
|
|
31252
|
+
inputSchema = {
|
|
31253
|
+
type: "object",
|
|
31254
|
+
properties: {
|
|
31255
|
+
action: {
|
|
31256
|
+
type: "string",
|
|
31257
|
+
enum: ["navigate", "click", "fill", "press", "wait_for", "extract_text"],
|
|
31258
|
+
description: "What to do."
|
|
31259
|
+
},
|
|
31260
|
+
url: { type: "string", description: "For navigate: the http(s) address to open." },
|
|
31261
|
+
selector: {
|
|
31262
|
+
type: "string",
|
|
31263
|
+
description: "CSS selector for click, fill and wait_for. Optional for extract_text (defaults to the whole page)."
|
|
31264
|
+
},
|
|
31265
|
+
value: { type: "string", description: "For fill: the text to type." },
|
|
31266
|
+
key: { type: "string", description: 'For press: a key name such as "Enter", "Tab" or "Escape".' },
|
|
31267
|
+
timeoutMs: { type: "number", description: "For wait_for: how long to wait before giving up (default 10000, max 30000)." }
|
|
31268
|
+
},
|
|
31269
|
+
required: ["action"]
|
|
31270
|
+
};
|
|
31271
|
+
controller;
|
|
31272
|
+
release;
|
|
31273
|
+
/** One-way. See `revoke`. */
|
|
31274
|
+
revoked = false;
|
|
31275
|
+
constructor(controller, release) {
|
|
31276
|
+
super();
|
|
31277
|
+
this.controller = controller;
|
|
31278
|
+
this.release = release;
|
|
31279
|
+
}
|
|
31280
|
+
/**
|
|
31281
|
+
* A worker has finished. Ends its hold on the browser.
|
|
31282
|
+
*
|
|
31283
|
+
* Called from the tier's terminal path rather than by the model, because a
|
|
31284
|
+
* model cannot be relied on to announce that it is done — which is why the
|
|
31285
|
+
* host cannot use idle time as the boundary instead.
|
|
31286
|
+
*/
|
|
31287
|
+
releaseActor(actorId) {
|
|
31288
|
+
this.release?.(actorId);
|
|
31289
|
+
}
|
|
31290
|
+
/**
|
|
31291
|
+
* Permanently disable this tool, whatever the registry still holds.
|
|
31292
|
+
*
|
|
31293
|
+
* Registration-time gates cannot express "this run turned out to be
|
|
31294
|
+
* unattended": a host that wires the controller and only then declares the
|
|
31295
|
+
* run unattended has already registered the tool, and a check that ran at
|
|
31296
|
+
* registration has no second chance. Enforcing at EXECUTION time makes the
|
|
31297
|
+
* guarantee independent of call order, which is the whole point of stating
|
|
31298
|
+
* the invariant rather than relying on the scheduler's current sequence.
|
|
31299
|
+
*
|
|
31300
|
+
* One-way on purpose. A safety gate that can be re-opened by a later call is
|
|
31301
|
+
* a gate that a refactor re-opens by accident.
|
|
31302
|
+
*/
|
|
31303
|
+
revoke() {
|
|
31304
|
+
this.revoked = true;
|
|
31305
|
+
}
|
|
31306
|
+
// Acting on a session the user is signed into is the highest-consequence
|
|
31307
|
+
// thing in the tool set — higher than shell, arguably, because the blast
|
|
31308
|
+
// radius is someone else's server rather than this machine.
|
|
31309
|
+
isDangerous() {
|
|
31310
|
+
return true;
|
|
31311
|
+
}
|
|
31312
|
+
async execute(input, options) {
|
|
31313
|
+
if (this.revoked) {
|
|
31314
|
+
return "Error: browser control is not available in this run. It requires a person watching who can stop it, and nobody is.";
|
|
31315
|
+
}
|
|
31316
|
+
const kind = input["action"];
|
|
31317
|
+
if (!kind) return "Error: action is required.";
|
|
31318
|
+
if (options?.signal?.aborted) return "Error: the run was cancelled before this action ran.";
|
|
31319
|
+
const missing = requiredFieldFor(kind, input);
|
|
31320
|
+
if (missing) return `Error: "${kind}" needs ${missing}.`;
|
|
31321
|
+
const action = {
|
|
31322
|
+
kind,
|
|
31323
|
+
...typeof input["url"] === "string" ? { url: input["url"] } : {},
|
|
31324
|
+
...typeof input["selector"] === "string" ? { selector: input["selector"] } : {},
|
|
31325
|
+
...typeof input["value"] === "string" ? { value: input["value"] } : {},
|
|
31326
|
+
...typeof input["key"] === "string" ? { key: input["key"] } : {},
|
|
31327
|
+
...typeof input["timeoutMs"] === "number" ? { timeoutMs: Math.min(Math.max(input["timeoutMs"], 0), 3e4) } : {}
|
|
31328
|
+
};
|
|
31329
|
+
let outcome;
|
|
31330
|
+
try {
|
|
31331
|
+
outcome = await this.controller(action, {
|
|
31332
|
+
// `sessionId` is the run. The host scopes both revocation and its
|
|
31333
|
+
// single-owner lease on it, so passing it is not telemetry — drop it
|
|
31334
|
+
// and a Stop in one run silently stops every later one.
|
|
31335
|
+
sessionId: options?.sessionId ?? "",
|
|
31336
|
+
actorId: options?.tierId ?? options?.sessionId ?? "",
|
|
31337
|
+
...options?.signal ? { signal: options.signal } : {}
|
|
31338
|
+
});
|
|
31339
|
+
} catch (err) {
|
|
31340
|
+
return `Error: the browser could not perform "${kind}" \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
31341
|
+
}
|
|
31342
|
+
const where = outcome.url ? `
|
|
31343
|
+
Page: ${outcome.title ? `${outcome.title} \u2014 ` : ""}${outcome.url}` : "";
|
|
31344
|
+
return `${outcome.ok ? "" : "Failed: "}${outcome.detail}${where}`;
|
|
31345
|
+
}
|
|
31346
|
+
};
|
|
31347
|
+
function requiredFieldFor(kind, input) {
|
|
31348
|
+
const has = (k) => typeof input[k] === "string" && input[k].length > 0;
|
|
31349
|
+
const given = (k) => typeof input[k] === "string";
|
|
31350
|
+
switch (kind) {
|
|
31351
|
+
case "navigate":
|
|
31352
|
+
return has("url") ? null : "a url";
|
|
31353
|
+
case "click":
|
|
31354
|
+
return has("selector") ? null : "a selector";
|
|
31355
|
+
case "fill":
|
|
31356
|
+
return has("selector") ? given("value") ? null : "a value" : "a selector";
|
|
31357
|
+
case "press":
|
|
31358
|
+
return has("key") ? null : "a key";
|
|
31359
|
+
case "wait_for":
|
|
31360
|
+
return has("selector") ? null : "a selector";
|
|
31361
|
+
// extract_text works with or without a selector.
|
|
31362
|
+
case "extract_text":
|
|
31363
|
+
return null;
|
|
31364
|
+
default:
|
|
31365
|
+
return null;
|
|
31366
|
+
}
|
|
31367
|
+
}
|
|
31368
|
+
|
|
31217
31369
|
// src/core/cascade.ts
|
|
31218
31370
|
var ESCALATION_DECISION_TIMEOUT_MS = 5 * 6e4;
|
|
31219
31371
|
function buildContextualPrompt(prompt, history = []) {
|
|
@@ -31706,6 +31858,49 @@ ${input}` }],
|
|
|
31706
31858
|
if ((this.config.tools?.disabledTools ?? []).includes("read_current_page")) return;
|
|
31707
31859
|
this.toolRegistry.register(new CurrentPageTool(provider));
|
|
31708
31860
|
}
|
|
31861
|
+
/**
|
|
31862
|
+
* True when nobody is watching this run — a scheduled/cron task, which
|
|
31863
|
+
* auto-approves every tool because there is no one to ask.
|
|
31864
|
+
*
|
|
31865
|
+
* That auto-approval is fine for ordinary tools and disqualifying for this
|
|
31866
|
+
* one: `browser_control` drives the signed-in browser, and "every run asks
|
|
31867
|
+
* first" is the promise the Settings copy makes. The property used to hold
|
|
31868
|
+
* only because `runScheduledTask` happens not to call `setBrowserController`
|
|
31869
|
+
* — a guarantee resting on an ABSENT call, which is exactly what a later
|
|
31870
|
+
* refactor adds without noticing. Stated and enforced instead.
|
|
31871
|
+
*/
|
|
31872
|
+
unattended = false;
|
|
31873
|
+
/**
|
|
31874
|
+
* Declare that no human is watching this run, so capabilities that depend on
|
|
31875
|
+
* a person being able to see and stop them are refused outright.
|
|
31876
|
+
*/
|
|
31877
|
+
setUnattended(on) {
|
|
31878
|
+
this.unattended = on;
|
|
31879
|
+
if (!on) return;
|
|
31880
|
+
const tool = this.toolRegistry.getTool("browser_control");
|
|
31881
|
+
tool?.revoke?.();
|
|
31882
|
+
}
|
|
31883
|
+
/**
|
|
31884
|
+
* Wire the host's browser so a run can ACT on the open page, not just read it.
|
|
31885
|
+
*
|
|
31886
|
+
* Two gates before this registers anything, and both matter:
|
|
31887
|
+
*
|
|
31888
|
+
* - `tools.agentBrowserControl` must be on. Unlike read_current_page, which
|
|
31889
|
+
* reaches nothing the user has not already opened themselves, this drives
|
|
31890
|
+
* the session they are signed into. That is opt-in, not a default.
|
|
31891
|
+
* - `disabledTools` still removes it, the same as every other tool.
|
|
31892
|
+
* - The run must not be UNATTENDED. See `setUnattended`.
|
|
31893
|
+
*
|
|
31894
|
+
* Host-supplied like the page provider: the tool exists only where there is a
|
|
31895
|
+
* real browser to drive, so it never has to be refused in the CLI or a hosted
|
|
31896
|
+
* run — it is simply not there.
|
|
31897
|
+
*/
|
|
31898
|
+
setBrowserController(controller, release) {
|
|
31899
|
+
if (this.unattended) return;
|
|
31900
|
+
if (!this.config.tools?.agentBrowserControl) return;
|
|
31901
|
+
if ((this.config.tools?.disabledTools ?? []).includes("browser_control")) return;
|
|
31902
|
+
this.toolRegistry.register(new BrowserControlTool(controller, release));
|
|
31903
|
+
}
|
|
31709
31904
|
registerMediaTools(workspacePath) {
|
|
31710
31905
|
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
31711
31906
|
const permitted = (name) => !denied.has(name);
|
|
@@ -32311,6 +32506,7 @@ ${prompt}` : prompt;
|
|
|
32311
32506
|
this.router.setRunSignal(options.signal);
|
|
32312
32507
|
const startMs = Date.now();
|
|
32313
32508
|
const taskId = randomUUID();
|
|
32509
|
+
this.emit("run:started", { taskId });
|
|
32314
32510
|
this.decisionLog = [];
|
|
32315
32511
|
this.currentRunCompleted = [];
|
|
32316
32512
|
this.wroteCheckpointThisRun = false;
|
|
@@ -38208,6 +38404,7 @@ function applySettingsPayload(config, data) {
|
|
|
38208
38404
|
if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
|
|
38209
38405
|
if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
|
|
38210
38406
|
if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
|
|
38407
|
+
if (typeof a["agentBrowserControl"] === "boolean") config.tools = { ...config.tools ?? {}, agentBrowserControl: a["agentBrowserControl"] };
|
|
38211
38408
|
if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
|
|
38212
38409
|
if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
|
|
38213
38410
|
if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
|
|
@@ -38265,6 +38462,7 @@ function settingsSnapshot(config) {
|
|
|
38265
38462
|
forceTier: config.routing?.forceTier,
|
|
38266
38463
|
benchmarksLive: config.benchmarks?.live,
|
|
38267
38464
|
dynamicToolSandbox: config.tools?.dynamicToolSandbox,
|
|
38465
|
+
agentBrowserControl: config.tools?.agentBrowserControl,
|
|
38268
38466
|
factsExtraction: config.knowledge?.factsExtraction,
|
|
38269
38467
|
rememberSessions: config.memory?.rememberSessions,
|
|
38270
38468
|
enableToolCreation: config.enableToolCreation,
|
|
@@ -38775,6 +38973,14 @@ var DashboardServer = class {
|
|
|
38775
38973
|
* always failing.
|
|
38776
38974
|
*/
|
|
38777
38975
|
currentPageProvider;
|
|
38976
|
+
/**
|
|
38977
|
+
* Supplied by the desktop shell so a run can ACT on that page — click, type,
|
|
38978
|
+
* submit — not just read it. Same reasoning as the provider above: absent in
|
|
38979
|
+
* the CLI, so the tool is not registered rather than present and failing.
|
|
38980
|
+
* Cascade applies the `tools.agentBrowserControl` gate on top of this.
|
|
38981
|
+
*/
|
|
38982
|
+
browserController;
|
|
38983
|
+
browserActorRelease;
|
|
38778
38984
|
activeControllers = /* @__PURE__ */ new Map();
|
|
38779
38985
|
/**
|
|
38780
38986
|
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
@@ -38836,6 +39042,7 @@ var DashboardServer = class {
|
|
|
38836
39042
|
});
|
|
38837
39043
|
this.socket.onConfigUpdate(async (data) => {
|
|
38838
39044
|
const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
|
|
39045
|
+
if (result.ok) this.settingsChangeHook?.(this.config);
|
|
38839
39046
|
const refused = result.refused.map((r) => ({
|
|
38840
39047
|
type: r.type,
|
|
38841
39048
|
reason: r.reason,
|
|
@@ -38859,6 +39066,7 @@ var DashboardServer = class {
|
|
|
38859
39066
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
38860
39067
|
this.activeSessions.set(sessionId, cascade);
|
|
38861
39068
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
39069
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
38862
39070
|
cascade.on("stream:token", (e) => {
|
|
38863
39071
|
this.emitToSessionClients(socketId, sessionId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
38864
39072
|
});
|
|
@@ -38882,13 +39090,16 @@ var DashboardServer = class {
|
|
|
38882
39090
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
38883
39091
|
this.emitToSessionClients(socketId, sessionId, "escalation:timeout", { sessionId, ...e });
|
|
38884
39092
|
});
|
|
39093
|
+
cascade.on("run:started", (e) => {
|
|
39094
|
+
const id = e.taskId;
|
|
39095
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
39096
|
+
});
|
|
38885
39097
|
try {
|
|
38886
39098
|
const result = await cascade.run({
|
|
38887
39099
|
prompt: runPrompt,
|
|
38888
39100
|
signal: abortController.signal,
|
|
38889
39101
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
38890
39102
|
});
|
|
38891
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
38892
39103
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
38893
39104
|
this.captureWhy(sessionId, cascade, result);
|
|
38894
39105
|
this.emitToSessionClients(socketId, sessionId, "session:complete", { sessionId, result });
|
|
@@ -38954,6 +39165,49 @@ var DashboardServer = class {
|
|
|
38954
39165
|
setCurrentPageProvider(provider) {
|
|
38955
39166
|
this.currentPageProvider = provider;
|
|
38956
39167
|
}
|
|
39168
|
+
/** Let runs act on that view, not just read it (desktop only). */
|
|
39169
|
+
setBrowserController(controller, release) {
|
|
39170
|
+
this.browserController = controller;
|
|
39171
|
+
this.browserActorRelease = release;
|
|
39172
|
+
}
|
|
39173
|
+
/**
|
|
39174
|
+
* Told whenever settings are committed through THIS server.
|
|
39175
|
+
*
|
|
39176
|
+
* Settings reach the live config by two routes — the desktop's Electron IPC
|
|
39177
|
+
* handler and this server's `config:update` socket — and both run the same
|
|
39178
|
+
* `commitSettings`. The desktop mirrors `tools.agentBrowserControl` into its
|
|
39179
|
+
* browser module after its own route, but had no way to learn about the
|
|
39180
|
+
* other, so a socket write left the module's copy stale: an enable the module
|
|
39181
|
+
* still refused, or worse a disable the module still honoured for a run
|
|
39182
|
+
* already holding the tool. This is the same sibling-writer drift
|
|
39183
|
+
* `settings-payload.ts` exists to prevent, one layer up.
|
|
39184
|
+
*/
|
|
39185
|
+
settingsChangeHook;
|
|
39186
|
+
onSettingsChanged(hook) {
|
|
39187
|
+
this.settingsChangeHook = hook;
|
|
39188
|
+
}
|
|
39189
|
+
/**
|
|
39190
|
+
* Told when a run ends, so a host holding run-scoped state can release it.
|
|
39191
|
+
*
|
|
39192
|
+
* The desktop uses this to retire a run's browser Stop control: that has to
|
|
39193
|
+
* outlive each individual action (the gap between two actions is when the
|
|
39194
|
+
* user most wants it) but must not outlive the run.
|
|
39195
|
+
*/
|
|
39196
|
+
/**
|
|
39197
|
+
* Fired when a run ends, carrying BOTH identifiers explicitly.
|
|
39198
|
+
*
|
|
39199
|
+
* They are different things and conflating them was a live bug: a chat
|
|
39200
|
+
* `sessionId` can contain many runs (hence `sessionTaskIds`), while
|
|
39201
|
+
* `Cascade.run()` mints a fresh random `taskId` per run — and it is the
|
|
39202
|
+
* taskId that tools see as `ToolExecuteOptions.sessionId`. Passing only the
|
|
39203
|
+
* chat id meant browser cleanup keyed on the task id matched nothing, so a
|
|
39204
|
+
* finished run kept its Stop banner armed and held its browser lease until
|
|
39205
|
+
* the deadlock ceiling.
|
|
39206
|
+
*/
|
|
39207
|
+
runEndedHook;
|
|
39208
|
+
onRunEnded(hook) {
|
|
39209
|
+
this.runEndedHook = hook;
|
|
39210
|
+
}
|
|
38957
39211
|
async start() {
|
|
38958
39212
|
const isLoopback = this.host === "127.0.0.1" || this.host === "::1" || this.host === "localhost";
|
|
38959
39213
|
if (!isLoopback) {
|
|
@@ -39144,6 +39398,8 @@ var DashboardServer = class {
|
|
|
39144
39398
|
}
|
|
39145
39399
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
39146
39400
|
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
39401
|
+
const endedTaskId = result?.taskId ?? this.sessionTaskIds.get(sessionId)?.at(-1);
|
|
39402
|
+
this.runEndedHook?.({ sessionId, ...endedTaskId ? { taskId: endedTaskId } : {} });
|
|
39147
39403
|
try {
|
|
39148
39404
|
if (reply && reply.trim()) {
|
|
39149
39405
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -39237,6 +39493,7 @@ var DashboardServer = class {
|
|
|
39237
39493
|
const prompt = task.prompt;
|
|
39238
39494
|
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
39239
39495
|
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
39496
|
+
cascade.setUnattended(true);
|
|
39240
39497
|
this.activeSessions.set(sessionId, cascade);
|
|
39241
39498
|
cascade.on("tier:status", (e) => {
|
|
39242
39499
|
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
@@ -39244,13 +39501,16 @@ var DashboardServer = class {
|
|
|
39244
39501
|
cascade.on("peer:message", (e) => {
|
|
39245
39502
|
this.socket.emitPeerMessage(e);
|
|
39246
39503
|
});
|
|
39504
|
+
cascade.on("run:started", (e) => {
|
|
39505
|
+
const id = e.taskId;
|
|
39506
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
39507
|
+
});
|
|
39247
39508
|
try {
|
|
39248
39509
|
const result = await cascade.run({
|
|
39249
39510
|
prompt,
|
|
39250
39511
|
identityId: task.identityId,
|
|
39251
39512
|
approvalCallback: async () => ({ approved: true, always: false })
|
|
39252
39513
|
});
|
|
39253
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
39254
39514
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
39255
39515
|
this.captureWhy(sessionId, cascade, result);
|
|
39256
39516
|
this.socket.broadcast("session:complete", { sessionId, result });
|
|
@@ -39883,6 +40143,7 @@ ${prompt}`;
|
|
|
39883
40143
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
39884
40144
|
this.activeSessions.set(sessionId, cascade);
|
|
39885
40145
|
if (this.currentPageProvider) cascade.setCurrentPageProvider(this.currentPageProvider);
|
|
40146
|
+
if (this.browserController) cascade.setBrowserController(this.browserController, this.browserActorRelease);
|
|
39886
40147
|
cascade.on("stream:token", (e) => {
|
|
39887
40148
|
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
39888
40149
|
});
|
|
@@ -39906,13 +40167,16 @@ ${prompt}`;
|
|
|
39906
40167
|
if (ev.requestId) this.clearPendingEscalation(ev.requestId);
|
|
39907
40168
|
this.socket.broadcastToRoom(`session:${sessionId}`, "escalation:timeout", { sessionId, ...e });
|
|
39908
40169
|
});
|
|
40170
|
+
cascade.on("run:started", (e) => {
|
|
40171
|
+
const id = e.taskId;
|
|
40172
|
+
if (id) this.recordSessionTask(sessionId, id);
|
|
40173
|
+
});
|
|
39909
40174
|
try {
|
|
39910
40175
|
const result = await cascade.run({
|
|
39911
40176
|
prompt: runPrompt,
|
|
39912
40177
|
identityId: body.identityId,
|
|
39913
40178
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
39914
40179
|
});
|
|
39915
|
-
this.recordSessionTask(sessionId, result.taskId);
|
|
39916
40180
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
39917
40181
|
this.captureWhy(sessionId, cascade, result);
|
|
39918
40182
|
this.socket.broadcast("cost:update", {
|