@taskless-app/swarm 0.6.55 → 0.6.56
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 +98 -8
- package/dist/overmind.js +227 -10
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -697,6 +697,9 @@ var init_modelPolicy = __esm({
|
|
|
697
697
|
});
|
|
698
698
|
|
|
699
699
|
// ../swarm-core/src/status.ts
|
|
700
|
+
function isAwaitingBudget(status, stopReason) {
|
|
701
|
+
return (status === "paused" || status === "blocked") && !!stopReason?.startsWith(AWAITING_BUDGET_REASON);
|
|
702
|
+
}
|
|
700
703
|
function statusMeta(status) {
|
|
701
704
|
return STATUS_META[status] ?? FALLBACK;
|
|
702
705
|
}
|
|
@@ -710,7 +713,7 @@ function statusTuples() {
|
|
|
710
713
|
}
|
|
711
714
|
return out;
|
|
712
715
|
}
|
|
713
|
-
var STATUS_META, FALLBACK, TERMINAL_STATUSES;
|
|
716
|
+
var STATUS_META, AWAITING_BUDGET_REASON, FALLBACK, TERMINAL_STATUSES;
|
|
714
717
|
var init_status = __esm({
|
|
715
718
|
"../swarm-core/src/status.ts"() {
|
|
716
719
|
"use strict";
|
|
@@ -734,6 +737,7 @@ var init_status = __esm({
|
|
|
734
737
|
// цвет предупреждающий, а не destructive: смотреть должен АВТОР задачи, чинить нечего.
|
|
735
738
|
returned: { pill: "returned", label: "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443", color: "--warning", terminal: true }
|
|
736
739
|
};
|
|
740
|
+
AWAITING_BUDGET_REASON = "\u0436\u0434\u0451\u0442 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043F\u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0443 \u2014 \u0434\u043E\u0448\u0451\u043B \u0434\u043E \u043F\u043E\u0442\u043E\u043B\u043A\u0430 \u0442\u0440\u0430\u0442";
|
|
737
741
|
FALLBACK = STATUS_META.queued;
|
|
738
742
|
TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
739
743
|
(s) => STATUS_META[s].terminal
|
|
@@ -1232,6 +1236,7 @@ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
|
|
|
1232
1236
|
var init_runGuards = __esm({
|
|
1233
1237
|
"../swarm-core/src/runGuards.ts"() {
|
|
1234
1238
|
"use strict";
|
|
1239
|
+
init_status();
|
|
1235
1240
|
DEFAULT_RUN_LIMITS = {
|
|
1236
1241
|
maxCostUsd: 20,
|
|
1237
1242
|
runTimeoutMs: 8 * 60 * 6e4,
|
|
@@ -1257,19 +1262,38 @@ function grantedCeilingUsd(events) {
|
|
|
1257
1262
|
}
|
|
1258
1263
|
return max;
|
|
1259
1264
|
}
|
|
1265
|
+
function isBudgetApprovedEvent(e) {
|
|
1266
|
+
return has(e, BUDGET_APPROVED_MARK);
|
|
1267
|
+
}
|
|
1268
|
+
function approvedCeilingUsd(events) {
|
|
1269
|
+
let max = null;
|
|
1270
|
+
for (const e of events) {
|
|
1271
|
+
if (!isBudgetApprovedEvent(e)) continue;
|
|
1272
|
+
const m = /потолок \$([0-9]+(?:\.[0-9]+)?)/.exec(e.text ?? "");
|
|
1273
|
+
if (!m) continue;
|
|
1274
|
+
const v = Number(m[1]);
|
|
1275
|
+
if (Number.isFinite(v) && (max == null || v > max)) max = v;
|
|
1276
|
+
}
|
|
1277
|
+
return max;
|
|
1278
|
+
}
|
|
1260
1279
|
function effectiveCostCeilingUsd(baseUsd, events) {
|
|
1261
1280
|
if (!(baseUsd > 0)) return baseUsd;
|
|
1281
|
+
let ceiling = baseUsd;
|
|
1262
1282
|
const granted = grantedCeilingUsd(events);
|
|
1263
|
-
if (granted
|
|
1264
|
-
|
|
1283
|
+
if (granted != null) ceiling = Math.max(ceiling, Math.min(granted, baseUsd * HEAVY_CEILING_MAX_FACTOR));
|
|
1284
|
+
const approved = approvedCeilingUsd(events);
|
|
1285
|
+
if (approved != null) ceiling = Math.max(ceiling, Math.min(approved, baseUsd * BUDGET_APPROVAL_MAX_FACTOR));
|
|
1286
|
+
return ceiling;
|
|
1265
1287
|
}
|
|
1266
|
-
var HEAVY_CEILING_MAX_FACTOR, HEAVY_GRANT_MARK, has;
|
|
1288
|
+
var HEAVY_CEILING_MAX_FACTOR, HEAVY_GRANT_MARK, has, BUDGET_APPROVAL_MAX_FACTOR, BUDGET_APPROVED_MARK;
|
|
1267
1289
|
var init_taskWeight = __esm({
|
|
1268
1290
|
"../swarm-core/src/taskWeight.ts"() {
|
|
1269
1291
|
"use strict";
|
|
1270
1292
|
HEAVY_CEILING_MAX_FACTOR = 2;
|
|
1271
1293
|
HEAVY_GRANT_MARK = "\u0432\u0435\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430: \u0442\u044F\u0436\u0451\u043B\u0430\u044F";
|
|
1272
1294
|
has = (e, mark) => !!e.text?.includes(mark);
|
|
1295
|
+
BUDGET_APPROVAL_MAX_FACTOR = 3;
|
|
1296
|
+
BUDGET_APPROVED_MARK = "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E";
|
|
1273
1297
|
}
|
|
1274
1298
|
});
|
|
1275
1299
|
|
|
@@ -3496,7 +3520,7 @@ var init_package = __esm({
|
|
|
3496
3520
|
"package.json"() {
|
|
3497
3521
|
package_default = {
|
|
3498
3522
|
name: "@taskless-app/swarm",
|
|
3499
|
-
version: "0.6.
|
|
3523
|
+
version: "0.6.56",
|
|
3500
3524
|
type: "module",
|
|
3501
3525
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3502
3526
|
keywords: [
|
|
@@ -3547,7 +3571,7 @@ var init_package = __esm({
|
|
|
3547
3571
|
execa: "^10.0.0",
|
|
3548
3572
|
zod: "3.25.76"
|
|
3549
3573
|
},
|
|
3550
|
-
gitHead: "
|
|
3574
|
+
gitHead: "1922a9b8079c0af2b2b8a9b5fc8756b0218f7446"
|
|
3551
3575
|
};
|
|
3552
3576
|
}
|
|
3553
3577
|
});
|
|
@@ -5597,7 +5621,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5597
5621
|
).run(answer, by, question_id);
|
|
5598
5622
|
const q = this.getQuestion(question_id);
|
|
5599
5623
|
if (q && q.status === "answered") {
|
|
5600
|
-
this.
|
|
5624
|
+
const run = this.getRun(q.run_id);
|
|
5625
|
+
if (!(run && isAwaitingBudget(run.status, run.stop_reason))) this.updateRun(q.run_id, { status: "running" });
|
|
5601
5626
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
5602
5627
|
}
|
|
5603
5628
|
return q;
|
|
@@ -7431,11 +7456,16 @@ function parseLogRequests(raw) {
|
|
|
7431
7456
|
}
|
|
7432
7457
|
return out;
|
|
7433
7458
|
}
|
|
7459
|
+
var LIVE_STATUSES = Object.keys(STATUS_META).filter((s) => !STATUS_META[s].terminal);
|
|
7460
|
+
var LIVE_SWEEP_MAX_PAGES = 10;
|
|
7434
7461
|
var SwarmClient = class _SwarmClient {
|
|
7435
7462
|
call;
|
|
7436
7463
|
defaultRepo;
|
|
7437
7464
|
autoFlush;
|
|
7438
7465
|
listLimit;
|
|
7466
|
+
liveSweepMs;
|
|
7467
|
+
liveSweptAt = null;
|
|
7468
|
+
// TASWM-589: когда живые за окном добраны целиком
|
|
7439
7469
|
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
7440
7470
|
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
7441
7471
|
repoFullNames;
|
|
@@ -7551,6 +7581,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7551
7581
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
7552
7582
|
this.autoFlush = opts.autoFlush ?? true;
|
|
7553
7583
|
this.listLimit = opts.listLimit ?? 200;
|
|
7584
|
+
this.liveSweepMs = opts.liveSweepMs ?? 6e4;
|
|
7554
7585
|
const registry = opts.repoRegistry ?? [];
|
|
7555
7586
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
7556
7587
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
@@ -8300,7 +8331,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
8300
8331
|
q.answer = answer;
|
|
8301
8332
|
q.answered_by = by;
|
|
8302
8333
|
q.answered_at = stamp();
|
|
8303
|
-
this.
|
|
8334
|
+
const run = this.runs.get(q.run_id);
|
|
8335
|
+
if (!(run && isAwaitingBudget(run.status, run.stop_reason))) this.updateRun(q.run_id, { status: "running" });
|
|
8304
8336
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
8305
8337
|
}
|
|
8306
8338
|
return q ? { ...q } : null;
|
|
@@ -8515,9 +8547,60 @@ var SwarmClient = class _SwarmClient {
|
|
|
8515
8547
|
}
|
|
8516
8548
|
if (res?.isError) return;
|
|
8517
8549
|
const runs = res.structuredContent?.runs ?? [];
|
|
8550
|
+
const seen = new Set(runs.map((pr) => String(pr.id ?? "")));
|
|
8551
|
+
for (const pr of await this.liveBeyondWindow()) {
|
|
8552
|
+
const id = String(pr.id ?? "");
|
|
8553
|
+
if (seen.has(id)) continue;
|
|
8554
|
+
seen.add(id);
|
|
8555
|
+
runs.push(pr);
|
|
8556
|
+
}
|
|
8518
8557
|
for (const pr of runs) this.hydrateRun(pr);
|
|
8519
8558
|
for (const pr of runs) this.linkRefs(pr);
|
|
8520
8559
|
}
|
|
8560
|
+
/**
|
|
8561
|
+
* TASWM-589: ЖИВЫЕ прогоны за окном гидрации. Окно — `limit` последних по updatedAt, общее на
|
|
8562
|
+
* все репозитории владельца, и в плотный день оно заполняется за часы. Прогон, которого никто
|
|
8563
|
+
* не трогал, из окна выпадает; пока демон жив, это не страшно (зеркало его помнит), но после
|
|
8564
|
+
* рестарта его в зеркале нет — и демон не знает, что его надо стартовать. 29.09 так простоял
|
|
8565
|
+
* ANS-314: демон перезапустили в 13:43 UTC, а после его заведения в 09:12 завели ещё 316
|
|
8566
|
+
* прогонов. `wake_run` по десятку таких же отвечал «активного прогона … у меня нет».
|
|
8567
|
+
*
|
|
8568
|
+
* Поэтому нетерминальные прогоны тянем выборкой по статусу, постранично и без окна. Живых
|
|
8569
|
+
* обычно десятки, а не сотни, но выборка тяжёлая (лента событий в каждой строке), и на
|
|
8570
|
+
* каждом периодическом re-hydrate (раз в 15 с) она не нужна: пока демон жив, прогон попадает
|
|
8571
|
+
* в зеркало, когда его заводят или трогают, — то есть через окно. Добор нужен на старте и как
|
|
8572
|
+
* страховка, поэтому он идёт при первой гидрации и дальше не чаще раза в `liveSweepMs`.
|
|
8573
|
+
*
|
|
8574
|
+
* Fail-open: прод отказал по какому-то статусу — отдаём, что успели, и не засчитываем добор,
|
|
8575
|
+
* чтобы повторить его на следующей гидрации, а не через минуту. Граница машины (isMine) и
|
|
8576
|
+
* слияние с уже известными прогонами — обычный hydrateRun.
|
|
8577
|
+
*/
|
|
8578
|
+
async liveBeyondWindow() {
|
|
8579
|
+
const now = this.now();
|
|
8580
|
+
if (this.liveSweptAt != null && now - this.liveSweptAt < this.liveSweepMs) return [];
|
|
8581
|
+
const byStatus = await Promise.all(LIVE_STATUSES.map((status) => this.listAllByStatus(status)));
|
|
8582
|
+
if (byStatus.every((rows) => rows !== null)) this.liveSweptAt = now;
|
|
8583
|
+
return byStatus.flatMap((rows) => rows ?? []);
|
|
8584
|
+
}
|
|
8585
|
+
/** Все прогоны одного статуса, страницами по 200. null — прод не ответил. */
|
|
8586
|
+
async listAllByStatus(status) {
|
|
8587
|
+
const out = [];
|
|
8588
|
+
let cursor = null;
|
|
8589
|
+
for (let page = 0; page < LIVE_SWEEP_MAX_PAGES; page++) {
|
|
8590
|
+
let res;
|
|
8591
|
+
try {
|
|
8592
|
+
res = await this.call("swarm_list_runs", { status, limit: 200, ...cursor ? { cursor } : {} });
|
|
8593
|
+
} catch {
|
|
8594
|
+
return null;
|
|
8595
|
+
}
|
|
8596
|
+
if (!res || res.isError) return null;
|
|
8597
|
+
out.push(...res.structuredContent?.runs ?? []);
|
|
8598
|
+
const next = res.structuredContent?.nextCursor;
|
|
8599
|
+
if (typeof next !== "string" || !next) break;
|
|
8600
|
+
cursor = next;
|
|
8601
|
+
}
|
|
8602
|
+
return out;
|
|
8603
|
+
}
|
|
8521
8604
|
/**
|
|
8522
8605
|
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
8523
8606
|
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
@@ -8553,6 +8636,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
8553
8636
|
let alive = 0;
|
|
8554
8637
|
let dead = 0;
|
|
8555
8638
|
for (const d of this.runs.values()) {
|
|
8639
|
+
if (isAwaitingBudget(d.status, d.stop_reason) && d.resolves_run_id == null && d.reviews_run_id == null && d.verifies_run_id == null) {
|
|
8640
|
+
if (this.runtime.get(d.id)?.worktree_path == null) {
|
|
8641
|
+
const wt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
8642
|
+
if (wtExists(wt)) this.runtime.set(d.id, { pid: null, worktree_path: wt });
|
|
8643
|
+
}
|
|
8644
|
+
continue;
|
|
8645
|
+
}
|
|
8556
8646
|
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
8557
8647
|
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
8558
8648
|
const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
package/dist/overmind.js
CHANGED
|
@@ -697,6 +697,9 @@ var init_modelPolicy = __esm({
|
|
|
697
697
|
function isAwaitingCi(status, stopReason) {
|
|
698
698
|
return status === "paused" && stopReason === AWAITING_CI_REASON;
|
|
699
699
|
}
|
|
700
|
+
function isAwaitingBudget(status, stopReason) {
|
|
701
|
+
return (status === "paused" || status === "blocked") && !!stopReason?.startsWith(AWAITING_BUDGET_REASON);
|
|
702
|
+
}
|
|
700
703
|
function statusMeta(status) {
|
|
701
704
|
return STATUS_META[status] ?? FALLBACK;
|
|
702
705
|
}
|
|
@@ -710,7 +713,7 @@ function statusTuples() {
|
|
|
710
713
|
}
|
|
711
714
|
return out;
|
|
712
715
|
}
|
|
713
|
-
var STATUS_META, AWAITING_CI_REASON, FALLBACK, TERMINAL_STATUSES;
|
|
716
|
+
var STATUS_META, AWAITING_CI_REASON, AWAITING_BUDGET_REASON, FALLBACK, TERMINAL_STATUSES;
|
|
714
717
|
var init_status = __esm({
|
|
715
718
|
"../swarm-core/src/status.ts"() {
|
|
716
719
|
"use strict";
|
|
@@ -735,6 +738,7 @@ var init_status = __esm({
|
|
|
735
738
|
returned: { pill: "returned", label: "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443", color: "--warning", terminal: true }
|
|
736
739
|
};
|
|
737
740
|
AWAITING_CI_REASON = "\u0436\u0434\u0451\u0442 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 CI \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 PR \u0435\u0449\u0451 \u0438\u0434\u0443\u0442";
|
|
741
|
+
AWAITING_BUDGET_REASON = "\u0436\u0434\u0451\u0442 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043F\u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0443 \u2014 \u0434\u043E\u0448\u0451\u043B \u0434\u043E \u043F\u043E\u0442\u043E\u043B\u043A\u0430 \u0442\u0440\u0430\u0442";
|
|
738
742
|
FALLBACK = STATUS_META.queued;
|
|
739
743
|
TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
740
744
|
(s) => STATUS_META[s].terminal
|
|
@@ -1343,6 +1347,7 @@ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
|
|
|
1343
1347
|
var init_runGuards = __esm({
|
|
1344
1348
|
"../swarm-core/src/runGuards.ts"() {
|
|
1345
1349
|
"use strict";
|
|
1350
|
+
init_status();
|
|
1346
1351
|
DEFAULT_RUN_LIMITS = {
|
|
1347
1352
|
maxCostUsd: 20,
|
|
1348
1353
|
runTimeoutMs: 8 * 60 * 6e4,
|
|
@@ -1456,6 +1461,29 @@ function grantedCeilingUsd(events) {
|
|
|
1456
1461
|
}
|
|
1457
1462
|
return max;
|
|
1458
1463
|
}
|
|
1464
|
+
function budgetApprovedText(ceilingUsd, answer) {
|
|
1465
|
+
return `${BUDGET_APPROVED_MARK} (\u043F\u043E\u0442\u043E\u043B\u043E\u043A $${ceilingUsd.toFixed(2)}) \u2014 \u043E\u0442\u0432\u0435\u0442: \xAB${answer.trim()}\xBB`;
|
|
1466
|
+
}
|
|
1467
|
+
function isBudgetApprovedEvent(e) {
|
|
1468
|
+
return has(e, BUDGET_APPROVED_MARK);
|
|
1469
|
+
}
|
|
1470
|
+
function approvedCeilingUsd(events) {
|
|
1471
|
+
let max = null;
|
|
1472
|
+
for (const e of events) {
|
|
1473
|
+
if (!isBudgetApprovedEvent(e)) continue;
|
|
1474
|
+
const m = /потолок \$([0-9]+(?:\.[0-9]+)?)/.exec(e.text ?? "");
|
|
1475
|
+
if (!m) continue;
|
|
1476
|
+
const v = Number(m[1]);
|
|
1477
|
+
if (Number.isFinite(v) && (max == null || v > max)) max = v;
|
|
1478
|
+
}
|
|
1479
|
+
return max;
|
|
1480
|
+
}
|
|
1481
|
+
function nextApprovedCeilingUsd(baseUsd, events, spentUsd = 0) {
|
|
1482
|
+
if (!(baseUsd > 0)) return null;
|
|
1483
|
+
const from = Math.max(effectiveCostCeilingUsd(baseUsd, events), Number.isFinite(spentUsd) ? spentUsd : 0);
|
|
1484
|
+
const next = Math.round(Math.min(from + baseUsd * BUDGET_APPROVAL_STEP, baseUsd * BUDGET_APPROVAL_MAX_FACTOR) * 100) / 100;
|
|
1485
|
+
return next > from ? next : null;
|
|
1486
|
+
}
|
|
1459
1487
|
function countAcceptanceChecks(events) {
|
|
1460
1488
|
return events.filter((e) => has(e, ACCEPTANCE_CHECK_MARK)).length;
|
|
1461
1489
|
}
|
|
@@ -1472,14 +1500,17 @@ function hasOpenHeavyClaim(events) {
|
|
|
1472
1500
|
}
|
|
1473
1501
|
function effectiveCostCeilingUsd(baseUsd, events) {
|
|
1474
1502
|
if (!(baseUsd > 0)) return baseUsd;
|
|
1503
|
+
let ceiling = baseUsd;
|
|
1475
1504
|
const granted = grantedCeilingUsd(events);
|
|
1476
|
-
if (granted
|
|
1477
|
-
|
|
1505
|
+
if (granted != null) ceiling = Math.max(ceiling, Math.min(granted, baseUsd * HEAVY_CEILING_MAX_FACTOR));
|
|
1506
|
+
const approved = approvedCeilingUsd(events);
|
|
1507
|
+
if (approved != null) ceiling = Math.max(ceiling, Math.min(approved, baseUsd * BUDGET_APPROVAL_MAX_FACTOR));
|
|
1508
|
+
return ceiling;
|
|
1478
1509
|
}
|
|
1479
1510
|
function intakeHoldReason(heavyLabel) {
|
|
1480
1511
|
return `\u0442\u044F\u0436\u0451\u043B\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 (${heavyLabel}) \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u043F\u043E\u043A\u0430 \u043E\u043D \u043D\u0435 \u0441\u0434\u0430\u0441\u0442: \u0435\u043C\u0443 \u043D\u0443\u0436\u043D\u043E \u043C\u0435\u0441\u0442\u043E, \u0430 \u043D\u0435 \u0441\u043E\u0441\u0435\u0434\u0438 (TASWM-535)`;
|
|
1481
1512
|
}
|
|
1482
|
-
var DEFAULT_WEIGHT_THRESHOLDS, HEAVY_CLAIM_MIN_SPENT_RATIO, HEAVY_CEILING_STEP, HEAVY_CEILING_MAX_FACTOR, HEAVY_CLAIM_MARK, HEAVY_GRANT_MARK, HEAVY_REFUSED_MARK, ACCEPTANCE_CHECK_MARK, has;
|
|
1513
|
+
var DEFAULT_WEIGHT_THRESHOLDS, HEAVY_CLAIM_MIN_SPENT_RATIO, HEAVY_CEILING_STEP, HEAVY_CEILING_MAX_FACTOR, HEAVY_CLAIM_MARK, HEAVY_GRANT_MARK, HEAVY_REFUSED_MARK, ACCEPTANCE_CHECK_MARK, has, BUDGET_APPROVAL_STEP, BUDGET_APPROVAL_MAX_FACTOR, BUDGET_APPROVED_MARK;
|
|
1483
1514
|
var init_taskWeight = __esm({
|
|
1484
1515
|
"../swarm-core/src/taskWeight.ts"() {
|
|
1485
1516
|
"use strict";
|
|
@@ -1496,6 +1527,9 @@ var init_taskWeight = __esm({
|
|
|
1496
1527
|
HEAVY_REFUSED_MARK = "\u0432\u0435\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430: \u043F\u0440\u043E\u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F \u043D\u0435\u0442";
|
|
1497
1528
|
ACCEPTANCE_CHECK_MARK = "\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u043F\u0440\u0438\u0451\u043C\u043A\u0438";
|
|
1498
1529
|
has = (e, mark) => !!e.text?.includes(mark);
|
|
1530
|
+
BUDGET_APPROVAL_STEP = 0.5;
|
|
1531
|
+
BUDGET_APPROVAL_MAX_FACTOR = 3;
|
|
1532
|
+
BUDGET_APPROVED_MARK = "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E";
|
|
1499
1533
|
}
|
|
1500
1534
|
});
|
|
1501
1535
|
|
|
@@ -7040,7 +7074,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
7040
7074
|
).run(answer, by, question_id);
|
|
7041
7075
|
const q = this.getQuestion(question_id);
|
|
7042
7076
|
if (q && q.status === "answered") {
|
|
7043
|
-
this.
|
|
7077
|
+
const run = this.getRun(q.run_id);
|
|
7078
|
+
if (!(run && isAwaitingBudget(run.status, run.stop_reason))) this.updateRun(q.run_id, { status: "running" });
|
|
7044
7079
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
7045
7080
|
}
|
|
7046
7081
|
return q;
|
|
@@ -7676,11 +7711,16 @@ function parseLogRequests(raw) {
|
|
|
7676
7711
|
}
|
|
7677
7712
|
return out;
|
|
7678
7713
|
}
|
|
7714
|
+
var LIVE_STATUSES = Object.keys(STATUS_META).filter((s) => !STATUS_META[s].terminal);
|
|
7715
|
+
var LIVE_SWEEP_MAX_PAGES = 10;
|
|
7679
7716
|
var SwarmClient = class _SwarmClient {
|
|
7680
7717
|
call;
|
|
7681
7718
|
defaultRepo;
|
|
7682
7719
|
autoFlush;
|
|
7683
7720
|
listLimit;
|
|
7721
|
+
liveSweepMs;
|
|
7722
|
+
liveSweptAt = null;
|
|
7723
|
+
// TASWM-589: когда живые за окном добраны целиком
|
|
7684
7724
|
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
7685
7725
|
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
7686
7726
|
repoFullNames;
|
|
@@ -7796,6 +7836,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7796
7836
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
7797
7837
|
this.autoFlush = opts.autoFlush ?? true;
|
|
7798
7838
|
this.listLimit = opts.listLimit ?? 200;
|
|
7839
|
+
this.liveSweepMs = opts.liveSweepMs ?? 6e4;
|
|
7799
7840
|
const registry = opts.repoRegistry ?? [];
|
|
7800
7841
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
7801
7842
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
@@ -8545,7 +8586,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
8545
8586
|
q.answer = answer;
|
|
8546
8587
|
q.answered_by = by;
|
|
8547
8588
|
q.answered_at = stamp();
|
|
8548
|
-
this.
|
|
8589
|
+
const run = this.runs.get(q.run_id);
|
|
8590
|
+
if (!(run && isAwaitingBudget(run.status, run.stop_reason))) this.updateRun(q.run_id, { status: "running" });
|
|
8549
8591
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
8550
8592
|
}
|
|
8551
8593
|
return q ? { ...q } : null;
|
|
@@ -8760,9 +8802,60 @@ var SwarmClient = class _SwarmClient {
|
|
|
8760
8802
|
}
|
|
8761
8803
|
if (res?.isError) return;
|
|
8762
8804
|
const runs = res.structuredContent?.runs ?? [];
|
|
8805
|
+
const seen = new Set(runs.map((pr) => String(pr.id ?? "")));
|
|
8806
|
+
for (const pr of await this.liveBeyondWindow()) {
|
|
8807
|
+
const id = String(pr.id ?? "");
|
|
8808
|
+
if (seen.has(id)) continue;
|
|
8809
|
+
seen.add(id);
|
|
8810
|
+
runs.push(pr);
|
|
8811
|
+
}
|
|
8763
8812
|
for (const pr of runs) this.hydrateRun(pr);
|
|
8764
8813
|
for (const pr of runs) this.linkRefs(pr);
|
|
8765
8814
|
}
|
|
8815
|
+
/**
|
|
8816
|
+
* TASWM-589: ЖИВЫЕ прогоны за окном гидрации. Окно — `limit` последних по updatedAt, общее на
|
|
8817
|
+
* все репозитории владельца, и в плотный день оно заполняется за часы. Прогон, которого никто
|
|
8818
|
+
* не трогал, из окна выпадает; пока демон жив, это не страшно (зеркало его помнит), но после
|
|
8819
|
+
* рестарта его в зеркале нет — и демон не знает, что его надо стартовать. 29.09 так простоял
|
|
8820
|
+
* ANS-314: демон перезапустили в 13:43 UTC, а после его заведения в 09:12 завели ещё 316
|
|
8821
|
+
* прогонов. `wake_run` по десятку таких же отвечал «активного прогона … у меня нет».
|
|
8822
|
+
*
|
|
8823
|
+
* Поэтому нетерминальные прогоны тянем выборкой по статусу, постранично и без окна. Живых
|
|
8824
|
+
* обычно десятки, а не сотни, но выборка тяжёлая (лента событий в каждой строке), и на
|
|
8825
|
+
* каждом периодическом re-hydrate (раз в 15 с) она не нужна: пока демон жив, прогон попадает
|
|
8826
|
+
* в зеркало, когда его заводят или трогают, — то есть через окно. Добор нужен на старте и как
|
|
8827
|
+
* страховка, поэтому он идёт при первой гидрации и дальше не чаще раза в `liveSweepMs`.
|
|
8828
|
+
*
|
|
8829
|
+
* Fail-open: прод отказал по какому-то статусу — отдаём, что успели, и не засчитываем добор,
|
|
8830
|
+
* чтобы повторить его на следующей гидрации, а не через минуту. Граница машины (isMine) и
|
|
8831
|
+
* слияние с уже известными прогонами — обычный hydrateRun.
|
|
8832
|
+
*/
|
|
8833
|
+
async liveBeyondWindow() {
|
|
8834
|
+
const now = this.now();
|
|
8835
|
+
if (this.liveSweptAt != null && now - this.liveSweptAt < this.liveSweepMs) return [];
|
|
8836
|
+
const byStatus = await Promise.all(LIVE_STATUSES.map((status) => this.listAllByStatus(status)));
|
|
8837
|
+
if (byStatus.every((rows) => rows !== null)) this.liveSweptAt = now;
|
|
8838
|
+
return byStatus.flatMap((rows) => rows ?? []);
|
|
8839
|
+
}
|
|
8840
|
+
/** Все прогоны одного статуса, страницами по 200. null — прод не ответил. */
|
|
8841
|
+
async listAllByStatus(status) {
|
|
8842
|
+
const out = [];
|
|
8843
|
+
let cursor = null;
|
|
8844
|
+
for (let page = 0; page < LIVE_SWEEP_MAX_PAGES; page++) {
|
|
8845
|
+
let res;
|
|
8846
|
+
try {
|
|
8847
|
+
res = await this.call("swarm_list_runs", { status, limit: 200, ...cursor ? { cursor } : {} });
|
|
8848
|
+
} catch {
|
|
8849
|
+
return null;
|
|
8850
|
+
}
|
|
8851
|
+
if (!res || res.isError) return null;
|
|
8852
|
+
out.push(...res.structuredContent?.runs ?? []);
|
|
8853
|
+
const next = res.structuredContent?.nextCursor;
|
|
8854
|
+
if (typeof next !== "string" || !next) break;
|
|
8855
|
+
cursor = next;
|
|
8856
|
+
}
|
|
8857
|
+
return out;
|
|
8858
|
+
}
|
|
8766
8859
|
/**
|
|
8767
8860
|
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
8768
8861
|
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
@@ -8798,6 +8891,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
8798
8891
|
let alive = 0;
|
|
8799
8892
|
let dead = 0;
|
|
8800
8893
|
for (const d of this.runs.values()) {
|
|
8894
|
+
if (isAwaitingBudget(d.status, d.stop_reason) && d.resolves_run_id == null && d.reviews_run_id == null && d.verifies_run_id == null) {
|
|
8895
|
+
if (this.runtime.get(d.id)?.worktree_path == null) {
|
|
8896
|
+
const wt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
8897
|
+
if (wtExists(wt)) this.runtime.set(d.id, { pid: null, worktree_path: wt });
|
|
8898
|
+
}
|
|
8899
|
+
continue;
|
|
8900
|
+
}
|
|
8801
8901
|
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
8802
8902
|
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
8803
8903
|
const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
@@ -12493,7 +12593,7 @@ function parkReasonUnretriable(reason) {
|
|
|
12493
12593
|
// package.json
|
|
12494
12594
|
var package_default = {
|
|
12495
12595
|
name: "@taskless-app/swarm",
|
|
12496
|
-
version: "0.6.
|
|
12596
|
+
version: "0.6.56",
|
|
12497
12597
|
type: "module",
|
|
12498
12598
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12499
12599
|
keywords: [
|
|
@@ -12544,7 +12644,7 @@ var package_default = {
|
|
|
12544
12644
|
execa: "^10.0.0",
|
|
12545
12645
|
zod: "3.25.76"
|
|
12546
12646
|
},
|
|
12547
|
-
gitHead: "
|
|
12647
|
+
gitHead: "1922a9b8079c0af2b2b8a9b5fc8756b0218f7446"
|
|
12548
12648
|
};
|
|
12549
12649
|
|
|
12550
12650
|
// src/version.ts
|
|
@@ -18527,7 +18627,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
|
|
|
18527
18627
|
var CHANGES_FIX_COST_MARK = "\u0442\u0440\u0430\u0442\u044B \u0434\u043E \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438";
|
|
18528
18628
|
var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
|
|
18529
18629
|
var BUDGET_STOP_REASON = "\u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430";
|
|
18530
|
-
var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON];
|
|
18630
|
+
var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON, AWAITING_BUDGET_REASON];
|
|
18531
18631
|
var REVIEW_NO_VERDICT_REASON = "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430: \u043D\u0438 approve, \u043D\u0438 changes \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434 (TASWM-375)";
|
|
18532
18632
|
function verdictCommentMark(createdAt) {
|
|
18533
18633
|
return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
|
|
@@ -18993,7 +19093,7 @@ async function rescueReviewlessPrs(deps, stormActive, now) {
|
|
|
18993
19093
|
noteReviewBlocked(deps, run, stop);
|
|
18994
19094
|
continue;
|
|
18995
19095
|
}
|
|
18996
|
-
const authorGone = AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
|
|
19096
|
+
const authorGone = !isAwaitingBudget(run.status, run.stop_reason) && AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
|
|
18997
19097
|
if (authorGone) {
|
|
18998
19098
|
const st = await deps.prStatus(run.pr_url).catch(() => null);
|
|
18999
19099
|
const liveness = prLiveness(st);
|
|
@@ -21087,10 +21187,120 @@ async function handleBudgetExhausted(deps, run, ctx) {
|
|
|
21087
21187
|
narrate(deps, `${droneLabel(run)} (\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440) \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0430\u0435\u0442 \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 PR.`);
|
|
21088
21188
|
return;
|
|
21089
21189
|
}
|
|
21190
|
+
const base3 = budgetBaseUsd(deps);
|
|
21191
|
+
const events = deps.db.events(run.id);
|
|
21192
|
+
const next = run.verifies_run_id == null ? nextApprovedCeilingUsd(base3, events, spentUsd) : null;
|
|
21193
|
+
if (next != null) {
|
|
21194
|
+
const ceiling = effectiveCostCeilingUsd(base3, events);
|
|
21195
|
+
askBudget(deps, run, `${AWAITING_BUDGET_REASON} (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent} \u043F\u0440\u0438 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 $${ceiling.toFixed(2)})`, budgetQuestionText({ spentUsd, ceilingUsd: ceiling, nextUsd: next, prUrl: run.pr_url }));
|
|
21196
|
+
deps.db.addEvent(run.id, "paused", `${AWAITING_BUDGET_REASON} \u2014 \u0441\u043F\u0440\u043E\u0441\u0438\u043B, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043B\u0438; worktree \u0446\u0435\u043B, \u0436\u0434\u0443 \u043E\u0442\u0432\u0435\u0442\u0430`);
|
|
21197
|
+
narrate(deps, `${droneLabel(run)} \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0441\u043F\u0440\u043E\u0441\u0438\u043B, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043B\u0438: \xAB\u0434\u0430\xBB \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0434\u043E $${next.toFixed(2)}.`);
|
|
21198
|
+
return;
|
|
21199
|
+
}
|
|
21090
21200
|
deps.db.updateRun(run.id, { status: "paused", stop_reason: reason });
|
|
21091
21201
|
deps.db.addEvent(run.id, "paused", `${reason} \u2014 worktree \u0446\u0435\u043B, \u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u044F \u043D\u0435\u0442: \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0431\u044E\u0434\u0436\u0435\u0442\u0430 \u0443\u0436\u0435 \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u0442`);
|
|
21092
21202
|
narrate(deps, `${droneLabel(run)} \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 worktree \u0446\u0435\u043B\u0430, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C.`);
|
|
21093
21203
|
}
|
|
21204
|
+
var BUDGET_QUESTION_PREFIX = "\u0423\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442";
|
|
21205
|
+
function isBudgetQuestion(text) {
|
|
21206
|
+
return text.startsWith(BUDGET_QUESTION_PREFIX);
|
|
21207
|
+
}
|
|
21208
|
+
function budgetQuestionText(q, unclearAnswer) {
|
|
21209
|
+
return [
|
|
21210
|
+
unclearAnswer != null ? `${BUDGET_QUESTION_PREFIX}: \u043E\u0442\u0432\u0435\u0442 \xAB${unclearAnswer}\xBB \u043D\u0435 \u043F\u043E\u043D\u044F\u043B, \u043D\u0443\u0436\u0435\u043D \u044F\u0441\u043D\u044B\u0439 \xAB\u0434\u0430\xBB \u0438\u043B\u0438 \xAB\u043D\u0435\u0442\xBB. \u041F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E $${q.spentUsd.toFixed(2)} \u043F\u0440\u0438 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 $${q.ceilingUsd.toFixed(2)}.` : `${BUDGET_QUESTION_PREFIX}: \u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E $${q.spentUsd.toFixed(2)} \u043F\u0440\u0438 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 $${q.ceilingUsd.toFixed(2)}, \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442.`,
|
|
21211
|
+
q.prUrl ? `PR \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442: ${q.prUrl}.` : "\u0420\u0430\u0431\u043E\u0442\u0430 \u0432 worktree \u0446\u0435\u043B\u0430.",
|
|
21212
|
+
`\u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C? \u041E\u0442\u0432\u0435\u0442\u044C \xAB\u0434\u0430\xBB \u2014 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0441\u0442\u0430\u043D\u0435\u0442 $${q.nextUsd.toFixed(2)}, \u0438 \u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0443 \u0441 \u0442\u043E\u0433\u043E \u0436\u0435 \u043C\u0435\u0441\u0442\u0430. \xAB\u041D\u0435\u0442\xBB \u2014 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u044E\u0441\u044C.`
|
|
21213
|
+
].join(" ");
|
|
21214
|
+
}
|
|
21215
|
+
var BUDGET_YES_WORDS = /* @__PURE__ */ new Set(["\u0434\u0430", "\u0430\u0433\u0430", "\u0443\u0433\u0443", "\u043A\u043E\u043D\u0435\u0447\u043D\u043E", "\u0441\u043E\u0433\u043B\u0430\u0441\u0435\u043D", "\u0441\u043E\u0433\u043B\u0430\u0441\u043D\u0430", "\u0440\u0430\u0437\u0440\u0435\u0448\u0430\u044E", "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439", "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439\u0442\u0435", "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C", "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438", "\u0434\u0430\u0432\u0430\u0439", "\u043E\u043A", "\u043E\u043A\u0435\u0439", "ok", "okay", "yes", "yep", "continue", "go", "+"]);
|
|
21216
|
+
var BUDGET_NO_WORDS = /* @__PURE__ */ new Set(["\u043D\u0435\u0442", "\u043D\u0435", "no", "nope", "\u0441\u0442\u043E\u043F", "stop", "\u0445\u0432\u0430\u0442\u0438\u0442", "\u043E\u0442\u043C\u0435\u043D\u0430", "\u043E\u0442\u043C\u0435\u043D\u044F\u0439", "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438", "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C", "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043C\u0441\u044F", "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C", "-"]);
|
|
21217
|
+
function budgetAnswer(answer) {
|
|
21218
|
+
const words = answer.toLowerCase().replace(/[«»"'“”„.,!?;:()]/g, " ").split(/\s+/u).filter(Boolean);
|
|
21219
|
+
if (words.length === 0) return "unclear";
|
|
21220
|
+
const hasNo = words.some((w) => BUDGET_NO_WORDS.has(w));
|
|
21221
|
+
if (BUDGET_YES_WORDS.has(words[0]) && !hasNo) return "yes";
|
|
21222
|
+
if (BUDGET_NO_WORDS.has(words[0])) return "no";
|
|
21223
|
+
return "unclear";
|
|
21224
|
+
}
|
|
21225
|
+
function budgetBaseUsd(deps) {
|
|
21226
|
+
return deps.maxCostUsd?.() ?? envCapUsd(process.env);
|
|
21227
|
+
}
|
|
21228
|
+
function askBudget(deps, run, awaitingReason, question) {
|
|
21229
|
+
deps.db.updateRun(run.id, { status: "paused", stop_reason: awaitingReason, pid: null });
|
|
21230
|
+
if (!deps.db.questionsFor(run.id).some((q) => q.status === "open" && isBudgetQuestion(q.text))) {
|
|
21231
|
+
deps.db.ask(run.id, question);
|
|
21232
|
+
deps.db.updateRun(run.id, { status: "paused", stop_reason: awaitingReason });
|
|
21233
|
+
}
|
|
21234
|
+
}
|
|
21235
|
+
var BUDGET_SYNC_MS = 3e4;
|
|
21236
|
+
var budgetSyncAt = /* @__PURE__ */ new Map();
|
|
21237
|
+
async function answerBudgetQuestions(deps, ctx) {
|
|
21238
|
+
const nowMs = deps.now ? deps.now() : Date.now();
|
|
21239
|
+
for (const run of deps.db.listRuns("paused")) {
|
|
21240
|
+
if (!isAwaitingBudget(run.status, run.stop_reason)) continue;
|
|
21241
|
+
if (deps.db.syncQuestions && nowMs - (budgetSyncAt.get(run.id) ?? 0) >= BUDGET_SYNC_MS) {
|
|
21242
|
+
budgetSyncAt.set(run.id, nowMs);
|
|
21243
|
+
await deps.db.syncQuestions(run.id);
|
|
21244
|
+
}
|
|
21245
|
+
const q = deps.db.questionsFor(run.id).filter((x) => isBudgetQuestion(x.text)).sort((a, b) => a.id - b.id).at(-1);
|
|
21246
|
+
if (!q || q.status !== "answered" || q.answer == null) continue;
|
|
21247
|
+
const answer = q.answer.trim();
|
|
21248
|
+
const spent = `$${run.cost_usd.toFixed(2)}`;
|
|
21249
|
+
const verdict = budgetAnswer(answer);
|
|
21250
|
+
if (verdict === "no") {
|
|
21251
|
+
const reason = `${BUDGET_STOP_REASON} (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent}) \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u043B\u0438`;
|
|
21252
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
21253
|
+
});
|
|
21254
|
+
deps.db.cancel(run.id, `${reason}: \xAB${answer}\xBB`);
|
|
21255
|
+
budgetSyncAt.delete(run.id);
|
|
21256
|
+
narrate(deps, `${droneLabel(run)}: \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 \u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B\u0438 \xAB${answer}\xBB \u2014 \u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E (${spent}).`);
|
|
21257
|
+
continue;
|
|
21258
|
+
}
|
|
21259
|
+
const base3 = budgetBaseUsd(deps);
|
|
21260
|
+
const events = deps.db.events(run.id);
|
|
21261
|
+
if (verdict === "unclear") {
|
|
21262
|
+
const next = nextApprovedCeilingUsd(base3, events, run.cost_usd);
|
|
21263
|
+
if (next != null) {
|
|
21264
|
+
const ceiling = effectiveCostCeilingUsd(base3, events);
|
|
21265
|
+
askBudget(deps, run, run.stop_reason, budgetQuestionText({ spentUsd: run.cost_usd, ceilingUsd: ceiling, nextUsd: next, prUrl: run.pr_url }, answer));
|
|
21266
|
+
}
|
|
21267
|
+
continue;
|
|
21268
|
+
}
|
|
21269
|
+
if (!deps.respawn || ctx.stormActive) continue;
|
|
21270
|
+
if (deps.resourcesOk && !deps.resourcesOk()) continue;
|
|
21271
|
+
if (countActiveDrones(deps.db) >= ctx.droneCap) continue;
|
|
21272
|
+
if (!await projectQuotaGateAllows(deps, run)) continue;
|
|
21273
|
+
const askedMs = stampMs(q.asked_at);
|
|
21274
|
+
const granted = events.some((e) => isBudgetApprovedEvent(e) && stampMs(e.at) >= askedMs);
|
|
21275
|
+
if (!granted) {
|
|
21276
|
+
const next = nextApprovedCeilingUsd(base3, events, run.cost_usd);
|
|
21277
|
+
if (next == null) {
|
|
21278
|
+
const reason = `${BUDGET_STOP_REASON} (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent})`;
|
|
21279
|
+
deps.db.updateRun(run.id, { status: "paused", stop_reason: reason });
|
|
21280
|
+
deps.db.addEvent(run.id, "paused", `${reason} \u2014 \u043D\u0430 \xAB\u0434\u0430\xBB \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C \u0443\u0436\u0435 \u043D\u0435\u043A\u0443\u0434\u0430: \u043F\u0440\u0435\u0434\u0435\u043B \u0442\u0440\u0451\u0445 \u0431\u0430\u0437\u043E\u0432\u044B\u0445 \u043F\u043E\u0442\u043E\u043B\u043A\u043E\u0432, \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C`);
|
|
21281
|
+
continue;
|
|
21282
|
+
}
|
|
21283
|
+
deps.db.addEvent(run.id, "note", budgetApprovedText(next, answer));
|
|
21284
|
+
}
|
|
21285
|
+
const woken = await wakeRun(
|
|
21286
|
+
{
|
|
21287
|
+
db: deps.db,
|
|
21288
|
+
isAlive: deps.isAlive,
|
|
21289
|
+
respawn: deps.respawn,
|
|
21290
|
+
...deps.worktreeExists ? { worktreeExists: deps.worktreeExists } : {},
|
|
21291
|
+
...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
|
|
21292
|
+
narrate: (m) => narrate(deps, m)
|
|
21293
|
+
},
|
|
21294
|
+
run.id
|
|
21295
|
+
);
|
|
21296
|
+
if (woken.ok) {
|
|
21297
|
+
budgetSyncAt.delete(run.id);
|
|
21298
|
+
continue;
|
|
21299
|
+
}
|
|
21300
|
+
const note = `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u043F\u043E \u043E\u0442\u0432\u0435\u0442\u0443 \xAB\u0434\u0430\xBB \u043D\u0435 \u0432\u044B\u0448\u043B\u0430: ${woken.why} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u044E`;
|
|
21301
|
+
if (deps.db.events(run.id).at(-1)?.text !== note) deps.db.addEvent(run.id, "step", note);
|
|
21302
|
+
}
|
|
21303
|
+
}
|
|
21094
21304
|
function buildEscalationCandidates(db) {
|
|
21095
21305
|
const candidates = [];
|
|
21096
21306
|
for (const run of db.listRuns("review")) {
|
|
@@ -21761,6 +21971,7 @@ async function tickOnce(deps) {
|
|
|
21761
21971
|
for (const run of deps.db.listRuns("paused")) {
|
|
21762
21972
|
if (run.kind === "hero") continue;
|
|
21763
21973
|
if (run.stop_reason === AWAITING_CI_REASON || isReliefPause(run.stop_reason)) continue;
|
|
21974
|
+
if (isAwaitingBudget(run.status, run.stop_reason)) continue;
|
|
21764
21975
|
const last = lastActivityMs(deps.db, run);
|
|
21765
21976
|
if (now - last <= STALL_FLOOR_MS) continue;
|
|
21766
21977
|
const evs = deps.db.events(run.id);
|
|
@@ -21809,6 +22020,11 @@ async function tickOnce(deps) {
|
|
|
21809
22020
|
} catch (e) {
|
|
21810
22021
|
console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
|
|
21811
22022
|
}
|
|
22023
|
+
try {
|
|
22024
|
+
await answerBudgetQuestions(deps, { stormActive, droneCap });
|
|
22025
|
+
} catch (e) {
|
|
22026
|
+
console.error("[overmind] \u0440\u0430\u0437\u0431\u043E\u0440 \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
22027
|
+
}
|
|
21812
22028
|
if (deps.anomalies && !deps.control?.paused) {
|
|
21813
22029
|
const az = deps.anomalies;
|
|
21814
22030
|
let ready = [];
|
|
@@ -23742,6 +23958,7 @@ export {
|
|
|
23742
23958
|
VERIFY_SPAWN_LIMIT,
|
|
23743
23959
|
WATCHDOG_PARK_REASON,
|
|
23744
23960
|
abortReason,
|
|
23961
|
+
budgetAnswer,
|
|
23745
23962
|
buildChangesFixPrompt,
|
|
23746
23963
|
buildCiReadyNudge,
|
|
23747
23964
|
buildCiWakeNudge,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.56",
|
|
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": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"execa": "^10.0.0",
|
|
40
40
|
"zod": "3.25.76"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "1922a9b8079c0af2b2b8a9b5fc8756b0218f7446",
|
|
43
43
|
"scripts": {
|
|
44
44
|
"overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
|
|
45
45
|
"swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
|