@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 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 == null) return baseUsd;
1260
- return Math.min(Math.max(baseUsd, granted), baseUsd * HEAVY_CEILING_MAX_FACTOR);
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.54",
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: "3b1e278b9e6191317bd081fe0d649abb2cef0e60"
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.updateRun(q.run_id, { status: "running" });
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.updateRun(q.run_id, { status: "running" });
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 == null) return baseUsd;
1473
- return Math.min(Math.max(baseUsd, granted), baseUsd * HEAVY_CEILING_MAX_FACTOR);
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.updateRun(q.run_id, { status: "running" });
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.updateRun(q.run_id, { status: "running" });
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.54",
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: "3b1e278b9e6191317bd081fe0d649abb2cef0e60"
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.54",
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": "3b1e278b9e6191317bd081fe0d649abb2cef0e60",
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",