kimiflare 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/index.js CHANGED
@@ -11985,7 +11985,7 @@ var init_supervisor = __esm({
11985
11985
  * Future: the coordinator will call this automatically when intent classification
11986
11986
  * signals a "heavy" task (see docs/plans/multi-agent-standalone-workers-plan.md).
11987
11987
  */
11988
- async spawnWorkers(workers, onUpdate) {
11988
+ async spawnWorkers(workers, onUpdate, signal) {
11989
11989
  const cfg = await loadConfig().catch(() => null);
11990
11990
  const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT ?? cfg?.workerEndpoint ?? cfg?.remoteWorkerUrl;
11991
11991
  if (!endpoint) {
@@ -12068,7 +12068,7 @@ var init_supervisor = __esm({
12068
12068
  prBody: w.prBody
12069
12069
  } : {}
12070
12070
  };
12071
- const timeoutMs = parseInt(process.env.KIMIFLARE_WORKER_TIMEOUT_MS ?? "600000", 10);
12071
+ const timeoutMs = parseInt(process.env.KIMIFLARE_WORKER_TIMEOUT_MS ?? "900000", 10);
12072
12072
  worker.logs.push(`[coordinator] Sending payload (${JSON.stringify(payload).length} bytes)`);
12073
12073
  worker.logs.push(`[coordinator] Worker will clone ${repo.owner}/${repo.repo} and run kimiflare inside Cloudflare Sandbox`);
12074
12074
  worker.logs.push(`[coordinator] Typical runtime: 1\u20134 min. Timeout: ${Math.round(timeoutMs / 1e3)}s`);
@@ -12086,13 +12086,27 @@ var init_supervisor = __esm({
12086
12086
  throw new Error(`Worker start failed: ${startRes.status} ${text.slice(0, 200)}`);
12087
12087
  }
12088
12088
  const { workerId: remoteWorkerId } = await startRes.json();
12089
+ worker.remoteWorkerId = remoteWorkerId;
12089
12090
  worker.logs.push(`[coordinator] Worker started (id: ${remoteWorkerId}) \u2014 polling for progress\u2026`);
12090
12091
  onUpdate?.([...activeWorkers.values()]);
12091
12092
  const pollInterval = 3e3;
12092
12093
  const startTime = Date.now();
12093
12094
  let lastLogCount = 0;
12095
+ let lastStep = "";
12094
12096
  let data;
12095
12097
  while (Date.now() - startTime < timeoutMs) {
12098
+ if (signal?.aborted) {
12099
+ worker.logs.push(`[coordinator] Cancelling worker (id: ${remoteWorkerId})\u2026`);
12100
+ onUpdate?.([...activeWorkers.values()]);
12101
+ try {
12102
+ await fetch(`${endpoint}/worker/${remoteWorkerId}/cancel`, {
12103
+ method: "POST",
12104
+ headers: apiKey ? { "X-Worker-Api-Key": apiKey } : {}
12105
+ });
12106
+ } catch {
12107
+ }
12108
+ throw new Error("Cancelled by user");
12109
+ }
12096
12110
  await new Promise((r) => setTimeout(r, pollInterval));
12097
12111
  let progressRes;
12098
12112
  let lastPollErr;
@@ -12125,8 +12139,14 @@ var init_supervisor = __esm({
12125
12139
  for (const logLine of newLogs) {
12126
12140
  worker.logs.push(`[worker] ${logLine}`);
12127
12141
  }
12128
- worker.logs.push(`[coordinator] Step ${progress.stepIndex}/${progress.totalSteps}: ${progress.message}`);
12129
- onUpdate?.([...activeWorkers.values()]);
12142
+ const stepKey = `${progress.stepIndex}:${progress.step}`;
12143
+ if (stepKey !== lastStep) {
12144
+ lastStep = stepKey;
12145
+ worker.logs.push(`[coordinator] Step ${progress.stepIndex}/${progress.totalSteps}: ${progress.message}`);
12146
+ onUpdate?.([...activeWorkers.values()]);
12147
+ } else if (newLogs.length > 0) {
12148
+ onUpdate?.([...activeWorkers.values()]);
12149
+ }
12130
12150
  if (progress.status === "completed" || progress.status === "failed") {
12131
12151
  if (progress.result) {
12132
12152
  data = progress.result;
@@ -12272,11 +12292,16 @@ var init_supervisor = __esm({
12272
12292
  *
12273
12293
  * Returns the synthesized plan after all workers complete.
12274
12294
  */
12275
- async autoSpawnWorkers(prompt, context, onUpdate, onPhaseChange) {
12295
+ async autoSpawnWorkers(prompt, context, onUpdate, onPhaseChange, signal) {
12296
+ if (this._activeWorkers.size > 0) {
12297
+ throw new Error(
12298
+ `Multi-agent already active (${this._activeWorkers.size} worker(s) in flight). Wait for completion or cancel before starting a new heavy task.`
12299
+ );
12300
+ }
12276
12301
  const workers = decomposePrompt(prompt, context);
12277
12302
  onPhaseChange?.("spawning");
12278
12303
  try {
12279
- const results = await this.spawnWorkers(workers, onUpdate);
12304
+ const results = await this.spawnWorkers(workers, onUpdate, signal);
12280
12305
  onPhaseChange?.("synthesizing");
12281
12306
  const synth = this.synthesizeFindings(results);
12282
12307
  const cfg = await loadConfig().catch(() => null);
@@ -12294,7 +12319,8 @@ var init_supervisor = __esm({
12294
12319
  try {
12295
12320
  const execResults = await this.spawnWorkers(
12296
12321
  [{ mode: "execute", task: executeTask, context }],
12297
- onUpdate
12322
+ onUpdate,
12323
+ signal
12298
12324
  );
12299
12325
  const exec2 = execResults[0];
12300
12326
  if (!exec2) {
@@ -12719,6 +12745,70 @@ import { mkdtemp, readFile as readFile13, writeFile as writeFile10, rm } from "f
12719
12745
  import { tmpdir as tmpdir3 } from "os";
12720
12746
  import { join as join22 } from "path";
12721
12747
  import { randomBytes as randomBytes2 } from "crypto";
12748
+ async function cfApiFetch(accountId, apiToken, path, init) {
12749
+ const url = `${CF_API}/accounts/${encodeURIComponent(accountId)}${path}`;
12750
+ const res = await fetch(url, {
12751
+ ...init,
12752
+ headers: {
12753
+ Authorization: `Bearer ${apiToken}`,
12754
+ "Content-Type": "application/json",
12755
+ "User-Agent": getUserAgent(),
12756
+ ...init?.headers
12757
+ }
12758
+ });
12759
+ const json = await res.json();
12760
+ return json;
12761
+ }
12762
+ async function listDurableObjectNamespaces(accountId, apiToken) {
12763
+ const json = await cfApiFetch(
12764
+ accountId,
12765
+ apiToken,
12766
+ "/workers/durable_objects/namespaces"
12767
+ );
12768
+ if (!json.success || !json.result) {
12769
+ throw new Error(
12770
+ json.errors?.map((e) => e.message).join(", ") ?? "Failed to list DO namespaces"
12771
+ );
12772
+ }
12773
+ return json.result;
12774
+ }
12775
+ async function deleteDurableObjectNamespace(accountId, apiToken, namespaceId) {
12776
+ const json = await cfApiFetch(
12777
+ accountId,
12778
+ apiToken,
12779
+ `/workers/durable_objects/namespaces/${encodeURIComponent(namespaceId)}`,
12780
+ { method: "DELETE" }
12781
+ );
12782
+ if (!json.success) {
12783
+ throw new Error(
12784
+ json.errors?.map((e) => e.message).join(", ") ?? "Failed to delete DO namespace"
12785
+ );
12786
+ }
12787
+ }
12788
+ async function listContainerApplications(accountId, apiToken) {
12789
+ const json = await cfApiFetch(
12790
+ accountId,
12791
+ apiToken,
12792
+ "/containers/applications"
12793
+ );
12794
+ if (!json.success || !json.result) {
12795
+ return [];
12796
+ }
12797
+ return json.result;
12798
+ }
12799
+ async function deleteContainerApplication(accountId, apiToken, appId) {
12800
+ const json = await cfApiFetch(
12801
+ accountId,
12802
+ apiToken,
12803
+ `/containers/applications/${encodeURIComponent(appId)}`,
12804
+ { method: "DELETE" }
12805
+ );
12806
+ if (!json.success) {
12807
+ throw new Error(
12808
+ json.errors?.map((e) => e.message).join(", ") ?? "Failed to delete container application"
12809
+ );
12810
+ }
12811
+ }
12722
12812
  function generateSecret2() {
12723
12813
  return randomBytes2(32).toString("hex");
12724
12814
  }
@@ -12991,21 +13081,23 @@ ${(install.stderr || install.stdout).slice(-1200).trim()}`,
12991
13081
  `$1${finalKvId}$2`
12992
13082
  );
12993
13083
  toml = toml.replace(/\n\[\[artifacts\]\][\s\S]*?(?=\n\[|\n*$)/g, "\n");
12994
- const existingMigrations = toml.match(/\[\[migrations\]\]/);
12995
- if (existingMigrations) {
12996
- toml = toml.replace(
12997
- /\[\[migrations\]\][\s\S]*?new_sqlite_classes\s*=\s*\[[^\]]*\]/,
12998
- `[[migrations]]
12999
- tag = "v2"
13084
+ if (!workerExists) {
13085
+ const existingMigrations = toml.match(/\[\[migrations\]\]/);
13086
+ if (existingMigrations) {
13087
+ toml = toml.replace(
13088
+ /\[\[migrations\]\][\s\S]*?new_sqlite_classes\s*=\s*\[[^\]]*\]/,
13089
+ `[[migrations]]
13090
+ tag = "v1"
13000
13091
  new_sqlite_classes = ["SessionDO", "WorkerDO", "Sandbox"]`
13001
- );
13002
- } else {
13003
- toml += `
13092
+ );
13093
+ } else {
13094
+ toml += `
13004
13095
  # Auto-added by kimiflare /multi-agent \u2192 Set up (fresh deploy only)
13005
13096
  [[migrations]]
13006
13097
  tag = "v1"
13007
13098
  new_sqlite_classes = ["SessionDO", "WorkerDO", "Sandbox"]
13008
13099
  `;
13100
+ }
13009
13101
  }
13010
13102
  if (!/\[observability\.logs\]/.test(toml)) {
13011
13103
  toml += `
@@ -13061,6 +13153,7 @@ invocation_logs = true
13061
13153
  ...cfg,
13062
13154
  workerEndpoint: workerUrl,
13063
13155
  workerApiKey,
13156
+ workerName,
13064
13157
  multiAgentEnabled: true
13065
13158
  };
13066
13159
  await saveConfig(next);
@@ -13070,12 +13163,13 @@ invocation_logs = true
13070
13163
  yield { message: "Setup complete \u2014 multi-agent is ready to use.", done: true };
13071
13164
  return { workerEndpoint: workerUrl, workerApiKey };
13072
13165
  }
13073
- async function* teardownCommute() {
13166
+ async function* teardownCommute(opts2 = {}) {
13074
13167
  const cfg = await loadConfig();
13075
13168
  if (!cfg?.accountId || !cfg?.apiToken) {
13076
13169
  yield { message: "Cloudflare credentials missing \u2014 nothing to tear down.", error: true };
13077
13170
  throw new Error("missing CF creds");
13078
13171
  }
13172
+ const workerName = opts2.workerName ?? cfg.workerName ?? WORKER_NAME;
13079
13173
  const cfEnv = {
13080
13174
  CLOUDFLARE_ACCOUNT_ID: cfg.accountId,
13081
13175
  CLOUDFLARE_API_TOKEN: cfg.apiToken
@@ -13084,25 +13178,72 @@ async function* teardownCommute() {
13084
13178
  yield { message: "wrangler not found. Install: npm install -g wrangler", error: true };
13085
13179
  throw new Error("wrangler missing");
13086
13180
  }
13087
- yield { message: `Deleting Worker "${WORKER_NAME}"\u2026` };
13088
- const del = await runCmd("wrangler", ["delete", "--name", WORKER_NAME], {
13181
+ yield { message: `Deleting Worker "${workerName}"\u2026` };
13182
+ const del = await runCmd("wrangler", ["delete", "--name", workerName], {
13089
13183
  env: cfEnv,
13090
13184
  input: "y\n",
13091
13185
  timeoutMs: 6e4
13092
13186
  });
13093
13187
  if (del.code === 0) {
13094
- yield { message: `Worker "${WORKER_NAME}" deleted`, ok: true };
13188
+ yield { message: `Worker "${workerName}" deleted`, ok: true };
13095
13189
  } else {
13096
13190
  const combined = (del.stdout + del.stderr).toLowerCase();
13097
13191
  if (combined.includes("not found") || combined.includes("does not exist") || combined.includes("10007")) {
13098
13192
  yield { message: "Worker not found (already deleted or never created)", ok: true };
13099
13193
  } else {
13100
13194
  yield {
13101
- message: explainWranglerFailure(`wrangler delete --name ${WORKER_NAME}`, del.stdout, del.stderr),
13195
+ message: explainWranglerFailure(`wrangler delete --name ${workerName}`, del.stdout, del.stderr),
13102
13196
  error: true
13103
13197
  };
13104
13198
  }
13105
13199
  }
13200
+ yield { message: `Looking up Durable Object namespaces for "${workerName}"\u2026` };
13201
+ try {
13202
+ const namespaces = await listDurableObjectNamespaces(cfg.accountId, cfg.apiToken);
13203
+ const targets = namespaces.filter(
13204
+ (ns) => ns.script === workerName || ns.name.startsWith(`${workerName}_`)
13205
+ );
13206
+ if (targets.length === 0) {
13207
+ yield { message: "No Durable Object namespaces found (nothing to delete)", ok: true };
13208
+ } else {
13209
+ for (const ns of targets) {
13210
+ try {
13211
+ await deleteDurableObjectNamespace(cfg.accountId, cfg.apiToken, ns.id);
13212
+ yield { message: `DO namespace ${ns.name} deleted (${ns.id.slice(0, 8)}\u2026)`, ok: true };
13213
+ } catch (err) {
13214
+ yield {
13215
+ message: `DO namespace ${ns.name} delete warning: ${err instanceof Error ? err.message : String(err)}`
13216
+ };
13217
+ }
13218
+ }
13219
+ }
13220
+ } catch (err) {
13221
+ yield {
13222
+ message: `DO namespace lookup warning: ${err instanceof Error ? err.message : String(err)}`
13223
+ };
13224
+ }
13225
+ yield { message: `Looking up container applications for "${workerName}"\u2026` };
13226
+ try {
13227
+ const apps = await listContainerApplications(cfg.accountId, cfg.apiToken);
13228
+ const appPrefix = `${workerName}-`.toLowerCase();
13229
+ const targets = apps.filter((app) => app.name.toLowerCase().startsWith(appPrefix));
13230
+ if (targets.length === 0) {
13231
+ yield { message: "No container applications found (nothing to delete)", ok: true };
13232
+ } else {
13233
+ for (const app of targets) {
13234
+ try {
13235
+ await deleteContainerApplication(cfg.accountId, cfg.apiToken, app.id);
13236
+ yield { message: `Container application ${app.name} deleted (${app.id.slice(0, 8)}\u2026)`, ok: true };
13237
+ } catch (err) {
13238
+ yield {
13239
+ message: `Container application ${app.name} delete warning: ${err instanceof Error ? err.message : String(err)}`
13240
+ };
13241
+ }
13242
+ }
13243
+ }
13244
+ } catch {
13245
+ yield { message: "(could not list container applications \u2014 skipping container cleanup)" };
13246
+ }
13106
13247
  yield { message: "Listing KV namespaces to find OAUTH_KV\u2026" };
13107
13248
  const kvList = await runCmd("wrangler", ["kv", "namespace", "list"], { env: cfEnv, timeoutMs: 3e4 });
13108
13249
  if (kvList.code === 0) {
@@ -13136,6 +13277,7 @@ async function* teardownCommute() {
13136
13277
  ...cfg,
13137
13278
  workerEndpoint: void 0,
13138
13279
  workerApiKey: void 0,
13280
+ workerName: void 0,
13139
13281
  multiAgentEnabled: false,
13140
13282
  autoExecute: false
13141
13283
  };
@@ -13143,15 +13285,17 @@ async function* teardownCommute() {
13143
13285
  yield { message: "Local multi-agent config cleared", ok: true };
13144
13286
  yield { message: "Tear-down complete \u2014 multi-agent is fully removed.", done: true };
13145
13287
  }
13146
- var COMMUTE_REPO, COMMUTE_BRANCH, WORKER_NAME, KV_TITLE, TOKEN_TEMPLATE_URL;
13288
+ var COMMUTE_REPO, COMMUTE_BRANCH, WORKER_NAME, KV_TITLE, CF_API, TOKEN_TEMPLATE_URL;
13147
13289
  var init_deploy_commute = __esm({
13148
13290
  "src/remote/deploy-commute.ts"() {
13149
13291
  "use strict";
13150
13292
  init_config();
13293
+ init_version();
13151
13294
  COMMUTE_REPO = "https://github.com/sinameraji/kimiflare-commute.git";
13152
13295
  COMMUTE_BRANCH = "main";
13153
13296
  WORKER_NAME = "kimiflare-multi-agent";
13154
13297
  KV_TITLE = "kimiflare-multi-agent-OAUTH_KV";
13298
+ CF_API = "https://api.cloudflare.com/client/v4";
13155
13299
  TOKEN_TEMPLATE_URL = "https://dash.cloudflare.com/profile/api-tokens";
13156
13300
  }
13157
13301
  });
@@ -15588,7 +15732,7 @@ ${err instanceof Error ? err.message : err}`);
15588
15732
  cam.send("AssistantStreamStarted", { stream_id: sid });
15589
15733
  cam.send("AssistantTokenDelta", { stream_id: sid, token: "# Tearing down multi-agent\n\n" });
15590
15734
  try {
15591
- for await (const step of teardownCommute()) {
15735
+ for await (const step of teardownCommute({ workerName: cfg.workerName })) {
15592
15736
  const prefix = step.error ? "\u2717 " : step.done || step.ok ? "\u2713 " : "\xB7 ";
15593
15737
  cam.send("AssistantTokenDelta", { stream_id: sid, token: `${prefix}${step.message}
15594
15738
  ` });
@@ -21222,14 +21366,14 @@ async function cfFetch(url, init) {
21222
21366
  }
21223
21367
  async function ensureStore(accountId, apiToken) {
21224
21368
  const listed = await cfFetch(
21225
- `${CF_API}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores`,
21369
+ `${CF_API2}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores`,
21226
21370
  { method: "GET", apiToken }
21227
21371
  );
21228
21372
  if (!listed.ok) return listed;
21229
21373
  const existing = listed.value.find((s) => s.name === STORE_NAME);
21230
21374
  if (existing) return { ok: true, value: existing.id };
21231
21375
  const created = await cfFetch(
21232
- `${CF_API}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores`,
21376
+ `${CF_API2}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores`,
21233
21377
  { method: "POST", apiToken, body: { name: STORE_NAME } }
21234
21378
  );
21235
21379
  if (!created.ok) return created;
@@ -21237,7 +21381,7 @@ async function ensureStore(accountId, apiToken) {
21237
21381
  }
21238
21382
  async function pushProviderKey(accountId, apiToken, storeId, name, value, comment = "kimi-code BYOK") {
21239
21383
  const res = await cfFetch(
21240
- `${CF_API}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores/${encodeURIComponent(
21384
+ `${CF_API2}/accounts/${encodeURIComponent(accountId)}/secrets_store/stores/${encodeURIComponent(
21241
21385
  storeId
21242
21386
  )}/secrets`,
21243
21387
  {
@@ -21256,12 +21400,12 @@ async function pushProviderKey(accountId, apiToken, storeId, name, value, commen
21256
21400
  function aliasFor(provider) {
21257
21401
  return `kimi-code-${provider}`;
21258
21402
  }
21259
- var CF_API, STORE_NAME;
21403
+ var CF_API2, STORE_NAME;
21260
21404
  var init_secrets_store = __esm({
21261
21405
  "src/agent/secrets-store.ts"() {
21262
21406
  "use strict";
21263
21407
  init_version();
21264
- CF_API = "https://api.cloudflare.com/client/v4";
21408
+ CF_API2 = "https://api.cloudflare.com/client/v4";
21265
21409
  STORE_NAME = "kimi-code";
21266
21410
  }
21267
21411
  });
@@ -25241,13 +25385,14 @@ function MultiAgentModal({ initial, onSave, onDone, remoteWorkerUrl, remoteAuthS
25241
25385
  setDeploying(true);
25242
25386
  setDeployLog(["Starting tear-down\u2026"]);
25243
25387
  try {
25244
- for await (const step of teardownCommute()) {
25388
+ for await (const step of teardownCommute({ workerName: state.workerName })) {
25245
25389
  const prefix = step.error ? "\u2717 " : step.done || step.ok ? "\u2713 " : "\xB7 ";
25246
25390
  setDeployLog((l) => [...l, `${prefix}${step.message}`]);
25247
25391
  if (step.done) {
25248
25392
  persist({
25249
25393
  workerEndpoint: void 0,
25250
25394
  workerApiKey: void 0,
25395
+ workerName: void 0,
25251
25396
  multiAgentEnabled: false,
25252
25397
  autoExecute: false
25253
25398
  });
@@ -25259,7 +25404,7 @@ function MultiAgentModal({ initial, onSave, onDone, remoteWorkerUrl, remoteAuthS
25259
25404
  } finally {
25260
25405
  setDeploying(false);
25261
25406
  }
25262
- }, [persist]);
25407
+ }, [persist, state.workerName]);
25263
25408
  const beginEdit = useCallback7((f) => {
25264
25409
  if (f === "deploy") {
25265
25410
  void runDeploy2();
@@ -29851,6 +29996,7 @@ ${wcagWarnings.join("\n")}` }
29851
29996
  const mcpInitRef = useRef7(false);
29852
29997
  const submitRef = useRef7(() => {
29853
29998
  });
29999
+ const multiAgentAbortRef = useRef7(null);
29854
30000
  const lspManagerRef = useRef7(new LspManager());
29855
30001
  const lspToolsRef = useRef7([]);
29856
30002
  const lspInitRef = useRef7(false);
@@ -30234,6 +30380,11 @@ ${wcagWarnings.join("\n")}` }
30234
30380
  hasPerm: hasPendingPermission(),
30235
30381
  hasLimit: limitResolveRef.current !== null
30236
30382
  });
30383
+ if (multiAgentAbortRef.current) {
30384
+ multiAgentAbortRef.current.abort();
30385
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "cancelling multi-agent workers\u2026" }]);
30386
+ return;
30387
+ }
30237
30388
  const outcome = interruptOrExit(interruptDepsRef.current);
30238
30389
  if (!outcome.didInterruptTurn && !outcome.hadPermission && !outcome.hadLimit && !outcome.hadLoop) {
30239
30390
  logger.info("input:ctrl+c:exiting");
@@ -30243,10 +30394,18 @@ ${wcagWarnings.join("\n")}` }
30243
30394
  if (key.escape) {
30244
30395
  const now2 = Date.now();
30245
30396
  const modalOpen = perm !== null || limitModal !== null || loopModal !== null || showLspWizard || showCommandList || commandWizard !== null || commandToDelete !== null || resumeSessions !== null || checkpointSession !== null || showThemePicker;
30246
- if (!modalOpen && (busyRef.current || supervisorRef.current.isRunning) && activeScopeRef.current && !isAbortingRef.current && now2 - lastEscapeAtRef.current > 500) {
30247
- lastEscapeAtRef.current = now2;
30248
- interruptTurn(interruptDepsRef.current);
30249
- return;
30397
+ if (!modalOpen && !isAbortingRef.current && now2 - lastEscapeAtRef.current > 500) {
30398
+ if (multiAgentAbortRef.current) {
30399
+ lastEscapeAtRef.current = now2;
30400
+ multiAgentAbortRef.current.abort();
30401
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "cancelling multi-agent workers\u2026" }]);
30402
+ return;
30403
+ }
30404
+ if ((busyRef.current || supervisorRef.current.isRunning) && activeScopeRef.current) {
30405
+ lastEscapeAtRef.current = now2;
30406
+ interruptTurn(interruptDepsRef.current);
30407
+ return;
30408
+ }
30250
30409
  }
30251
30410
  }
30252
30411
  if (key.ctrl && inputChar === "r") {
@@ -30277,6 +30436,11 @@ ${wcagWarnings.join("\n")}` }
30277
30436
  hasLimit: limitResolveRef.current !== null,
30278
30437
  hasLoop: loopResolveRef.current !== null
30279
30438
  });
30439
+ if (multiAgentAbortRef.current) {
30440
+ multiAgentAbortRef.current.abort();
30441
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "cancelling multi-agent workers\u2026" }]);
30442
+ return;
30443
+ }
30280
30444
  const outcome = interruptOrExit({
30281
30445
  ...interruptDepsRef.current,
30282
30446
  skipPendingToolCleanup: true
@@ -30928,17 +31092,25 @@ ${wcagWarnings.join("\n")}` }
30928
31092
  ...e,
30929
31093
  { kind: "info", key: mkKey(), text: `multi-agent mode active, but task is ${classification.tier} \u2014 running locally` }
30930
31094
  ]);
31095
+ } else if (activeWorkers.length > 0) {
31096
+ setEvents((e) => [
31097
+ ...e,
31098
+ { kind: "info", key: mkKey(), text: `multi-agent workers already active (${activeWorkers.length}) \u2014 routing to coordinator` }
31099
+ ]);
30931
31100
  } else {
30932
31101
  setEvents((e) => [
30933
31102
  ...e,
30934
31103
  { kind: "info", key: mkKey(), text: "multi-agent mode: spawning parallel research workers..." }
30935
31104
  ]);
31105
+ const controller = new AbortController();
31106
+ multiAgentAbortRef.current = controller;
30936
31107
  try {
30937
31108
  const { plan, conflicts, recommendations, prUrl, executor } = await supervisorRef.current.autoSpawnWorkers(
30938
31109
  trimmed,
30939
31110
  `Current project: ${process.cwd()}`,
30940
31111
  (workers) => setActiveWorkers(workers),
30941
- (phase) => setIsSynthesizing(phase === "synthesizing")
31112
+ (phase) => setIsSynthesizing(phase === "synthesizing"),
31113
+ controller.signal
30942
31114
  );
30943
31115
  setEvents((e) => [
30944
31116
  ...e,
@@ -30968,12 +31140,21 @@ ${conflicts.join("\n")}` }
30968
31140
  } catch (e) {
30969
31141
  setIsSynthesizing(false);
30970
31142
  const err = e;
30971
- setEvents((e2) => [
30972
- ...e2,
30973
- { kind: "error", key: mkKey(), text: `multi-agent spawn failed: ${err.message}` }
30974
- ]);
31143
+ if (err.message === "Cancelled by user") {
31144
+ setEvents((e2) => [
31145
+ ...e2,
31146
+ { kind: "info", key: mkKey(), text: "multi-agent cancelled" }
31147
+ ]);
31148
+ } else {
31149
+ setEvents((e2) => [
31150
+ ...e2,
31151
+ { kind: "error", key: mkKey(), text: `multi-agent spawn failed: ${err.message}` }
31152
+ ]);
31153
+ }
30975
31154
  endTurn();
30976
31155
  return;
31156
+ } finally {
31157
+ multiAgentAbortRef.current = null;
30977
31158
  }
30978
31159
  }
30979
31160
  }
@@ -31465,6 +31646,7 @@ ${conflicts.join("\n")}` }
31465
31646
  multiAgentEnabled: cfg.multiAgentEnabled,
31466
31647
  workerEndpoint: cfg.workerEndpoint,
31467
31648
  workerApiKey: cfg.workerApiKey,
31649
+ workerName: cfg.workerName,
31468
31650
  autoExecute: cfg.autoExecute
31469
31651
  } : void 0,
31470
31652
  onMultiAgentSave: async (patch) => {