@i14ks/ccv 0.15.3 → 0.16.1

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.
Files changed (27) hide show
  1. package/bundle/ccv.mjs +296 -40
  2. package/package.json +50 -50
  3. package/web/assets/{codemirror-Dg4vvH8i.js → codemirror-XwUmM20V.js} +1 -1
  4. package/web/assets/{index-DB0qYzJa.js → index-B3KPQ0Jq.js} +1 -1
  5. package/web/assets/{index-CTrrHYKx.js → index-B3X6gKS4.js} +1 -1
  6. package/web/assets/{index-D04xl5g2.js → index-B3ZG7Mgh.js} +1 -1
  7. package/web/assets/{index-DMpA08Ie.js → index-B3mW0ScK.js} +1 -1
  8. package/web/assets/{index-Bpw3TKRV.js → index-BAMLmkAm.js} +1 -1
  9. package/web/assets/{index-Bg6okyOc.js → index-BBdH0WyS.js} +1 -1
  10. package/web/assets/{index-CM3XoNnk.js → index-BCeVhzZf.js} +1 -1
  11. package/web/assets/{index-0iOvZWuA.js → index-BiGxRIQL.js} +1 -1
  12. package/web/assets/{index-CRyJJS40.js → index-Bjmeb0mL.js} +1 -1
  13. package/web/assets/{index-B96oAtZO.js → index-C3lLfrzk.js} +1 -1
  14. package/web/assets/index-CPM-bl8z.css +1 -0
  15. package/web/assets/{index-ZEjcSNDk.js → index-CWJg4zA3.js} +1 -1
  16. package/web/assets/{index-CanbdA5V.js → index-CXp-N-a3.js} +1 -1
  17. package/web/assets/{index-kBFqQTK1.js → index-CdFloc-n.js} +1 -1
  18. package/web/assets/{index-LHO2W5bU.js → index-Ceqv0JQi.js} +1 -1
  19. package/web/assets/{index-SxQ8qp4S.js → index-Cu0rli8K.js} +1 -1
  20. package/web/assets/{index-CrC2iAsw.js → index-DaN46jfw.js} +1 -1
  21. package/web/assets/{index-CB6_9alP.js → index-EhM5ESQH.js} +1 -1
  22. package/web/assets/{index-CzbpsO2-.js → index-PcpAtAyR.js} +1 -1
  23. package/web/assets/index-_rsPujXt.js +134 -0
  24. package/web/assets/{index-CNi4N4PL.js → index-byQstcIo.js} +1 -1
  25. package/web/index.html +2 -2
  26. package/web/assets/index-Bo84aMH8.js +0 -134
  27. package/web/assets/index-COiPHc3p.css +0 -1
package/bundle/ccv.mjs CHANGED
@@ -1112,6 +1112,8 @@ var USAGE_MIN_INTERVAL_MS = 2e4;
1112
1112
  var HANDOFF_TOOL_DENIAL = "\u041B\u0438\u043C\u0438\u0442 \u043F\u043B\u0430\u043D\u0430 \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D, \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u044B. \u041E\u0442\u0432\u0435\u0442\u044C \u0445\u044D\u043D\u0434\u043E\u0444\u0444\u043E\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C, \u0431\u0435\u0437 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432.";
1113
1113
  var USAGE_TURN_INTERVAL_MS = 3e4;
1114
1114
  var INTERRUPT_SETTLE_MS = 15e3;
1115
+ var TASK_STOP_TIMEOUT_MS = 5e3;
1116
+ var TASK_WAKE_MS = 5e3;
1115
1117
  var CONTEXT_MIN_INTERVAL_MS = 5e3;
1116
1118
  var ClaudeSession = class extends EventEmitter {
1117
1119
  id;
@@ -1176,6 +1178,39 @@ var ClaudeSession = class extends EventEmitter {
1176
1178
  * `#setState` делает это сам, см. там же.
1177
1179
  */
1178
1180
  #backgroundWork = /* @__PURE__ */ new Set();
1181
+ /**
1182
+ * Запущенные задачи основного разговора — по `system/task_*` (см.
1183
+ * `SessionTask`), в порядке запуска.
1184
+ *
1185
+ * Края (`task_started` / `task_notification`) могут потеряться, поэтому
1186
+ * список ещё и подрезается: по `background_tasks_changed` уходят фоновые,
1187
+ * которых нет в уровне, по `result` основного хода — задачи переднего
1188
+ * плана, а перезапуск процесса CLI обнуляет его целиком.
1189
+ */
1190
+ #tasks = /* @__PURE__ */ new Map();
1191
+ /**
1192
+ * Живые фоновые задачи по `background_tasks_changed` — уровень, а не края:
1193
+ * каждое событие заменяет набор целиком. В отличие от `#backgroundWork`,
1194
+ * новый ход его не трогает — фоновый агент переживает любой ход.
1195
+ *
1196
+ * `null` — этот процесс CLI уровня ещё не присылал (или не умеет вовсе);
1197
+ * тогда о фоновой работе судит `#backgroundWork`.
1198
+ */
1199
+ #backgroundLevel = null;
1200
+ /**
1201
+ * toolUseId вызовов, сделанных сабагентами. `owned_by_subagent` CLI ставит
1202
+ * не всем их задачам — команда агента на переднем плане приходит без него, —
1203
+ * поэтому вложенность узнаём ещё и по тому, чей вызов задачу запустил.
1204
+ */
1205
+ #subagentToolUses = /* @__PURE__ */ new Set();
1206
+ /** Отложенная рассылка сводки по `task_progress` — он приходит на каждый инструмент агента. */
1207
+ #taskSummaryTimer = null;
1208
+ /**
1209
+ * Когда сессия в последний раз работала — шла ход или крутились задачи.
1210
+ * Сторожу лимита: порог мог перейти между опросами, и сессия, которая
1211
+ * только что его перешла, к моменту проверки уже может простаивать.
1212
+ */
1213
+ #lastActiveAt = 0;
1179
1214
  #capabilities = null;
1180
1215
  #pendingPermissions = /* @__PURE__ */ new Map();
1181
1216
  #model = null;
@@ -1342,7 +1377,10 @@ var ClaudeSession = class extends EventEmitter {
1342
1377
  title: this.#title,
1343
1378
  state: this.#state,
1344
1379
  lastTurn: this.#lastTurn,
1345
- backgroundWork: this.#backgroundWork.size > 0,
1380
+ backgroundWork: this.#backgroundLevel ? this.#backgroundLevel.size > 0 : this.#backgroundWork.size > 0,
1381
+ // Команда на переднем плане — это просто идущий вызов инструмента, он и
1382
+ // так виден в ленте; задачей она становится, только уйдя в фон.
1383
+ tasks: [...this.#tasks.values()].filter((t) => t.kind !== "shell" || t.background),
1346
1384
  model: this.#model,
1347
1385
  permissionMode: this.#permissionMode,
1348
1386
  effort: this.#effort,
@@ -1445,6 +1483,7 @@ var ClaudeSession = class extends EventEmitter {
1445
1483
  options2.allowedTools = READ_ONLY_TOOLS;
1446
1484
  options2.strictMcpConfig = true;
1447
1485
  }
1486
+ this.#resetTasks();
1448
1487
  const q = query({ prompt: this.#inbox, options: options2 });
1449
1488
  this.#query = q;
1450
1489
  void this.#consume(q);
@@ -1565,6 +1604,7 @@ var ClaudeSession = class extends EventEmitter {
1565
1604
  } catch {
1566
1605
  }
1567
1606
  this.#query = null;
1607
+ this.#resetTasks();
1568
1608
  }
1569
1609
  // -------------------------------------------------------------------------
1570
1610
  // Переподключение
@@ -1621,8 +1661,8 @@ var ClaudeSession = class extends EventEmitter {
1621
1661
  if (this.#closing || this.#reconnectTimer)
1622
1662
  return;
1623
1663
  this.#teardown("\u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
1624
- const delay = RECONNECT_DELAYS_MS[this.#reconnectAttempt];
1625
- if (delay === void 0) {
1664
+ const delay2 = RECONNECT_DELAYS_MS[this.#reconnectAttempt];
1665
+ if (delay2 === void 0) {
1626
1666
  this.#pendingRetry = [];
1627
1667
  this.#record({
1628
1668
  kind: "error",
@@ -1634,13 +1674,13 @@ var ClaudeSession = class extends EventEmitter {
1634
1674
  this.#reconnectAttempt += 1;
1635
1675
  this.#record({
1636
1676
  kind: "error",
1637
- message: `\u0421\u0432\u044F\u0437\u044C \u0441 Claude \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 ${Math.round(delay / 1e3)} \u0441 (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${this.#reconnectAttempt} \u0438\u0437 ${RECONNECT_DELAYS_MS.length}).`
1677
+ message: `\u0421\u0432\u044F\u0437\u044C \u0441 Claude \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 ${Math.round(delay2 / 1e3)} \u0441 (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${this.#reconnectAttempt} \u0438\u0437 ${RECONNECT_DELAYS_MS.length}).`
1638
1678
  });
1639
1679
  this.#setState("reconnecting");
1640
1680
  this.#reconnectTimer = setTimeout(() => {
1641
1681
  this.#reconnectTimer = null;
1642
1682
  this.#restart(reason);
1643
- }, delay);
1683
+ }, delay2);
1644
1684
  this.#reconnectTimer.unref?.();
1645
1685
  }
1646
1686
  // -------------------------------------------------------------------------
@@ -1782,23 +1822,52 @@ var ClaudeSession = class extends EventEmitter {
1782
1822
  this.#interruptRequested = true;
1783
1823
  await this.#query?.interrupt();
1784
1824
  }
1825
+ /** Когда сессия в последний раз работала (см. `#lastActiveAt`); идущий ход — сейчас. */
1826
+ get lastActiveAt() {
1827
+ return BUSY_STATES.has(this.#state) || this.#tasks.size > 0 ? Date.now() : this.#lastActiveAt;
1828
+ }
1829
+ /**
1830
+ * Остановить одну задачу. Из списка она уйдёт по уведомлению CLI
1831
+ * (`task_notification` со `stopped`), а не здесь: не остановилась — пусть
1832
+ * и видно будет, что работает.
1833
+ */
1834
+ async stopTask(taskId) {
1835
+ await this.#query?.stopTask(taskId);
1836
+ }
1837
+ /**
1838
+ * Остановить все задачи сессии — и из списка, и из уровня фоновых (там же
1839
+ * и вложенные). `interrupt()` фоновых агентов не трогает: они переживают
1840
+ * ход. `true` — было что останавливать.
1841
+ */
1842
+ async stopTasks() {
1843
+ const q = this.#query;
1844
+ const ids = /* @__PURE__ */ new Set([...this.#tasks.keys(), ...this.#backgroundLevel ?? []]);
1845
+ if (!q || ids.size === 0)
1846
+ return false;
1847
+ await Promise.all([...ids].map((id) => Promise.race([q.stopTask(id), delay(TASK_STOP_TIMEOUT_MS)]).catch(() => void 0)));
1848
+ return true;
1849
+ }
1785
1850
  /**
1786
- * Ход хэндоффа у лимита: прервать текущий ход (вместе с субагентами), затем
1787
- * отправить служебный `prompt` с запретом на любые инструменты и вернуть
1788
- * текст ответа. `null` — хода не вышло: ошибка, пустой ответ или ход не
1789
- * кончился за `timeoutMs`; тогда хэндофф собирают из журнала.
1851
+ * Ход хэндоффа: остановить задачи сессии, прервать текущий ход (вместе с
1852
+ * субагентами переднего плана), затем отправить служебный `prompt` с
1853
+ * запретом на любые инструменты и вернуть текст ответа. `null` — хода не
1854
+ * вышло: ошибка, пустой ответ, ход не кончился за `timeoutMs` или `prompt`
1855
+ * не задан (спрашивать агента бессмысленно — например, окно уже исчерпано);
1856
+ * тогда хэндофф собирают из журнала.
1790
1857
  */
1791
1858
  async runHandoff(prompt, timeoutMs) {
1792
1859
  if (this.#closing)
1793
1860
  return null;
1794
1861
  this.#handoffMode = true;
1795
1862
  try {
1863
+ if (await this.stopTasks())
1864
+ await this.#waitForState("running", TASK_WAKE_MS);
1796
1865
  if (this.#state === "running" || this.#state === "waiting_permission") {
1797
1866
  const settled = this.#nextTurnEnd(INTERRUPT_SETTLE_MS);
1798
1867
  await this.interrupt().catch(() => void 0);
1799
1868
  await settled;
1800
1869
  }
1801
- if (this.#closing || !this.#query || this.#state !== "idle")
1870
+ if (!prompt || this.#closing || !this.#query || this.#state !== "idle")
1802
1871
  return null;
1803
1872
  const reply = this.#nextTurnEnd(timeoutMs);
1804
1873
  this.sendPrompt(prompt, void 0, true);
@@ -1816,6 +1885,25 @@ var ClaudeSession = class extends EventEmitter {
1816
1885
  this.#handoffMode = false;
1817
1886
  }
1818
1887
  }
1888
+ /** Дождаться состояния `state` не дольше `timeoutMs`; `true` — дождались. */
1889
+ #waitForState(state, timeoutMs) {
1890
+ if (this.#state === state)
1891
+ return Promise.resolve(true);
1892
+ return new Promise((resolve) => {
1893
+ const onSummary = (summary) => {
1894
+ if (summary.state !== state)
1895
+ return;
1896
+ clearTimeout(timer);
1897
+ this.off("summary", onSummary);
1898
+ resolve(true);
1899
+ };
1900
+ const timer = setTimeout(() => {
1901
+ this.off("summary", onSummary);
1902
+ resolve(false);
1903
+ }, timeoutMs);
1904
+ this.on("summary", onSummary);
1905
+ });
1906
+ }
1819
1907
  /** Ближайший конец хода (`result`) или `null`, если не дождались. */
1820
1908
  #nextTurnEnd(timeoutMs) {
1821
1909
  return new Promise((resolve) => {
@@ -2208,7 +2296,7 @@ var ClaudeSession = class extends EventEmitter {
2208
2296
  \u0421\u0435\u0441\u0441\u0438\u044E \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u043B Claude Code, ${whence}: ${this.#executable.path}. \u0415\u0441\u043B\u0438 \u0434\u0435\u043B\u043E \u0432 \u043D\u0451\u043C, CCV_CLAUDE_PATH \u0443\u043A\u0430\u0436\u0435\u0442 \u0434\u0440\u0443\u0433\u043E\u0439, \u0430 CCV_BUNDLED_CLAUDE=1 \u0432\u0435\u0440\u043D\u0451\u0442 ccv \u043D\u0430 \u0431\u0438\u043D\u0430\u0440\u043D\u0438\u043A \u0438\u0437 Agent SDK.`;
2209
2297
  }
2210
2298
  #handleMessage(message) {
2211
- if (this.#state === "idle" && (message.type === "assistant" || message.type === "stream_event")) {
2299
+ if (this.#state === "idle" && (message.type === "assistant" || message.type === "stream_event") && message.parent_tool_use_id === null) {
2212
2300
  this.#setState("running");
2213
2301
  }
2214
2302
  switch (message.type) {
@@ -2242,6 +2330,14 @@ var ClaudeSession = class extends EventEmitter {
2242
2330
  });
2243
2331
  if (isBackgroundLaunch(block.input))
2244
2332
  this.#backgroundWork.add(block.id);
2333
+ if (message.parent_tool_use_id !== null) {
2334
+ this.#subagentToolUses.add(block.id);
2335
+ if (this.#subagentToolUses.size > 256) {
2336
+ const oldest = this.#subagentToolUses.values().next().value;
2337
+ if (oldest !== void 0)
2338
+ this.#subagentToolUses.delete(oldest);
2339
+ }
2340
+ }
2245
2341
  }
2246
2342
  }
2247
2343
  break;
@@ -2298,6 +2394,10 @@ var ClaudeSession = class extends EventEmitter {
2298
2394
  }
2299
2395
  }
2300
2396
  this.#reconnectAttempt = 0;
2397
+ for (const [id, task] of this.#tasks) {
2398
+ if (!task.background && !this.#backgroundLevel?.has(id))
2399
+ this.#tasks.delete(id);
2400
+ }
2301
2401
  const alreadyIdle = this.#state === "idle";
2302
2402
  this.#setState("idle");
2303
2403
  if (alreadyIdle)
@@ -2351,9 +2451,120 @@ var ClaudeSession = class extends EventEmitter {
2351
2451
  void this.refreshContext(true);
2352
2452
  return;
2353
2453
  }
2454
+ if (this.#handleTask(message))
2455
+ return;
2354
2456
  const { type: _type, subtype, ...rest } = message;
2355
2457
  this.#record({ kind: "system", subtype, data: rest });
2356
2458
  }
2459
+ /**
2460
+ * События задач CLI → `#tasks` и `#backgroundLevel`. `true` — событие
2461
+ * разобрано и в журнал не идёт; `task_started` и `task_notification`
2462
+ * (редкие края) журнал по-прежнему получает.
2463
+ *
2464
+ * Типы SDK тут не помогают: подтипов в союзе `SDKMessage` больше, чем
2465
+ * объявлено, и поля вроде `owned_by_subagent` в них не описаны — читаем
2466
+ * как есть и проверяем форму сами.
2467
+ */
2468
+ #handleTask(message) {
2469
+ const subtype = message.subtype;
2470
+ const taskId = typeof message.task_id === "string" ? message.task_id : null;
2471
+ switch (subtype) {
2472
+ case "task_started": {
2473
+ this.#lastActiveAt = Date.now();
2474
+ const toolUseId = str(message.tool_use_id);
2475
+ if (!taskId || message.owned_by_subagent === true || toolUseId !== null && this.#subagentToolUses.has(toolUseId)) {
2476
+ return false;
2477
+ }
2478
+ this.#tasks.set(taskId, {
2479
+ id: taskId,
2480
+ toolUseId,
2481
+ kind: taskKind(str(message.task_type) ?? (message.subagent_type ? "local_agent" : null)),
2482
+ agentType: str(message.subagent_type),
2483
+ description: str(message.description) ?? "",
2484
+ activity: null,
2485
+ lastTool: null,
2486
+ background: message.is_backgrounded === true || this.#backgroundLevel?.has(taskId) === true,
2487
+ startedAt: Date.now(),
2488
+ tokens: 0,
2489
+ toolUses: 0
2490
+ });
2491
+ this.emit("summary", this.summary());
2492
+ return false;
2493
+ }
2494
+ case "task_progress": {
2495
+ this.#lastActiveAt = Date.now();
2496
+ if (Date.now() - this.#usageAt >= USAGE_TURN_INTERVAL_MS)
2497
+ void this.refreshUsage();
2498
+ const task = taskId ? this.#tasks.get(taskId) : void 0;
2499
+ if (!task)
2500
+ return true;
2501
+ const usage = message.usage ?? {};
2502
+ task.activity = str(message.summary) ?? str(message.description) ?? task.activity;
2503
+ task.lastTool = str(message.last_tool_name) ?? task.lastTool;
2504
+ if (typeof usage.total_tokens === "number")
2505
+ task.tokens = usage.total_tokens;
2506
+ if (typeof usage.tool_uses === "number")
2507
+ task.toolUses = usage.tool_uses;
2508
+ this.#emitTasksSoon();
2509
+ return true;
2510
+ }
2511
+ case "task_updated": {
2512
+ const task = taskId ? this.#tasks.get(taskId) : void 0;
2513
+ if (!task)
2514
+ return true;
2515
+ const patch = message.patch ?? {};
2516
+ const status = patch.status;
2517
+ if (status === "completed" || status === "failed" || status === "killed") {
2518
+ this.#tasks.delete(task.id);
2519
+ } else {
2520
+ if (typeof patch.is_backgrounded === "boolean")
2521
+ task.background = patch.is_backgrounded;
2522
+ if (typeof patch.description === "string")
2523
+ task.description = patch.description;
2524
+ }
2525
+ this.emit("summary", this.summary());
2526
+ return true;
2527
+ }
2528
+ case "task_notification":
2529
+ this.#lastActiveAt = Date.now();
2530
+ if (taskId && this.#tasks.delete(taskId))
2531
+ this.emit("summary", this.summary());
2532
+ return false;
2533
+ case "background_tasks_changed": {
2534
+ this.#lastActiveAt = Date.now();
2535
+ const live = Array.isArray(message.tasks) ? message.tasks.flatMap((t) => typeof t.task_id === "string" ? [t.task_id] : []) : [];
2536
+ this.#backgroundLevel = new Set(live);
2537
+ for (const [id, task] of this.#tasks) {
2538
+ if (this.#backgroundLevel.has(id))
2539
+ task.background = true;
2540
+ else if (task.background)
2541
+ this.#tasks.delete(id);
2542
+ }
2543
+ this.emit("summary", this.summary());
2544
+ return true;
2545
+ }
2546
+ default:
2547
+ return false;
2548
+ }
2549
+ }
2550
+ /** Сводка по прогрессу задач — не чаще раза в секунду. */
2551
+ #emitTasksSoon() {
2552
+ if (this.#taskSummaryTimer)
2553
+ return;
2554
+ this.#taskSummaryTimer = setTimeout(() => {
2555
+ this.#taskSummaryTimer = null;
2556
+ this.emit("summary", this.summary());
2557
+ }, 1e3);
2558
+ }
2559
+ #resetTasks() {
2560
+ this.#tasks.clear();
2561
+ this.#backgroundLevel = null;
2562
+ this.#backgroundWork.clear();
2563
+ this.#subagentToolUses.clear();
2564
+ if (this.#taskSummaryTimer)
2565
+ clearTimeout(this.#taskSummaryTimer);
2566
+ this.#taskSummaryTimer = null;
2567
+ }
2357
2568
  /**
2358
2569
  * Слэш-команды и модели — то, из чего UI строит меню «/» и выбор модели.
2359
2570
  *
@@ -2464,6 +2675,8 @@ var ClaudeSession = class extends EventEmitter {
2464
2675
  #setState(state) {
2465
2676
  if (this.#state === state)
2466
2677
  return;
2678
+ if (BUSY_STATES.has(this.#state) || BUSY_STATES.has(state))
2679
+ this.#lastActiveAt = Date.now();
2467
2680
  this.#state = state;
2468
2681
  if (state === "running")
2469
2682
  this.#backgroundWork.clear();
@@ -2471,6 +2684,26 @@ var ClaudeSession = class extends EventEmitter {
2471
2684
  this.emit("summary", this.summary());
2472
2685
  }
2473
2686
  };
2687
+ var BUSY_STATES = /* @__PURE__ */ new Set(["running", "waiting_permission"]);
2688
+ function delay(ms) {
2689
+ return new Promise((resolve) => setTimeout(resolve, ms));
2690
+ }
2691
+ function str(value) {
2692
+ return typeof value === "string" && value ? value : null;
2693
+ }
2694
+ function taskKind(type) {
2695
+ switch (type) {
2696
+ case "local_agent":
2697
+ case "remote_agent":
2698
+ return "agent";
2699
+ case "local_bash":
2700
+ return "shell";
2701
+ case "local_workflow":
2702
+ return "workflow";
2703
+ default:
2704
+ return "other";
2705
+ }
2706
+ }
2474
2707
  function describe(error) {
2475
2708
  return error instanceof Error ? error.message : String(error);
2476
2709
  }
@@ -2850,6 +3083,7 @@ var AntigravitySession = class extends EventEmitter2 {
2850
3083
  state: this.#state,
2851
3084
  lastTurn: this.#lastTurn,
2852
3085
  backgroundWork: false,
3086
+ tasks: [],
2853
3087
  model: this.#record.model,
2854
3088
  permissionMode: this.#record.permissionMode,
2855
3089
  effort: this.#record.effort,
@@ -4397,20 +4631,20 @@ var GitHubAccount = class {
4397
4631
  auto_init: false
4398
4632
  }
4399
4633
  });
4400
- const cloneUrl = str(created.clone_url);
4634
+ const cloneUrl = str2(created.clone_url);
4401
4635
  if (!cloneUrl)
4402
4636
  throw new Error("GitHub \u0441\u043E\u0437\u0434\u0430\u043B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0430\u0434\u0440\u0435\u0441 \u0434\u043B\u044F git");
4403
4637
  return {
4404
4638
  cloneUrl,
4405
- htmlUrl: str(created.html_url) || cloneUrl.replace(/\.git$/, ""),
4406
- fullName: str(created.full_name) || `${options2.owner}/${options2.name}`
4639
+ htmlUrl: str2(created.html_url) || cloneUrl.replace(/\.git$/, ""),
4640
+ fullName: str2(created.full_name) || `${options2.owner}/${options2.name}`
4407
4641
  };
4408
4642
  }
4409
4643
  // ---------------------------------------------------------------- внутреннее
4410
4644
  /** Логин владельца токена; заодно это и есть проверка токена на живость. */
4411
4645
  async #login(token) {
4412
4646
  const user = await this.#api(token, "/user");
4413
- const login = str(user.login);
4647
+ const login = str2(user.login);
4414
4648
  if (!login)
4415
4649
  throw new Error("GitHub \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u0443\u0447\u0451\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u2014 \u0442\u043E\u043A\u0435\u043D \u043D\u0435 \u043F\u043E\u0434\u043E\u0448\u0451\u043B");
4416
4650
  return login;
@@ -4471,9 +4705,9 @@ function apiError(status, body) {
4471
4705
  let details = "";
4472
4706
  try {
4473
4707
  const parsed = JSON.parse(body);
4474
- message = str(parsed.message);
4708
+ message = str2(parsed.message);
4475
4709
  if (Array.isArray(parsed.errors)) {
4476
- details = parsed.errors.map((item) => str(item.message)).filter(Boolean).join("; ");
4710
+ details = parsed.errors.map((item) => str2(item.message)).filter(Boolean).join("; ");
4477
4711
  }
4478
4712
  } catch {
4479
4713
  message = body.slice(0, 200).trim();
@@ -4492,7 +4726,7 @@ function apiError(status, body) {
4492
4726
  return `GitHub \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${status}${text ? `: ${text}` : ""}`;
4493
4727
  }
4494
4728
  }
4495
- function str(value) {
4729
+ function str2(value) {
4496
4730
  return typeof value === "string" ? value : "";
4497
4731
  }
4498
4732
  function describe4(error) {
@@ -5712,6 +5946,7 @@ var HandoffStore = class {
5712
5946
  return path18.join(this.#dir, `${sessionId}.json`);
5713
5947
  }
5714
5948
  };
5949
+ var SAME_WINDOW_MS = 5 * 6e4;
5715
5950
  var LimitStopState = class {
5716
5951
  #fired = {};
5717
5952
  #storePath;
@@ -5731,9 +5966,19 @@ var LimitStopState = class {
5731
5966
  this.#fired = {};
5732
5967
  }
5733
5968
  }
5734
- /** Окно `limit` уже сработало и ещё не сбросилось. */
5735
- fired(limit, now = Date.now()) {
5736
- return (this.#fired[limit] ?? 0) > now;
5969
+ /**
5970
+ * Окно `limit` со сроком сброса `resets` уже сработало и ещё не сбросилось.
5971
+ *
5972
+ * Срок сверяется с допуском: окно со сроком сброса дальше `SAME_WINDOW_MS`
5973
+ * от записанного — уже другое, даже если прежний срок не наступил (так
5974
+ * 7.10 новое окно до 21:20 пропустили, приняв его за сработавшее до 20:30).
5975
+ * `null` — срок не разобрать, сверять не с чем, решает только записанный.
5976
+ */
5977
+ fired(limit, resets, now = Date.now()) {
5978
+ const until = this.#fired[limit] ?? 0;
5979
+ if (until <= now)
5980
+ return false;
5981
+ return resets === null || Math.abs(resets - until) <= SAME_WINDOW_MS;
5737
5982
  }
5738
5983
  /** Запомнить срабатывание до `until` (момент сброса окна). */
5739
5984
  async mark(limit, until) {
@@ -5746,6 +5991,7 @@ var LimitStopState = class {
5746
5991
  // ../host/dist/limit-guard.js
5747
5992
  var HANDOFF_TIMEOUT_MS = 3 * 6e4;
5748
5993
  var LIMIT_SWEEP_MS = 6e4;
5994
+ var LIMIT_RECENT_MS = 2 * LIMIT_SWEEP_MS;
5749
5995
  var WINDOW_SPAN_MS = {
5750
5996
  fiveHour: 5 * 60 * 6e4,
5751
5997
  sevenDay: 7 * 24 * 60 * 6e4
@@ -5764,10 +6010,11 @@ function crossedLimit(usage, settings, fired, now = Date.now()) {
5764
6010
  for (const [limit, window, threshold] of checks) {
5765
6011
  if (!window || threshold <= 0 || window.utilization < threshold)
5766
6012
  continue;
5767
- if (fired(limit))
6013
+ const parsed = Date.parse(window.resetsAt);
6014
+ const resets = Number.isFinite(parsed) && parsed > now ? parsed : null;
6015
+ if (fired(limit, resets))
5768
6016
  continue;
5769
- const resets = Date.parse(window.resetsAt);
5770
- const until = Number.isFinite(resets) && resets > now ? resets : now + WINDOW_SPAN_MS[limit];
6017
+ const until = resets ?? now + WINDOW_SPAN_MS[limit];
5771
6018
  return { limit, window, until };
5772
6019
  }
5773
6020
  return null;
@@ -6169,13 +6416,13 @@ function parseStatus2(raw) {
6169
6416
  }
6170
6417
  return {
6171
6418
  loggedIn: parsed.loggedIn === true,
6172
- method: str2(parsed.authMethod) || "none",
6173
- email: str2(parsed.email),
6174
- organization: str2(parsed.orgName),
6175
- plan: str2(parsed.subscriptionType)
6419
+ method: str3(parsed.authMethod) || "none",
6420
+ email: str3(parsed.email),
6421
+ organization: str3(parsed.orgName),
6422
+ plan: str3(parsed.subscriptionType)
6176
6423
  };
6177
6424
  }
6178
- function str2(value) {
6425
+ function str3(value) {
6179
6426
  return typeof value === "string" ? value : "";
6180
6427
  }
6181
6428
  function describe5(error) {
@@ -6199,8 +6446,8 @@ var RETRY_MAX_MS = 6e4;
6199
6446
  var HANDSHAKE_TIMEOUT_MS = 15e3;
6200
6447
  var CONNECT_TIMEOUT_MS = 3e4;
6201
6448
  var SILENCE_TIMEOUT_MS = 95e3;
6202
- function jitter(delay) {
6203
- return Math.round(delay * (0.8 + Math.random() * 0.4));
6449
+ function jitter(delay2) {
6450
+ return Math.round(delay2 * (0.8 + Math.random() * 0.4));
6204
6451
  }
6205
6452
  var RelayLink = class extends EventEmitter4 {
6206
6453
  #credentials = null;
@@ -6493,12 +6740,12 @@ var RelayLink = class extends EventEmitter4 {
6493
6740
  #scheduleRetry(rejected) {
6494
6741
  if (this.#closed || !this.#credentials || this.#retryTimer)
6495
6742
  return;
6496
- const delay = rejected ? RETRY_MAX_MS : this.#retry;
6743
+ const delay2 = rejected ? RETRY_MAX_MS : this.#retry;
6497
6744
  this.#retry = Math.min(this.#retry * 2, RETRY_MAX_MS);
6498
6745
  this.#retryTimer = setTimeout(() => {
6499
6746
  this.#retryTimer = null;
6500
6747
  this.#connect();
6501
- }, jitter(delay));
6748
+ }, jitter(delay2));
6502
6749
  this.#retryTimer.unref();
6503
6750
  }
6504
6751
  #onMessage(socket, raw) {
@@ -6898,7 +7145,7 @@ var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435
6898
7145
  var HISTORY_PAGE = 250;
6899
7146
  var HISTORY_PAGE_MAX = 500;
6900
7147
  var AUTO_STOP_SWEEP_MS = 30 * 1e3;
6901
- var BUSY_STATES = /* @__PURE__ */ new Set([
7148
+ var BUSY_STATES2 = /* @__PURE__ */ new Set([
6902
7149
  "running",
6903
7150
  "waiting_permission",
6904
7151
  "reconnecting"
@@ -7779,6 +8026,12 @@ var HostServer = class {
7779
8026
  await this.#handoffs.remove(frame.sessionId);
7780
8027
  this.#broadcast({ type: "handoff.state", sessionId: frame.sessionId, handoff: null });
7781
8028
  return;
8029
+ case "task.stop": {
8030
+ const session = this.#sessions.get(frame.sessionId);
8031
+ if (session instanceof ClaudeSession)
8032
+ await session.stopTask(frame.taskId);
8033
+ return;
8034
+ }
7782
8035
  case "session.handoff":
7783
8036
  this.#send(socket, {
7784
8037
  type: "handoff.written",
@@ -8032,10 +8285,10 @@ var HostServer = class {
8032
8285
  continue;
8033
8286
  if (drafted.has(session.id))
8034
8287
  continue;
8035
- if (BUSY_STATES.has(summary.state) || summary.backgroundWork)
8288
+ if (BUSY_STATES2.has(summary.state) || summary.backgroundWork)
8036
8289
  continue;
8037
8290
  const side = this.#sides.get(session.id);
8038
- if (side && BUSY_STATES.has(side.state))
8291
+ if (side && BUSY_STATES2.has(side.state))
8039
8292
  continue;
8040
8293
  const lastActivity = Math.max(summary.updatedAt, side?.summary().updatedAt ?? 0, this.#draftTouchedAt.get(session.id) ?? 0);
8041
8294
  if (now - lastActivity < after)
@@ -8057,7 +8310,7 @@ var HostServer = class {
8057
8310
  }
8058
8311
  #isBusy(session) {
8059
8312
  const summary = session.summary();
8060
- return BUSY_STATES.has(summary.state) || summary.backgroundWork;
8313
+ return BUSY_STATES2.has(summary.state) || summary.backgroundWork || summary.tasks.length > 0;
8061
8314
  }
8062
8315
  /**
8063
8316
  * Перечитать лимиты, пока хоть одна сессия занята. Сама сессия делает это
@@ -8072,7 +8325,7 @@ var HostServer = class {
8072
8325
  async #checkLimit(usage) {
8073
8326
  if (this.#limitStopping)
8074
8327
  return;
8075
- const hit = crossedLimit(usage, this.#settings.current, (limit) => this.#limitStop.fired(limit));
8328
+ const hit = crossedLimit(usage, this.#settings.current, (limit, resets) => this.#limitStop.fired(limit, resets));
8076
8329
  if (!hit)
8077
8330
  return;
8078
8331
  this.#limitStopping = true;
@@ -8096,8 +8349,9 @@ var HostServer = class {
8096
8349
  const sessions = this.#claudeUserSessions();
8097
8350
  if (sessions.length === 0)
8098
8351
  return;
8099
- const busy = sessions.filter((s) => this.#isBusy(s));
8100
- const prompt = handoffPrompt(hit);
8352
+ const now = Date.now();
8353
+ const busy = sessions.filter((s) => this.#isBusy(s) || now - s.lastActiveAt < LIMIT_RECENT_MS);
8354
+ const prompt = hit.window.utilization >= 100 ? null : handoffPrompt(hit);
8101
8355
  const results = await Promise.all(busy.map(async (session) => {
8102
8356
  const text = await session.runHandoff(prompt, HANDOFF_TIMEOUT_MS);
8103
8357
  const summary = session.summary();
@@ -8699,6 +8953,7 @@ var HostServer = class {
8699
8953
  lastTurn: null,
8700
8954
  // Сессия с диска не поднята — фоновой задаче взяться неоткуда.
8701
8955
  backgroundWork: false,
8956
+ tasks: [],
8702
8957
  model: null,
8703
8958
  permissionMode: "default",
8704
8959
  // Сессия с диска не поднята: чем она была настроена, знает только её
@@ -8810,6 +9065,7 @@ function antigravitySummary(record) {
8810
9065
  state: "closed",
8811
9066
  lastTurn: null,
8812
9067
  backgroundWork: false,
9068
+ tasks: [],
8813
9069
  model: record.model,
8814
9070
  permissionMode: record.permissionMode,
8815
9071
  effort: record.effort,