@taskless-app/swarm 0.6.54 → 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 +103 -9
- package/dist/overmind.js +232 -11
- 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
|
|
@@ -1211,7 +1215,11 @@ var init_modelPricing = __esm({
|
|
|
1211
1215
|
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1212
1216
|
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1213
1217
|
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
1214
|
-
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1218
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
|
|
1219
|
+
// OpenAI (TAS-773) — модель по умолчанию для воркспейсов на своём ключе OpenAI.
|
|
1220
|
+
// 0.40/1.60, cached input $0.10 → чтение из кеша 25 % входа, а не общие 10 %.
|
|
1221
|
+
// Без рассуждений: весь max_tokens уходит в ответ, выход до 32 768 токенов.
|
|
1222
|
+
"gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6, cacheReadFactor: 0.25 }
|
|
1215
1223
|
};
|
|
1216
1224
|
CACHE_READ_FACTOR = 0.1;
|
|
1217
1225
|
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
@@ -1228,6 +1236,7 @@ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
|
|
|
1228
1236
|
var init_runGuards = __esm({
|
|
1229
1237
|
"../swarm-core/src/runGuards.ts"() {
|
|
1230
1238
|
"use strict";
|
|
1239
|
+
init_status();
|
|
1231
1240
|
DEFAULT_RUN_LIMITS = {
|
|
1232
1241
|
maxCostUsd: 20,
|
|
1233
1242
|
runTimeoutMs: 8 * 60 * 6e4,
|
|
@@ -1253,19 +1262,38 @@ function grantedCeilingUsd(events) {
|
|
|
1253
1262
|
}
|
|
1254
1263
|
return max;
|
|
1255
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
|
+
}
|
|
1256
1279
|
function effectiveCostCeilingUsd(baseUsd, events) {
|
|
1257
1280
|
if (!(baseUsd > 0)) return baseUsd;
|
|
1281
|
+
let ceiling = baseUsd;
|
|
1258
1282
|
const granted = grantedCeilingUsd(events);
|
|
1259
|
-
if (granted
|
|
1260
|
-
|
|
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;
|
|
1261
1287
|
}
|
|
1262
|
-
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;
|
|
1263
1289
|
var init_taskWeight = __esm({
|
|
1264
1290
|
"../swarm-core/src/taskWeight.ts"() {
|
|
1265
1291
|
"use strict";
|
|
1266
1292
|
HEAVY_CEILING_MAX_FACTOR = 2;
|
|
1267
1293
|
HEAVY_GRANT_MARK = "\u0432\u0435\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430: \u0442\u044F\u0436\u0451\u043B\u0430\u044F";
|
|
1268
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";
|
|
1269
1297
|
}
|
|
1270
1298
|
});
|
|
1271
1299
|
|
|
@@ -3492,7 +3520,7 @@ var init_package = __esm({
|
|
|
3492
3520
|
"package.json"() {
|
|
3493
3521
|
package_default = {
|
|
3494
3522
|
name: "@taskless-app/swarm",
|
|
3495
|
-
version: "0.6.
|
|
3523
|
+
version: "0.6.56",
|
|
3496
3524
|
type: "module",
|
|
3497
3525
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3498
3526
|
keywords: [
|
|
@@ -3543,7 +3571,7 @@ var init_package = __esm({
|
|
|
3543
3571
|
execa: "^10.0.0",
|
|
3544
3572
|
zod: "3.25.76"
|
|
3545
3573
|
},
|
|
3546
|
-
gitHead: "
|
|
3574
|
+
gitHead: "1922a9b8079c0af2b2b8a9b5fc8756b0218f7446"
|
|
3547
3575
|
};
|
|
3548
3576
|
}
|
|
3549
3577
|
});
|
|
@@ -5593,7 +5621,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5593
5621
|
).run(answer, by, question_id);
|
|
5594
5622
|
const q = this.getQuestion(question_id);
|
|
5595
5623
|
if (q && q.status === "answered") {
|
|
5596
|
-
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" });
|
|
5597
5626
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
5598
5627
|
}
|
|
5599
5628
|
return q;
|
|
@@ -7427,11 +7456,16 @@ function parseLogRequests(raw) {
|
|
|
7427
7456
|
}
|
|
7428
7457
|
return out;
|
|
7429
7458
|
}
|
|
7459
|
+
var LIVE_STATUSES = Object.keys(STATUS_META).filter((s) => !STATUS_META[s].terminal);
|
|
7460
|
+
var LIVE_SWEEP_MAX_PAGES = 10;
|
|
7430
7461
|
var SwarmClient = class _SwarmClient {
|
|
7431
7462
|
call;
|
|
7432
7463
|
defaultRepo;
|
|
7433
7464
|
autoFlush;
|
|
7434
7465
|
listLimit;
|
|
7466
|
+
liveSweepMs;
|
|
7467
|
+
liveSweptAt = null;
|
|
7468
|
+
// TASWM-589: когда живые за окном добраны целиком
|
|
7435
7469
|
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
7436
7470
|
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
7437
7471
|
repoFullNames;
|
|
@@ -7547,6 +7581,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7547
7581
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
7548
7582
|
this.autoFlush = opts.autoFlush ?? true;
|
|
7549
7583
|
this.listLimit = opts.listLimit ?? 200;
|
|
7584
|
+
this.liveSweepMs = opts.liveSweepMs ?? 6e4;
|
|
7550
7585
|
const registry = opts.repoRegistry ?? [];
|
|
7551
7586
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
7552
7587
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
@@ -8296,7 +8331,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
8296
8331
|
q.answer = answer;
|
|
8297
8332
|
q.answered_by = by;
|
|
8298
8333
|
q.answered_at = stamp();
|
|
8299
|
-
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" });
|
|
8300
8336
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
8301
8337
|
}
|
|
8302
8338
|
return q ? { ...q } : null;
|
|
@@ -8511,9 +8547,60 @@ var SwarmClient = class _SwarmClient {
|
|
|
8511
8547
|
}
|
|
8512
8548
|
if (res?.isError) return;
|
|
8513
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
|
+
}
|
|
8514
8557
|
for (const pr of runs) this.hydrateRun(pr);
|
|
8515
8558
|
for (const pr of runs) this.linkRefs(pr);
|
|
8516
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
|
+
}
|
|
8517
8604
|
/**
|
|
8518
8605
|
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
8519
8606
|
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
@@ -8549,6 +8636,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
8549
8636
|
let alive = 0;
|
|
8550
8637
|
let dead = 0;
|
|
8551
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
|
+
}
|
|
8552
8646
|
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
8553
8647
|
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
8554
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
|
|
@@ -1322,7 +1326,11 @@ var init_modelPricing = __esm({
|
|
|
1322
1326
|
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1323
1327
|
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1324
1328
|
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
1325
|
-
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1329
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
|
|
1330
|
+
// OpenAI (TAS-773) — модель по умолчанию для воркспейсов на своём ключе OpenAI.
|
|
1331
|
+
// 0.40/1.60, cached input $0.10 → чтение из кеша 25 % входа, а не общие 10 %.
|
|
1332
|
+
// Без рассуждений: весь max_tokens уходит в ответ, выход до 32 768 токенов.
|
|
1333
|
+
"gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6, cacheReadFactor: 0.25 }
|
|
1326
1334
|
};
|
|
1327
1335
|
CACHE_READ_FACTOR = 0.1;
|
|
1328
1336
|
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
@@ -1339,6 +1347,7 @@ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
|
|
|
1339
1347
|
var init_runGuards = __esm({
|
|
1340
1348
|
"../swarm-core/src/runGuards.ts"() {
|
|
1341
1349
|
"use strict";
|
|
1350
|
+
init_status();
|
|
1342
1351
|
DEFAULT_RUN_LIMITS = {
|
|
1343
1352
|
maxCostUsd: 20,
|
|
1344
1353
|
runTimeoutMs: 8 * 60 * 6e4,
|
|
@@ -1452,6 +1461,29 @@ function grantedCeilingUsd(events) {
|
|
|
1452
1461
|
}
|
|
1453
1462
|
return max;
|
|
1454
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
|
+
}
|
|
1455
1487
|
function countAcceptanceChecks(events) {
|
|
1456
1488
|
return events.filter((e) => has(e, ACCEPTANCE_CHECK_MARK)).length;
|
|
1457
1489
|
}
|
|
@@ -1468,14 +1500,17 @@ function hasOpenHeavyClaim(events) {
|
|
|
1468
1500
|
}
|
|
1469
1501
|
function effectiveCostCeilingUsd(baseUsd, events) {
|
|
1470
1502
|
if (!(baseUsd > 0)) return baseUsd;
|
|
1503
|
+
let ceiling = baseUsd;
|
|
1471
1504
|
const granted = grantedCeilingUsd(events);
|
|
1472
|
-
if (granted
|
|
1473
|
-
|
|
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;
|
|
1474
1509
|
}
|
|
1475
1510
|
function intakeHoldReason(heavyLabel) {
|
|
1476
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)`;
|
|
1477
1512
|
}
|
|
1478
|
-
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;
|
|
1479
1514
|
var init_taskWeight = __esm({
|
|
1480
1515
|
"../swarm-core/src/taskWeight.ts"() {
|
|
1481
1516
|
"use strict";
|
|
@@ -1492,6 +1527,9 @@ var init_taskWeight = __esm({
|
|
|
1492
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";
|
|
1493
1528
|
ACCEPTANCE_CHECK_MARK = "\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u043F\u0440\u0438\u0451\u043C\u043A\u0438";
|
|
1494
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";
|
|
1495
1533
|
}
|
|
1496
1534
|
});
|
|
1497
1535
|
|
|
@@ -7036,7 +7074,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
7036
7074
|
).run(answer, by, question_id);
|
|
7037
7075
|
const q = this.getQuestion(question_id);
|
|
7038
7076
|
if (q && q.status === "answered") {
|
|
7039
|
-
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" });
|
|
7040
7079
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
7041
7080
|
}
|
|
7042
7081
|
return q;
|
|
@@ -7672,11 +7711,16 @@ function parseLogRequests(raw) {
|
|
|
7672
7711
|
}
|
|
7673
7712
|
return out;
|
|
7674
7713
|
}
|
|
7714
|
+
var LIVE_STATUSES = Object.keys(STATUS_META).filter((s) => !STATUS_META[s].terminal);
|
|
7715
|
+
var LIVE_SWEEP_MAX_PAGES = 10;
|
|
7675
7716
|
var SwarmClient = class _SwarmClient {
|
|
7676
7717
|
call;
|
|
7677
7718
|
defaultRepo;
|
|
7678
7719
|
autoFlush;
|
|
7679
7720
|
listLimit;
|
|
7721
|
+
liveSweepMs;
|
|
7722
|
+
liveSweptAt = null;
|
|
7723
|
+
// TASWM-589: когда живые за окном добраны целиком
|
|
7680
7724
|
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
7681
7725
|
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
7682
7726
|
repoFullNames;
|
|
@@ -7792,6 +7836,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7792
7836
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
7793
7837
|
this.autoFlush = opts.autoFlush ?? true;
|
|
7794
7838
|
this.listLimit = opts.listLimit ?? 200;
|
|
7839
|
+
this.liveSweepMs = opts.liveSweepMs ?? 6e4;
|
|
7795
7840
|
const registry = opts.repoRegistry ?? [];
|
|
7796
7841
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
7797
7842
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
@@ -8541,7 +8586,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
8541
8586
|
q.answer = answer;
|
|
8542
8587
|
q.answered_by = by;
|
|
8543
8588
|
q.answered_at = stamp();
|
|
8544
|
-
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" });
|
|
8545
8591
|
this.addEvent(q.run_id, "answered", answer, by);
|
|
8546
8592
|
}
|
|
8547
8593
|
return q ? { ...q } : null;
|
|
@@ -8756,9 +8802,60 @@ var SwarmClient = class _SwarmClient {
|
|
|
8756
8802
|
}
|
|
8757
8803
|
if (res?.isError) return;
|
|
8758
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
|
+
}
|
|
8759
8812
|
for (const pr of runs) this.hydrateRun(pr);
|
|
8760
8813
|
for (const pr of runs) this.linkRefs(pr);
|
|
8761
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
|
+
}
|
|
8762
8859
|
/**
|
|
8763
8860
|
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
8764
8861
|
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
@@ -8794,6 +8891,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
8794
8891
|
let alive = 0;
|
|
8795
8892
|
let dead = 0;
|
|
8796
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
|
+
}
|
|
8797
8901
|
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
8798
8902
|
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
8799
8903
|
const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
@@ -12489,7 +12593,7 @@ function parkReasonUnretriable(reason) {
|
|
|
12489
12593
|
// package.json
|
|
12490
12594
|
var package_default = {
|
|
12491
12595
|
name: "@taskless-app/swarm",
|
|
12492
|
-
version: "0.6.
|
|
12596
|
+
version: "0.6.56",
|
|
12493
12597
|
type: "module",
|
|
12494
12598
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12495
12599
|
keywords: [
|
|
@@ -12540,7 +12644,7 @@ var package_default = {
|
|
|
12540
12644
|
execa: "^10.0.0",
|
|
12541
12645
|
zod: "3.25.76"
|
|
12542
12646
|
},
|
|
12543
|
-
gitHead: "
|
|
12647
|
+
gitHead: "1922a9b8079c0af2b2b8a9b5fc8756b0218f7446"
|
|
12544
12648
|
};
|
|
12545
12649
|
|
|
12546
12650
|
// src/version.ts
|
|
@@ -18523,7 +18627,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
|
|
|
18523
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";
|
|
18524
18628
|
var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
|
|
18525
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";
|
|
18526
|
-
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];
|
|
18527
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)";
|
|
18528
18632
|
function verdictCommentMark(createdAt) {
|
|
18529
18633
|
return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
|
|
@@ -18989,7 +19093,7 @@ async function rescueReviewlessPrs(deps, stormActive, now) {
|
|
|
18989
19093
|
noteReviewBlocked(deps, run, stop);
|
|
18990
19094
|
continue;
|
|
18991
19095
|
}
|
|
18992
|
-
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)));
|
|
18993
19097
|
if (authorGone) {
|
|
18994
19098
|
const st = await deps.prStatus(run.pr_url).catch(() => null);
|
|
18995
19099
|
const liveness = prLiveness(st);
|
|
@@ -21083,10 +21187,120 @@ async function handleBudgetExhausted(deps, run, ctx) {
|
|
|
21083
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.`);
|
|
21084
21188
|
return;
|
|
21085
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
|
+
}
|
|
21086
21200
|
deps.db.updateRun(run.id, { status: "paused", stop_reason: reason });
|
|
21087
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`);
|
|
21088
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.`);
|
|
21089
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
|
+
}
|
|
21090
21304
|
function buildEscalationCandidates(db) {
|
|
21091
21305
|
const candidates = [];
|
|
21092
21306
|
for (const run of db.listRuns("review")) {
|
|
@@ -21757,6 +21971,7 @@ async function tickOnce(deps) {
|
|
|
21757
21971
|
for (const run of deps.db.listRuns("paused")) {
|
|
21758
21972
|
if (run.kind === "hero") continue;
|
|
21759
21973
|
if (run.stop_reason === AWAITING_CI_REASON || isReliefPause(run.stop_reason)) continue;
|
|
21974
|
+
if (isAwaitingBudget(run.status, run.stop_reason)) continue;
|
|
21760
21975
|
const last = lastActivityMs(deps.db, run);
|
|
21761
21976
|
if (now - last <= STALL_FLOOR_MS) continue;
|
|
21762
21977
|
const evs = deps.db.events(run.id);
|
|
@@ -21805,6 +22020,11 @@ async function tickOnce(deps) {
|
|
|
21805
22020
|
} catch (e) {
|
|
21806
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);
|
|
21807
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
|
+
}
|
|
21808
22028
|
if (deps.anomalies && !deps.control?.paused) {
|
|
21809
22029
|
const az = deps.anomalies;
|
|
21810
22030
|
let ready = [];
|
|
@@ -23738,6 +23958,7 @@ export {
|
|
|
23738
23958
|
VERIFY_SPAWN_LIMIT,
|
|
23739
23959
|
WATCHDOG_PARK_REASON,
|
|
23740
23960
|
abortReason,
|
|
23961
|
+
budgetAnswer,
|
|
23741
23962
|
buildChangesFixPrompt,
|
|
23742
23963
|
buildCiReadyNudge,
|
|
23743
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",
|