@taskless-app/swarm 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -371,14 +371,20 @@ var init_slug = __esm({
371
371
  function sanitizeEventText(text) {
372
372
  if (text == null) return null;
373
373
  const cleaned = text.replace(ANSI_CSI_RE, "").replace(CONTROL_CHARS_RE, " ").replace(/ {2,}/g, " ").trim();
374
- return cleaned.length ? cleaned : null;
374
+ if (!cleaned.length) return null;
375
+ return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
375
376
  }
376
- var ANSI_CSI_RE, CONTROL_CHARS_RE;
377
+ function clampEventText(text) {
378
+ const note = ` \u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u0431\u044B\u043B\u043E ${text.length} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432)`;
379
+ return text.slice(0, Math.max(0, EVENT_TEXT_MAX - note.length)) + note;
380
+ }
381
+ var ANSI_CSI_RE, CONTROL_CHARS_RE, EVENT_TEXT_MAX;
377
382
  var init_text = __esm({
378
383
  "../swarm-core/src/text.ts"() {
379
384
  "use strict";
380
385
  ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
381
386
  CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
387
+ EVENT_TEXT_MAX = 4e3;
382
388
  }
383
389
  });
384
390
 
@@ -557,6 +563,19 @@ var init_hosts = __esm({
557
563
  }
558
564
  });
559
565
 
566
+ // ../swarm-core/src/hostCommands.ts
567
+ var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS;
568
+ var init_hostCommands = __esm({
569
+ "../swarm-core/src/hostCommands.ts"() {
570
+ "use strict";
571
+ HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
572
+ TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
573
+ HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
574
+ HOST_COMMAND_TTL_MS = 30 * 6e4;
575
+ HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
576
+ }
577
+ });
578
+
560
579
  // ../swarm-core/src/relief.ts
561
580
  var init_relief = __esm({
562
581
  "../swarm-core/src/relief.ts"() {
@@ -581,6 +600,7 @@ var init_src = __esm({
581
600
  init_names();
582
601
  init_baseBranch();
583
602
  init_hosts();
603
+ init_hostCommands();
584
604
  init_relief();
585
605
  }
586
606
  });
@@ -1784,6 +1804,11 @@ function runRef(runKey) {
1784
1804
  function isRunRef(v) {
1785
1805
  return typeof v === "object" && v !== null && "__runRef" in v;
1786
1806
  }
1807
+ function isHostCommandShape(v) {
1808
+ const c = v;
1809
+ return !!c && typeof c === "object" && typeof c.id === "string" && !!c.id && typeof c.kind === "string";
1810
+ }
1811
+ var EMPTY_HEARTBEAT_REPLY = { commands: [], logRequests: [] };
1787
1812
  function jsonFromTextContent(res) {
1788
1813
  const text = res?.content?.find((c) => c.type === "text")?.text;
1789
1814
  if (text) {
@@ -2955,17 +2980,24 @@ var SwarmClient = class _SwarmClient {
2955
2980
  * прислал предыдущий (более свежий по коду) процесс демона.
2956
2981
  */
2957
2982
  /**
2958
- * TASWM-322: ответ на пульс — ЕДИНСТВЕННЫЙ канал «прод → демон» (демон за домашним
2959
- * роутером, прод к нему не постучится). Оттуда приезжают команды машине — сейчас одна:
2960
- * «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
2961
- * прод старый (поля нет) → пустой список, всё как раньше.
2962
- */
2963
- /**
2983
+ * TASWM-321: ВОЗВРАЩАЕТ команды этой машине (пауза/рестарт/обновление/…), приехавшие
2984
+ * в ответе на пульс. Это единственный момент, когда прод может что-то сказать демону:
2985
+ * тот живёт за домашним роутером без белого адреса, и связь всегда инициирует он сам.
2986
+ * Прод, отдав команды, метит их доставленными — повторный пульс их не вернёт, поэтому
2987
+ * потерять ответ здесь значит потерять команду; парсим бережно и не бросаем.
2988
+ *
2989
+ * TASWM-322: тем же ответом приезжают запросы «пришли хвост лога» (logRequests) —
2990
+ * канал «прод → демон» один на оба вида поручений, поэтому и разбираем их вместе.
2991
+ * Возвращаем ОБА списка объектом: два разных потребителя в overmind, и слепить их в
2992
+ * один список значило бы заставить каждого фильтровать чужое.
2993
+ *
2964
2994
  * TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
2965
2995
  * pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
2966
2996
  * и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
2967
2997
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
2968
2998
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
2999
+ *
3000
+ * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
2969
3001
  */
2970
3002
  async heartbeat(daemonVersion, pause) {
2971
3003
  try {
@@ -2980,9 +3012,33 @@ var SwarmClient = class _SwarmClient {
2980
3012
  args.hostName = this.host.hostName;
2981
3013
  }
2982
3014
  const res = await this.call("swarm_heartbeat", args);
2983
- return parseLogRequests(res?.structuredContent?.logRequests);
3015
+ if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
3016
+ const data = res?.structuredContent ?? {};
3017
+ return {
3018
+ commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
3019
+ logRequests: parseLogRequests(data.logRequests)
3020
+ };
3021
+ } catch {
3022
+ return EMPTY_HEARTBEAT_REPLY;
3023
+ }
3024
+ }
3025
+ /**
3026
+ * TASWM-321: подтвердить исполнение команды (`swarm_command_ack`). ПРЯМОЙ вызов, не
3027
+ * outbox: подтверждение — «скажи СЕЙЧАС», ради него команду и заводили («без
3028
+ * подтверждения непонятно, обновился он или команда потерялась по дороге»), а
3029
+ * промежуточные шаги обновления, доехавшие после самого обновления, бессмысленны.
3030
+ * Не бросает: сорвавшийся ack не должен рушить тик демона — команда останется в
3031
+ * прошлом состоянии, следующий шаг/финал её всё равно подвинут.
3032
+ */
3033
+ async ackCommand(commandId, status, patch = {}) {
3034
+ try {
3035
+ await this.call("swarm_command_ack", {
3036
+ commandId,
3037
+ status,
3038
+ ...patch.step ? { step: patch.step } : {},
3039
+ ...patch.result ? { result: patch.result.slice(0, 2e3) } : {}
3040
+ });
2984
3041
  } catch {
2985
- return [];
2986
3042
  }
2987
3043
  }
2988
3044
  /**
@@ -4117,7 +4173,7 @@ async function collectRunDiff(worktree, opts = {}) {
4117
4173
  // package.json
4118
4174
  var package_default = {
4119
4175
  name: "@taskless-app/swarm",
4120
- version: "0.4.9",
4176
+ version: "0.4.10",
4121
4177
  type: "module",
4122
4178
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
4123
4179
  keywords: [
package/dist/overmind.js CHANGED
@@ -419,14 +419,20 @@ function extractText(d) {
419
419
  function sanitizeEventText(text) {
420
420
  if (text == null) return null;
421
421
  const cleaned = text.replace(ANSI_CSI_RE, "").replace(CONTROL_CHARS_RE, " ").replace(/ {2,}/g, " ").trim();
422
- return cleaned.length ? cleaned : null;
422
+ if (!cleaned.length) return null;
423
+ return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
423
424
  }
424
- var ANSI_CSI_RE, CONTROL_CHARS_RE;
425
+ function clampEventText(text) {
426
+ const note = ` \u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u0431\u044B\u043B\u043E ${text.length} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432)`;
427
+ return text.slice(0, Math.max(0, EVENT_TEXT_MAX - note.length)) + note;
428
+ }
429
+ var ANSI_CSI_RE, CONTROL_CHARS_RE, EVENT_TEXT_MAX;
425
430
  var init_text = __esm({
426
431
  "../swarm-core/src/text.ts"() {
427
432
  "use strict";
428
433
  ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
429
434
  CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
435
+ EVENT_TEXT_MAX = 4e3;
430
436
  }
431
437
  });
432
438
 
@@ -709,6 +715,50 @@ var init_hosts = __esm({
709
715
  }
710
716
  });
711
717
 
718
+ // ../swarm-core/src/hostCommands.ts
719
+ function isHostCommandKind(v) {
720
+ return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
721
+ }
722
+ function isHostCommandExpired(expiresAt, nowMs) {
723
+ if (!expiresAt) return false;
724
+ const at = Date.parse(expiresAt);
725
+ return Number.isFinite(at) && at <= nowMs;
726
+ }
727
+ function hostCommandLabel(cmd) {
728
+ const base3 = HOST_COMMAND_LABELS[cmd.kind];
729
+ if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
730
+ if (cmd.kind === "stop_run" && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
731
+ return base3;
732
+ }
733
+ var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, MAX_DRONES_LIMIT;
734
+ var init_hostCommands = __esm({
735
+ "../swarm-core/src/hostCommands.ts"() {
736
+ "use strict";
737
+ HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run"];
738
+ HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
739
+ TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
740
+ HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
741
+ UPDATE_STEP_LABELS = {
742
+ fetch: "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434",
743
+ checkout: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C \u043D\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 main",
744
+ install: "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438",
745
+ build: "\u0441\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u043A\u0435\u0442\u044B \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430",
746
+ restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u044E\u0441\u044C"
747
+ };
748
+ HOST_COMMAND_TTL_MS = 30 * 6e4;
749
+ HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
750
+ HOST_COMMAND_LABELS = {
751
+ pause: "\u043F\u0430\u0443\u0437\u0430",
752
+ resume: "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
753
+ restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
754
+ update: "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043A\u043E\u0434\u0430",
755
+ set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
756
+ stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"
757
+ };
758
+ MAX_DRONES_LIMIT = 8;
759
+ }
760
+ });
761
+
712
762
  // ../swarm-core/src/relief.ts
713
763
  var RELIEF_PAUSE_MARK;
714
764
  var init_relief = __esm({
@@ -735,6 +785,7 @@ var init_src = __esm({
735
785
  init_names();
736
786
  init_baseBranch();
737
787
  init_hosts();
788
+ init_hostCommands();
738
789
  init_relief();
739
790
  }
740
791
  });
@@ -1498,8 +1549,8 @@ function pathUnderInCmd(cmd, dir) {
1498
1549
  if (before && !/["'\s=;]/.test(before)) continue;
1499
1550
  const after = hay.slice(at + needle.length);
1500
1551
  if (after && !after.startsWith("/")) continue;
1501
- const tail = after.match(/^(?:\/[^"';]*)?/)[0];
1502
- return needle + tail;
1552
+ const tail2 = after.match(/^(?:\/[^"';]*)?/)[0];
1553
+ return needle + tail2;
1503
1554
  }
1504
1555
  return null;
1505
1556
  }
@@ -2464,8 +2515,8 @@ function pathOf(url) {
2464
2515
  return url.split(/[?#]/)[0] ?? "";
2465
2516
  }
2466
2517
  function extOf(nameOrPath) {
2467
- const tail = nameOrPath.split("/").pop() ?? "";
2468
- const m = /\.([A-Za-z0-9]{1,8})$/.exec(tail);
2518
+ const tail2 = nameOrPath.split("/").pop() ?? "";
2519
+ const m = /\.([A-Za-z0-9]{1,8})$/.exec(tail2);
2469
2520
  return m ? m[1].toLowerCase() : "";
2470
2521
  }
2471
2522
  function mb(bytes) {
@@ -3051,14 +3102,14 @@ async function fetchTaskContext(client, task) {
3051
3102
  const parts = [];
3052
3103
  const comments = task.comments ?? [];
3053
3104
  if (comments.length > 0) {
3054
- const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
3055
- const skipped = comments.length - tail.length;
3056
- const lines = tail.map((c) => {
3105
+ const tail2 = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
3106
+ const skipped = comments.length - tail2.length;
3107
+ const lines = tail2.map((c) => {
3057
3108
  const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
3058
3109
  const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
3059
3110
  return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
3060
3111
  });
3061
- const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
3112
+ const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail2.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
3062
3113
  parts.push([header, ...lines].join("\n"));
3063
3114
  }
3064
3115
  const documents = task.documents ?? [];
@@ -4139,8 +4190,8 @@ var init_droneStore = __esm({
4139
4190
 
4140
4191
  // src/overmind.ts
4141
4192
  import { homedir as homedir3 } from "os";
4142
- import { existsSync as existsSync12, readFileSync as readFileSync17, readdirSync as readdirSync10, renameSync as renameSync7 } from "fs";
4143
- import { basename as basename4, dirname as dirname5, join as join21 } from "path";
4193
+ import { existsSync as existsSync13, readFileSync as readFileSync18, readdirSync as readdirSync10, renameSync as renameSync8 } from "fs";
4194
+ import { basename as basename4, dirname as dirname5, join as join23 } from "path";
4144
4195
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4145
4196
  import { spawn as spawn3 } from "child_process";
4146
4197
 
@@ -4291,25 +4342,25 @@ function runLogTail(runLogsDir, localRunId, opts) {
4291
4342
  const parts = [];
4292
4343
  let budget = opts.maxBytes;
4293
4344
  if (launch.err) {
4294
- const tail = readTail(launch.err, { lines: opts.lines, maxBytes: Math.floor(opts.maxBytes / 2) });
4295
- if (tail && tail.text.trim() !== "") {
4296
- const block = `${fileHeader(`${launch.index}.err.log`, tail.size, tail.shown)}
4297
- ${tail.text}`;
4345
+ const tail2 = readTail(launch.err, { lines: opts.lines, maxBytes: Math.floor(opts.maxBytes / 2) });
4346
+ if (tail2 && tail2.text.trim() !== "") {
4347
+ const block = `${fileHeader(`${launch.index}.err.log`, tail2.size, tail2.shown)}
4348
+ ${tail2.text}`;
4298
4349
  parts.push(block);
4299
4350
  budget -= Buffer.byteLength(block, "utf8");
4300
4351
  }
4301
4352
  }
4302
4353
  if (launch.out) {
4303
- const tail = readTail(launch.out, { lines: opts.lines, maxBytes: Math.max(1024, budget) });
4304
- if (tail) {
4354
+ const tail2 = readTail(launch.out, { lines: opts.lines, maxBytes: Math.max(1024, budget) });
4355
+ if (tail2) {
4305
4356
  parts.push(
4306
- tail.size === 0 ? (
4357
+ tail2.size === 0 ? (
4307
4358
  // Пустой stdout при живом дроне — норма, а не поломка: claude в headless-режиме
4308
4359
  // (`--output-format json`) печатает результат ОДНИМ объектом в самом конце.
4309
4360
  `=== ${launch.index}.json (\u043F\u0443\u0441\u0442) ===
4310
4361
  Stdout \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D: claude \u043E\u0442\u0434\u0430\u0451\u0442 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u043E\u0434\u043D\u0438\u043C JSON \u0432 \u043A\u043E\u043D\u0446\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0414\u0440\u043E\u043D, \u0441\u043A\u043E\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E, \u0435\u0449\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u2014 \u0441\u043C\u043E\u0442\u0440\u0438 stderr \u0432\u044B\u0448\u0435 \u0438 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`
4311
- ) : `${fileHeader(`${launch.index}.json`, tail.size, tail.shown)}
4312
- ${tail.text}`
4362
+ ) : `${fileHeader(`${launch.index}.json`, tail2.size, tail2.shown)}
4363
+ ${tail2.text}`
4313
4364
  );
4314
4365
  }
4315
4366
  }
@@ -4319,15 +4370,15 @@ ${tail.text}`
4319
4370
  return { text: head + parts.join("\n\n") };
4320
4371
  }
4321
4372
  function daemonLogTail(path, opts) {
4322
- const tail = readTail(path, opts);
4323
- if (!tail) {
4373
+ const tail2 = readTail(path, opts);
4374
+ if (!tail2) {
4324
4375
  return {
4325
4376
  error: `\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${path} \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F. \u0414\u0435\u043C\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u0432 stdout \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u0444\u0430\u0439\u043B \u043F\u0435\u0440\u0435\u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C (\`swarm start >> ${path} 2>&1\`) \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438 \u0441\u0432\u043E\u0439 \u043F\u0443\u0442\u044C \u0432 SWARM_DAEMON_LOG.`
4326
4377
  };
4327
4378
  }
4328
- if (tail.size === 0) return { error: `\u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0443\u0441\u0442 (${path})` };
4329
- return { text: `${fileHeader(path, tail.size, tail.shown)}
4330
- ${tail.text}` };
4379
+ if (tail2.size === 0) return { error: `\u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0443\u0441\u0442 (${path})` };
4380
+ return { text: `${fileHeader(path, tail2.size, tail2.shown)}
4381
+ ${tail2.text}` };
4331
4382
  }
4332
4383
  async function serveLogRequests(deps, commands) {
4333
4384
  for (const cmd of commands) {
@@ -4366,6 +4417,11 @@ function runRef(runKey) {
4366
4417
  function isRunRef(v) {
4367
4418
  return typeof v === "object" && v !== null && "__runRef" in v;
4368
4419
  }
4420
+ function isHostCommandShape(v) {
4421
+ const c = v;
4422
+ return !!c && typeof c === "object" && typeof c.id === "string" && !!c.id && typeof c.kind === "string";
4423
+ }
4424
+ var EMPTY_HEARTBEAT_REPLY = { commands: [], logRequests: [] };
4369
4425
  function jsonFromTextContent(res) {
4370
4426
  const text = res?.content?.find((c) => c.type === "text")?.text;
4371
4427
  if (text) {
@@ -5537,17 +5593,24 @@ var SwarmClient = class _SwarmClient {
5537
5593
  * прислал предыдущий (более свежий по коду) процесс демона.
5538
5594
  */
5539
5595
  /**
5540
- * TASWM-322: ответ на пульс — ЕДИНСТВЕННЫЙ канал «прод → демон» (демон за домашним
5541
- * роутером, прод к нему не постучится). Оттуда приезжают команды машине — сейчас одна:
5542
- * «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
5543
- * прод старый (поля нет) → пустой список, всё как раньше.
5544
- */
5545
- /**
5596
+ * TASWM-321: ВОЗВРАЩАЕТ команды этой машине (пауза/рестарт/обновление/…), приехавшие
5597
+ * в ответе на пульс. Это единственный момент, когда прод может что-то сказать демону:
5598
+ * тот живёт за домашним роутером без белого адреса, и связь всегда инициирует он сам.
5599
+ * Прод, отдав команды, метит их доставленными — повторный пульс их не вернёт, поэтому
5600
+ * потерять ответ здесь значит потерять команду; парсим бережно и не бросаем.
5601
+ *
5602
+ * TASWM-322: тем же ответом приезжают запросы «пришли хвост лога» (logRequests) —
5603
+ * канал «прод → демон» один на оба вида поручений, поэтому и разбираем их вместе.
5604
+ * Возвращаем ОБА списка объектом: два разных потребителя в overmind, и слепить их в
5605
+ * один список значило бы заставить каждого фильтровать чужое.
5606
+ *
5546
5607
  * TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
5547
5608
  * pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
5548
5609
  * и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
5549
5610
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5550
5611
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5612
+ *
5613
+ * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5551
5614
  */
5552
5615
  async heartbeat(daemonVersion, pause) {
5553
5616
  try {
@@ -5562,9 +5625,33 @@ var SwarmClient = class _SwarmClient {
5562
5625
  args.hostName = this.host.hostName;
5563
5626
  }
5564
5627
  const res = await this.call("swarm_heartbeat", args);
5565
- return parseLogRequests(res?.structuredContent?.logRequests);
5628
+ if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
5629
+ const data = res?.structuredContent ?? {};
5630
+ return {
5631
+ commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
5632
+ logRequests: parseLogRequests(data.logRequests)
5633
+ };
5634
+ } catch {
5635
+ return EMPTY_HEARTBEAT_REPLY;
5636
+ }
5637
+ }
5638
+ /**
5639
+ * TASWM-321: подтвердить исполнение команды (`swarm_command_ack`). ПРЯМОЙ вызов, не
5640
+ * outbox: подтверждение — «скажи СЕЙЧАС», ради него команду и заводили («без
5641
+ * подтверждения непонятно, обновился он или команда потерялась по дороге»), а
5642
+ * промежуточные шаги обновления, доехавшие после самого обновления, бессмысленны.
5643
+ * Не бросает: сорвавшийся ack не должен рушить тик демона — команда останется в
5644
+ * прошлом состоянии, следующий шаг/финал её всё равно подвинут.
5645
+ */
5646
+ async ackCommand(commandId, status, patch = {}) {
5647
+ try {
5648
+ await this.call("swarm_command_ack", {
5649
+ commandId,
5650
+ status,
5651
+ ...patch.step ? { step: patch.step } : {},
5652
+ ...patch.result ? { result: patch.result.slice(0, 2e3) } : {}
5653
+ });
5566
5654
  } catch {
5567
- return [];
5568
5655
  }
5569
5656
  }
5570
5657
  /**
@@ -7564,8 +7651,8 @@ function fullCommitHeadline(headline, body) {
7564
7651
  const head = typeof headline === "string" ? headline : "";
7565
7652
  if (!head) return null;
7566
7653
  if (!head.endsWith("\u2026") || typeof body !== "string" || !body.startsWith("\u2026")) return head;
7567
- const tail = body.slice(1).split("\n", 1)[0] ?? "";
7568
- return head.slice(0, -1) + tail;
7654
+ const tail2 = body.slice(1).split("\n", 1)[0] ?? "";
7655
+ return head.slice(0, -1) + tail2;
7569
7656
  }
7570
7657
  var NO_CHECKS_STDERR = /no checks reported/i;
7571
7658
  var realChecksRunner = async (pr) => {
@@ -9203,7 +9290,7 @@ async function collectRunDiff(worktree, opts = {}) {
9203
9290
  // package.json
9204
9291
  var package_default = {
9205
9292
  name: "@taskless-app/swarm",
9206
- version: "0.4.9",
9293
+ version: "0.4.10",
9207
9294
  type: "module",
9208
9295
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9209
9296
  keywords: [
@@ -10862,6 +10949,221 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
10862
10949
  }
10863
10950
  }
10864
10951
 
10952
+ // src/hostCommands.ts
10953
+ init_src();
10954
+ import { readFileSync as readFileSync16, renameSync as renameSync6, writeFileSync as writeFileSync12 } from "fs";
10955
+ import { join as join20 } from "path";
10956
+ function hostCommandStatePath(home) {
10957
+ return join20(home, "host-command.json");
10958
+ }
10959
+ function readState(path) {
10960
+ try {
10961
+ return JSON.parse(readFileSync16(path, "utf8"));
10962
+ } catch {
10963
+ return {};
10964
+ }
10965
+ }
10966
+ function writeState(path, state) {
10967
+ try {
10968
+ writeFileSync12(`${path}.tmp`, JSON.stringify(state));
10969
+ renameSync6(`${path}.tmp`, path);
10970
+ } catch {
10971
+ }
10972
+ }
10973
+ function savePendingAck(path, ack) {
10974
+ writeState(path, { ...readState(path), pendingAck: ack });
10975
+ }
10976
+ function readPendingAck(path) {
10977
+ return readState(path).pendingAck ?? null;
10978
+ }
10979
+ function clearPendingAck(path) {
10980
+ const state = readState(path);
10981
+ const pending2 = state.pendingAck ?? null;
10982
+ if (pending2) writeState(path, { ...state, pendingAck: null });
10983
+ return pending2;
10984
+ }
10985
+ async function flushPendingAck(path, ack) {
10986
+ const state = readState(path);
10987
+ const pending2 = state.pendingAck;
10988
+ if (!pending2) return null;
10989
+ writeState(path, { ...state, pendingAck: null });
10990
+ try {
10991
+ await ack(pending2.commandId, "done", { step: "restart", result: pending2.note });
10992
+ } catch {
10993
+ }
10994
+ return pending2;
10995
+ }
10996
+ function loadMaxDronesOverride(path) {
10997
+ const n = readState(path).maxDrones;
10998
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
10999
+ }
11000
+ function saveMaxDronesOverride(path, n) {
11001
+ writeState(path, { ...readState(path), maxDrones: n });
11002
+ }
11003
+ async function executeHostCommands(opts) {
11004
+ const now = opts.nowMs ?? Date.now();
11005
+ let restarting = false;
11006
+ for (const cmd of opts.commands) {
11007
+ if (restarting) {
11008
+ await opts.ack(cmd.id, "rejected", { result: "\u0434\u0435\u043C\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u043E\u0442\u0434\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0437\u0430\u043D\u043E\u0432\u043E, \u043A\u043E\u0433\u0434\u0430 \u043E\u043D \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F" });
11009
+ continue;
11010
+ }
11011
+ if (isHostCommandExpired(cmd.expiresAt, now)) {
11012
+ await opts.ack(cmd.id, "rejected", { result: "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u0430 \u2014 \u044F \u0431\u044B\u043B \u043D\u0435 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E" });
11013
+ continue;
11014
+ }
11015
+ if (!isHostCommandKind(cmd.kind)) {
11016
+ await opts.ack(cmd.id, "rejected", { result: `\u044D\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \xAB${cmd.kind}\xBB \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u0434\u0435\u043C\u043E\u043D` });
11017
+ continue;
11018
+ }
11019
+ restarting = await executeOne(cmd, opts);
11020
+ }
11021
+ }
11022
+ async function executeOne(cmd, opts) {
11023
+ const { effects, ack, statePath } = opts;
11024
+ const label = hostCommandLabel(cmd);
11025
+ effects.narrate(`\u{1F4DF} \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless: ${label}`);
11026
+ try {
11027
+ switch (cmd.kind) {
11028
+ case "pause":
11029
+ effects.pause();
11030
+ await ack(cmd.id, "done", { result: "\u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0443, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442" });
11031
+ return false;
11032
+ case "resume":
11033
+ if (!effects.resume()) {
11034
+ await ack(cmd.id, "rejected", {
11035
+ result: "\u0436\u0434\u0443 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u2014 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u043D\u0438\u043C\u0443 \u043F\u0430\u0443\u0437\u0443, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u043E\u0442\u044B \u043D\u0435 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u043D\u0435 \u0441\u043B\u0443\u0447\u0438\u0442\u0441\u044F"
11036
+ });
11037
+ return false;
11038
+ }
11039
+ await ack(cmd.id, "done", { result: "\u0441\u043D\u044F\u043B \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0443 \u0437\u0430\u0434\u0430\u0447\u0438" });
11040
+ return false;
11041
+ case "set_max_drones": {
11042
+ const n = cmd.maxDrones;
11043
+ if (n == null || !Number.isFinite(n) || n < 1 || n > MAX_DRONES_LIMIT) {
11044
+ await ack(cmd.id, "rejected", { result: `\u0447\u0438\u0441\u043B\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043E\u0442 1 \u0434\u043E ${MAX_DRONES_LIMIT}` });
11045
+ return false;
11046
+ }
11047
+ const value = Math.trunc(n);
11048
+ saveMaxDronesOverride(statePath, value);
11049
+ effects.setMaxDrones(value);
11050
+ await ack(cmd.id, "done", { result: `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${value}` });
11051
+ return false;
11052
+ }
11053
+ case "stop_run": {
11054
+ const slug = cmd.runSlug?.trim();
11055
+ if (!slug) {
11056
+ await ack(cmd.id, "rejected", { result: "\u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043A\u0430\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0442\u044C" });
11057
+ return false;
11058
+ }
11059
+ const stopped = await effects.stopRun(slug, "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u0438\u0437 Taskless");
11060
+ if (!stopped) {
11061
+ await ack(cmd.id, "rejected", { result: `\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E ${slug} \u0443 \u043C\u0435\u043D\u044F \u043D\u0435\u0442` });
11062
+ return false;
11063
+ }
11064
+ await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
11065
+ return false;
11066
+ }
11067
+ case "restart":
11068
+ return await doRestart(cmd, opts, "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless");
11069
+ case "update": {
11070
+ const res = await effects.update(async (step, note) => {
11071
+ await ack(cmd.id, "running", { step, result: note });
11072
+ });
11073
+ if (!res.ok) {
11074
+ await ack(cmd.id, "failed", { step: res.step, result: `${UPDATE_STEP_LABELS[res.step]}: ${res.message}` });
11075
+ effects.narrate(`\u26A0\uFE0F \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0448\u0430\u0433\u0435 \xAB${UPDATE_STEP_LABELS[res.step]}\xBB: ${res.message}`);
11076
+ return false;
11077
+ }
11078
+ return await doRestart(cmd, opts, res.message);
11079
+ }
11080
+ }
11081
+ } catch (e) {
11082
+ await ack(cmd.id, "failed", { result: e instanceof Error ? e.message : String(e) });
11083
+ return false;
11084
+ }
11085
+ }
11086
+ async function doRestart(cmd, opts, note) {
11087
+ const { effects, ack, statePath } = opts;
11088
+ savePendingAck(statePath, { commandId: cmd.id, kind: cmd.kind, note });
11089
+ const { waiting, sessions } = effects.requestRestart();
11090
+ if (waiting) {
11091
+ await ack(cmd.id, "running", {
11092
+ step: "restart",
11093
+ result: `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0443, \u043A\u043E\u0433\u0434\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0441\u043B\u043E\u0442\u044B: \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 ${sessions.length} \u2014 \u043E\u043D\u0438 \u0431\u044B \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u0438`
11094
+ });
11095
+ effects.narrate(`\u23F3 \u0420\u0435\u0441\u0442\u0430\u0440\u0442 \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 (${sessions.length})`);
11096
+ return true;
11097
+ }
11098
+ await ack(cmd.id, "running", { step: "restart", result: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u044E\u0441\u044C" });
11099
+ return true;
11100
+ }
11101
+
11102
+ // src/selfUpdate.ts
11103
+ init_git();
11104
+ import { existsSync as existsSync11 } from "fs";
11105
+ import { join as join21 } from "path";
11106
+ import { $ as $6 } from "execa";
11107
+ var DAEMON_PACKAGE = "@taskless-app/swarm";
11108
+ var defaultRun = async (cmd, args, cwd) => {
11109
+ const res = await $6({ reject: false, cwd })(cmd, args);
11110
+ return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
11111
+ };
11112
+ function tail(text, max = 400) {
11113
+ const t = text.trim();
11114
+ return t.length <= max ? t : `\u2026${t.slice(-max)}`;
11115
+ }
11116
+ async function runSelfUpdate(io) {
11117
+ const run = io.run ?? defaultRun;
11118
+ const exists = io.exists ?? existsSync11;
11119
+ const repo = io.repo;
11120
+ await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11121
+ const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
11122
+ if (fetched.exitCode !== 0) {
11123
+ return { ok: false, step: "fetch", message: `git fetch \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(fetched.stderr || fetched.stdout)}` };
11124
+ }
11125
+ await io.onStep("checkout", "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C \u043D\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 main");
11126
+ const dirty = await run("git", ["-C", repo, "status", "--porcelain"], repo);
11127
+ if (dirty.exitCode !== 0) {
11128
+ return { ok: false, step: "checkout", message: `\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F: ${tail(dirty.stderr)}` };
11129
+ }
11130
+ if (dirty.stdout.trim()) {
11131
+ return {
11132
+ ok: false,
11133
+ step: "checkout",
11134
+ message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u0435\u0441\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E \u0438\u0445. \u041F\u0440\u0438\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435."
11135
+ };
11136
+ }
11137
+ const ref = await io.mainRef(repo);
11138
+ const merged = await run("git", ["-C", repo, "merge", "--ff-only", ref], repo);
11139
+ if (merged.exitCode !== 0) {
11140
+ return {
11141
+ ok: false,
11142
+ step: "checkout",
11143
+ message: `\u043D\u0435 \u043F\u0435\u0440\u0435\u043C\u0430\u0442\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0430 ${ref} (\u0441\u0432\u043E\u044F \u0432\u0435\u0442\u043A\u0430/\u0440\u0430\u0441\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0441 main): ${tail(merged.stderr || merged.stdout)}`
11144
+ };
11145
+ }
11146
+ await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
11147
+ if (!exists(join21(repo, "pnpm-lock.yaml"))) {
11148
+ return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
11149
+ }
11150
+ const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
11151
+ if (installed.exitCode !== 0) {
11152
+ return { ok: false, step: "install", message: `pnpm install \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(installed.stderr || installed.stdout)}` };
11153
+ }
11154
+ await io.onStep("build", "\u0441\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u043A\u0435\u0442\u044B \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430");
11155
+ const built = await run("pnpm", ["--filter", `${DAEMON_PACKAGE}...`, "build"], repo);
11156
+ if (built.exitCode !== 0) {
11157
+ return { ok: false, step: "build", message: `\u0441\u0431\u043E\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430: ${tail(built.stderr || built.stdout)}` };
11158
+ }
11159
+ const head = await run("git", ["-C", repo, "rev-parse", "--short", "HEAD"], repo);
11160
+ const sha = head.exitCode === 0 ? head.stdout.trim() : "";
11161
+ return { ok: true, step: "build", message: sha ? `\u043A\u043E\u0434 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0434\u043E ${sha}, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0438 \u0441\u0431\u043E\u0440\u043A\u0430 \u0433\u043E\u0442\u043E\u0432\u044B` : "\u043A\u043E\u0434 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0438 \u0441\u0431\u043E\u0440\u043A\u0430 \u0433\u043E\u0442\u043E\u0432\u044B" };
11162
+ }
11163
+ function defaultSelfUpdateIO(repo, onStep) {
11164
+ return { repo, run: defaultRun, mainRef: remoteMainRef, onStep };
11165
+ }
11166
+
10865
11167
  // src/narration.ts
10866
11168
  var RING_LIMIT = 20;
10867
11169
  function makeOvermindNarrator() {
@@ -10932,16 +11234,16 @@ async function fetchExternalStorm(url, opts = {}) {
10932
11234
  }
10933
11235
 
10934
11236
  // src/anomalyState.ts
10935
- import { existsSync as existsSync11, readFileSync as readFileSync16, renameSync as renameSync6, writeFileSync as writeFileSync12 } from "fs";
10936
- import { join as join20 } from "path";
11237
+ import { existsSync as existsSync12, readFileSync as readFileSync17, renameSync as renameSync7, writeFileSync as writeFileSync13 } from "fs";
11238
+ import { join as join22 } from "path";
10937
11239
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
10938
11240
  var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
10939
11241
  function anomalyStatePath(home) {
10940
- return join20(home, "anomaly-state.json");
11242
+ return join22(home, "anomaly-state.json");
10941
11243
  }
10942
11244
  function loadAnomalyState(home, now = Date.now(), io = {}) {
10943
- const exists = io.exists ?? existsSync11;
10944
- const readFile2 = io.readFile ?? ((p) => readFileSync16(p, "utf8"));
11245
+ const exists = io.exists ?? existsSync12;
11246
+ const readFile2 = io.readFile ?? ((p) => readFileSync17(p, "utf8"));
10945
11247
  const path = anomalyStatePath(home);
10946
11248
  if (!exists(path)) return { ...EMPTY };
10947
11249
  try {
@@ -10967,8 +11269,8 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
10967
11269
  function saveAnomalyState(home, state, io = {}) {
10968
11270
  const path = anomalyStatePath(home);
10969
11271
  const writeFile2 = io.writeFile ?? ((p, data) => {
10970
- writeFileSync12(`${p}.tmp`, data);
10971
- renameSync6(`${p}.tmp`, p);
11272
+ writeFileSync13(`${p}.tmp`, data);
11273
+ renameSync7(`${p}.tmp`, p);
10972
11274
  });
10973
11275
  writeFile2(path, JSON.stringify(state, null, 2));
10974
11276
  }
@@ -11610,10 +11912,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11610
11912
  }
11611
11913
  return parts.join("\n");
11612
11914
  }
11613
- var HERO_CHARTER_PATH = join21(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11915
+ var HERO_CHARTER_PATH = join23(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11614
11916
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
11615
11917
  const name = run.name || `#${run.id}`;
11616
- const charter = readFileSync17(HERO_CHARTER_PATH, "utf8");
11918
+ const charter = readFileSync18(HERO_CHARTER_PATH, "utf8");
11617
11919
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
11618
11920
 
11619
11921
  ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
@@ -11864,7 +12166,7 @@ async function wakeRun(deps, runId) {
11864
12166
  if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
11865
12167
  if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
11866
12168
  if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
11867
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12169
+ const worktreeExists = deps.worktreeExists ?? existsSync13;
11868
12170
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11869
12171
  const wt = run.worktree_path;
11870
12172
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -11949,7 +12251,7 @@ function repoHasCi(deps, run) {
11949
12251
  function repoHasCiWorkflows(worktreePath, repo) {
11950
12252
  for (const base3 of [worktreePath, repo]) {
11951
12253
  if (!base3) continue;
11952
- const dir = join21(base3, ".github", "workflows");
12254
+ const dir = join23(base3, ".github", "workflows");
11953
12255
  try {
11954
12256
  if (readdirSync10(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
11955
12257
  } catch {
@@ -12098,7 +12400,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12098
12400
  return true;
12099
12401
  }
12100
12402
  function worktreeAndTranscriptAlive(deps, worktree) {
12101
- const exists = deps.worktreeExists ?? existsSync12;
12403
+ const exists = deps.worktreeExists ?? existsSync13;
12102
12404
  const transcript = deps.hasTranscript ?? sessionTranscriptExists;
12103
12405
  return exists(worktree) && transcript(worktree);
12104
12406
  }
@@ -12303,7 +12605,7 @@ async function resumeWatchdogParked(deps, droneCap) {
12303
12605
  }
12304
12606
  if (!candidate) return;
12305
12607
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12306
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12608
+ const worktreeExists = deps.worktreeExists ?? existsSync13;
12307
12609
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12308
12610
  const wt = candidate.worktree_path;
12309
12611
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -12472,7 +12774,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
12472
12774
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
12473
12775
  if (!deps.respawn) return;
12474
12776
  const wt = resumeCandidate.worktree_path;
12475
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12777
+ const worktreeExists = deps.worktreeExists ?? existsSync13;
12476
12778
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12477
12779
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12478
12780
  const mode = canContinue ? "continue" : "fresh";
@@ -12634,7 +12936,7 @@ function buildEscalationCandidates(db) {
12634
12936
  }
12635
12937
  async function tickOnce(deps) {
12636
12938
  const now = deps.now ? deps.now() : Date.now();
12637
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12939
+ const worktreeExists = deps.worktreeExists ?? existsSync13;
12638
12940
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12639
12941
  if (deps.rehydrate) {
12640
12942
  const rh = deps.rehydrate;
@@ -13240,7 +13542,7 @@ async function resolveConflict(deps, reviewRun) {
13240
13542
  workspace_id: reviewRun.workspace_id
13241
13543
  // та же задача, тот же воркспейс (TASWM-288)
13242
13544
  });
13243
- const wt = join21(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13545
+ const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13244
13546
  try {
13245
13547
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13246
13548
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13292,7 +13594,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13292
13594
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13293
13595
  workspace_id: reviewRun.workspace_id
13294
13596
  });
13295
- const wt = join21(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13597
+ const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13296
13598
  try {
13297
13599
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13298
13600
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13327,7 +13629,7 @@ async function respawnRun(deps, run, mode, nudge) {
13327
13629
  ]);
13328
13630
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13329
13631
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13330
- if (!existsSync12(wt)) {
13632
+ if (!existsSync13(wt)) {
13331
13633
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13332
13634
  const source = await addWorktree(repo, wt, branch, {
13333
13635
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -13356,7 +13658,7 @@ ${notice}` : freshPrompt;
13356
13658
  deps.db.updateRun(run.id, { status: "running", pid });
13357
13659
  }
13358
13660
  async function syncCrashedOnStartup(deps) {
13359
- const worktreeExists = deps.worktreeExists ?? existsSync12;
13661
+ const worktreeExists = deps.worktreeExists ?? existsSync13;
13360
13662
  let orphaned = 0;
13361
13663
  let merged = 0;
13362
13664
  let requeued = 0;
@@ -13485,7 +13787,7 @@ async function migrateWorktreesAtStartup() {
13485
13787
  legacyDir: WORKTREES_LEGACY_DIR,
13486
13788
  targetDir: WORKTREES_DIR,
13487
13789
  envOverride: process.env.SWARM_WORKTREES || "",
13488
- exists: (d) => existsSync12(d),
13790
+ exists: (d) => existsSync13(d),
13489
13791
  isNonEmptyDir: (d) => {
13490
13792
  try {
13491
13793
  return readdirSync10(d).length > 0;
@@ -13494,13 +13796,13 @@ async function migrateWorktreesAtStartup() {
13494
13796
  }
13495
13797
  },
13496
13798
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
13497
- rename: (from, to) => renameSync7(from, to),
13799
+ rename: (from, to) => renameSync8(from, to),
13498
13800
  repairMoved: (t) => repairMovedWorktrees(t),
13499
13801
  log: (m) => console.log(m)
13500
13802
  });
13501
13803
  } catch (e) {
13502
13804
  console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
13503
- return existsSync12(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13805
+ return existsSync13(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13504
13806
  }
13505
13807
  }
13506
13808
  async function reportWorkspaceAccess(tl, db) {
@@ -13555,6 +13857,8 @@ async function runDaemon(opts) {
13555
13857
  }
13556
13858
  } : void 0;
13557
13859
  let lastDroneCap = null;
13860
+ const hostCommandState = hostCommandStatePath(SWARM_HOME);
13861
+ let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
13558
13862
  const launch = makeLauncher({
13559
13863
  overmindEntry: THIS_FILE,
13560
13864
  dbPath: DB_PATH,
@@ -13593,22 +13897,26 @@ async function runDaemon(opts) {
13593
13897
  intervalMs: SWARM_VERSION_CHECK_MS
13594
13898
  };
13595
13899
  }
13900
+ let onHostCommands = () => {
13901
+ };
13596
13902
  let heartbeat;
13597
13903
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
13598
13904
  const client = db;
13599
13905
  heartbeat = {
13600
13906
  push: async () => {
13601
- const commands = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
13602
- if (commands.length === 0) return;
13603
- await serveLogRequests(
13604
- {
13605
- runLogsDir: RUN_LOGS_DIR,
13606
- daemonLogPath: DAEMON_LOG_PATH,
13607
- localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
13608
- deliver: (requestId, payload) => client.deliverLog(requestId, payload)
13609
- },
13610
- commands
13611
- );
13907
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
13908
+ if (reply.commands.length) onHostCommands(reply.commands);
13909
+ if (reply.logRequests.length) {
13910
+ await serveLogRequests(
13911
+ {
13912
+ runLogsDir: RUN_LOGS_DIR,
13913
+ daemonLogPath: DAEMON_LOG_PATH,
13914
+ localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
13915
+ deliver: (requestId, payload) => client.deliverLog(requestId, payload)
13916
+ },
13917
+ reply.logRequests
13918
+ );
13919
+ }
13612
13920
  },
13613
13921
  intervalMs: SWARM_PROD_HEARTBEAT_MS
13614
13922
  };
@@ -13682,7 +13990,7 @@ async function runDaemon(opts) {
13682
13990
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
13683
13991
  ttlMs: 1e4
13684
13992
  });
13685
- maxDrones = async () => lastDroneCap = await readMaxDrones();
13993
+ maxDrones = async () => lastDroneCap = hostMaxDrones ?? await readMaxDrones();
13686
13994
  }
13687
13995
  let verifyModeReader;
13688
13996
  let baseBranchReader;
@@ -13980,7 +14288,7 @@ async function runDaemon(opts) {
13980
14288
  mode,
13981
14289
  nudge
13982
14290
  ),
13983
- worktreeExists: (p) => existsSync12(p),
14291
+ worktreeExists: (p) => existsSync13(p),
13984
14292
  hasTranscript: (p) => sessionTranscriptExists(p),
13985
14293
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
13986
14294
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -14071,6 +14379,8 @@ async function runDaemon(opts) {
14071
14379
  narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
14072
14380
  return { waiting: true, sessions };
14073
14381
  };
14382
+ let onRestartWaitCancelled = null;
14383
+ let onRestartStillWaiting = null;
14074
14384
  const requestStop = (o) => {
14075
14385
  const sessions = liveSessionIds(db, pidAlive);
14076
14386
  if (o.force || sessions.length === 0) {
@@ -14102,8 +14412,78 @@ async function runDaemon(opts) {
14102
14412
  restartAutoPaused = false;
14103
14413
  anomalyState.markResumed(Date.now());
14104
14414
  }
14415
+ onRestartWaitCancelled?.();
14105
14416
  narrator.narrate(wasStop ? "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0434\u0435\u043C\u043E\u043D\u0430" : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
14106
14417
  };
14418
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
14419
+ const client = db;
14420
+ const ack = (id, status, patch) => client.ackCommand(id, status, patch ?? {});
14421
+ const effects = {
14422
+ pause: () => {
14423
+ control.paused = true;
14424
+ },
14425
+ resume: () => {
14426
+ if (restart.phase === "waiting") return false;
14427
+ control.paused = false;
14428
+ return true;
14429
+ },
14430
+ requestRestart: () => requestRestart(false),
14431
+ setMaxDrones: (n) => {
14432
+ hostMaxDrones = n;
14433
+ lastDroneCap = n;
14434
+ },
14435
+ stopRun: async (slug, reason) => {
14436
+ const run = await cancelRun(
14437
+ {
14438
+ db,
14439
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
14440
+ removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
14441
+ },
14442
+ slug,
14443
+ reason
14444
+ );
14445
+ return run ? { id: run.id } : null;
14446
+ },
14447
+ update: async (onStep) => {
14448
+ if (!daemonStart) {
14449
+ return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
14450
+ }
14451
+ return runSelfUpdate(defaultSelfUpdateIO(daemonStart.repo, onStep));
14452
+ },
14453
+ narrate: narrator.narrate
14454
+ };
14455
+ let commandQueue = Promise.resolve();
14456
+ onHostCommands = (commands) => {
14457
+ commandQueue = commandQueue.then(() => executeHostCommands({ commands, effects, ack, statePath: hostCommandState })).catch((e) => {
14458
+ console.error("[overmind] \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043C\u0430\u0448\u0438\u043D\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
14459
+ });
14460
+ };
14461
+ onRestartWaitCancelled = () => {
14462
+ const pending2 = clearPendingAck(hostCommandState);
14463
+ if (!pending2) return;
14464
+ void ack(pending2.commandId, "rejected", {
14465
+ step: "restart",
14466
+ result: "\u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E \u043D\u0430 \u0441\u0430\u043C\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u043E\u0442\u0434\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0437\u0430\u043D\u043E\u0432\u043E"
14467
+ }).catch(() => {
14468
+ });
14469
+ };
14470
+ let lastWaitAck = 0;
14471
+ onRestartStillWaiting = (sessions) => {
14472
+ const nowMs = Date.now();
14473
+ if (nowMs - lastWaitAck < HOST_COMMAND_KEEPALIVE_MS) return;
14474
+ const pending2 = readPendingAck(hostCommandState);
14475
+ if (!pending2) return;
14476
+ lastWaitAck = nowMs;
14477
+ void ack(pending2.commandId, "running", {
14478
+ step: "restart",
14479
+ result: `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0443, \u043A\u043E\u0433\u0434\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0441\u043B\u043E\u0442\u044B: \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 ${sessions} \u2014 \u043E\u043D\u0438 \u0431\u044B \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u0438`
14480
+ }).catch(() => {
14481
+ });
14482
+ };
14483
+ void flushPendingAck(hostCommandState, ack).then((done) => {
14484
+ if (done) narrator.narrate(`\u2705 \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430: ${done.note}`);
14485
+ });
14486
+ }
14107
14487
  if (channelSecret) {
14108
14488
  droneChannel = startDroneChannel(
14109
14489
  {
@@ -14210,8 +14590,10 @@ async function runDaemon(opts) {
14210
14590
  try {
14211
14591
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
14212
14592
  await tickOnce(deps);
14213
- if (restart.phase === "waiting" && liveSessionIds(db, pidAlive).length === 0) {
14214
- performRestart([], restart.intent);
14593
+ if (restart.phase === "waiting") {
14594
+ const live = liveSessionIds(db, pidAlive);
14595
+ if (live.length === 0) performRestart([], restart.intent);
14596
+ else onRestartStillWaiting?.(live.length);
14215
14597
  }
14216
14598
  } catch (e) {
14217
14599
  console.error("[overmind] tick error:", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [