@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 +67 -11
- package/dist/overmind.js +456 -74
- package/package.json +1 -1
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
|
-
|
|
374
|
+
if (!cleaned.length) return null;
|
|
375
|
+
return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
|
|
375
376
|
}
|
|
376
|
-
|
|
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-
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
422
|
+
if (!cleaned.length) return null;
|
|
423
|
+
return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
|
|
423
424
|
}
|
|
424
|
-
|
|
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
|
|
1502
|
-
return needle +
|
|
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
|
|
2468
|
-
const m = /\.([A-Za-z0-9]{1,8})$/.exec(
|
|
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
|
|
3055
|
-
const skipped = comments.length -
|
|
3056
|
-
const lines =
|
|
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 ${
|
|
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
|
|
4143
|
-
import { basename as basename4, dirname as dirname5, join as
|
|
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
|
|
4295
|
-
if (
|
|
4296
|
-
const block = `${fileHeader(`${launch.index}.err.log`,
|
|
4297
|
-
${
|
|
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
|
|
4304
|
-
if (
|
|
4354
|
+
const tail2 = readTail(launch.out, { lines: opts.lines, maxBytes: Math.max(1024, budget) });
|
|
4355
|
+
if (tail2) {
|
|
4305
4356
|
parts.push(
|
|
4306
|
-
|
|
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`,
|
|
4312
|
-
${
|
|
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
|
|
4323
|
-
if (!
|
|
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 (
|
|
4329
|
-
return { text: `${fileHeader(path,
|
|
4330
|
-
${
|
|
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-
|
|
5541
|
-
*
|
|
5542
|
-
*
|
|
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
|
-
|
|
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
|
|
7568
|
-
return head.slice(0, -1) +
|
|
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.
|
|
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
|
|
10936
|
-
import { join as
|
|
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
|
|
11242
|
+
return join22(home, "anomaly-state.json");
|
|
10941
11243
|
}
|
|
10942
11244
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
10943
|
-
const exists = io.exists ??
|
|
10944
|
-
const readFile2 = io.readFile ?? ((p) =>
|
|
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
|
-
|
|
10971
|
-
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 ??
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
13602
|
-
if (commands.length
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
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) =>
|
|
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"
|
|
14214
|
-
|
|
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