@sjawhar/claude-envoy 1.12.0 → 1.13.0

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.
@@ -2,5 +2,5 @@
2
2
  "name": "claude-envoy",
3
3
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
4
4
  "mcpServers": "./.mcp.json",
5
- "version": "1.12.0"
5
+ "version": "1.13.0"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-envoy",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
5
5
  "mcpServers": {}
6
6
  }
package/dist/dispatch.js CHANGED
@@ -17565,9 +17565,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
17565
17565
  exec(file, args, { cwd, signal: controller.signal }),
17566
17566
  expired.promise
17567
17567
  ]);
17568
- return answer?.stdout ?? null;
17568
+ if (answer === null) {
17569
+ return { failure: "timedOut", ok: false };
17570
+ }
17571
+ return { ok: true, stdout: answer.stdout };
17569
17572
  } catch {
17570
- return null;
17573
+ return { failure: "refused", ok: false };
17571
17574
  } finally {
17572
17575
  clearTimeout(timer);
17573
17576
  }
@@ -17608,13 +17611,13 @@ function parseGitHubRemoteUrl(url) {
17608
17611
  return null;
17609
17612
  }
17610
17613
  async function resolveCwdRepo(cwd, exec) {
17611
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
17612
- if (jjOutput !== null) {
17613
- const url = selectRemoteUrl(parseRemoteList(jjOutput));
17614
+ const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
17615
+ if (remoteList.ok) {
17616
+ const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
17614
17617
  return url ? parseGitHubRemoteUrl(url) : null;
17615
17618
  }
17616
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
17617
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
17619
+ const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
17620
+ return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
17618
17621
  }
17619
17622
  async function resolveOrigin(env, exec, cwd) {
17620
17623
  const origin = {
@@ -17623,8 +17626,8 @@ async function resolveOrigin(env, exec, cwd) {
17623
17626
  };
17624
17627
  const pane = env.TMUX_PANE;
17625
17628
  if (pane) {
17626
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
17627
- const [target, paneId] = output?.trim().split(" ") ?? [];
17629
+ const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
17630
+ const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
17628
17631
  if (target)
17629
17632
  origin.tmux = target;
17630
17633
  if (paneId)
@@ -27532,6 +27535,7 @@ function dispatchTarget(env, config) {
27532
27535
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
27533
27536
  var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
27534
27537
  var MIN_PASS_DEADLINE_MS = 30000;
27538
+ var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
27535
27539
  var MIN_INTERVAL_MS = 25;
27536
27540
  var MAX_INTERVAL_MS = 2147483647;
27537
27541
  var REGISTRATION_SETTLE_MS = 5000;
@@ -27545,6 +27549,10 @@ function namedInterval(raw) {
27545
27549
  const parsed = Number(raw);
27546
27550
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
27547
27551
  }
27552
+ function resolvedInterval(raw, fallback) {
27553
+ const named = namedInterval(raw);
27554
+ return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
27555
+ }
27548
27556
  function natsSeedFromEnvironment(env) {
27549
27557
  const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
27550
27558
  if (file !== undefined) {
@@ -27635,8 +27643,8 @@ function envoySettings(env, config, options = {}) {
27635
27643
  inbound = { off: messageFor(error) };
27636
27644
  }
27637
27645
  }
27638
- const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
27639
- const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
27646
+ const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
27647
+ const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
27640
27648
  const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
27641
27649
  return {
27642
27650
  enabled: true,
@@ -27644,6 +27652,7 @@ function envoySettings(env, config, options = {}) {
27644
27652
  credential,
27645
27653
  inbound,
27646
27654
  heartbeatMs,
27655
+ tmuxPlaceCacheMs,
27647
27656
  passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
27648
27657
  passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
27649
27658
  };
@@ -28063,6 +28072,7 @@ function createEnvoyClient(config) {
28063
28072
  ...input.delivery === undefined ? {} : { delivery: input.delivery },
28064
28073
  ...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
28065
28074
  host: machineID(),
28075
+ ...input.place === undefined ? {} : { tmux_place: input.place },
28066
28076
  plugin: input.plugin.name,
28067
28077
  plugin_version: input.plugin.version
28068
28078
  }, { actingSession: input.sessionID }))),
@@ -44626,6 +44626,7 @@ function fileBearer(bearer, url, section, env, commands = "run") {
44626
44626
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
44627
44627
  var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
44628
44628
  var MIN_PASS_DEADLINE_MS = 30000;
44629
+ var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
44629
44630
  var MIN_INTERVAL_MS = 25;
44630
44631
  var MAX_INTERVAL_MS = 2147483647;
44631
44632
  var REGISTRATION_SETTLE_MS = 5000;
@@ -44639,6 +44640,10 @@ function namedInterval(raw) {
44639
44640
  const parsed = Number(raw);
44640
44641
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
44641
44642
  }
44643
+ function resolvedInterval(raw, fallback) {
44644
+ const named = namedInterval(raw);
44645
+ return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
44646
+ }
44642
44647
  function natsSeedFromEnvironment(env) {
44643
44648
  const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
44644
44649
  if (file !== undefined) {
@@ -44729,8 +44734,8 @@ function envoySettings(env, config, options = {}) {
44729
44734
  inbound = { off: messageFor(error) };
44730
44735
  }
44731
44736
  }
44732
- const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
44733
- const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
44737
+ const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
44738
+ const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
44734
44739
  const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
44735
44740
  return {
44736
44741
  enabled: true,
@@ -44738,6 +44743,7 @@ function envoySettings(env, config, options = {}) {
44738
44743
  credential,
44739
44744
  inbound,
44740
44745
  heartbeatMs,
44746
+ tmuxPlaceCacheMs,
44741
44747
  passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
44742
44748
  passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
44743
44749
  };
@@ -45064,6 +45070,7 @@ function createEnvoyClient(config) {
45064
45070
  ...input.delivery === undefined ? {} : { delivery: input.delivery },
45065
45071
  ...input.clientInstanceID === undefined ? {} : { client_instance_id: input.clientInstanceID },
45066
45072
  host: machineID(),
45073
+ ...input.place === undefined ? {} : { tmux_place: input.place },
45067
45074
  plugin: input.plugin.name,
45068
45075
  plugin_version: input.plugin.version
45069
45076
  }, { actingSession: input.sessionID }))),
@@ -46869,9 +46876,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
46869
46876
  exec(file, args, { cwd, signal: controller.signal }),
46870
46877
  expired.promise
46871
46878
  ]);
46872
- return answer?.stdout ?? null;
46879
+ if (answer === null) {
46880
+ return { failure: "timedOut", ok: false };
46881
+ }
46882
+ return { ok: true, stdout: answer.stdout };
46873
46883
  } catch {
46874
- return null;
46884
+ return { failure: "refused", ok: false };
46875
46885
  } finally {
46876
46886
  clearTimeout(timer);
46877
46887
  }
@@ -46912,13 +46922,13 @@ function parseGitHubRemoteUrl(url) {
46912
46922
  return null;
46913
46923
  }
46914
46924
  async function resolveCwdRepo(cwd, exec) {
46915
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
46916
- if (jjOutput !== null) {
46917
- const url = selectRemoteUrl(parseRemoteList(jjOutput));
46925
+ const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
46926
+ if (remoteList.ok) {
46927
+ const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
46918
46928
  return url ? parseGitHubRemoteUrl(url) : null;
46919
46929
  }
46920
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
46921
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
46930
+ const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
46931
+ return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
46922
46932
  }
46923
46933
  async function resolveOrigin(env, exec, cwd) {
46924
46934
  const origin = {
@@ -46927,8 +46937,8 @@ async function resolveOrigin(env, exec, cwd) {
46927
46937
  };
46928
46938
  const pane = env.TMUX_PANE;
46929
46939
  if (pane) {
46930
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
46931
- const [target, paneId] = output?.trim().split(" ") ?? [];
46940
+ const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
46941
+ const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
46932
46942
  if (target)
46933
46943
  origin.tmux = target;
46934
46944
  if (paneId)
@@ -55145,7 +55155,7 @@ var import_nats2 = __toESM(require_mod4(), 1);
55145
55155
 
55146
55156
  // package.json
55147
55157
  var name = "@sjawhar/claude-envoy";
55148
- var version2 = "1.12.0";
55158
+ var version2 = "1.13.0";
55149
55159
  var legion = {
55150
55160
  daemonApiVersion: 55
55151
55161
  };
@@ -57218,9 +57228,63 @@ function replayStateWriter(file, restored) {
57218
57228
  }
57219
57229
 
57220
57230
  // src/session-title.ts
57231
+ import { basename as basename4 } from "path";
57232
+
57233
+ // ../envoy-client/src/process-ancestry.ts
57221
57234
  import { execFileSync } from "child_process";
57222
57235
  import { readFileSync as readFileSync9 } from "fs";
57223
- import { basename as basename4 } from "path";
57236
+ var MAX_ANCESTORS = 64;
57237
+ var PARENT_LOOKUP_MS = 1000;
57238
+ function procParentPid(pid) {
57239
+ try {
57240
+ const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
57241
+ const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
57242
+ return Number.isInteger(parent) ? parent : undefined;
57243
+ } catch {
57244
+ return;
57245
+ }
57246
+ }
57247
+ function processTable() {
57248
+ const table = new Map;
57249
+ try {
57250
+ const listing = execFileSync("ps", ["-Ao", "pid=,ppid="], {
57251
+ encoding: "utf8",
57252
+ killSignal: "SIGKILL",
57253
+ timeout: PARENT_LOOKUP_MS
57254
+ });
57255
+ for (const line of listing.split(`
57256
+ `)) {
57257
+ const [, pid = "", parent = ""] = line.match(/^\s*(\d+)\s+(\d+)\s*$/) ?? [];
57258
+ if (pid !== "")
57259
+ table.set(Number(pid), Number(parent));
57260
+ }
57261
+ } catch {}
57262
+ return table;
57263
+ }
57264
+ function isAncestor(candidate, pid = process.pid) {
57265
+ let table;
57266
+ const parentPid = (child) => {
57267
+ if (process.platform === "linux")
57268
+ return procParentPid(child);
57269
+ table ??= processTable();
57270
+ return table.get(child);
57271
+ };
57272
+ let current = pid;
57273
+ for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
57274
+ if (String(current) === candidate)
57275
+ return true;
57276
+ current = parentPid(current);
57277
+ }
57278
+ return false;
57279
+ }
57280
+
57281
+ // ../envoy-client/src/tmux-server.ts
57282
+ function tmuxServer(value) {
57283
+ const parts = (value ?? "").split(",");
57284
+ return { serverPid: parts.at(-2) ?? "", socket: parts.slice(0, -2).join(",") };
57285
+ }
57286
+
57287
+ // src/session-title.ts
57224
57288
  var TMUX_PANE_FORMAT = [
57225
57289
  "#{session_name}",
57226
57290
  "#{window_name}",
@@ -57231,7 +57295,6 @@ var TMUX_PANE_FORMAT = [
57231
57295
  ].join("\t");
57232
57296
  var TMUX_TIMEOUT_MS = 1000;
57233
57297
  var SHORT_ID_LENGTH = 8;
57234
- var MAX_ANCESTORS = 64;
57235
57298
  var CSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "gu");
57236
57299
  function oneLine(value) {
57237
57300
  return value.replace(CSI_SEQUENCE, "").replace(/[\u202a-\u202e\u2066-\u2069]/gu, "").replace(/[\s\p{Cc}]+/gu, " ").trim();
@@ -57239,32 +57302,6 @@ function oneLine(value) {
57239
57302
  function visible(value) {
57240
57303
  return value.replace(/[\s\p{Cc}\p{Cf}\p{Cs}\p{Cn}\p{M}\p{Default_Ignorable_Code_Point}\u2800]/gu, "") !== "";
57241
57304
  }
57242
- function parentPid(pid) {
57243
- try {
57244
- if (process.platform === "linux") {
57245
- const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
57246
- const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
57247
- return Number.isInteger(parent) ? parent : undefined;
57248
- }
57249
- const parent = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
57250
- encoding: "utf8",
57251
- timeout: TMUX_TIMEOUT_MS,
57252
- killSignal: "SIGKILL"
57253
- }).trim());
57254
- return Number.isInteger(parent) ? parent : undefined;
57255
- } catch {
57256
- return;
57257
- }
57258
- }
57259
- function isAncestor(candidate, pid = process.pid) {
57260
- let current = pid;
57261
- for (let step = 0;current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
57262
- if (String(current) === candidate)
57263
- return true;
57264
- current = parentPid(current);
57265
- }
57266
- return false;
57267
- }
57268
57305
  async function queryTmux(socket, pane, format) {
57269
57306
  let child;
57270
57307
  try {
@@ -57328,9 +57365,7 @@ async function sessionTitle(environment, directory, sources = {}) {
57328
57365
  const explicit = oneLine(configuredValue(environment.ENVOY_SESSION_TITLE) ?? "");
57329
57366
  if (visible(explicit))
57330
57367
  return () => explicit;
57331
- const parts = configuredValue(environment.TMUX)?.split(",") ?? [];
57332
- const socket = parts.slice(0, -2).join(",");
57333
- const serverPid = parts.at(-2) ?? "";
57368
+ const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX));
57334
57369
  const pane = configuredValue(environment.TMUX_PANE)?.trim();
57335
57370
  const directoryName = oneLine(basename4(directory) || directory);
57336
57371
  const name = (socket === "" || serverPid === "" || pane === undefined ? undefined : await tmuxPaneTitle(socket, serverPid, pane, ownPane, query)) ?? (visible(directoryName) ? directoryName : "session");
@@ -16569,9 +16569,12 @@ async function tryExec(exec, file, args, cwd, limitMs) {
16569
16569
  exec(file, args, { cwd, signal: controller.signal }),
16570
16570
  expired.promise
16571
16571
  ]);
16572
- return answer?.stdout ?? null;
16572
+ if (answer === null) {
16573
+ return { failure: "timedOut", ok: false };
16574
+ }
16575
+ return { ok: true, stdout: answer.stdout };
16573
16576
  } catch {
16574
- return null;
16577
+ return { failure: "refused", ok: false };
16575
16578
  } finally {
16576
16579
  clearTimeout(timer);
16577
16580
  }
@@ -16612,13 +16615,13 @@ function parseGitHubRemoteUrl(url) {
16612
16615
  return null;
16613
16616
  }
16614
16617
  async function resolveCwdRepo(cwd, exec) {
16615
- const jjOutput = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
16616
- if (jjOutput !== null) {
16617
- const url = selectRemoteUrl(parseRemoteList(jjOutput));
16618
+ const remoteList = await tryExec(exec, "jj", ["git", "remote", "list"], cwd, REMOTE_LOOKUP_MS);
16619
+ if (remoteList.ok) {
16620
+ const url = selectRemoteUrl(parseRemoteList(remoteList.stdout));
16618
16621
  return url ? parseGitHubRemoteUrl(url) : null;
16619
16622
  }
16620
- const originUrl = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
16621
- return originUrl ? parseGitHubRemoteUrl(originUrl) : null;
16623
+ const originRemote = await tryExec(exec, "git", ["remote", "get-url", "origin"], cwd, REMOTE_LOOKUP_MS);
16624
+ return originRemote.ok ? parseGitHubRemoteUrl(originRemote.stdout) : null;
16622
16625
  }
16623
16626
  async function resolveOrigin(env, exec, cwd) {
16624
16627
  const origin = {
@@ -16627,8 +16630,8 @@ async function resolveOrigin(env, exec, cwd) {
16627
16630
  };
16628
16631
  const pane = env.TMUX_PANE;
16629
16632
  if (pane) {
16630
- const output = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
16631
- const [target, paneId] = output?.trim().split(" ") ?? [];
16633
+ const answer = await tryExec(exec, "tmux", ["display-message", "-p", "-t", pane, "#S:#I.#P #{pane_id}"], cwd, TMUX_PANE_LOOKUP_MS);
16634
+ const [target, paneId] = answer.ok ? answer.stdout.trim().split(" ") : [];
16632
16635
  if (target)
16633
16636
  origin.tmux = target;
16634
16637
  if (paneId)
@@ -26411,6 +26414,7 @@ function dispatchTarget(env, config) {
26411
26414
  var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
26412
26415
  var DEFAULT_HEARTBEAT_MS = SESSION_HEARTBEAT_MS;
26413
26416
  var MIN_PASS_DEADLINE_MS = 30000;
26417
+ var DEFAULT_TMUX_PLACE_CACHE_MS = 30000;
26414
26418
  var MIN_INTERVAL_MS = 25;
26415
26419
  var MAX_INTERVAL_MS = 2147483647;
26416
26420
  var REGISTRATION_SETTLE_MS = 5000;
@@ -26424,6 +26428,10 @@ function namedInterval(raw) {
26424
26428
  const parsed = Number(raw);
26425
26429
  return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
26426
26430
  }
26431
+ function resolvedInterval(raw, fallback) {
26432
+ const named = namedInterval(raw);
26433
+ return named === undefined ? fallback : Math.min(Math.max(named, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
26434
+ }
26427
26435
  function natsSeedFromEnvironment(env) {
26428
26436
  const { NATS_NKEY_SEED_FILE: file, NATS_NKEY_SEED: plain } = env;
26429
26437
  if (file !== undefined) {
@@ -26514,8 +26522,8 @@ function envoySettings(env, config, options = {}) {
26514
26522
  inbound = { off: messageFor(error) };
26515
26523
  }
26516
26524
  }
26517
- const namedHeartbeatMs = namedInterval(env.ENVOY_HEARTBEAT_MS);
26518
- const heartbeatMs = namedHeartbeatMs === undefined ? DEFAULT_HEARTBEAT_MS : Math.min(Math.max(namedHeartbeatMs, MIN_INTERVAL_MS), MAX_INTERVAL_MS);
26525
+ const heartbeatMs = resolvedInterval(env.ENVOY_HEARTBEAT_MS, DEFAULT_HEARTBEAT_MS);
26526
+ const tmuxPlaceCacheMs = resolvedInterval(env.ENVOY_TMUX_PLACE_CACHE_MS, DEFAULT_TMUX_PLACE_CACHE_MS);
26519
26527
  const namedPassDeadlineMs = namedInterval(env.ENVOY_PASS_DEADLINE_MS);
26520
26528
  return {
26521
26529
  enabled: true,
@@ -26523,6 +26531,7 @@ function envoySettings(env, config, options = {}) {
26523
26531
  credential,
26524
26532
  inbound,
26525
26533
  heartbeatMs,
26534
+ tmuxPlaceCacheMs,
26526
26535
  passDeadlineMs: Math.min(Math.max(namedPassDeadlineMs ?? MIN_PASS_DEADLINE_MS, heartbeatMs), MAX_INTERVAL_MS),
26527
26536
  passDeadlineRaisedFrom: namedPassDeadlineMs !== undefined && namedPassDeadlineMs < heartbeatMs ? namedPassDeadlineMs : undefined
26528
26537
  };
@@ -55,13 +55,17 @@ other change to the head voids it.
55
55
  (`gh api repos/<o>/<r>/commits/<headRefOid>/check-runs --paginate --jq '.check_runs[] | [.name, .status, .conclusion] | @tsv'`):
56
56
  every required name present, `completed` and `success` (GitHub counts `neutral` and `skipped`
57
57
  as passing for a required check). A required check
58
- `queued` or `in_progress`: wait. One `completed` as `cancelled`, or absent: re-run that run's
58
+ `queued` or `in_progress`: wait. One `completed` as `cancelled`, or absent: on a `pull_request`
59
+ run, re-run that run's
59
60
  failed and cancelled jobs, once, then wait — never the whole run, which runs again every job that
60
- succeeded too. Find the run and job at the head: the head's runs
61
+ succeeded too. A merge-group run is never re-run: the implementer finishes its round instead,
62
+ and the pull request is tested again when it is queued again.
63
+ Find the run and job at the head: the head's runs
61
64
  (`gh api 'repos/<o>/<r>/actions/runs?head_sha=<headRefOid>&per_page=100' --jq '.workflow_runs[] | [.id, .name, .event, .status, .conclusion] | @tsv'`),
62
65
  then each candidate run's jobs
63
66
  (`gh api repos/<o>/<r>/actions/runs/<run-id>/jobs --jq '.jobs[] | [.id, .name, .status, .conclusion] | @tsv'`).
64
- A cancelled job: `gh run rerun <run-id> --failed`, which GitHub applies to the run's failed and
67
+ A cancelled job of a pull-request run: `gh run rerun <run-id> --failed`, which GitHub applies to
68
+ the run's failed and
65
69
  cancelled jobs and the jobs that depend on them, and to nothing that succeeded. Never
66
70
  `--job <job-id>` alone: under a routing workflow, re-running only the final job reads the
67
71
  cancelled results again and stays red. GitHub refuses a re-run while an attempt of that run is
@@ -54,11 +54,11 @@ gives them).
54
54
  Under the Go daemon a check or workflow the base branch requires that settles red on the head,
55
55
  before the completion or after it, sends the work back to `implementing` with the failing checks
56
56
  named, until `max_fix_attempts` is spent, when the architect decides. The one CI result an implementer waits for is a re-run: when its task names a red that
57
- failed on its own, it re-runs the failed jobs once (`gh run rerun <run-id> --failed`) and completes once
57
+ failed on its own in a `pull_request` run, it re-runs the failed jobs once (`gh run rerun <run-id> --failed`) and completes once
58
58
  the re-run settles green, woken by the pull request's checks event (`skill://envoy`, *Wait for
59
- CI or a merge*: subscribe, then end the turn), never by a sleep loop. A completion while that red
60
- still stands is sent back, at once for a required check and at the daemon's next read of the runs
61
- (within two minutes) for a required workflow.
59
+ CI or a merge*: subscribe, then end the turn), never by a sleep loop.
60
+ A merge-group run is never re-run: the implementer finishes its round instead, and the pull request is tested again when it is queued again.
61
+ A completion while that red still stands is sent back, at once for a required check and at the daemon's next read of the runs (within two minutes) for a required workflow.
62
62
 
63
63
  A pull-request body edit re-runs a check that judges the body at once, on the head as it is. While
64
64
  the pull request conflicts with its base (GitHub shows it `CONFLICTING`), no workflow triggered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/claude-envoy",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Claude Code channel plugin for Legion's Envoy bus",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
- import { execFileSync } from "node:child_process"
2
- import { readFileSync } from "node:fs"
3
1
  import { basename } from "node:path"
2
+ import { isAncestor } from "@legion/envoy-client/process-ancestry"
3
+ import { tmuxServer } from "@legion/envoy-client/tmux-server"
4
4
  import { configuredValue } from "./claude-session"
5
5
 
6
6
  export interface SessionTitleEnvironment {
@@ -48,9 +48,6 @@ const TMUX_PANE_FORMAT = [
48
48
  const TMUX_TIMEOUT_MS = 1_000
49
49
  const SHORT_ID_LENGTH = 8
50
50
 
51
- /** How many parents `isAncestor` follows before it stops. */
52
- const MAX_ANCESTORS = 64
53
-
54
51
  /** A terminal control sequence (a colour code: ESC [ parameters final byte). Built from the ESC
55
52
  * code, since a control character in a regular expression literal is a lint error. */
56
53
  const CSI_SEQUENCE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-?]*[ -/]*[@-~]`, "gu")
@@ -80,39 +77,6 @@ function visible(value: string): boolean {
80
77
  )
81
78
  }
82
79
 
83
- /** The parent of process `pid`, or undefined when it cannot be read. */
84
- function parentPid(pid: number): number | undefined {
85
- try {
86
- if (process.platform === "linux") {
87
- // The command name, in parentheses, may hold spaces; the parent is the second field after it.
88
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8")
89
- const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1])
90
- return Number.isInteger(parent) ? parent : undefined
91
- }
92
- const parent = Number(
93
- execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
94
- encoding: "utf8",
95
- timeout: TMUX_TIMEOUT_MS,
96
- killSignal: "SIGKILL",
97
- }).trim(),
98
- )
99
- return Number.isInteger(parent) ? parent : undefined
100
- } catch {
101
- return undefined
102
- }
103
- }
104
-
105
- /** Whether process `candidate` is `pid` or one of its ancestors, walking up from `pid` and
106
- * stopping at the first match, at init, or after `MAX_ANCESTORS` steps. */
107
- export function isAncestor(candidate: string, pid: number = process.pid): boolean {
108
- let current: number | undefined = pid
109
- for (let step = 0; current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
110
- if (String(current) === candidate) return true
111
- current = parentPid(current)
112
- }
113
- return false
114
- }
115
-
116
80
  /**
117
81
  * `tmux -u -S <socket> display-message -p -t <pane> <format>`; undefined when tmux is missing, slow
118
82
  * or refuses, or prints nothing: a running tmux server answers a pane it does not have with an
@@ -232,10 +196,7 @@ export async function sessionTitle(
232
196
  // A title with nothing that shows once cleaned (a zero-width space, a colour code alone) would
233
197
  // be a blank row.
234
198
  if (visible(explicit)) return () => explicit
235
- // `TMUX` is `<socket>,<server pid>,<session>`, and a socket path may hold a comma.
236
- const parts = configuredValue(environment.TMUX)?.split(",") ?? []
237
- const socket = parts.slice(0, -2).join(",")
238
- const serverPid = parts.at(-2) ?? ""
199
+ const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX))
239
200
  const pane = configuredValue(environment.TMUX_PANE)?.trim()
240
201
  // A directory name holds whatever the filesystem allows, newlines and overrides among them.
241
202
  const directoryName = oneLine(basename(directory) || directory)