@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 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 == null) return baseUsd;
1264
- 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;
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.55",
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: "fbb55fb91a238ba4f209f841f45cc08f4f79c454"
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.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" });
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.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" });
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 == null) return baseUsd;
1477
- 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;
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.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" });
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.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" });
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.55",
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: "fbb55fb91a238ba4f209f841f45cc08f4f79c454"
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.55",
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": "fbb55fb91a238ba4f209f841f45cc08f4f79c454",
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",