@taskless-app/swarm 0.6.67 → 0.6.69

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