cascade-ai 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.cjs +824 -82
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +824 -82
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +826 -84
- package/dist/index.cjs +824 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -8
- package/dist/index.d.ts +223 -8
- package/dist/index.js +824 -82
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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.
|
|
112
|
+
CASCADE_VERSION = "0.15.0";
|
|
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";
|
|
@@ -1415,29 +1415,71 @@ var init_ollama = __esm({
|
|
|
1415
1415
|
async countTokens(text) {
|
|
1416
1416
|
return Math.ceil(text.length / 4);
|
|
1417
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Ask the Ollama server what a model can actually do. Modern Ollama returns a
|
|
1420
|
+
* `capabilities` array ("completion", "tools", "vision", …) and the model's
|
|
1421
|
+
* real context length in `model_info` — authoritative, unlike the hardcoded
|
|
1422
|
+
* family-name allowlist, which silently misclassifies any unlisted family.
|
|
1423
|
+
* Best-effort: null on old servers / errors, callers fall back to heuristics.
|
|
1424
|
+
*/
|
|
1425
|
+
async showModel(name) {
|
|
1426
|
+
const ac = new AbortController();
|
|
1427
|
+
const timer = setTimeout(() => ac.abort(), 2500);
|
|
1428
|
+
try {
|
|
1429
|
+
const resp = await fetch(`${this.baseUrl}/api/show`, {
|
|
1430
|
+
method: "POST",
|
|
1431
|
+
headers: { "Content-Type": "application/json" },
|
|
1432
|
+
body: JSON.stringify({ model: name }),
|
|
1433
|
+
signal: ac.signal
|
|
1434
|
+
});
|
|
1435
|
+
if (!resp.ok) return null;
|
|
1436
|
+
const data = await resp.json();
|
|
1437
|
+
const out = {};
|
|
1438
|
+
if (Array.isArray(data.capabilities)) {
|
|
1439
|
+
out.tools = data.capabilities.includes("tools");
|
|
1440
|
+
out.vision = data.capabilities.includes("vision");
|
|
1441
|
+
}
|
|
1442
|
+
for (const [k, v] of Object.entries(data.model_info ?? {})) {
|
|
1443
|
+
if (k.endsWith(".context_length") && typeof v === "number" && v > 0) {
|
|
1444
|
+
out.contextWindow = v;
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return out.tools !== void 0 || out.contextWindow ? out : null;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return null;
|
|
1451
|
+
} finally {
|
|
1452
|
+
clearTimeout(timer);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1418
1455
|
async listModels() {
|
|
1419
1456
|
try {
|
|
1420
1457
|
const response = await fetch(`${this.baseUrl}/api/tags`);
|
|
1421
1458
|
if (!response.ok) return [];
|
|
1422
1459
|
const data = await response.json();
|
|
1423
1460
|
const supportedKeywords = ["llama3", "llama2", "gemma", "mistral", "mixtral", "qwen", "phi3", "codellama", "deepseek", "llava", "starcoder", "stable-code", "nomic-embed"];
|
|
1424
|
-
|
|
1461
|
+
const entries = data.models.filter((m) => {
|
|
1425
1462
|
const name = m.name.toLowerCase();
|
|
1426
1463
|
return supportedKeywords.some((k) => name.includes(k));
|
|
1427
|
-
})
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1464
|
+
});
|
|
1465
|
+
const shows = await Promise.all(entries.map((m) => this.showModel(m.name)));
|
|
1466
|
+
return entries.map((m, i) => {
|
|
1467
|
+
const show = shows[i];
|
|
1468
|
+
return {
|
|
1469
|
+
id: m.name,
|
|
1470
|
+
name: m.name,
|
|
1471
|
+
provider: "ollama",
|
|
1472
|
+
contextWindow: show?.contextWindow ?? 128e3,
|
|
1473
|
+
isVisionCapable: show?.vision ?? (m.name.includes("llava") || m.name.includes("vision")),
|
|
1474
|
+
inputCostPer1kTokens: 0,
|
|
1475
|
+
outputCostPer1kTokens: 0,
|
|
1476
|
+
maxOutputTokens: 4e3,
|
|
1477
|
+
supportsStreaming: true,
|
|
1478
|
+
isLocal: true,
|
|
1479
|
+
supportsToolUse: show?.tools ?? isToolCapable(m.name),
|
|
1480
|
+
minSizeB: this.parseSizeB(m.details?.parameter_size)
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1441
1483
|
} catch {
|
|
1442
1484
|
return [];
|
|
1443
1485
|
}
|
|
@@ -2360,6 +2402,78 @@ Original error: ${err.message}`
|
|
|
2360
2402
|
`).all(`%${query}%`, limit);
|
|
2361
2403
|
return rows.map(this.deserializeMessage);
|
|
2362
2404
|
}
|
|
2405
|
+
// ── Export / Import ──────────────────────────
|
|
2406
|
+
//
|
|
2407
|
+
// Chats travel as full Session objects (messages included) inside a
|
|
2408
|
+
// plaintext-JSON bundle. Import NEVER overwrites: every imported session
|
|
2409
|
+
// gets a fresh id (title suffixed "(imported)") and fresh message ids, so
|
|
2410
|
+
// re-importing the same bundle can duplicate but can never clobber.
|
|
2411
|
+
/** Full sessions (with messages) for the given ids, or ALL sessions. */
|
|
2412
|
+
exportSessions(ids) {
|
|
2413
|
+
const targets = ids && ids.length > 0 ? ids : this.db.prepare("SELECT id FROM sessions ORDER BY updated_at DESC").all().map((r) => r.id);
|
|
2414
|
+
const out = [];
|
|
2415
|
+
for (const id of targets) {
|
|
2416
|
+
const s = this.getSession(id);
|
|
2417
|
+
if (s) out.push(s);
|
|
2418
|
+
}
|
|
2419
|
+
return out;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Import sessions from a bundle. Returns the created `{id, title}` rows so
|
|
2423
|
+
* the caller can mirror them into the runtime session list (the sidebar
|
|
2424
|
+
* reads runtime_sessions, not sessions). Malformed entries are skipped.
|
|
2425
|
+
*/
|
|
2426
|
+
importSessions(sessions) {
|
|
2427
|
+
const out = [];
|
|
2428
|
+
const msgStmt = this.db.prepare(`
|
|
2429
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
|
|
2430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2431
|
+
`);
|
|
2432
|
+
for (const s of sessions) {
|
|
2433
|
+
if (!s || typeof s !== "object") continue;
|
|
2434
|
+
const newId = crypto.randomUUID();
|
|
2435
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2436
|
+
const title = `${String(s.title || "Untitled").slice(0, 200)} (imported)`;
|
|
2437
|
+
const metadata = s.metadata && typeof s.metadata === "object" ? s.metadata : { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 };
|
|
2438
|
+
this.db.prepare(`
|
|
2439
|
+
INSERT INTO sessions (id, title, created_at, updated_at, identity_id, workspace_path, metadata)
|
|
2440
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2441
|
+
`).run(newId, title, s.createdAt ?? now, s.updatedAt ?? now, s.identityId ?? "default", s.workspacePath ?? "", JSON.stringify(metadata));
|
|
2442
|
+
for (const m of Array.isArray(s.messages) ? s.messages : []) {
|
|
2443
|
+
if (!m || m.role !== "user" && m.role !== "assistant" && m.role !== "system") continue;
|
|
2444
|
+
msgStmt.run(
|
|
2445
|
+
crypto.randomUUID(),
|
|
2446
|
+
newId,
|
|
2447
|
+
m.role,
|
|
2448
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
2449
|
+
m.timestamp ?? now,
|
|
2450
|
+
m.tokens ? JSON.stringify(m.tokens) : null,
|
|
2451
|
+
m.agentMessages ? JSON.stringify(m.agentMessages) : null
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
out.push({ id: newId, title });
|
|
2455
|
+
}
|
|
2456
|
+
return out;
|
|
2457
|
+
}
|
|
2458
|
+
/** Import identities/personas, deduped by (case-insensitive) name — an
|
|
2459
|
+
* existing name is skipped, never replaced. Imports are never the default. */
|
|
2460
|
+
importIdentities(identities) {
|
|
2461
|
+
const existing = new Set(this.listIdentities().map((i) => i.name.toLowerCase()));
|
|
2462
|
+
let imported = 0;
|
|
2463
|
+
for (const ident of identities ?? []) {
|
|
2464
|
+
if (!ident?.name || typeof ident.name !== "string") continue;
|
|
2465
|
+
if (existing.has(ident.name.toLowerCase())) continue;
|
|
2466
|
+
this.createIdentity({
|
|
2467
|
+
...ident,
|
|
2468
|
+
id: crypto.randomUUID(),
|
|
2469
|
+
isDefault: false,
|
|
2470
|
+
createdAt: ident.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2471
|
+
});
|
|
2472
|
+
existing.add(ident.name.toLowerCase());
|
|
2473
|
+
imported++;
|
|
2474
|
+
}
|
|
2475
|
+
return imported;
|
|
2476
|
+
}
|
|
2363
2477
|
// ── Identities ────────────────────────────────
|
|
2364
2478
|
createIdentity(identity) {
|
|
2365
2479
|
this.db.prepare(`
|
|
@@ -2537,6 +2651,23 @@ Original error: ${err.message}`
|
|
|
2537
2651
|
const row = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(`${provider}:${modelId}`);
|
|
2538
2652
|
return row ? JSON.parse(row.metadata) : void 0;
|
|
2539
2653
|
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Persist a probed capability verdict (currently: native tool support) into
|
|
2656
|
+
* the model's cached profile, merging with whatever is already recorded.
|
|
2657
|
+
* Read back via getModelProfile — so a model is probed once ever, not once
|
|
2658
|
+
* per run.
|
|
2659
|
+
*/
|
|
2660
|
+
saveModelCapability(modelId, provider, caps) {
|
|
2661
|
+
const cacheKey = `${provider}:${modelId}`;
|
|
2662
|
+
const existing = this.db.prepare("SELECT metadata FROM model_cache WHERE id = ?").get(cacheKey);
|
|
2663
|
+
const meta = existing ? JSON.parse(existing.metadata) : { id: modelId, provider, name: modelId, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
|
|
2664
|
+
if (caps.supportsToolUse !== void 0) meta.supportsToolUse = caps.supportsToolUse;
|
|
2665
|
+
this.db.prepare(`
|
|
2666
|
+
INSERT INTO model_cache (id, provider, model_id, name, metadata, updated_at)
|
|
2667
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2668
|
+
ON CONFLICT(id) DO UPDATE SET metadata = excluded.metadata, updated_at = excluded.updated_at
|
|
2669
|
+
`).run(cacheKey, provider, modelId, meta.name ?? modelId, JSON.stringify(meta), (/* @__PURE__ */ new Date()).toISOString());
|
|
2670
|
+
}
|
|
2540
2671
|
getProfiledModelIds() {
|
|
2541
2672
|
const rows = this.db.prepare(
|
|
2542
2673
|
"SELECT model_id FROM model_cache WHERE json_extract(metadata, '$.specializations') IS NOT NULL"
|
|
@@ -2826,7 +2957,15 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
2826
2957
|
mcpServers: zod.z.array(McpServerConfigSchema).optional(),
|
|
2827
2958
|
mcpTrusted: zod.z.array(zod.z.string()).optional(),
|
|
2828
2959
|
/** Web search backends — at least one should be configured for best results */
|
|
2829
|
-
webSearch: WebSearchConfigSchema.optional()
|
|
2960
|
+
webSearch: WebSearchConfigSchema.optional(),
|
|
2961
|
+
/**
|
|
2962
|
+
* Sandbox runtime for LLM-authored dynamic tools:
|
|
2963
|
+
* - 'isolate': hard V8 isolate (isolated-vm) — no Node globals, true capability
|
|
2964
|
+
* confinement. Requires the optional native `isolated-vm` dependency.
|
|
2965
|
+
* - 'worker': node:worker_threads (resource/kill limits, but not capability-confined).
|
|
2966
|
+
* - 'auto' (default): use the isolate when `isolated-vm` loads, else fall back to worker.
|
|
2967
|
+
*/
|
|
2968
|
+
dynamicToolSandbox: zod.z.enum(["isolate", "worker", "auto"]).default("auto")
|
|
2830
2969
|
});
|
|
2831
2970
|
var HookDefinitionSchema = zod.z.object({
|
|
2832
2971
|
name: zod.z.string().optional(),
|
|
@@ -2938,10 +3077,20 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
2938
3077
|
/**
|
|
2939
3078
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
2940
3079
|
* at runtime via the ToolCreator when no existing tool can handle a required operation.
|
|
2941
|
-
* Generated tools are session-scoped and sandboxed
|
|
3080
|
+
* Generated tools are session-scoped and sandboxed (see tools.dynamicToolSandbox).
|
|
2942
3081
|
* HTTP calls from generated tools require approval.
|
|
2943
3082
|
*/
|
|
2944
3083
|
enableToolCreation: zod.z.boolean().default(true),
|
|
3084
|
+
/**
|
|
3085
|
+
* Project knowledge (world state). `factsExtraction`: after each worker
|
|
3086
|
+
* completes, run a cheap extraction pass that distills its output into
|
|
3087
|
+
* queryable entity/relation/value facts (superseding older facts), which T1
|
|
3088
|
+
* queries during planning instead of replaying the whole linear log. Best-effort
|
|
3089
|
+
* and non-blocking; set false to keep only the raw linear log.
|
|
3090
|
+
*/
|
|
3091
|
+
knowledge: zod.z.object({
|
|
3092
|
+
factsExtraction: zod.z.boolean().default(true)
|
|
3093
|
+
}).default({}),
|
|
2945
3094
|
/**
|
|
2946
3095
|
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
2947
3096
|
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
@@ -3040,6 +3189,8 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
3040
3189
|
privacy: zod.z.object({
|
|
3041
3190
|
paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
|
|
3042
3191
|
}).optional(),
|
|
3192
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
3193
|
+
routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
3043
3194
|
/**
|
|
3044
3195
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
3045
3196
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -4037,6 +4188,7 @@ function normalizeModelId(id) {
|
|
|
4037
4188
|
var LiveDataProvider = class {
|
|
4038
4189
|
snapshot = null;
|
|
4039
4190
|
prices = /* @__PURE__ */ new Map();
|
|
4191
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
4040
4192
|
source = "bundled";
|
|
4041
4193
|
fetchedAt = 0;
|
|
4042
4194
|
loaded = false;
|
|
@@ -4065,6 +4217,9 @@ var LiveDataProvider = class {
|
|
|
4065
4217
|
if (cache.prices) {
|
|
4066
4218
|
for (const [id, p] of Object.entries(cache.prices)) this.prices.set(id, p);
|
|
4067
4219
|
}
|
|
4220
|
+
if (cache.capabilities) {
|
|
4221
|
+
for (const [id, c] of Object.entries(cache.capabilities)) this.capabilities.set(id, c);
|
|
4222
|
+
}
|
|
4068
4223
|
this.fetchedAt = cache.fetchedAt ?? 0;
|
|
4069
4224
|
} catch {
|
|
4070
4225
|
}
|
|
@@ -4085,9 +4240,9 @@ var LiveDataProvider = class {
|
|
|
4085
4240
|
const ttlMs = this.opts.refreshHours * 36e5;
|
|
4086
4241
|
const fresh = ttlMs > 0 && Date.now() - this.fetchedAt < ttlMs;
|
|
4087
4242
|
if (!force && fresh && this.source !== "bundled") return;
|
|
4088
|
-
const [snap,
|
|
4243
|
+
const [snap, catalog] = await Promise.all([
|
|
4089
4244
|
this.opts.live ? this.fetchSnapshot() : Promise.resolve(null),
|
|
4090
|
-
this.opts.pricingLive ? this.
|
|
4245
|
+
this.opts.pricingLive ? this.fetchCatalog() : Promise.resolve(null)
|
|
4091
4246
|
]);
|
|
4092
4247
|
let changed = false;
|
|
4093
4248
|
if (snap) {
|
|
@@ -4095,8 +4250,12 @@ var LiveDataProvider = class {
|
|
|
4095
4250
|
this.source = "live";
|
|
4096
4251
|
changed = true;
|
|
4097
4252
|
}
|
|
4098
|
-
if (
|
|
4099
|
-
this.prices = prices;
|
|
4253
|
+
if (catalog && catalog.prices.size > 0) {
|
|
4254
|
+
this.prices = catalog.prices;
|
|
4255
|
+
changed = true;
|
|
4256
|
+
}
|
|
4257
|
+
if (catalog && catalog.capabilities.size > 0) {
|
|
4258
|
+
this.capabilities = catalog.capabilities;
|
|
4100
4259
|
changed = true;
|
|
4101
4260
|
}
|
|
4102
4261
|
if (changed) {
|
|
@@ -4118,21 +4277,43 @@ var LiveDataProvider = class {
|
|
|
4118
4277
|
return null;
|
|
4119
4278
|
}
|
|
4120
4279
|
}
|
|
4121
|
-
|
|
4280
|
+
/**
|
|
4281
|
+
* One fetch of the OpenRouter catalog yields BOTH pricing and capability
|
|
4282
|
+
* facts (context window, native tool support, input modalities) — the
|
|
4283
|
+
* capability fields used to be discarded while providers guessed/hardcoded
|
|
4284
|
+
* them in their listModels() stubs.
|
|
4285
|
+
*/
|
|
4286
|
+
async fetchCatalog() {
|
|
4122
4287
|
try {
|
|
4123
4288
|
const resp = await withTimeout(fetch(OPENROUTER_MODELS_URL), FETCH_TIMEOUT_MS, "pricing fetch timed out");
|
|
4124
4289
|
if (!resp.ok) return null;
|
|
4125
4290
|
const data = await resp.json();
|
|
4126
4291
|
if (!Array.isArray(data?.data)) return null;
|
|
4127
|
-
const
|
|
4292
|
+
const prices = /* @__PURE__ */ new Map();
|
|
4293
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
4128
4294
|
for (const m of data.data) {
|
|
4129
|
-
if (!m?.id
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4295
|
+
if (!m?.id) continue;
|
|
4296
|
+
const key = normalizeModelId(m.id);
|
|
4297
|
+
if (m.pricing) {
|
|
4298
|
+
const input = Number(m.pricing.prompt) * 1e3;
|
|
4299
|
+
const output = Number(m.pricing.completion) * 1e3;
|
|
4300
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
4301
|
+
prices.set(key, { input, output });
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
const cap = {};
|
|
4305
|
+
if (typeof m.context_length === "number" && m.context_length > 0) {
|
|
4306
|
+
cap.contextWindow = m.context_length;
|
|
4307
|
+
}
|
|
4308
|
+
if (Array.isArray(m.supported_parameters)) {
|
|
4309
|
+
cap.supportsTools = m.supported_parameters.includes("tools");
|
|
4310
|
+
}
|
|
4311
|
+
if (Array.isArray(m.architecture?.input_modalities)) {
|
|
4312
|
+
cap.inputModalities = m.architecture.input_modalities;
|
|
4313
|
+
}
|
|
4314
|
+
if (Object.keys(cap).length > 0) capabilities.set(key, cap);
|
|
4134
4315
|
}
|
|
4135
|
-
return
|
|
4316
|
+
return { prices, capabilities };
|
|
4136
4317
|
} catch {
|
|
4137
4318
|
return null;
|
|
4138
4319
|
}
|
|
@@ -4143,7 +4324,8 @@ var LiveDataProvider = class {
|
|
|
4143
4324
|
const cache = {
|
|
4144
4325
|
fetchedAt: this.fetchedAt,
|
|
4145
4326
|
snapshot: this.snapshot ?? void 0,
|
|
4146
|
-
prices: Object.fromEntries(this.prices)
|
|
4327
|
+
prices: Object.fromEntries(this.prices),
|
|
4328
|
+
capabilities: Object.fromEntries(this.capabilities)
|
|
4147
4329
|
};
|
|
4148
4330
|
await fs9__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
4149
4331
|
} catch {
|
|
@@ -4168,6 +4350,27 @@ var LiveDataProvider = class {
|
|
|
4168
4350
|
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
4169
4351
|
});
|
|
4170
4352
|
}
|
|
4353
|
+
/** Current capability facts for a model id, or null when unknown. */
|
|
4354
|
+
getCapability(modelId) {
|
|
4355
|
+
return this.capabilities.get(normalizeModelId(modelId)) ?? null;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Returns capability-corrected copies of each model (originals untouched):
|
|
4359
|
+
* real context windows replace the providers' hardcoded guesses, native
|
|
4360
|
+
* tool support replaces the assume-by-provider default, and vision is set
|
|
4361
|
+
* from the declared input modalities.
|
|
4362
|
+
*/
|
|
4363
|
+
applyLiveCapabilities(models) {
|
|
4364
|
+
return models.map((m) => {
|
|
4365
|
+
const c = this.getCapability(m.id);
|
|
4366
|
+
if (!c) return m;
|
|
4367
|
+
const next = { ...m };
|
|
4368
|
+
if (c.contextWindow) next.contextWindow = c.contextWindow;
|
|
4369
|
+
if (c.supportsTools !== void 0) next.supportsToolUse = c.supportsTools;
|
|
4370
|
+
if (c.inputModalities) next.isVisionCapable = c.inputModalities.includes("image");
|
|
4371
|
+
return next;
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4171
4374
|
/** Where the active quality data came from — for /why and `cascade models`. */
|
|
4172
4375
|
getDataSource() {
|
|
4173
4376
|
return this.source;
|
|
@@ -4178,6 +4381,9 @@ var LiveDataProvider = class {
|
|
|
4178
4381
|
hasLivePricing() {
|
|
4179
4382
|
return this.prices.size > 0;
|
|
4180
4383
|
}
|
|
4384
|
+
hasCapabilities() {
|
|
4385
|
+
return this.capabilities.size > 0;
|
|
4386
|
+
}
|
|
4181
4387
|
};
|
|
4182
4388
|
|
|
4183
4389
|
// src/core/router/benchmarks.ts
|
|
@@ -4366,6 +4572,57 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4366
4572
|
profiler.profileAll(allModels).catch(() => {
|
|
4367
4573
|
});
|
|
4368
4574
|
}
|
|
4575
|
+
/**
|
|
4576
|
+
* One-time native tool-call probe for local/compat models with NO capability
|
|
4577
|
+
* metadata. llama.cpp / LM Studio "/models" endpoints return ids only, so
|
|
4578
|
+
* `supportsToolUse` stays undefined and the T3 tool gate ASSUMES native
|
|
4579
|
+
* support — wrong for many local builds, which then fumble tool-format
|
|
4580
|
+
* output. Each unknown model is probed once with a trivial tool; the verdict
|
|
4581
|
+
* persists in the MemoryStore's model_cache (via the previously-dormant
|
|
4582
|
+
* getModelProfile read-back), so a model is probed once EVER, not per run.
|
|
4583
|
+
* Cloud providers are never probed — their metadata is authoritative.
|
|
4584
|
+
*/
|
|
4585
|
+
async probeLocalToolSupport(store) {
|
|
4586
|
+
const unknown = this.selector.getAllAvailableModels().filter(
|
|
4587
|
+
(m) => m.provider === "openai-compatible" && m.supportsToolUse === void 0
|
|
4588
|
+
);
|
|
4589
|
+
for (const model of unknown) {
|
|
4590
|
+
const cached = store.getModelProfile(model.id, model.provider);
|
|
4591
|
+
let verdict = cached?.supportsToolUse;
|
|
4592
|
+
if (verdict === void 0) {
|
|
4593
|
+
const probed = await this.probeNativeToolCall(model);
|
|
4594
|
+
if (probed === null) continue;
|
|
4595
|
+
verdict = probed;
|
|
4596
|
+
store.saveModelCapability(model.id, model.provider, { supportsToolUse: verdict });
|
|
4597
|
+
}
|
|
4598
|
+
this.selector.addDynamicModel({ ...model, supportsToolUse: verdict });
|
|
4599
|
+
}
|
|
4600
|
+
for (const tier of ["T1", "T2", "T3"]) {
|
|
4601
|
+
const cur = this.tierModels.get(tier);
|
|
4602
|
+
if (!cur) continue;
|
|
4603
|
+
const fresh = this.selector.getModelById(cur.id);
|
|
4604
|
+
if (fresh) this.tierModels.set(tier, fresh);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
async probeNativeToolCall(model) {
|
|
4608
|
+
try {
|
|
4609
|
+
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
4610
|
+
const provider = this.createProvider(cfg, model);
|
|
4611
|
+
const result = await withTimeout(provider.generate({
|
|
4612
|
+
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
4613
|
+
tools: [{
|
|
4614
|
+
name: "echo",
|
|
4615
|
+
description: "Echo the given text back to the caller.",
|
|
4616
|
+
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
4617
|
+
}],
|
|
4618
|
+
maxTokens: 80,
|
|
4619
|
+
temperature: 0
|
|
4620
|
+
}), 3e4, "tool-support probe timed out");
|
|
4621
|
+
return (result.toolCalls?.length ?? 0) > 0;
|
|
4622
|
+
} catch {
|
|
4623
|
+
return null;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4369
4626
|
/**
|
|
4370
4627
|
* Cascade Auto live data: discover/validate real model ids from each cloud
|
|
4371
4628
|
* provider, then fetch current public quality scores + per-token prices and
|
|
@@ -4415,12 +4672,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4415
4672
|
await Promise.allSettled(tasks);
|
|
4416
4673
|
}
|
|
4417
4674
|
/**
|
|
4418
|
-
* Replace available models with live-priced
|
|
4419
|
-
*
|
|
4675
|
+
* Replace available models with live-priced AND capability-corrected copies
|
|
4676
|
+
* (real context windows, native tool support, vision from modalities), then
|
|
4677
|
+
* refresh the already resolved tier models so shared-tier cost accounting and
|
|
4678
|
+
* the tool-use gate both see current data.
|
|
4420
4679
|
*/
|
|
4421
4680
|
applyLivePricing() {
|
|
4422
|
-
if (!this.liveData
|
|
4423
|
-
|
|
4681
|
+
if (!this.liveData) return;
|
|
4682
|
+
if (!this.liveData.hasLivePricing() && !this.liveData.hasCapabilities()) return;
|
|
4683
|
+
let updated = this.selector.getAllAvailableModels();
|
|
4684
|
+
if (this.liveData.hasLivePricing()) updated = this.liveData.applyLivePricing(updated);
|
|
4685
|
+
if (this.liveData.hasCapabilities()) updated = this.liveData.applyLiveCapabilities(updated);
|
|
4424
4686
|
for (const m of updated) this.selector.addDynamicModel(m);
|
|
4425
4687
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
4426
4688
|
const cur = this.tierModels.get(tier);
|
|
@@ -4595,6 +4857,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4595
4857
|
const r = this.config?.reinforcements;
|
|
4596
4858
|
return { enabled: r?.enabled === true, maxPerSection: r?.maxPerSection ?? 4 };
|
|
4597
4859
|
}
|
|
4860
|
+
/** Project-knowledge settings (config.knowledge). Facts extraction on by default. */
|
|
4861
|
+
getKnowledgeConfig() {
|
|
4862
|
+
return { factsExtraction: this.config?.knowledge?.factsExtraction !== false };
|
|
4863
|
+
}
|
|
4598
4864
|
/**
|
|
4599
4865
|
* Resolved T3 wave execution mode. 'auto' becomes 'sequential' when the T3
|
|
4600
4866
|
* tier resolves to a LOCAL model (the single-GPU queue serializes anyway, so
|
|
@@ -4681,10 +4947,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4681
4947
|
* null when Cascade Auto is off (callers then use the shared tier model).
|
|
4682
4948
|
* Pure heuristic — no extra LLM call.
|
|
4683
4949
|
*/
|
|
4684
|
-
async selectModelForSubtask(tier, text) {
|
|
4950
|
+
async selectModelForSubtask(tier, text, opts) {
|
|
4685
4951
|
if (!this.config?.cascadeAuto || !this.taskAnalyzer || !text.trim()) return null;
|
|
4686
4952
|
try {
|
|
4687
|
-
return await this.taskAnalyzer.selectModel(text, tier, this.selector);
|
|
4953
|
+
return await this.taskAnalyzer.selectModel(text, tier, this.selector, opts);
|
|
4688
4954
|
} catch {
|
|
4689
4955
|
return null;
|
|
4690
4956
|
}
|
|
@@ -5025,6 +5291,13 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5025
5291
|
hierarchyContext = "";
|
|
5026
5292
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
5027
5293
|
signal;
|
|
5294
|
+
/**
|
|
5295
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
5296
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
5297
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
5298
|
+
* which would interleave, are not tagged.
|
|
5299
|
+
*/
|
|
5300
|
+
isPresenter = false;
|
|
5028
5301
|
constructor(role, id, parentId) {
|
|
5029
5302
|
super();
|
|
5030
5303
|
this.role = role;
|
|
@@ -5032,6 +5305,10 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
5032
5305
|
this.parentId = parentId;
|
|
5033
5306
|
this.label = this.id;
|
|
5034
5307
|
}
|
|
5308
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
5309
|
+
setPresenter(on = true) {
|
|
5310
|
+
this.isPresenter = on;
|
|
5311
|
+
}
|
|
5035
5312
|
getStatus() {
|
|
5036
5313
|
return this.status;
|
|
5037
5314
|
}
|
|
@@ -5429,6 +5706,13 @@ EXAMPLE \u2014 calling the "shell" tool to list files:
|
|
|
5429
5706
|
{"name": "shell", "input": {"command": "ls -la"}}
|
|
5430
5707
|
</tool_call>
|
|
5431
5708
|
|
|
5709
|
+
When you have enough information, stop calling tools and write your final answer.`;
|
|
5710
|
+
}
|
|
5711
|
+
function buildTextToolReminder(tools) {
|
|
5712
|
+
return `
|
|
5713
|
+
TOOL USE REMINDER:
|
|
5714
|
+
Call tools with a single <tool_call>{"name": "<tool_name>", "input": { ... }}</tool_call> block (valid JSON, double quotes, one tool per turn), matching the argument shapes of your earlier calls in this conversation.
|
|
5715
|
+
Available tools: ${tools.map((t) => t.name).join(", ")}.
|
|
5432
5716
|
When you have enough information, stop calling tools and write your final answer.`;
|
|
5433
5717
|
}
|
|
5434
5718
|
|
|
@@ -5668,6 +5952,9 @@ Now execute your subtask using this context where relevant.`
|
|
|
5668
5952
|
} catch (e) {
|
|
5669
5953
|
this.log("Failed to write to World State DB");
|
|
5670
5954
|
}
|
|
5955
|
+
if (this.router.getKnowledgeConfig?.().factsExtraction !== false) {
|
|
5956
|
+
await this.extractAndStoreFacts(db, assignment, output);
|
|
5957
|
+
}
|
|
5671
5958
|
}
|
|
5672
5959
|
this.setStatus("COMPLETED", output);
|
|
5673
5960
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
@@ -5729,7 +6016,7 @@ Now execute your subtask using this context where relevant.`
|
|
|
5729
6016
|
let subtaskModel;
|
|
5730
6017
|
try {
|
|
5731
6018
|
const subtaskText = `${this.assignment?.subtaskTitle ?? ""} ${this.assignment?.description ?? ""} ${this.assignment?.expectedOutput ?? ""}`;
|
|
5732
|
-
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText) ?? void 0;
|
|
6019
|
+
subtaskModel = await this.router.selectModelForSubtask("T3", subtaskText, { requiresToolUse: tools.length > 0 }) ?? void 0;
|
|
5733
6020
|
if (subtaskModel) {
|
|
5734
6021
|
this.log(`Cascade Auto: routing this subtask to ${subtaskModel.provider}:${subtaskModel.id}`);
|
|
5735
6022
|
}
|
|
@@ -5737,7 +6024,8 @@ Now execute your subtask using this context where relevant.`
|
|
|
5737
6024
|
}
|
|
5738
6025
|
const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
|
|
5739
6026
|
const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
|
|
5740
|
-
|
|
6027
|
+
let sentFullTextContract = false;
|
|
6028
|
+
let textContractSignature = "";
|
|
5741
6029
|
while (iterations < MAX_ITERATIONS) {
|
|
5742
6030
|
iterations++;
|
|
5743
6031
|
this.throwIfCancelled();
|
|
@@ -5751,6 +6039,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5751
6039
|
${g.text}`
|
|
5752
6040
|
});
|
|
5753
6041
|
}
|
|
6042
|
+
let textToolSuffix = "";
|
|
6043
|
+
if (useTextTools) {
|
|
6044
|
+
const signature = tools.map((t) => t.name).join(",");
|
|
6045
|
+
if (!sentFullTextContract || signature !== textContractSignature) {
|
|
6046
|
+
textToolSuffix = buildTextToolSystemPrompt(tools);
|
|
6047
|
+
sentFullTextContract = true;
|
|
6048
|
+
textContractSignature = signature;
|
|
6049
|
+
} else {
|
|
6050
|
+
textToolSuffix = buildTextToolReminder(tools);
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
5754
6053
|
const options = {
|
|
5755
6054
|
messages: this.context.getMessages(),
|
|
5756
6055
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -5767,7 +6066,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5767
6066
|
"T3",
|
|
5768
6067
|
options,
|
|
5769
6068
|
(chunk) => {
|
|
5770
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
6069
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5771
6070
|
}
|
|
5772
6071
|
);
|
|
5773
6072
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -6240,6 +6539,41 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6240
6539
|
};
|
|
6241
6540
|
}
|
|
6242
6541
|
}
|
|
6542
|
+
/**
|
|
6543
|
+
* world-state v2: distill a completed subtask's output into durable
|
|
6544
|
+
* `(entity, relation, value)` facts and upsert them into the knowledge graph.
|
|
6545
|
+
* A bounded, cheap T3-tier call; entirely best-effort — any failure is swallowed
|
|
6546
|
+
* so it never blocks or fails the subtask. Respects the subtask's privacy tier
|
|
6547
|
+
* (a local-only subtask extracts on a local model too, never leaking to cloud).
|
|
6548
|
+
*/
|
|
6549
|
+
async extractAndStoreFacts(db, assignment, output) {
|
|
6550
|
+
try {
|
|
6551
|
+
const prompt = `Extract durable project facts from this completed subtask.
|
|
6552
|
+
Return ONLY a JSON array of {"entity","relation","value"} triples describing lasting
|
|
6553
|
+
facts about the codebase/project \u2014 e.g. {"entity":"auth module","relation":"uses","value":"JWT"}.
|
|
6554
|
+
Ignore transient step-by-step details. At most 6 triples. If nothing durable, return [].
|
|
6555
|
+
|
|
6556
|
+
Subtask: ${assignment.subtaskTitle}
|
|
6557
|
+
Output:
|
|
6558
|
+
${output.slice(0, 4e3)}`;
|
|
6559
|
+
const result = await this.router.generate("T3", {
|
|
6560
|
+
messages: [{ role: "user", content: prompt }],
|
|
6561
|
+
maxTokens: 300,
|
|
6562
|
+
temperature: 0,
|
|
6563
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
6564
|
+
});
|
|
6565
|
+
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
6566
|
+
if (!match) return;
|
|
6567
|
+
const facts = JSON.parse(match[0]);
|
|
6568
|
+
if (!Array.isArray(facts)) return;
|
|
6569
|
+
for (const f of facts.slice(0, 8)) {
|
|
6570
|
+
if (f && typeof f.entity === "string" && typeof f.relation === "string" && typeof f.value === "string") {
|
|
6571
|
+
db.upsertFact(f.entity, f.relation, f.value, this.id);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
} catch {
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6243
6577
|
async correctOutput(originalOutput, failures) {
|
|
6244
6578
|
const correctionPrompt = `The following output failed these checks: ${failures.join(", ")}.
|
|
6245
6579
|
|
|
@@ -7228,6 +7562,7 @@ ${peerOutputs}` : "";
|
|
|
7228
7562
|
chunkEnd++;
|
|
7229
7563
|
}
|
|
7230
7564
|
i = chunkEnd;
|
|
7565
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
7231
7566
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
7232
7567
|
${currentSummary ? `
|
|
7233
7568
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -7237,6 +7572,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7237
7572
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
7238
7573
|
const messages = [{ role: "user", content: prompt }];
|
|
7239
7574
|
try {
|
|
7575
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
7240
7576
|
const result = await this.router.generate("T2", {
|
|
7241
7577
|
messages,
|
|
7242
7578
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -7244,7 +7580,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
7244
7580
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
7245
7581
|
maxTokens: 500,
|
|
7246
7582
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7247
|
-
});
|
|
7583
|
+
}, streamFinal);
|
|
7248
7584
|
currentSummary = result.content;
|
|
7249
7585
|
} catch (err) {
|
|
7250
7586
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7296,14 +7632,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7296
7632
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7297
7633
|
});
|
|
7298
7634
|
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
|
-
}
|
|
7635
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7636
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
7305
7637
|
return null;
|
|
7306
7638
|
} catch {
|
|
7639
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
7307
7640
|
return null;
|
|
7308
7641
|
}
|
|
7309
7642
|
}
|
|
@@ -7646,10 +7979,24 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7646
7979
|
}
|
|
7647
7980
|
async decomposeTask(prompt, systemContext) {
|
|
7648
7981
|
const db = this.router.getWorldStateDB?.();
|
|
7649
|
-
|
|
7982
|
+
let worldStateContext = "";
|
|
7983
|
+
if (db) {
|
|
7984
|
+
const knowledge = db.getFormattedKnowledge(prompt);
|
|
7985
|
+
if (knowledge) {
|
|
7986
|
+
worldStateContext = `
|
|
7987
|
+
|
|
7988
|
+
PROJECT KNOWLEDGE (relevant facts about this codebase):
|
|
7989
|
+
${knowledge}`;
|
|
7990
|
+
} else {
|
|
7991
|
+
const log = db.getFormattedState();
|
|
7992
|
+
if (log && log !== "World State is currently empty.") {
|
|
7993
|
+
worldStateContext = `
|
|
7650
7994
|
|
|
7651
7995
|
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7652
|
-
${
|
|
7996
|
+
${log}`;
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
7653
8000
|
const contextSection = systemContext ? `
|
|
7654
8001
|
Project context:
|
|
7655
8002
|
${systemContext}` : "";
|
|
@@ -7985,7 +8332,7 @@ Instructions:
|
|
|
7985
8332
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
7986
8333
|
maxTokens: 8e3
|
|
7987
8334
|
}, (chunk) => {
|
|
7988
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
8335
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
7989
8336
|
});
|
|
7990
8337
|
return result.content;
|
|
7991
8338
|
}
|
|
@@ -8014,14 +8361,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
8014
8361
|
temperature: 0
|
|
8015
8362
|
});
|
|
8016
8363
|
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
|
-
}
|
|
8364
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
8365
|
+
(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
8366
|
return null;
|
|
8024
8367
|
} catch {
|
|
8368
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
8025
8369
|
return null;
|
|
8026
8370
|
}
|
|
8027
8371
|
}
|
|
@@ -10035,13 +10379,17 @@ var TaskAnalyzer = class {
|
|
|
10035
10379
|
* Scores tier-eligible models using cost efficiency + historical performance.
|
|
10036
10380
|
* Falls back to the priority-list default when no candidates have history.
|
|
10037
10381
|
*/
|
|
10038
|
-
async selectModel(prompt, tier, selector) {
|
|
10382
|
+
async selectModel(prompt, tier, selector, opts) {
|
|
10039
10383
|
const profile = await this.analyze(prompt);
|
|
10040
10384
|
if (profile.requiresVision) {
|
|
10041
10385
|
return selector.selectVisionModel();
|
|
10042
10386
|
}
|
|
10043
|
-
|
|
10387
|
+
let candidates2 = selector.getCandidatesForTier(tier);
|
|
10044
10388
|
if (candidates2.length === 0) return selector.selectForTier(tier);
|
|
10389
|
+
if (opts?.requiresToolUse) {
|
|
10390
|
+
const toolCapable = candidates2.filter((m) => m.supportsToolUse !== false);
|
|
10391
|
+
if (toolCapable.length > 0) candidates2 = toolCapable;
|
|
10392
|
+
}
|
|
10045
10393
|
const scored = candidates2.map((m) => ({
|
|
10046
10394
|
model: m,
|
|
10047
10395
|
score: this.scoreModel(m, profile)
|
|
@@ -10220,6 +10568,28 @@ var ModelPerformanceTracker = class {
|
|
|
10220
10568
|
return Math.max(0.1, 1 - normalised * complexityWeight);
|
|
10221
10569
|
}
|
|
10222
10570
|
};
|
|
10571
|
+
var ivmCache;
|
|
10572
|
+
var ivmWarned = false;
|
|
10573
|
+
async function loadIsolatedVm() {
|
|
10574
|
+
if (ivmCache !== void 0) return ivmCache;
|
|
10575
|
+
try {
|
|
10576
|
+
const specifier = "isolated-vm";
|
|
10577
|
+
const mod = await import(specifier);
|
|
10578
|
+
ivmCache = mod.default ?? mod;
|
|
10579
|
+
} catch {
|
|
10580
|
+
ivmCache = null;
|
|
10581
|
+
}
|
|
10582
|
+
return ivmCache;
|
|
10583
|
+
}
|
|
10584
|
+
function safeJsonParse(text) {
|
|
10585
|
+
if (typeof text !== "string") return {};
|
|
10586
|
+
try {
|
|
10587
|
+
const v = JSON.parse(text);
|
|
10588
|
+
return v && typeof v === "object" ? v : {};
|
|
10589
|
+
} catch {
|
|
10590
|
+
return {};
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10223
10593
|
var DYNAMIC_TOOLS_FILE = "dynamic-tools.json";
|
|
10224
10594
|
function normalizeToolSchema(schema) {
|
|
10225
10595
|
if (schema && schema["type"] === "object" && typeof schema["properties"] === "object") {
|
|
@@ -10322,7 +10692,12 @@ var DynamicTool = class extends BaseTool {
|
|
|
10322
10692
|
getEscalator;
|
|
10323
10693
|
/** Untrusted = loaded from disk / a peer; its dangerous calls always re-prompt. */
|
|
10324
10694
|
trusted;
|
|
10325
|
-
|
|
10695
|
+
/** Resolve the configured sandbox mode at call time (default 'auto'). */
|
|
10696
|
+
getSandboxMode;
|
|
10697
|
+
/** Optional diagnostics sink (routed through the host so it survives the Ink TUI). */
|
|
10698
|
+
log;
|
|
10699
|
+
constructor(spec, registry, getEscalator, trusted, getSandboxMode = () => "auto", log = () => {
|
|
10700
|
+
}) {
|
|
10326
10701
|
super();
|
|
10327
10702
|
this.name = spec.name;
|
|
10328
10703
|
this.description = spec.description;
|
|
@@ -10332,6 +10707,8 @@ var DynamicTool = class extends BaseTool {
|
|
|
10332
10707
|
this.registry = registry;
|
|
10333
10708
|
this.getEscalator = getEscalator;
|
|
10334
10709
|
this.trusted = trusted;
|
|
10710
|
+
this.getSandboxMode = getSandboxMode;
|
|
10711
|
+
this.log = log;
|
|
10335
10712
|
}
|
|
10336
10713
|
isDangerous() {
|
|
10337
10714
|
return this._isDangerous;
|
|
@@ -10368,8 +10745,94 @@ var DynamicTool = class extends BaseTool {
|
|
|
10368
10745
|
return `Error calling ${toolName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
10369
10746
|
}
|
|
10370
10747
|
};
|
|
10748
|
+
const mode = this.getSandboxMode();
|
|
10749
|
+
if (mode !== "worker") {
|
|
10750
|
+
const ivm = await loadIsolatedVm();
|
|
10751
|
+
if (ivm) return this.runInIsolate(ivm, input, callTool);
|
|
10752
|
+
if (mode === "isolate" && !ivmWarned) {
|
|
10753
|
+
ivmWarned = true;
|
|
10754
|
+
this.log("[tool-creator] isolated-vm is not available (not installed or failed to build) \u2014 dynamic tools fall back to the worker sandbox, which is NOT capability-confined. Install isolated-vm for a hard isolate.");
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10371
10757
|
return this.runInWorker(input, callTool);
|
|
10372
10758
|
}
|
|
10759
|
+
/**
|
|
10760
|
+
* Run the generated code in a hard V8 isolate (isolated-vm). The isolate global
|
|
10761
|
+
* has no Node built-ins, so the code cannot see `process`, `require`, the
|
|
10762
|
+
* filesystem, or the network — it reaches the host ONLY through the injected
|
|
10763
|
+
* `callTool` (escalator-gated on the main thread) and `fetch` (SSRF-guarded via
|
|
10764
|
+
* bridgeFetch). `script.run({ timeout })` bounds synchronous CPU; an outer
|
|
10765
|
+
* wall-clock race + `isolate.dispose()` bounds async runaway (a never-resolving
|
|
10766
|
+
* await), mirroring the worker's terminate().
|
|
10767
|
+
*/
|
|
10768
|
+
async runInIsolate(ivm, input, callTool) {
|
|
10769
|
+
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
10770
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
10771
|
+
let disposed = false;
|
|
10772
|
+
const dispose = () => {
|
|
10773
|
+
if (!disposed) {
|
|
10774
|
+
disposed = true;
|
|
10775
|
+
try {
|
|
10776
|
+
isolate.dispose();
|
|
10777
|
+
} catch {
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
};
|
|
10781
|
+
try {
|
|
10782
|
+
const context = await isolate.createContext();
|
|
10783
|
+
const jail = context.global;
|
|
10784
|
+
await jail.set("_callTool", new ivm.Reference(async (name, inputJson) => {
|
|
10785
|
+
const out = await callTool(String(name), safeJsonParse(inputJson));
|
|
10786
|
+
return String(out);
|
|
10787
|
+
}));
|
|
10788
|
+
await jail.set("_fetch", new ivm.Reference(async (url, initJson) => {
|
|
10789
|
+
const r = await bridgeFetch(String(url), safeJsonParse(initJson));
|
|
10790
|
+
return JSON.stringify(r);
|
|
10791
|
+
}));
|
|
10792
|
+
await jail.set("_input", new ivm.ExternalCopy(input).copyInto());
|
|
10793
|
+
await jail.set("_code", this.executeCode);
|
|
10794
|
+
const bootstrap = `
|
|
10795
|
+
const callTool = (name, toolInput) => _callTool.apply(undefined,
|
|
10796
|
+
[String(name), JSON.stringify(toolInput || {})],
|
|
10797
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10798
|
+
const fetch = async (url, init) => {
|
|
10799
|
+
const raw = await _fetch.apply(undefined,
|
|
10800
|
+
[String(url), JSON.stringify(init || null)],
|
|
10801
|
+
{ result: { promise: true }, arguments: { copy: true } });
|
|
10802
|
+
const r = JSON.parse(raw);
|
|
10803
|
+
if (r && r.__error) throw new Error(r.__error);
|
|
10804
|
+
return {
|
|
10805
|
+
ok: r.ok, status: r.status, statusText: r.statusText,
|
|
10806
|
+
headers: { get: (k) => (String(k).toLowerCase() === 'content-type' ? r.contentType : null) },
|
|
10807
|
+
text: async () => r.body,
|
|
10808
|
+
json: async () => JSON.parse(r.body),
|
|
10809
|
+
};
|
|
10810
|
+
};
|
|
10811
|
+
const console = { log() {}, error() {} };
|
|
10812
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
10813
|
+
const fn = new AsyncFunction('input', 'callTool', 'fetch', 'console', _code);
|
|
10814
|
+
(async () => { const r = await fn(_input, callTool, fetch, console); return String(r == null ? '' : r); })();
|
|
10815
|
+
`;
|
|
10816
|
+
const script = await isolate.compileScript(bootstrap);
|
|
10817
|
+
const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
|
|
10818
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
10819
|
+
const t = setTimeout(() => {
|
|
10820
|
+
dispose();
|
|
10821
|
+
resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
|
|
10822
|
+
}, timeoutMs + 500);
|
|
10823
|
+
t.unref?.();
|
|
10824
|
+
});
|
|
10825
|
+
return await Promise.race([runPromise, timeoutPromise]);
|
|
10826
|
+
} catch (err) {
|
|
10827
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10828
|
+
if (/timed? out/i.test(msg)) {
|
|
10829
|
+
return `Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`;
|
|
10830
|
+
}
|
|
10831
|
+
return `Tool error: ${msg}`;
|
|
10832
|
+
} finally {
|
|
10833
|
+
dispose();
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10373
10836
|
/** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
|
|
10374
10837
|
runInWorker(input, callTool) {
|
|
10375
10838
|
const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
|
|
@@ -10449,16 +10912,19 @@ var ToolCreator = class {
|
|
|
10449
10912
|
workspacePath;
|
|
10450
10913
|
/** When false, persisted tools are neither loaded nor written. */
|
|
10451
10914
|
persistEnabled;
|
|
10915
|
+
/** Sandbox runtime for generated tools; passed to each DynamicTool. */
|
|
10916
|
+
sandboxMode;
|
|
10452
10917
|
logger;
|
|
10453
10918
|
/** name → spec, for persistence, broadcast, and re-registration. */
|
|
10454
10919
|
specs = /* @__PURE__ */ new Map();
|
|
10455
10920
|
/** capability fingerprint → tool name, so the same need isn't re-generated. */
|
|
10456
10921
|
capabilityIndex = /* @__PURE__ */ new Map();
|
|
10457
|
-
constructor(router, registry, workspacePath, persistEnabled = true) {
|
|
10922
|
+
constructor(router, registry, workspacePath, persistEnabled = true, sandboxMode = "auto") {
|
|
10458
10923
|
this.router = router;
|
|
10459
10924
|
this.registry = registry;
|
|
10460
10925
|
this.workspacePath = workspacePath;
|
|
10461
10926
|
this.persistEnabled = persistEnabled;
|
|
10927
|
+
this.sandboxMode = sandboxMode;
|
|
10462
10928
|
}
|
|
10463
10929
|
setPermissionEscalator(escalator) {
|
|
10464
10930
|
this.escalator = escalator;
|
|
@@ -10546,7 +11012,14 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10546
11012
|
this.specs.set(spec.name, spec);
|
|
10547
11013
|
return;
|
|
10548
11014
|
}
|
|
10549
|
-
const tool = new DynamicTool(
|
|
11015
|
+
const tool = new DynamicTool(
|
|
11016
|
+
spec,
|
|
11017
|
+
this.registry,
|
|
11018
|
+
() => this.escalator,
|
|
11019
|
+
trusted,
|
|
11020
|
+
() => this.sandboxMode,
|
|
11021
|
+
(msg) => this.log(msg)
|
|
11022
|
+
);
|
|
10550
11023
|
this.registry.register(tool);
|
|
10551
11024
|
this.specs.set(spec.name, spec);
|
|
10552
11025
|
this.capabilityIndex.set(capabilityKey(`${spec.description}`), spec.name);
|
|
@@ -10595,6 +11068,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10595
11068
|
return Array.from(this.specs.keys());
|
|
10596
11069
|
}
|
|
10597
11070
|
};
|
|
11071
|
+
function normalizeKey(text) {
|
|
11072
|
+
return text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
11073
|
+
}
|
|
10598
11074
|
var WorldStateDB = class {
|
|
10599
11075
|
constructor(workspacePath, debugMode = false) {
|
|
10600
11076
|
this.workspacePath = workspacePath;
|
|
@@ -10616,6 +11092,16 @@ var WorldStateDB = class {
|
|
|
10616
11092
|
encrypted_payload TEXT NOT NULL
|
|
10617
11093
|
)
|
|
10618
11094
|
`);
|
|
11095
|
+
this.db.exec(`
|
|
11096
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
11097
|
+
entity TEXT NOT NULL,
|
|
11098
|
+
relation TEXT NOT NULL,
|
|
11099
|
+
encrypted_value TEXT NOT NULL,
|
|
11100
|
+
source_worker TEXT NOT NULL,
|
|
11101
|
+
timestamp TEXT NOT NULL,
|
|
11102
|
+
PRIMARY KEY (entity, relation)
|
|
11103
|
+
)
|
|
11104
|
+
`);
|
|
10619
11105
|
}
|
|
10620
11106
|
workspacePath;
|
|
10621
11107
|
debugMode;
|
|
@@ -10689,6 +11175,131 @@ var WorldStateDB = class {
|
|
|
10689
11175
|
if (entries.length === 0) return "World State is currently empty.";
|
|
10690
11176
|
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10691
11177
|
}
|
|
11178
|
+
// ── world-state v2: queryable facts ──────────────
|
|
11179
|
+
/**
|
|
11180
|
+
* Upsert a fact. `(entity, relation)` is normalized so casing/whitespace don't
|
|
11181
|
+
* fragment the key; an existing pair is superseded (value + provenance updated)
|
|
11182
|
+
* rather than duplicated. Empty entity/relation/value are ignored.
|
|
11183
|
+
*/
|
|
11184
|
+
upsertFact(entity, relation, value, sourceWorker, timestamp) {
|
|
11185
|
+
const e = normalizeKey(entity);
|
|
11186
|
+
const r = normalizeKey(relation);
|
|
11187
|
+
const v = value.trim();
|
|
11188
|
+
if (!e || !r || !v) return;
|
|
11189
|
+
const encrypted = this.encrypt(JSON.stringify({ value: v }));
|
|
11190
|
+
const stmt = this.db.prepare(`
|
|
11191
|
+
INSERT INTO facts (entity, relation, encrypted_value, source_worker, timestamp)
|
|
11192
|
+
VALUES (?, ?, ?, ?, ?)
|
|
11193
|
+
ON CONFLICT(entity, relation) DO UPDATE SET
|
|
11194
|
+
encrypted_value = excluded.encrypted_value,
|
|
11195
|
+
source_worker = excluded.source_worker,
|
|
11196
|
+
timestamp = excluded.timestamp
|
|
11197
|
+
`);
|
|
11198
|
+
stmt.run(e, r, encrypted, sourceWorker, timestamp ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
11199
|
+
this.dumpDebugIfNeeded();
|
|
11200
|
+
}
|
|
11201
|
+
rowToFact(row) {
|
|
11202
|
+
let value;
|
|
11203
|
+
try {
|
|
11204
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
11205
|
+
} catch {
|
|
11206
|
+
value = "[Decryption Failed - Payload Corrupted]";
|
|
11207
|
+
}
|
|
11208
|
+
return { entity: row.entity, relation: row.relation, value, sourceWorker: row.source_worker, timestamp: row.timestamp };
|
|
11209
|
+
}
|
|
11210
|
+
/** All facts whose entity matches one of the (normalized) query entities. */
|
|
11211
|
+
getFactsForEntities(entities) {
|
|
11212
|
+
const keys = Array.from(new Set(entities.map(normalizeKey).filter(Boolean)));
|
|
11213
|
+
if (keys.length === 0) return [];
|
|
11214
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
11215
|
+
const stmt = this.db.prepare(
|
|
11216
|
+
`SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts WHERE entity IN (${placeholders}) ORDER BY entity ASC, relation ASC`
|
|
11217
|
+
);
|
|
11218
|
+
return stmt.all(...keys).map((row) => this.rowToFact(row));
|
|
11219
|
+
}
|
|
11220
|
+
/** Every fact (used for tests / debug / whole-graph views). */
|
|
11221
|
+
getAllFacts() {
|
|
11222
|
+
const stmt = this.db.prepare("SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts ORDER BY entity ASC, relation ASC");
|
|
11223
|
+
return stmt.all().map((row) => this.rowToFact(row));
|
|
11224
|
+
}
|
|
11225
|
+
/**
|
|
11226
|
+
* A compact, human/LLM-readable fact block for T1/T2 planning. When `entities`
|
|
11227
|
+
* is given, only facts about those entities are included; otherwise all facts.
|
|
11228
|
+
* Returns '' when there are none so callers can fall back to the linear log.
|
|
11229
|
+
*/
|
|
11230
|
+
getFormattedFacts(entities) {
|
|
11231
|
+
const facts = entities && entities.length > 0 ? this.getFactsForEntities(entities) : this.getAllFacts();
|
|
11232
|
+
if (facts.length === 0) return "";
|
|
11233
|
+
return facts.map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11234
|
+
}
|
|
11235
|
+
/**
|
|
11236
|
+
* A compact knowledge block for T1/T2 planning. When `prompt` is given, facts
|
|
11237
|
+
* whose entity/relation/value mention a prompt token are preferred (relevance
|
|
11238
|
+
* filter); otherwise, or when nothing matches, all facts are used (capped at
|
|
11239
|
+
* `limit`). Returns '' when there are no facts, so the caller can fall back to
|
|
11240
|
+
* the raw linear log — this replaces replaying the whole log during planning.
|
|
11241
|
+
*/
|
|
11242
|
+
getFormattedKnowledge(prompt, limit = 40) {
|
|
11243
|
+
const all = this.getAllFacts();
|
|
11244
|
+
if (all.length === 0) return "";
|
|
11245
|
+
let selected = all;
|
|
11246
|
+
if (prompt && prompt.trim()) {
|
|
11247
|
+
const tokens = Array.from(new Set(prompt.toLowerCase().match(/[a-z0-9_./-]{3,}/g) ?? []));
|
|
11248
|
+
if (tokens.length > 0) {
|
|
11249
|
+
const matched = all.filter((f) => {
|
|
11250
|
+
const hay = `${f.entity} ${f.relation} ${f.value}`.toLowerCase();
|
|
11251
|
+
return tokens.some((t) => hay.includes(t));
|
|
11252
|
+
});
|
|
11253
|
+
if (matched.length > 0) selected = matched;
|
|
11254
|
+
}
|
|
11255
|
+
}
|
|
11256
|
+
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11257
|
+
}
|
|
11258
|
+
// ── Export / Import ──────────────────────────
|
|
11259
|
+
//
|
|
11260
|
+
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
11261
|
+
// JSON file — the encryption key never leaves this machine) and re-encrypts
|
|
11262
|
+
// with the LOCAL key on import.
|
|
11263
|
+
exportKnowledge() {
|
|
11264
|
+
return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
|
|
11265
|
+
}
|
|
11266
|
+
/**
|
|
11267
|
+
* Merge imported knowledge. Facts upsert on (entity, relation) with
|
|
11268
|
+
* newer-timestamp-wins (a local fact newer than the imported one is kept);
|
|
11269
|
+
* log entries append with fresh ids, skipping exact duplicates
|
|
11270
|
+
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
11271
|
+
*/
|
|
11272
|
+
importKnowledge(data) {
|
|
11273
|
+
let facts = 0;
|
|
11274
|
+
let logEntries = 0;
|
|
11275
|
+
if (Array.isArray(data.facts)) {
|
|
11276
|
+
const local = new Map(this.getAllFacts().map((f) => [`${f.entity}\0${f.relation}`, f.timestamp]));
|
|
11277
|
+
for (const f of data.facts) {
|
|
11278
|
+
if (!f || typeof f.entity !== "string" || typeof f.relation !== "string" || typeof f.value !== "string") continue;
|
|
11279
|
+
const key = `${normalizeKey(f.entity)}\0${normalizeKey(f.relation)}`;
|
|
11280
|
+
const localTs = local.get(key);
|
|
11281
|
+
const importTs = f.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11282
|
+
if (localTs && localTs >= importTs) continue;
|
|
11283
|
+
this.upsertFact(f.entity, f.relation, f.value, f.sourceWorker ?? "imported", importTs);
|
|
11284
|
+
facts++;
|
|
11285
|
+
}
|
|
11286
|
+
}
|
|
11287
|
+
if (Array.isArray(data.worldLog)) {
|
|
11288
|
+
const seen = new Set(this.getAllEntries().map((e) => `${e.workerId}\0${e.timestamp}\0${e.summary}`));
|
|
11289
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
11290
|
+
for (const e of data.worldLog) {
|
|
11291
|
+
if (!e || typeof e.summary !== "string") continue;
|
|
11292
|
+
const workerId = e.workerId ?? "imported";
|
|
11293
|
+
const timestamp = e.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
11294
|
+
const key = `${workerId}\0${timestamp}\0${e.summary}`;
|
|
11295
|
+
if (seen.has(key)) continue;
|
|
11296
|
+
stmt.run(crypto__default.default.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e.summary })));
|
|
11297
|
+
seen.add(key);
|
|
11298
|
+
logEntries++;
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
return { facts, logEntries };
|
|
11302
|
+
}
|
|
10692
11303
|
dumpDebugIfNeeded() {
|
|
10693
11304
|
if (!this.debugMode) return;
|
|
10694
11305
|
try {
|
|
@@ -10825,7 +11436,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
10825
11436
|
}
|
|
10826
11437
|
const cfg = this.config;
|
|
10827
11438
|
if (cfg["enableToolCreation"] === true) {
|
|
10828
|
-
|
|
11439
|
+
const sandboxMode = this.config.tools?.dynamicToolSandbox ?? "auto";
|
|
11440
|
+
this.toolCreator = new ToolCreator(this.router, this.toolRegistry, this.workspacePath, cfg["persistDynamicTools"] !== false, sandboxMode);
|
|
10829
11441
|
this.toolCreator.setLogger((m) => {
|
|
10830
11442
|
if (this.listenerCount("log") > 0) this.emit("log", { level: "info", message: m });
|
|
10831
11443
|
});
|
|
@@ -11063,6 +11675,10 @@ ${last.partialOutput}` : "");
|
|
|
11063
11675
|
this.router.profileModels(this.store).catch(() => {
|
|
11064
11676
|
});
|
|
11065
11677
|
}
|
|
11678
|
+
if (this.store) {
|
|
11679
|
+
this.router.probeLocalToolSupport(this.store).catch(() => {
|
|
11680
|
+
});
|
|
11681
|
+
}
|
|
11066
11682
|
if (this.config.cascadeAuto) {
|
|
11067
11683
|
this.router.refreshLiveData().catch(() => {
|
|
11068
11684
|
});
|
|
@@ -11117,6 +11733,21 @@ ${last.partialOutput}` : "");
|
|
|
11117
11733
|
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
11734
|
return inquiry && !producesArtifact;
|
|
11119
11735
|
}
|
|
11736
|
+
/**
|
|
11737
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
11738
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
11739
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
11740
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11741
|
+
* Simple/Moderate) don't get over-escalated.
|
|
11742
|
+
*/
|
|
11743
|
+
looksClearlyComplex(prompt) {
|
|
11744
|
+
const p = prompt.trim();
|
|
11745
|
+
if (p.length < 24) return false;
|
|
11746
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11747
|
+
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);
|
|
11748
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
11749
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
11750
|
+
}
|
|
11120
11751
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
11121
11752
|
static globCache = /* @__PURE__ */ new Map();
|
|
11122
11753
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -11200,10 +11831,16 @@ ${prompt}` : prompt;
|
|
|
11200
11831
|
let verdict;
|
|
11201
11832
|
if (match) {
|
|
11202
11833
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
11203
|
-
|
|
11834
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11835
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11836
|
+
verdict = "Complex";
|
|
11837
|
+
} else {
|
|
11838
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11839
|
+
}
|
|
11204
11840
|
} else {
|
|
11205
|
-
|
|
11206
|
-
|
|
11841
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
11842
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
11843
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
11207
11844
|
}
|
|
11208
11845
|
return verdict;
|
|
11209
11846
|
} catch {
|
|
@@ -11255,7 +11892,15 @@ ${prompt}` : prompt;
|
|
|
11255
11892
|
}
|
|
11256
11893
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
11257
11894
|
});
|
|
11258
|
-
const
|
|
11895
|
+
const forceTier = this.config.routing?.forceTier;
|
|
11896
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
11897
|
+
let complexity;
|
|
11898
|
+
if (forced) {
|
|
11899
|
+
complexity = forced;
|
|
11900
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
11901
|
+
} else {
|
|
11902
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
11903
|
+
}
|
|
11259
11904
|
this.telemetry.capture("cascade:session_start", {
|
|
11260
11905
|
complexity,
|
|
11261
11906
|
providerCount: this.config.providers.length,
|
|
@@ -11335,6 +11980,7 @@ ${prompt}` : prompt;
|
|
|
11335
11980
|
try {
|
|
11336
11981
|
if (complexity === "Simple") {
|
|
11337
11982
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
11983
|
+
t3.setPresenter(true);
|
|
11338
11984
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
11339
11985
|
if (identityPrompt) {
|
|
11340
11986
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -11359,6 +12005,7 @@ ${prompt}` : prompt;
|
|
|
11359
12005
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
11360
12006
|
} else if (complexity === "Moderate") {
|
|
11361
12007
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
12008
|
+
t2.setPresenter(true);
|
|
11362
12009
|
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
12010
|
if (identityPrompt) {
|
|
11364
12011
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -11408,6 +12055,7 @@ ${prompt}` : prompt;
|
|
|
11408
12055
|
}
|
|
11409
12056
|
} else {
|
|
11410
12057
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
12058
|
+
t1.setPresenter(true);
|
|
11411
12059
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
11412
12060
|
if (identityPrompt) {
|
|
11413
12061
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -15408,7 +16056,8 @@ var DashboardSocket = class {
|
|
|
15408
16056
|
socket.on("cascade:run", (payload) => {
|
|
15409
16057
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
15410
16058
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
15411
|
-
|
|
16059
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
16060
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
15412
16061
|
}
|
|
15413
16062
|
});
|
|
15414
16063
|
});
|
|
@@ -15438,6 +16087,13 @@ var DashboardServer = class {
|
|
|
15438
16087
|
* of runs the session performed in this server's lifetime.
|
|
15439
16088
|
*/
|
|
15440
16089
|
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
16090
|
+
/**
|
|
16091
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
16092
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
16093
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
16094
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
16095
|
+
*/
|
|
16096
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
15441
16097
|
port;
|
|
15442
16098
|
host;
|
|
15443
16099
|
workspacePath;
|
|
@@ -15496,17 +16152,18 @@ var DashboardServer = class {
|
|
|
15496
16152
|
}
|
|
15497
16153
|
this.persistConfig();
|
|
15498
16154
|
});
|
|
15499
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
16155
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
15500
16156
|
const sessionId = requestedSessionId ?? crypto.randomUUID();
|
|
15501
16157
|
const abortController = new AbortController();
|
|
15502
16158
|
this.activeControllers.set(sessionId, abortController);
|
|
15503
16159
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
15504
16160
|
const title = this.persistRunStart(sessionId, prompt);
|
|
15505
|
-
|
|
16161
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
16162
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
15506
16163
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
15507
16164
|
this.activeSessions.set(sessionId, cascade);
|
|
15508
16165
|
cascade.on("stream:token", (e) => {
|
|
15509
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16166
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
15510
16167
|
});
|
|
15511
16168
|
cascade.on("tier:status", (e) => {
|
|
15512
16169
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -15518,7 +16175,11 @@ var DashboardServer = class {
|
|
|
15518
16175
|
this.socket.emitPeerMessage(e);
|
|
15519
16176
|
});
|
|
15520
16177
|
try {
|
|
15521
|
-
const result = await cascade.run({
|
|
16178
|
+
const result = await cascade.run({
|
|
16179
|
+
prompt: runPrompt,
|
|
16180
|
+
signal: abortController.signal,
|
|
16181
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16182
|
+
});
|
|
15522
16183
|
this.recordSessionTask(sessionId, result.taskId);
|
|
15523
16184
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15524
16185
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
@@ -15537,10 +16198,18 @@ var DashboardServer = class {
|
|
|
15537
16198
|
} finally {
|
|
15538
16199
|
this.activeSessions.delete(sessionId);
|
|
15539
16200
|
this.activeControllers.delete(sessionId);
|
|
16201
|
+
this.denyPendingApprovals(sessionId);
|
|
15540
16202
|
}
|
|
15541
16203
|
});
|
|
15542
16204
|
this.socket.onSessionHalt((sessionId) => {
|
|
15543
16205
|
this.activeControllers.get(sessionId)?.abort();
|
|
16206
|
+
this.denyPendingApprovals(sessionId);
|
|
16207
|
+
});
|
|
16208
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16209
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
16210
|
+
if (!pending) return;
|
|
16211
|
+
this.pendingApprovals.delete(requestId);
|
|
16212
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
15544
16213
|
});
|
|
15545
16214
|
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15546
16215
|
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
@@ -15708,6 +16377,29 @@ var DashboardServer = class {
|
|
|
15708
16377
|
list.push(taskId);
|
|
15709
16378
|
this.sessionTaskIds.set(sessionId, list);
|
|
15710
16379
|
}
|
|
16380
|
+
/**
|
|
16381
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
16382
|
+
* user. The request itself was already pushed to the client via the
|
|
16383
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
16384
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
16385
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
16386
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
16387
|
+
* timeout denies it.
|
|
16388
|
+
*/
|
|
16389
|
+
makeApprovalCallback(sessionId) {
|
|
16390
|
+
return (request) => new Promise((resolve) => {
|
|
16391
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16392
|
+
});
|
|
16393
|
+
}
|
|
16394
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16395
|
+
denyPendingApprovals(sessionId) {
|
|
16396
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
16397
|
+
if (pending.sessionId === sessionId) {
|
|
16398
|
+
this.pendingApprovals.delete(id);
|
|
16399
|
+
pending.resolve({ approved: false, always: false });
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16402
|
+
}
|
|
15711
16403
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15712
16404
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15713
16405
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15896,15 +16588,6 @@ ${prompt}`;
|
|
|
15896
16588
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
15897
16589
|
res.json({ success: true, ...payload });
|
|
15898
16590
|
});
|
|
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
16591
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
15909
16592
|
const body = req.body;
|
|
15910
16593
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -15949,6 +16632,60 @@ ${prompt}`;
|
|
|
15949
16632
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15950
16633
|
res.json({ ok: true });
|
|
15951
16634
|
});
|
|
16635
|
+
this.app.get("/api/export", auth, (req, res) => {
|
|
16636
|
+
try {
|
|
16637
|
+
const sessionsParam = typeof req.query["sessions"] === "string" ? req.query["sessions"] : "all";
|
|
16638
|
+
const includeMemories = req.query["memories"] === "1" || req.query["memories"] === "true";
|
|
16639
|
+
const ids = sessionsParam === "all" ? void 0 : sessionsParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16640
|
+
const bundle = {
|
|
16641
|
+
format: "cascade-export@1",
|
|
16642
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16643
|
+
sessions: this.store.exportSessions(ids)
|
|
16644
|
+
};
|
|
16645
|
+
if (includeMemories) {
|
|
16646
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16647
|
+
try {
|
|
16648
|
+
bundle["memories"] = { ...ws.exportKnowledge(), identities: this.store.listIdentities() };
|
|
16649
|
+
} finally {
|
|
16650
|
+
ws.close();
|
|
16651
|
+
}
|
|
16652
|
+
}
|
|
16653
|
+
res.json(bundle);
|
|
16654
|
+
} catch (err) {
|
|
16655
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16656
|
+
}
|
|
16657
|
+
});
|
|
16658
|
+
this.app.post("/api/import", auth, mutationLimiter, (req, res) => {
|
|
16659
|
+
try {
|
|
16660
|
+
const bundle = req.body;
|
|
16661
|
+
if (!bundle || bundle.format !== "cascade-export@1") {
|
|
16662
|
+
res.status(400).json({ error: "Not a cascade-export@1 bundle." });
|
|
16663
|
+
return;
|
|
16664
|
+
}
|
|
16665
|
+
const importedSessions = Array.isArray(bundle.sessions) ? this.store.importSessions(bundle.sessions) : [];
|
|
16666
|
+
for (const s of importedSessions) this.persistRuntimeRow(s.id, s.title, "COMPLETED");
|
|
16667
|
+
let facts = 0;
|
|
16668
|
+
let logEntries = 0;
|
|
16669
|
+
let identities = 0;
|
|
16670
|
+
if (bundle.memories) {
|
|
16671
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
16672
|
+
try {
|
|
16673
|
+
const counts = ws.importKnowledge(bundle.memories);
|
|
16674
|
+
facts = counts.facts;
|
|
16675
|
+
logEntries = counts.logEntries;
|
|
16676
|
+
} finally {
|
|
16677
|
+
ws.close();
|
|
16678
|
+
}
|
|
16679
|
+
if (Array.isArray(bundle.memories.identities)) {
|
|
16680
|
+
identities = this.store.importIdentities(bundle.memories.identities);
|
|
16681
|
+
}
|
|
16682
|
+
}
|
|
16683
|
+
this.socket.broadcast("runtime:refresh", { scope: "workspace" });
|
|
16684
|
+
res.json({ ok: true, imported: { sessions: importedSessions.length, facts, logEntries, identities } });
|
|
16685
|
+
} catch (err) {
|
|
16686
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16687
|
+
}
|
|
16688
|
+
});
|
|
15952
16689
|
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
15953
16690
|
const sessionId = req.params.id;
|
|
15954
16691
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
@@ -16155,7 +16892,7 @@ ${prompt}`;
|
|
|
16155
16892
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
16156
16893
|
this.activeSessions.set(sessionId, cascade);
|
|
16157
16894
|
cascade.on("stream:token", (e) => {
|
|
16158
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16895
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
16159
16896
|
});
|
|
16160
16897
|
cascade.on("tier:status", (e) => {
|
|
16161
16898
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -16167,7 +16904,11 @@ ${prompt}`;
|
|
|
16167
16904
|
this.socket.emitPeerMessage(e);
|
|
16168
16905
|
});
|
|
16169
16906
|
try {
|
|
16170
|
-
const result = await cascade.run({
|
|
16907
|
+
const result = await cascade.run({
|
|
16908
|
+
prompt: runPrompt,
|
|
16909
|
+
identityId: body.identityId,
|
|
16910
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16911
|
+
});
|
|
16171
16912
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16172
16913
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16173
16914
|
this.socket.broadcast("cost:update", {
|
|
@@ -16185,6 +16926,7 @@ ${prompt}`;
|
|
|
16185
16926
|
});
|
|
16186
16927
|
} finally {
|
|
16187
16928
|
this.activeSessions.delete(sessionId);
|
|
16929
|
+
this.denyPendingApprovals(sessionId);
|
|
16188
16930
|
}
|
|
16189
16931
|
})();
|
|
16190
16932
|
});
|