@taskless-app/swarm 0.6.67 → 0.6.68

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 (3) hide show
  1. package/dist/cli.js +202 -39
  2. package/dist/overmind.js +299 -42
  3. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -3530,7 +3530,7 @@ var init_linkFailure = __esm({
3530
3530
  "use strict";
3531
3531
  init_agentToken();
3532
3532
  UNKNOWN_LINK_REASON = "\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043D\u0435\u0442, \u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B";
3533
- OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout/i;
3533
+ OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout|request timed out|-32001\b/i;
3534
3534
  MISSING_TOOL_RE = /tool[^.]{0,40}not found|unknown tool|tool_not_found|method not found|-32601\b/i;
3535
3535
  }
3536
3536
  });
@@ -3880,7 +3880,7 @@ var init_package = __esm({
3880
3880
  "package.json"() {
3881
3881
  package_default = {
3882
3882
  name: "@taskless-app/swarm",
3883
- version: "0.6.67",
3883
+ version: "0.6.68",
3884
3884
  type: "module",
3885
3885
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3886
3886
  keywords: [
@@ -3929,9 +3929,10 @@ var init_package = __esm({
3929
3929
  "@hono/node-server": "^2.0.12",
3930
3930
  "@modelcontextprotocol/sdk": "^1.29.0",
3931
3931
  execa: "^10.0.0",
3932
+ undici: "^7.25.0",
3932
3933
  zod: "3.25.76"
3933
3934
  },
3934
- gitHead: "92a3176867bc1668677a4b6a25e02e0cb6661ce0"
3935
+ gitHead: "4bfd78926aa37ef7de8ea06a6763d7e84a2fded6"
3935
3936
  };
3936
3937
  }
3937
3938
  });
@@ -4664,6 +4665,47 @@ var init_shotUpload = __esm({
4664
4665
  }
4665
4666
  });
4666
4667
 
4668
+ // src/prodHttp.ts
4669
+ import { Agent, fetch as undiciFetch } from "undici";
4670
+ function prodDispatcher() {
4671
+ agent ??= new Agent({
4672
+ keepAliveTimeout: PROD_KEEPALIVE_MS,
4673
+ keepAliveMaxTimeout: PROD_KEEPALIVE_MS,
4674
+ headersTimeout: PROD_SOCKET_TIMEOUT_MS,
4675
+ bodyTimeout: PROD_SOCKET_TIMEOUT_MS,
4676
+ connect: { timeout: PROD_CONNECT_TIMEOUT_MS }
4677
+ });
4678
+ return agent;
4679
+ }
4680
+ function isProdTimeout(e) {
4681
+ for (let cur = e, depth = 0; cur != null && depth < 4; cur = cur.cause, depth++) {
4682
+ const code = cur.code;
4683
+ const name = cur.name;
4684
+ const msg = cur instanceof Error ? cur.message : String(cur);
4685
+ if (code === -32001 || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") return true;
4686
+ if (name === "TimeoutError" || /request timed out/i.test(msg)) return true;
4687
+ }
4688
+ return false;
4689
+ }
4690
+ var PROD_CALL_TIMEOUT_MS, FETCH_GRACE_MS, PROD_KEEPALIVE_MS, PROD_SOCKET_TIMEOUT_MS, PROD_CONNECT_TIMEOUT_MS, agent, prodFetch;
4691
+ var init_prodHttp = __esm({
4692
+ "src/prodHttp.ts"() {
4693
+ "use strict";
4694
+ PROD_CALL_TIMEOUT_MS = 15e3;
4695
+ FETCH_GRACE_MS = 5e3;
4696
+ PROD_KEEPALIVE_MS = 1e4;
4697
+ PROD_SOCKET_TIMEOUT_MS = 3e4;
4698
+ PROD_CONNECT_TIMEOUT_MS = 1e4;
4699
+ agent = null;
4700
+ prodFetch = (url, init) => {
4701
+ const method = (init?.method ?? "GET").toUpperCase();
4702
+ const deadline = method === "GET" ? null : AbortSignal.timeout(PROD_CALL_TIMEOUT_MS + FETCH_GRACE_MS);
4703
+ const signal = deadline && init?.signal ? AbortSignal.any([init.signal, deadline]) : deadline ?? init?.signal ?? void 0;
4704
+ return undiciFetch(url, { ...init, ...signal ? { signal } : {}, dispatcher: prodDispatcher() });
4705
+ };
4706
+ }
4707
+ });
4708
+
4667
4709
  // src/workspace.ts
4668
4710
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
4669
4711
  import { join as join19 } from "path";
@@ -4715,8 +4757,8 @@ function parseJsonContent(res) {
4715
4757
  }
4716
4758
  async function resolveApiToken(api, explicit) {
4717
4759
  if (explicit) return explicit;
4718
- const agent = loadAgentToken();
4719
- if (agent?.token) return agent.token;
4760
+ const agent2 = loadAgentToken();
4761
+ if (agent2?.token) return agent2.token;
4720
4762
  const store = loadStore();
4721
4763
  if (store) {
4722
4764
  const r = await ensureAccess(store, { api });
@@ -4745,7 +4787,9 @@ async function connectSwarmMcp(cfg) {
4745
4787
  }
4746
4788
  const url = new URL(mcpEndpoint(api));
4747
4789
  const transport = new StreamableHTTPClientTransport(url, {
4748
- requestInit: { headers: { Authorization: `Bearer ${token}` } }
4790
+ requestInit: { headers: { Authorization: `Bearer ${token}` } },
4791
+ fetch: prodFetch
4792
+ // TASWM-611
4749
4793
  });
4750
4794
  const client = new Client({ name: "taskless-swarm-client", version: "0.1.0" }, { capabilities: {} });
4751
4795
  try {
@@ -4764,12 +4808,31 @@ async function connectSwarmMcp(cfg) {
4764
4808
  }
4765
4809
  swarmConnectFailure = null;
4766
4810
  noteTokenAccepted();
4767
- return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
4811
+ const timeout = cfg?.callTimeoutMs ?? PROD_CALL_TIMEOUT_MS;
4812
+ return async (name, args) => {
4813
+ try {
4814
+ return await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) }, void 0, { timeout });
4815
+ } catch (e) {
4816
+ if (isProdTimeout(e)) {
4817
+ console.warn(`${prodTimeoutLine("Swarm MCP", name, timeout)} \u2014 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u043E, \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0432\u044B\u0437\u043E\u0432 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0441\u044F`);
4818
+ }
4819
+ void client.close().catch(() => {
4820
+ });
4821
+ throw e;
4822
+ }
4823
+ };
4824
+ }
4825
+ function prodTimeoutLine(scope, tool, timeoutMs) {
4826
+ return `[${scope}] \u23F1 ${tool}: \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${Math.round(timeoutMs / 100) / 10}\u0441, \u0432\u044B\u0437\u043E\u0432 \u043F\u0440\u0435\u0440\u0432\u0430\u043D`;
4768
4827
  }
4769
4828
  function lastSwarmConnectFailure() {
4770
4829
  return swarmConnectFailure;
4771
4830
  }
4772
4831
  function swarmToolArgs(name, args, workspace) {
4832
+ if ("workspaceId" in args && args.workspaceId == null) {
4833
+ const { workspaceId: _drop, ...rest } = args;
4834
+ return rest;
4835
+ }
4773
4836
  if (!WS_SCOPED_SWARM_TOOLS.has(name) || !workspace) return args;
4774
4837
  return { workspaceId: workspace, ...args };
4775
4838
  }
@@ -4784,6 +4847,7 @@ var init_taskless = __esm({
4784
4847
  init_oauth();
4785
4848
  init_agentToken();
4786
4849
  init_linkFailure();
4850
+ init_prodHttp();
4787
4851
  init_toolFailure();
4788
4852
  init_authState();
4789
4853
  init_workspace();
@@ -7697,7 +7761,7 @@ var init_updateCli = __esm({
7697
7761
  // src/cli.ts
7698
7762
  init_cliIo();
7699
7763
  import { basename as basename6, dirname as dirname5, join as join27 } from "path";
7700
- import { existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync15, mkdirSync as mkdirSync11, readdirSync as readdirSync12, statSync as statSync12, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
7764
+ import { existsSync as existsSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync15, mkdirSync as mkdirSync11, readdirSync as readdirSync12, statSync as statSync12, openSync as openSync5, readSync as readSync2, closeSync as closeSync5 } from "fs";
7701
7765
  import { setTimeout as sleep2 } from "timers/promises";
7702
7766
 
7703
7767
  // src/swarmClient.ts
@@ -10061,7 +10125,7 @@ var SwarmClient = class _SwarmClient {
10061
10125
  // 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
10062
10126
  // слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
10063
10127
  taskIdCache = /* @__PURE__ */ new Map();
10064
- // task_slug → cuid прода
10128
+ // task_slug → cuid прода и воркспейс задачи
10065
10129
  // Create для этого runKey резолвился окончательно неудачно (задача не найдена/
10066
10130
  // недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
10067
10131
  // без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
@@ -10076,12 +10140,20 @@ var SwarmClient = class _SwarmClient {
10076
10140
  const cacheKey = `${taskRef}\0${repoFullName ?? ""}\0${workspaceId ?? ""}`;
10077
10141
  const cached = this.taskIdCache.get(cacheKey);
10078
10142
  if (cached) return cached;
10143
+ const first = await this.readTaskRef(taskRef, repoFullName, workspaceId);
10144
+ const res = first === null && repoFullName && !workspaceId ? await this.readTaskRef(taskRef, repoFullName, null) : first;
10145
+ if (res && res !== "transient") this.taskIdCache.set(cacheKey, res);
10146
+ return res;
10147
+ }
10148
+ /** Один get_task: cuid задачи и её воркспейс; null — прод ответил «нет такой». */
10149
+ async readTaskRef(taskRef, repoFullName, workspaceId) {
10079
10150
  let res;
10080
10151
  try {
10081
10152
  res = await this.call("get_task", {
10082
10153
  id: taskRef,
10083
10154
  ...repoFullName ? { repoFullName } : {},
10084
- ...workspaceId ? { workspaceId } : {}
10155
+ // null — «без пина демона» (см. swarmToolArgs), undefined — пин по умолчанию.
10156
+ ...workspaceId ? { workspaceId } : workspaceId === null ? { workspaceId: null } : {}
10085
10157
  });
10086
10158
  } catch {
10087
10159
  return "transient";
@@ -10091,10 +10163,9 @@ var SwarmClient = class _SwarmClient {
10091
10163
  const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
10092
10164
  return /not_found/i.test(text) ? null : "transient";
10093
10165
  }
10094
- const id = res.structuredContent?.task?.id;
10095
- if (!id) return null;
10096
- this.taskIdCache.set(cacheKey, id);
10097
- return id;
10166
+ const task = res.structuredContent?.task;
10167
+ if (!task?.id) return null;
10168
+ return { id: task.id, workspaceId: typeof task.workspaceId === "string" ? task.workspaceId : null };
10098
10169
  }
10099
10170
  // MCP-31: привести repo прогона к owner/repo-форме для подсказки резолверу
10100
10171
  // задачи. Прод сматчит подсказку с ProjectRepo.repoFullName → воркспейс. repo
@@ -10133,18 +10204,19 @@ var SwarmClient = class _SwarmClient {
10133
10204
  this.poisonedRunKeys.add(e.runKey);
10134
10205
  return "skip";
10135
10206
  }
10136
- const taskId = await this.resolveTaskCuid(e.taskRef, this.repoFullNameFor(fields.repo), e.workspaceId);
10137
- if (taskId === "transient") {
10207
+ const task = await this.resolveTaskCuid(e.taskRef, this.repoFullNameFor(fields.repo), e.workspaceId);
10208
+ if (task === "transient") {
10138
10209
  throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
10139
10210
  }
10140
- if (!taskId) {
10211
+ if (!task) {
10141
10212
  console.error(
10142
10213
  `[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
10143
10214
  );
10144
10215
  this.poisonedRunKeys.add(e.runKey);
10145
10216
  return "skip";
10146
10217
  }
10147
- return { tool: "swarm_run_upsert", args: { taskId, ...e.workspaceId ? { workspaceId: e.workspaceId } : {}, ...fields } };
10218
+ const workspaceId = e.workspaceId || task.workspaceId;
10219
+ return { tool: "swarm_run_upsert", args: { taskId: task.id, ...workspaceId ? { workspaceId } : {}, ...fields } };
10148
10220
  }
10149
10221
  if (this.poisonedRunKeys.has(e.runKey)) {
10150
10222
  console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
@@ -10196,6 +10268,7 @@ var SwarmClient = class _SwarmClient {
10196
10268
  } catch {
10197
10269
  break;
10198
10270
  }
10271
+ if (res?.isError && res.unavailable) break;
10199
10272
  if (res?.isError) {
10200
10273
  const reason = (res.content ?? []).map((c) => c.text).filter(Boolean).join(" ").trim() || "\u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
10201
10274
  const tries = (this.rejected.get(e.seq) ?? 0) + 1;
@@ -10448,7 +10521,7 @@ function tailFile(path, lines) {
10448
10521
  }
10449
10522
 
10450
10523
  // src/logRotate.ts
10451
- import { copyFileSync as copyFileSync4, renameSync as renameSync3, statSync as statSync7, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync8 } from "fs";
10524
+ import { copyFileSync as copyFileSync4, renameSync as renameSync3, statSync as statSync7, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync8, openSync as openSync3, writeSync, closeSync as closeSync3 } from "fs";
10452
10525
  var realIo = {
10453
10526
  size: (path) => {
10454
10527
  try {
@@ -10463,6 +10536,31 @@ var realIo = {
10463
10536
  unlink: unlinkSync2,
10464
10537
  exists: existsSync8
10465
10538
  };
10539
+ var realOwnerIo = {
10540
+ statFd: (fd) => {
10541
+ try {
10542
+ const s = fstatSync(fd);
10543
+ return { dev: s.dev, ino: s.ino, isFile: s.isFile() };
10544
+ } catch {
10545
+ return null;
10546
+ }
10547
+ },
10548
+ statPath: (path) => {
10549
+ try {
10550
+ const s = statSync7(path);
10551
+ return { dev: s.dev, ino: s.ino };
10552
+ } catch {
10553
+ return null;
10554
+ }
10555
+ }
10556
+ };
10557
+ function stdoutGoesTo(path, io = realOwnerIo) {
10558
+ if (teedTo === path) return true;
10559
+ const out = io.statFd(1);
10560
+ if (!out || !out.isFile) return false;
10561
+ const file = io.statPath(path);
10562
+ return file != null && file.dev === out.dev && file.ino === out.ino;
10563
+ }
10466
10564
  var STRAY_GENERATIONS = 8;
10467
10565
  function rotateDaemonLog(path, opts, io = realIo) {
10468
10566
  const size = io.size(path);
@@ -10482,6 +10580,33 @@ function rotateDaemonLog(path, opts, io = realIo) {
10482
10580
  io.truncate(path);
10483
10581
  return "rotated";
10484
10582
  }
10583
+ var teedTo = null;
10584
+ function teeStdioToFile(path) {
10585
+ if (teedTo === path || stdoutGoesTo(path)) return null;
10586
+ const fd = openSync3(path, "a");
10587
+ const restore = [];
10588
+ for (const stream of [process.stdout, process.stderr]) {
10589
+ const original = stream.write;
10590
+ restore.push(() => {
10591
+ stream.write = original;
10592
+ });
10593
+ const orig = original.bind(stream);
10594
+ stream.write = ((chunk, ...rest) => {
10595
+ try {
10596
+ if (typeof chunk === "string") writeSync(fd, chunk);
10597
+ else if (chunk instanceof Uint8Array) writeSync(fd, chunk);
10598
+ } catch {
10599
+ }
10600
+ return orig(chunk, ...rest);
10601
+ });
10602
+ }
10603
+ teedTo = path;
10604
+ return () => {
10605
+ for (const undo of restore) undo();
10606
+ teedTo = null;
10607
+ closeSync3(fd);
10608
+ };
10609
+ }
10485
10610
 
10486
10611
  // src/cli.ts
10487
10612
  init_procCleanup();
@@ -12807,7 +12932,7 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
12807
12932
 
12808
12933
  // src/launcher.ts
12809
12934
  import { spawn as spawn2 } from "child_process";
12810
- import { closeSync as closeSync3, existsSync as existsSync10, mkdirSync as mkdirSync10, openSync as openSync3, readdirSync as readdirSync9 } from "fs";
12935
+ import { closeSync as closeSync4, existsSync as existsSync10, mkdirSync as mkdirSync10, openSync as openSync4, readdirSync as readdirSync9 } from "fs";
12811
12936
  import { createRequire } from "module";
12812
12937
  import { homedir as homedir5 } from "os";
12813
12938
  import { dirname as dirname4, join as join15 } from "path";
@@ -13016,14 +13141,14 @@ function openNextRunLog(runLogsDir, runLogKeys) {
13016
13141
  const path = nextRunLogPath(runLogsDir, runLogKeys);
13017
13142
  let out;
13018
13143
  try {
13019
- out = openSync3(path, "wx");
13144
+ out = openSync4(path, "wx");
13020
13145
  } catch (e) {
13021
13146
  if (e.code !== "EEXIST") throw e;
13022
13147
  continue;
13023
13148
  }
13024
13149
  let err = null;
13025
13150
  try {
13026
- err = openSync3(`${path.slice(0, -".json".length)}.err.log`, "w");
13151
+ err = openSync4(`${path.slice(0, -".json".length)}.err.log`, "w");
13027
13152
  } catch {
13028
13153
  err = null;
13029
13154
  }
@@ -13153,8 +13278,8 @@ function makeLauncher(cfg) {
13153
13278
  // они про его собственное состояние, и перебивать их ройным нечем.
13154
13279
  env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
13155
13280
  });
13156
- if (outFd !== void 0) closeSync3(outFd);
13157
- if (errFd !== void 0) closeSync3(errFd);
13281
+ if (outFd !== void 0) closeSync4(outFd);
13282
+ if (errFd !== void 0) closeSync4(errFd);
13158
13283
  child.unref();
13159
13284
  if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
13160
13285
  return child.pid;
@@ -13171,11 +13296,17 @@ init_taskless();
13171
13296
  // src/mcpResilient.ts
13172
13297
  init_linkFailure();
13173
13298
  init_taskless();
13299
+ init_prodHttp();
13174
13300
  var DEFAULT_BACKOFF_MS = [5e3, 15e3, 3e4, 6e4];
13175
13301
  var DEFAULT_DOWN_AFTER_MS = 5 * 6e4;
13176
13302
  var DEFAULT_RETRY_DELAYS_MS = [200, 800];
13177
- function errorResult(reason) {
13178
- return { isError: true, content: [{ type: "text", text: `Swarm MCP \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${reason}` }] };
13303
+ var PROD_PASS_BUDGET_MS = 2e4;
13304
+ function errorResult(reason, unavailable) {
13305
+ return {
13306
+ isError: true,
13307
+ ...unavailable ? { unavailable: true } : {},
13308
+ content: [{ type: "text", text: `Swarm MCP \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${reason}` }]
13309
+ };
13179
13310
  }
13180
13311
  function resilientSwarmMcp(opts = {}) {
13181
13312
  const connect = opts.connect ?? (() => connectSwarmMcp());
@@ -13183,6 +13314,13 @@ function resilientSwarmMcp(opts = {}) {
13183
13314
  const downAfterMs = opts.downAfterMs ?? DEFAULT_DOWN_AFTER_MS;
13184
13315
  const retryDelays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
13185
13316
  const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
13317
+ const clock = opts.now ?? Date.now;
13318
+ const passBudgetMs = opts.passBudgetMs ?? PROD_PASS_BUDGET_MS;
13319
+ let passSpentMs = null;
13320
+ let budgetNoted = false;
13321
+ const spend = (since) => {
13322
+ if (passSpentMs !== null) passSpentMs += Math.max(0, clock() - since);
13323
+ };
13186
13324
  let caller = null;
13187
13325
  let lastTryAt = 0;
13188
13326
  let failures = 0;
@@ -13205,6 +13343,7 @@ function resilientSwarmMcp(opts = {}) {
13205
13343
  if (!force && lastTryAt !== 0 && now - lastTryAt < wait) return null;
13206
13344
  if (!force) lastTryAt = now;
13207
13345
  let fresh = null;
13346
+ const connectStart = clock();
13208
13347
  try {
13209
13348
  fresh = await connect();
13210
13349
  if (!fresh) lastConnectReason = lastSwarmConnectFailure() ?? UNKNOWN_LINK_REASON;
@@ -13221,17 +13360,30 @@ function resilientSwarmMcp(opts = {}) {
13221
13360
  downReported = false;
13222
13361
  return caller;
13223
13362
  }
13363
+ spend(connectStart);
13224
13364
  if (!force) failures += 1;
13225
13365
  if (downSince === null) downSince = now;
13226
13366
  return null;
13227
13367
  }
13228
13368
  const call = async (name, args) => {
13229
13369
  for (let attempt = 0; ; attempt++) {
13370
+ if (passSpentMs !== null && passSpentMs >= passBudgetMs) {
13371
+ if (!budgetNoted) {
13372
+ budgetNoted = true;
13373
+ opts.onBudgetSpent?.({ spentMs: passSpentMs, budgetMs: passBudgetMs });
13374
+ }
13375
+ maybeReportDown();
13376
+ return errorResult(
13377
+ `\u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442, \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0445\u043E\u0434\u0430 \u043D\u0430 \u0441\u0435\u0442\u044C (${Math.round(passBudgetMs / 1e3)}\u0441) \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0445\u043E\u0434\u0430 \u0431\u0435\u0437 \u0441\u0435\u0442\u0438${lastCallReason ?? lastConnectReason ? ` (\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043F\u0440\u0438\u0447\u0438\u043D\u0430: ${lastCallReason ?? lastConnectReason})` : ""}`,
13378
+ true
13379
+ );
13380
+ }
13230
13381
  const active = await ensureCaller(attempt > 0);
13231
13382
  if (!active) {
13232
13383
  maybeReportDown();
13233
- return errorResult(lastConnectReason ?? UNKNOWN_LINK_REASON);
13384
+ return errorResult(lastConnectReason ?? UNKNOWN_LINK_REASON, true);
13234
13385
  }
13386
+ const callStart = clock();
13235
13387
  try {
13236
13388
  const res = await active(name, args);
13237
13389
  lastOkAt = Date.now();
@@ -13239,13 +13391,14 @@ function resilientSwarmMcp(opts = {}) {
13239
13391
  return res;
13240
13392
  } catch (e) {
13241
13393
  caller = null;
13394
+ spend(callStart);
13242
13395
  if (downSince === null) downSince = Date.now();
13243
13396
  const { kind, detail: reason } = explainLinkFailure(e);
13244
13397
  lastCallReason = reason;
13245
13398
  const delay = retryDelays[attempt];
13246
- if (delay === void 0 || kind !== "offline") {
13399
+ if (delay === void 0 || kind !== "offline" || isProdTimeout(e)) {
13247
13400
  maybeReportDown();
13248
- return errorResult(reason);
13401
+ return errorResult(reason, kind === "offline" || kind === "unknown" || kind === "unauthorized" || isProdTimeout(e));
13249
13402
  }
13250
13403
  opts.onRetry?.({ tool: name, attempt: attempt + 1, reason });
13251
13404
  await sleep3(delay);
@@ -13253,6 +13406,10 @@ function resilientSwarmMcp(opts = {}) {
13253
13406
  }
13254
13407
  };
13255
13408
  return Object.assign(call, {
13409
+ beginPass: () => {
13410
+ passSpentMs = 0;
13411
+ budgetNoted = false;
13412
+ },
13256
13413
  // Причина — ТА ЖЕ, что уехала в лог: сначала свежий отказ вызова, иначе отказ
13257
13414
  // подключения. Своего разбора здесь нет намеренно (см. lastCallReason выше).
13258
13415
  link: () => ({ connected: caller !== null, downSince, lastOkAt, lastError: caller !== null ? null : lastCallReason ?? lastConnectReason })
@@ -13811,13 +13968,13 @@ async function stopDaemonForLogin() {
13811
13968
  async function runLoginFlow(db2, opts) {
13812
13969
  const web = startWeb({ db: db2 });
13813
13970
  try {
13814
- let agent = loadAgentToken();
13815
- if (agent && opts.force) {
13971
+ let agent2 = loadAgentToken();
13972
+ if (agent2 && opts.force) {
13816
13973
  console.log("--force: \u0432\u0445\u043E\u0436\u0443 \u0437\u0430\u043D\u043E\u0432\u043E, \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044F \u0441\u0442\u0430\u0440\u044B\u0439 \u0442\u043E\u043A\u0435\u043D.");
13817
- agent = null;
13974
+ agent2 = null;
13818
13975
  }
13819
- if (agent) {
13820
- const probe = await probeAgentToken({ api: TASKLESS_API, token: agent.token });
13976
+ if (agent2) {
13977
+ const probe = await probeAgentToken({ api: TASKLESS_API, token: agent2.token });
13821
13978
  if (probe === "ok") {
13822
13979
  console.log("\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u0442\u043E\u043B\u044C\u043A\u043E \u0432\u044B\u0431\u043E\u0440 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430.");
13823
13980
  } else {
@@ -13829,10 +13986,10 @@ async function runLoginFlow(db2, opts) {
13829
13986
  "\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0434/\u043E\u0442\u043A\u0430\u0437) \u2014 \u0432\u0445\u043E\u0436\u0443 \u0437\u0430\u043D\u043E\u0432\u043E: \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0448\u0435\u0434\u0448\u0443\u044E \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u0437\u0430 \xAB\u0432\u0441\u0451 \u0432 \u043F\u043E\u0440\u044F\u0434\u043A\u0435\xBB \u043D\u0435\u043B\u044C\u0437\u044F."
13830
13987
  );
13831
13988
  }
13832
- agent = null;
13989
+ agent2 = null;
13833
13990
  }
13834
13991
  }
13835
- if (!agent) {
13992
+ if (!agent2) {
13836
13993
  let store = loadStore();
13837
13994
  if (!store) {
13838
13995
  const loginUrl = `http://127.0.0.1:${web.port}/login`;
@@ -14021,14 +14178,14 @@ async function runDaemonLogs(argv) {
14021
14178
  }
14022
14179
  if (size < pos) pos = 0;
14023
14180
  if (size === pos) continue;
14024
- const fd = openSync4(DAEMON_LOG_PATH, "r");
14181
+ const fd = openSync5(DAEMON_LOG_PATH, "r");
14025
14182
  try {
14026
14183
  const buf = Buffer.alloc(size - pos);
14027
14184
  const read = readSync2(fd, buf, 0, buf.length, pos);
14028
14185
  process.stdout.write(buf.subarray(0, read).toString("utf8"));
14029
14186
  pos += read;
14030
14187
  } finally {
14031
- closeSync4(fd);
14188
+ closeSync5(fd);
14032
14189
  }
14033
14190
  }
14034
14191
  }
@@ -14306,7 +14463,7 @@ async function makeCliStore(repo) {
14306
14463
  onDown: (ms) => console.warn(`Swarm MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u043C \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0434\u043E\u043C.`),
14307
14464
  onUp: () => console.warn("Swarm MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
14308
14465
  });
14309
- const repoRegistry = await buildRepoRegistry(SWARM_REPO_PATHS);
14466
+ const repoRegistry = await buildRepoRegistry([...new Set([repo, ...SWARM_REPO_PATHS].filter(Boolean))]);
14310
14467
  const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry });
14311
14468
  await client.hydrate();
14312
14469
  client.reconcileRuntime({ worktreesDir: effectiveWorktreesDir(), listProcesses: listDroneProcesses });
@@ -14563,6 +14720,12 @@ if (cmd === "start") {
14563
14720
  if (!foreground) {
14564
14721
  process.exit(await runStartDetached(startArgs));
14565
14722
  }
14723
+ try {
14724
+ rotateDaemonLog(DAEMON_LOG_PATH, { maxBytes: DAEMON_LOG_MAX_BYTES, keep: DAEMON_LOG_KEEP });
14725
+ if (teeStdioToFile(DAEMON_LOG_PATH)) console.log(`\u043B\u043E\u0433 \u043F\u0438\u0448\u0435\u0442\u0441\u044F \u0438 \u0432 ${DAEMON_LOG_PATH}`);
14726
+ } catch (e) {
14727
+ console.warn(`\u043B\u043E\u0433 \u0432 \u0444\u0430\u0439\u043B \u043D\u0435 \u043F\u0438\u0448\u0435\u0442\u0441\u044F (${e instanceof Error ? e.message : e}) \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B`);
14728
+ }
14566
14729
  try {
14567
14730
  await runDaemon({ repo, repoFlag });
14568
14731
  } catch (e) {