@integrity-labs/agt-cli 0.28.611 → 0.28.613

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.
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-JWBUD6RZ.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-U7PE7ZDB.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-SZM65MVE.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-3UQ63455.js.map
@@ -53,7 +53,7 @@ import {
53
53
  safeWriteJsonAtomic,
54
54
  setConfigHash,
55
55
  tripClass
56
- } from "../chunk-6L5WLLVR.js";
56
+ } from "../chunk-D7EV77TB.js";
57
57
  import {
58
58
  getProjectDir as getProjectDir2,
59
59
  getReadyTasks,
@@ -109,7 +109,7 @@ import {
109
109
  takeZombieDetection,
110
110
  toOpencodeModel,
111
111
  writeEgressAllowlist
112
- } from "../chunk-DD6NRRYC.js";
112
+ } from "../chunk-JRL7754L.js";
113
113
  import {
114
114
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
115
115
  AnchorSessionClient,
@@ -186,7 +186,7 @@ import {
186
186
  subagentActivityAgeSeconds,
187
187
  sumTranscriptUsageInWindow,
188
188
  transcriptActivityAgeSeconds
189
- } from "../chunk-2ASO4HVN.js";
189
+ } from "../chunk-ZQXXK7JC.js";
190
190
  import {
191
191
  reapOrphanChannelMcps
192
192
  } from "../chunk-XWVM4KPK.js";
@@ -11287,7 +11287,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
11287
11287
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
11288
11288
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
11289
11289
  async function channelInboundActivityAgeSecondsFor(codeName) {
11290
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-GJEX44DE.js");
11290
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-K6DGNTXA.js");
11291
11291
  const newest = newestPendingInboundActivityMtimeMs(dirname9(paneLogPath(codeName)));
11292
11292
  if (newest === null) return null;
11293
11293
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -11926,7 +11926,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
11926
11926
  var lastVersionCheckAt = 0;
11927
11927
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
11928
11928
  var lastResponsivenessProbeAt = 0;
11929
- var agtCliVersion = true ? "0.28.611" : "dev";
11929
+ var agtCliVersion = true ? "0.28.613" : "dev";
11930
11930
  function resolveBrewPath(execFileSync3) {
11931
11931
  try {
11932
11932
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -13233,7 +13233,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
13233
13233
  if (codeNames.length === 0) return;
13234
13234
  void (async () => {
13235
13235
  try {
13236
- const { collectDiagnostics } = await import("../persistent-session-JWBUD6RZ.js");
13236
+ const { collectDiagnostics } = await import("../persistent-session-U7PE7ZDB.js");
13237
13237
  await api.post("/host/heartbeat", {
13238
13238
  host_id: hostId,
13239
13239
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor)
@@ -13341,7 +13341,7 @@ async function pollCycle() {
13341
13341
  }
13342
13342
  try {
13343
13343
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
13344
- const { collectDiagnostics } = await import("../persistent-session-JWBUD6RZ.js");
13344
+ const { collectDiagnostics } = await import("../persistent-session-U7PE7ZDB.js");
13345
13345
  const diagCodeNames = [...agentState.persistentSessionAgents];
13346
13346
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor) : void 0;
13347
13347
  let tailscaleHostname;
@@ -13464,7 +13464,7 @@ async function pollCycle() {
13464
13464
  collectPanelessActivityProbes,
13465
13465
  getResponsivenessIntervalMs,
13466
13466
  occupancyQualificationClassifications
13467
- } = await import("../responsiveness-probe-GJEX44DE.js");
13467
+ } = await import("../responsiveness-probe-K6DGNTXA.js");
13468
13468
  const probeIntervalMs = getResponsivenessIntervalMs();
13469
13469
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
13470
13470
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -13570,7 +13570,7 @@ async function pollCycle() {
13570
13570
  collectResponsivenessProbes,
13571
13571
  livePendingInboundOldestAgeSeconds,
13572
13572
  parkPendingInbound
13573
- } = await import("../responsiveness-probe-GJEX44DE.js");
13573
+ } = await import("../responsiveness-probe-K6DGNTXA.js");
13574
13574
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-NDP36U7O.js");
13575
13575
  const wedgeNow = /* @__PURE__ */ new Date();
13576
13576
  const liveAgents = agentState.persistentSessionAgents;
@@ -13726,7 +13726,7 @@ async function pollCycle() {
13726
13726
  }
13727
13727
  try {
13728
13728
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-JA437JIB.js");
13729
- const { probeSessionAuth } = await import("../session-auth-dead-LBRWZQPW.js");
13729
+ const { probeSessionAuth } = await import("../session-auth-dead-RPMWBVBH.js");
13730
13730
  const observations = [];
13731
13731
  const pendingCacheCommits = [];
13732
13732
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -17215,7 +17215,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
17215
17215
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
17216
17216
  void (async () => {
17217
17217
  try {
17218
- const { collectDiagnostics } = await import("../persistent-session-JWBUD6RZ.js");
17218
+ const { collectDiagnostics } = await import("../persistent-session-U7PE7ZDB.js");
17219
17219
  await api.post("/host/heartbeat", {
17220
17220
  host_id: hostId,
17221
17221
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor)
@@ -17266,7 +17266,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
17266
17266
  }
17267
17267
  try {
17268
17268
  const hostId = await getHostId();
17269
- const { collectDiagnostics } = await import("../persistent-session-JWBUD6RZ.js");
17269
+ const { collectDiagnostics } = await import("../persistent-session-U7PE7ZDB.js");
17270
17270
  await api.post("/host/heartbeat", {
17271
17271
  host_id: hostId,
17272
17272
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor)
@@ -17862,7 +17862,7 @@ async function processClaudePairSessions(agents) {
17862
17862
  killPairSession,
17863
17863
  pairTmuxSession,
17864
17864
  finalizeClaudePairOnboarding
17865
- } = await import("../claude-pair-runtime-SZM65MVE.js");
17865
+ } = await import("../claude-pair-runtime-3UQ63455.js");
17866
17866
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
17867
17867
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
17868
17868
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -30142,8 +30142,12 @@ function busyAckNoticeText() {
30142
30142
  return "\u{1F6E0}\uFE0F I'm in the middle of something right now \u2014 I'll follow up on this as soon as I'm free.";
30143
30143
  }
30144
30144
  var GIVE_UP_SIGNAL_MAX_AGE_MS = 30 * 60 * 1e3;
30145
- function turnFailedNoticeText() {
30146
- return "\u26A0\uFE0F Something went wrong on my side and I couldn\u2019t finish that. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.";
30145
+ function turnFailedNoticeText(failureClass) {
30146
+ const cause = failureClass === "overloaded" ? "the service I run on is overloaded right now \u2014 nothing to do with you or what you asked for" : failureClass === "server_error" ? "something upstream of me is failing right now \u2014 nothing to do with you or what you asked for" : null;
30147
+ if (cause === null) {
30148
+ return "\u26A0\uFE0F Something went wrong on my side and I couldn\u2019t finish that. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.";
30149
+ }
30150
+ return `\u26A0\uFE0F I couldn\u2019t finish that \u2014 ${cause}. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.`;
30147
30151
  }
30148
30152
  function oldestPendingMarkerAgeMs(dir, now = Date.now(), opts) {
30149
30153
  if (!dir) return null;
@@ -33928,7 +33932,14 @@ var AGENT_DIRECTED_NOTICE_KINDS = [
33928
33932
  // written for the machine. The user learns the same fact from the connect card
33929
33933
  // in that conversation flipping to its connected state, so showing this row as
33930
33934
  // well would say it twice, the second time in prose aimed at an agent.
33931
- "integration_connected"
33935
+ "integration_connected",
33936
+ // ENG-8745. The offer RESULT receipt (not the offer card — that one is
33937
+ // written for the human and is how they decided). Its content opens "Your
33938
+ // offer was ACCEPTED", carries an `[offer_result]` machine tag, and closes by
33939
+ // instructing the agent what not to do ("do not re-raise it yourself"). The
33940
+ // human needs none of it: they are the person who tapped the button, and the
33941
+ // card swapped under their finger to say so.
33942
+ "offer_result"
33932
33943
  ];
33933
33944
  var AGENT_DIRECTED_KIND_SET = new Set(AGENT_DIRECTED_NOTICE_KINDS);
33934
33945
 
@@ -34872,6 +34883,29 @@ registerTriggerSource(videoRenderTriggerAdapter);
34872
34883
 
34873
34884
  // ../core/dist/feature-flags/registry.js
34874
34885
  var FLAG_REGISTRY = [
34886
+ {
34887
+ key: "impersonation-enabled",
34888
+ description: "Kill switch for admin impersonation (ENG-9080), filed during the 18 Aug cross-tenant identity incident. ON (default) = impersonation works as today. OFF = both halves of the flow refuse: the mint route stops issuing redeem URLs, and \u2014 the half that actually matters \u2014 the REDEEM route refuses before it inserts the nonce or calls generateLink, so a URL that was already issued cannot still mint a session. Redeem tokens live REDEEM_TTL_SECONDS = 300, so gating only the mint route would leave five minutes in which new sessions keep appearing after an operator believes they have stopped it. Evaluated as a GLOBAL stage value, read UNCACHED on the redeem path (see readImpersonationEnabled): a kill switch that takes a cache TTL to bite is not a kill switch. Containment only \u2014 it does not fix the underlying defect that redeem writes the target's cookies into whatever browser context opens the URL (ENG-8923 / ENG-9020).",
34889
+ flagType: "boolean",
34890
+ // Declared safe value is `true` = impersonation available, i.e. today's
34891
+ // behaviour, so merging this changes nothing until an operator flips it.
34892
+ //
34893
+ // NOTE the asymmetry with the other gates in this file, and it is
34894
+ // deliberate: their safe direction is "off / don't do the new thing"
34895
+ // because they gate NEW behaviour. This one gates EXISTING behaviour, so
34896
+ // the no-op default is `true`. The READ direction is where the safety lives
34897
+ // instead — an unreachable flag store resolves to DISABLED, not to this
34898
+ // default. See readImpersonationEnabled for that argument.
34899
+ defaultValue: true,
34900
+ // Enforcement gate (ADR-0022 section 4): re-enabling impersonation is
34901
+ // "relaxing a security control", exactly the mutation that must not happen
34902
+ // on a single unguarded write. The emergency direction (turning it OFF)
34903
+ // costs one confirmation, which is the right price.
34904
+ sensitive: true
34905
+ // Deliberately NOT public. A browser has no business evaluating whether
34906
+ // impersonation is available; both consumers are server-side route
34907
+ // handlers.
34908
+ },
34875
34909
  {
34876
34910
  key: "at-rest-v2-writes",
34877
34911
  description: "ADR-0061 A2\u2192arm cutover (ENG-8944, parent ENG-8764). Gates whether the at-rest writer (encryptSecret/encryptAtRest) emits the v2 AAD-bound envelope when a DecryptContext is supplied; OFF (default) = the legacy v1 envelope (pre-ADR-0061 behaviour). Resolved FLEET-WIDE and process-global via setV2WritesArmed at the API + webapp + cron bootstrap seams, so ONLY the stage value applies \u2014 per-org overrides do not (the crypto writer carries no request/org context). Ships dark. Flip on only once every reader parses v2 (fleet-wide since release A1) AND the re-seal engine can re-seal v2 blobs before any AUTH_ENCRYPTION_KEY rotation (slice 5b). Reversible without redeploy: clearing it returns the writer to v1 within the flag cache TTL; already-written v2 blobs stay readable.",
@@ -38105,7 +38139,7 @@ function armTurnFailureWatch(args) {
38105
38139
  return;
38106
38140
  }
38107
38141
  lastTurnFailedNoticeAt.set(args.sessionId, now);
38108
- const ok = await postDirectChatNoticeMessage(args.sessionId, turnFailedNoticeText());
38142
+ const ok = await postDirectChatNoticeMessage(args.sessionId, turnFailedNoticeText(failure.failureClass));
38109
38143
  if (!ok) {
38110
38144
  lastTurnFailedNoticeAt.delete(args.sessionId);
38111
38145
  process.stderr.write(
@@ -40266,7 +40266,14 @@ var AGENT_DIRECTED_NOTICE_KINDS = [
40266
40266
  // written for the machine. The user learns the same fact from the connect card
40267
40267
  // in that conversation flipping to its connected state, so showing this row as
40268
40268
  // well would say it twice, the second time in prose aimed at an agent.
40269
- "integration_connected"
40269
+ "integration_connected",
40270
+ // ENG-8745. The offer RESULT receipt (not the offer card — that one is
40271
+ // written for the human and is how they decided). Its content opens "Your
40272
+ // offer was ACCEPTED", carries an `[offer_result]` machine tag, and closes by
40273
+ // instructing the agent what not to do ("do not re-raise it yourself"). The
40274
+ // human needs none of it: they are the person who tapped the button, and the
40275
+ // card swapped under their finger to say so.
40276
+ "offer_result"
40270
40277
  ];
40271
40278
  var AGENT_DIRECTED_KIND_SET = new Set(AGENT_DIRECTED_NOTICE_KINDS);
40272
40279
 
@@ -40763,6 +40770,29 @@ registerTriggerSource(videoRenderTriggerAdapter);
40763
40770
 
40764
40771
  // ../core/dist/feature-flags/registry.js
40765
40772
  var FLAG_REGISTRY = [
40773
+ {
40774
+ key: "impersonation-enabled",
40775
+ description: "Kill switch for admin impersonation (ENG-9080), filed during the 18 Aug cross-tenant identity incident. ON (default) = impersonation works as today. OFF = both halves of the flow refuse: the mint route stops issuing redeem URLs, and \u2014 the half that actually matters \u2014 the REDEEM route refuses before it inserts the nonce or calls generateLink, so a URL that was already issued cannot still mint a session. Redeem tokens live REDEEM_TTL_SECONDS = 300, so gating only the mint route would leave five minutes in which new sessions keep appearing after an operator believes they have stopped it. Evaluated as a GLOBAL stage value, read UNCACHED on the redeem path (see readImpersonationEnabled): a kill switch that takes a cache TTL to bite is not a kill switch. Containment only \u2014 it does not fix the underlying defect that redeem writes the target's cookies into whatever browser context opens the URL (ENG-8923 / ENG-9020).",
40776
+ flagType: "boolean",
40777
+ // Declared safe value is `true` = impersonation available, i.e. today's
40778
+ // behaviour, so merging this changes nothing until an operator flips it.
40779
+ //
40780
+ // NOTE the asymmetry with the other gates in this file, and it is
40781
+ // deliberate: their safe direction is "off / don't do the new thing"
40782
+ // because they gate NEW behaviour. This one gates EXISTING behaviour, so
40783
+ // the no-op default is `true`. The READ direction is where the safety lives
40784
+ // instead — an unreachable flag store resolves to DISABLED, not to this
40785
+ // default. See readImpersonationEnabled for that argument.
40786
+ defaultValue: true,
40787
+ // Enforcement gate (ADR-0022 section 4): re-enabling impersonation is
40788
+ // "relaxing a security control", exactly the mutation that must not happen
40789
+ // on a single unguarded write. The emergency direction (turning it OFF)
40790
+ // costs one confirmation, which is the right price.
40791
+ sensitive: true
40792
+ // Deliberately NOT public. A browser has no business evaluating whether
40793
+ // impersonation is available; both consumers are server-side route
40794
+ // handlers.
40795
+ },
40766
40796
  {
40767
40797
  key: "at-rest-v2-writes",
40768
40798
  description: "ADR-0061 A2\u2192arm cutover (ENG-8944, parent ENG-8764). Gates whether the at-rest writer (encryptSecret/encryptAtRest) emits the v2 AAD-bound envelope when a DecryptContext is supplied; OFF (default) = the legacy v1 envelope (pre-ADR-0061 behaviour). Resolved FLEET-WIDE and process-global via setV2WritesArmed at the API + webapp + cron bootstrap seams, so ONLY the stage value applies \u2014 per-org overrides do not (the crypto writer carries no request/org context). Ships dark. Flip on only once every reader parses v2 (fleet-wide since release A1) AND the re-seal engine can re-seal v2 blobs before any AUTH_ENCRYPTION_KEY rotation (slice 5b). Reversible without redeploy: clearing it returns the writer to v1 within the flag cache TTL; already-written v2 blobs stay readable.",
@@ -34317,7 +34317,14 @@ var AGENT_DIRECTED_NOTICE_KINDS = [
34317
34317
  // written for the machine. The user learns the same fact from the connect card
34318
34318
  // in that conversation flipping to its connected state, so showing this row as
34319
34319
  // well would say it twice, the second time in prose aimed at an agent.
34320
- "integration_connected"
34320
+ "integration_connected",
34321
+ // ENG-8745. The offer RESULT receipt (not the offer card — that one is
34322
+ // written for the human and is how they decided). Its content opens "Your
34323
+ // offer was ACCEPTED", carries an `[offer_result]` machine tag, and closes by
34324
+ // instructing the agent what not to do ("do not re-raise it yourself"). The
34325
+ // human needs none of it: they are the person who tapped the button, and the
34326
+ // card swapped under their finger to say so.
34327
+ "offer_result"
34321
34328
  ];
34322
34329
  var AGENT_DIRECTED_KIND_SET = new Set(AGENT_DIRECTED_NOTICE_KINDS);
34323
34330
 
@@ -35208,6 +35215,29 @@ registerTriggerSource(videoRenderTriggerAdapter);
35208
35215
 
35209
35216
  // ../core/dist/feature-flags/registry.js
35210
35217
  var FLAG_REGISTRY = [
35218
+ {
35219
+ key: "impersonation-enabled",
35220
+ description: "Kill switch for admin impersonation (ENG-9080), filed during the 18 Aug cross-tenant identity incident. ON (default) = impersonation works as today. OFF = both halves of the flow refuse: the mint route stops issuing redeem URLs, and \u2014 the half that actually matters \u2014 the REDEEM route refuses before it inserts the nonce or calls generateLink, so a URL that was already issued cannot still mint a session. Redeem tokens live REDEEM_TTL_SECONDS = 300, so gating only the mint route would leave five minutes in which new sessions keep appearing after an operator believes they have stopped it. Evaluated as a GLOBAL stage value, read UNCACHED on the redeem path (see readImpersonationEnabled): a kill switch that takes a cache TTL to bite is not a kill switch. Containment only \u2014 it does not fix the underlying defect that redeem writes the target's cookies into whatever browser context opens the URL (ENG-8923 / ENG-9020).",
35221
+ flagType: "boolean",
35222
+ // Declared safe value is `true` = impersonation available, i.e. today's
35223
+ // behaviour, so merging this changes nothing until an operator flips it.
35224
+ //
35225
+ // NOTE the asymmetry with the other gates in this file, and it is
35226
+ // deliberate: their safe direction is "off / don't do the new thing"
35227
+ // because they gate NEW behaviour. This one gates EXISTING behaviour, so
35228
+ // the no-op default is `true`. The READ direction is where the safety lives
35229
+ // instead — an unreachable flag store resolves to DISABLED, not to this
35230
+ // default. See readImpersonationEnabled for that argument.
35231
+ defaultValue: true,
35232
+ // Enforcement gate (ADR-0022 section 4): re-enabling impersonation is
35233
+ // "relaxing a security control", exactly the mutation that must not happen
35234
+ // on a single unguarded write. The emergency direction (turning it OFF)
35235
+ // costs one confirmation, which is the right price.
35236
+ sensitive: true
35237
+ // Deliberately NOT public. A browser has no business evaluating whether
35238
+ // impersonation is available; both consumers are server-side route
35239
+ // handlers.
35240
+ },
35211
35241
  {
35212
35242
  key: "at-rest-v2-writes",
35213
35243
  description: "ADR-0061 A2\u2192arm cutover (ENG-8944, parent ENG-8764). Gates whether the at-rest writer (encryptSecret/encryptAtRest) emits the v2 AAD-bound envelope when a DecryptContext is supplied; OFF (default) = the legacy v1 envelope (pre-ADR-0061 behaviour). Resolved FLEET-WIDE and process-global via setV2WritesArmed at the API + webapp + cron bootstrap seams, so ONLY the stage value applies \u2014 per-org overrides do not (the crypto writer carries no request/org context). Ships dark. Flip on only once every reader parses v2 (fleet-wide since release A1) AND the re-seal engine can re-seal v2 blobs before any AUTH_ENCRYPTION_KEY rotation (slice 5b). Reversible without redeploy: clearing it returns the writer to v1 within the flag cache TTL; already-written v2 blobs stay readable.",
@@ -36974,8 +37004,12 @@ function giveUpNoticeText(reason = null) {
36974
37004
  }
36975
37005
  return "\u26A0\uFE0F I couldn't read your last message \u2014 please resend it.";
36976
37006
  }
36977
- function turnFailedNoticeText() {
36978
- return "\u26A0\uFE0F Something went wrong on my side and I couldn\u2019t finish that. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.";
37007
+ function turnFailedNoticeText(failureClass) {
37008
+ const cause = failureClass === "overloaded" ? "the service I run on is overloaded right now \u2014 nothing to do with you or what you asked for" : failureClass === "server_error" ? "something upstream of me is failing right now \u2014 nothing to do with you or what you asked for" : null;
37009
+ if (cause === null) {
37010
+ return "\u26A0\uFE0F Something went wrong on my side and I couldn\u2019t finish that. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.";
37011
+ }
37012
+ return `\u26A0\uFE0F I couldn\u2019t finish that \u2014 ${cause}. Sorry \u2014 send it again when you get a chance and I\u2019ll pick it straight up.`;
36979
37013
  }
36980
37014
  function turnRetryingNoticeText() {
36981
37015
  return "\u23F3 Still working on this \u2014 things are running slower than usual on my side. I\u2019ll reply as soon as I have it.";
@@ -39367,6 +39401,91 @@ async function runOrRetry(fn, opts) {
39367
39401
  }
39368
39402
  }
39369
39403
 
39404
+ // src/slack-socket-reconnect.ts
39405
+ var RECONNECT_BASE_DELAY_MS = 1e3;
39406
+ var RECONNECT_MAX_DELAY_MS = 3e4;
39407
+ var SOCKET_OPEN_TIMEOUT_MS = 3e4;
39408
+ var CONNECT_FETCH_TIMEOUT_MS = 15e3;
39409
+ var SOCKET_DOWN_ALERT_AFTER_MS = 3 * 6e4;
39410
+ function reconnectDelayMs(attempt, opts = {}) {
39411
+ const base = opts.baseMs ?? RECONNECT_BASE_DELAY_MS;
39412
+ const max = opts.maxMs ?? RECONNECT_MAX_DELAY_MS;
39413
+ const random = opts.random ?? Math.random;
39414
+ const safeAttempt = Math.max(1, Math.min(Math.floor(attempt), 30));
39415
+ const ceiling = Math.min(max, base * 2 ** (safeAttempt - 1));
39416
+ const floor = Math.min(base, ceiling);
39417
+ return Math.round(floor + random() * (ceiling - floor));
39418
+ }
39419
+ function redactSocketUrl(text) {
39420
+ return text.replace(
39421
+ /([?&])(ticket|token|app_id_key)=[^&\s"']*/gi,
39422
+ (_m, sep2, key2) => `${sep2}${key2}=<redacted>`
39423
+ );
39424
+ }
39425
+ function describeWebSocketError(err) {
39426
+ const parts = [];
39427
+ const e = err ?? {};
39428
+ if (typeof e.message === "string" && e.message) parts.push(e.message);
39429
+ const cause = e.error;
39430
+ if (cause && typeof cause === "object") {
39431
+ if (typeof cause.message === "string" && cause.message && !parts.includes(cause.message)) {
39432
+ parts.push(cause.message);
39433
+ }
39434
+ if (typeof cause.code === "string" && cause.code) parts.push(`errno=${cause.code}`);
39435
+ }
39436
+ if (typeof e.code === "number") parts.push(`code=${e.code}`);
39437
+ if (typeof e.reason === "string" && e.reason) parts.push(`reason=${e.reason}`);
39438
+ if (typeof e.wasClean === "boolean") parts.push(`wasClean=${e.wasClean}`);
39439
+ if (parts.length === 0) {
39440
+ const type = typeof e.type === "string" && e.type ? e.type : typeof err;
39441
+ const ctor = err && typeof err === "object" ? err.constructor?.name ?? "Object" : typeof err;
39442
+ return redactSocketUrl(`no detail available (${ctor}, type=${type})`);
39443
+ }
39444
+ return redactSocketUrl(parts.join(" "));
39445
+ }
39446
+ function isWebSocketOpen(ws) {
39447
+ return ws?.readyState === 1;
39448
+ }
39449
+ function createReconnectScheduler(deps) {
39450
+ const now = deps.now ?? Date.now;
39451
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
39452
+ let pending = false;
39453
+ let attempt = 0;
39454
+ let downSince = now();
39455
+ return {
39456
+ schedule(reason) {
39457
+ if (deps.shouldStop() || pending) return;
39458
+ pending = true;
39459
+ if (downSince === null) downSince = now();
39460
+ attempt += 1;
39461
+ const opts = {};
39462
+ if (deps.baseMs !== void 0) opts.baseMs = deps.baseMs;
39463
+ if (deps.maxMs !== void 0) opts.maxMs = deps.maxMs;
39464
+ if (deps.random !== void 0) opts.random = deps.random;
39465
+ const delay = reconnectDelayMs(attempt, opts);
39466
+ deps.log(
39467
+ `slack-channel: Socket Mode reconnecting in ${delay}ms (attempt ${attempt}, ${reason})`
39468
+ );
39469
+ const timer = setTimer(() => {
39470
+ pending = false;
39471
+ if (deps.shouldStop()) return;
39472
+ deps.connect();
39473
+ }, delay);
39474
+ timer.unref?.();
39475
+ },
39476
+ noteOpen() {
39477
+ attempt = 0;
39478
+ downSince = null;
39479
+ },
39480
+ downForMs(connected) {
39481
+ if (connected) return null;
39482
+ return downSince === null ? null : now() - downSince;
39483
+ },
39484
+ attempts: () => attempt,
39485
+ isPending: () => pending
39486
+ };
39487
+ }
39488
+
39370
39489
  // src/turn-initiator-marker.ts
39371
39490
  import { writeFileSync as writeFileSync7, readFileSync as readFileSync9, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
39372
39491
  import { dirname as dirname4, join as join12 } from "path";
@@ -42678,7 +42797,7 @@ function armSlackTurnFailureWatch(args) {
42678
42797
  const posted = await postSlackTurnNotice({
42679
42798
  channel,
42680
42799
  threadTs: args.threadTs,
42681
- text: turnFailedNoticeText()
42800
+ text: turnFailedNoticeText(failure.failureClass)
42682
42801
  });
42683
42802
  if (!posted.ok) {
42684
42803
  lastSlackTurnFailedNoticeAt.delete(conversationKey);
@@ -43136,10 +43255,12 @@ function formatLastActivity() {
43136
43255
  return `${kindLabel} ${ago}`;
43137
43256
  }
43138
43257
  function buildAgentStatusReply(codeName) {
43139
- const connected = currentWs != null && !isShuttingDown;
43258
+ const connected = isSocketConnected();
43140
43259
  const dot = connected ? "\u{1F7E2}" : "\u{1F534}";
43141
43260
  const state = connected ? "online" : "offline";
43142
- const connectivityLine = `${dot} *${state}* \u2014 Socket Mode ${connected ? "connected" : "disconnected"}. Last activity: ${formatLastActivity()}.`;
43261
+ const downMs = socketDownForMs();
43262
+ const downSuffix = !connected && downMs !== null ? ` for ${Math.floor(downMs / 6e4)}m \u2014 inbound is dead` : "";
43263
+ const connectivityLine = `${dot} *${state}* \u2014 Socket Mode ${connected ? "connected" : `disconnected${downSuffix}`}. Last activity: ${formatLastActivity()}.`;
43143
43264
  const sessionState = readAgentSessionState(SLACK_AGENT_DIR);
43144
43265
  return buildAgentConfigReport({ codeName, connectivityLine, state: sessionState });
43145
43266
  }
@@ -43150,9 +43271,16 @@ function statusRuntimeConfig() {
43150
43271
  }
43151
43272
  async function computeSlackOwnChannelLiveStatus() {
43152
43273
  const checked_at = (/* @__PURE__ */ new Date()).toISOString();
43153
- const connected = currentWs != null && !isShuttingDown;
43274
+ const connected = isSocketConnected();
43154
43275
  if (!connected) {
43155
- return { level: "down", checked_at, reason: "Socket Mode disconnected" };
43276
+ const downMs = socketDownForMs();
43277
+ const forSuffix = downMs === null ? "" : ` for ${Math.floor(downMs / 1e3)}s`;
43278
+ const sustained = downMs !== null && downMs >= SOCKET_DOWN_ALERT_AFTER_MS;
43279
+ return {
43280
+ level: "down",
43281
+ checked_at,
43282
+ reason: `Socket Mode disconnected${forSuffix}${sustained ? " - inbound Slack is DEAD (outbound still works, so this is invisible from outside)" : ""}`
43283
+ };
43156
43284
  }
43157
43285
  const { id, authFailed } = await getBotUserId();
43158
43286
  if (id) {
@@ -43679,7 +43807,7 @@ async function handleSlashCommandEnvelope(payload) {
43679
43807
  );
43680
43808
  return;
43681
43809
  }
43682
- const connected = currentWs != null && !isShuttingDown;
43810
+ const connected = isSocketConnected();
43683
43811
  const dot = connected ? "\u{1F7E2}" : "\u{1F534}";
43684
43812
  const state = connected ? "online" : "offline";
43685
43813
  const pong = `${dot} pong - \`${codeName}\` is ${state} (manager \u2022 ${(/* @__PURE__ */ new Date()).toISOString()}). Last activity: ${formatLastActivity()}.`;
@@ -45751,6 +45879,47 @@ async function resolveUserEmail(userId) {
45751
45879
  }
45752
45880
  var currentWs = null;
45753
45881
  var isShuttingDown = false;
45882
+ var clearOpenWatchdog = null;
45883
+ function clearOpenWatchdogFor(ws) {
45884
+ if (currentWs !== ws) return;
45885
+ clearOpenWatchdog?.();
45886
+ clearOpenWatchdog = null;
45887
+ }
45888
+ var reconnectScheduler = createReconnectScheduler({
45889
+ connect: () => connectSocketModeSafely(),
45890
+ log: (line) => process.stderr.write(`${line}
45891
+ `),
45892
+ shouldStop: () => isShuttingDown
45893
+ });
45894
+ function isSocketConnected() {
45895
+ return !isShuttingDown && isWebSocketOpen(currentWs);
45896
+ }
45897
+ function scheduleReconnect(reason) {
45898
+ reconnectScheduler.schedule(reason);
45899
+ }
45900
+ function armOpenWatchdog(ws) {
45901
+ clearOpenWatchdog?.();
45902
+ const timer = setTimeout(() => {
45903
+ if (currentWs !== ws) return;
45904
+ if (isShuttingDown || isWebSocketOpen(ws)) return;
45905
+ process.stderr.write(
45906
+ `slack-channel: Socket Mode did not open within ${SOCKET_OPEN_TIMEOUT_MS}ms - abandoning it
45907
+ `
45908
+ );
45909
+ currentWs = null;
45910
+ clearOpenWatchdog = null;
45911
+ try {
45912
+ ws.close();
45913
+ } catch {
45914
+ }
45915
+ scheduleReconnect("open timeout");
45916
+ }, SOCKET_OPEN_TIMEOUT_MS);
45917
+ timer.unref?.();
45918
+ clearOpenWatchdog = () => clearTimeout(timer);
45919
+ }
45920
+ function socketDownForMs() {
45921
+ return reconnectScheduler.downForMs(isSocketConnected());
45922
+ }
45754
45923
  var acquiredLockPath = null;
45755
45924
  var stopLockHeartbeat = null;
45756
45925
  async function seedMutedChannelsFromApi() {
@@ -45801,14 +45970,27 @@ async function connectSocketMode() {
45801
45970
  if (isShuttingDown) return;
45802
45971
  const res = await fetch("https://slack.com/api/apps.connections.open", {
45803
45972
  method: "POST",
45804
- headers: { Authorization: `Bearer ${APP_TOKEN}` }
45973
+ headers: { Authorization: `Bearer ${APP_TOKEN}` },
45974
+ // CodeRabbit, #4655. Without a deadline a hanging fetch is the one failure
45975
+ // this whole change does not cover: it never rejects, so the retry chain is
45976
+ // never entered, and no socket exists yet so the open watchdog does not
45977
+ // apply either. The process ends up with nothing pending and no attempt in
45978
+ // flight — the incident's shape, reached a different way. A timeout turns
45979
+ // the hang into a rejection, which the retry chain already handles.
45980
+ signal: AbortSignal.timeout(CONNECT_FETCH_TIMEOUT_MS)
45981
+ // CodeRabbit, #4655. Without a deadline a hanging fetch is the one failure
45982
+ // this whole change does not cover: it never rejects, so the retry chain is
45983
+ // never entered, and no socket exists yet so the open watchdog does not
45984
+ // apply either. The process ends up with nothing pending and no attempt in
45985
+ // flight — the incident's shape, reached a different way. A timeout turns
45986
+ // the hang into a rejection, which the retry chain already handles.
45805
45987
  });
45806
45988
  const data = await res.json();
45807
45989
  if (isShuttingDown) return;
45808
45990
  if (!data.ok || !data.url) {
45809
45991
  process.stderr.write(`slack-channel: Socket Mode connection failed: ${data.error}
45810
45992
  `);
45811
- if (!isShuttingDown) setTimeout(connectSocketModeSafely, 1e4).unref?.();
45993
+ scheduleReconnect(`apps.connections.open ${data.error ?? "failed"}`);
45812
45994
  return;
45813
45995
  }
45814
45996
  const ws = new WebSocket(data.url);
@@ -45820,7 +46002,17 @@ async function connectSocketMode() {
45820
46002
  return;
45821
46003
  }
45822
46004
  currentWs = ws;
46005
+ armOpenWatchdog(ws);
45823
46006
  ws.onopen = () => {
46007
+ if (currentWs !== ws) {
46008
+ try {
46009
+ ws.close();
46010
+ } catch {
46011
+ }
46012
+ return;
46013
+ }
46014
+ clearOpenWatchdogFor(ws);
46015
+ reconnectScheduler.noteOpen();
45824
46016
  process.stderr.write("slack-channel: Socket Mode connected\n");
45825
46017
  recordActivity("connect");
45826
46018
  void setBotStatus(":large_green_circle:", "Active");
@@ -46466,15 +46658,35 @@ async function connectSocketMode() {
46466
46658
  `);
46467
46659
  }
46468
46660
  };
46469
- ws.onclose = () => {
46470
- if (currentWs === ws) currentWs = null;
46661
+ ws.onclose = (evt) => {
46662
+ if (currentWs !== ws) return;
46663
+ clearOpenWatchdogFor(ws);
46664
+ currentWs = null;
46471
46665
  if (isShuttingDown) return;
46472
- process.stderr.write("slack-channel: Socket Mode disconnected, reconnecting...\n");
46473
- setTimeout(connectSocketModeSafely, 5e3).unref?.();
46666
+ process.stderr.write(
46667
+ `slack-channel: Socket Mode disconnected (${describeWebSocketError(evt)})
46668
+ `
46669
+ );
46670
+ scheduleReconnect("close");
46474
46671
  };
46475
46672
  ws.onerror = (err) => {
46476
- process.stderr.write(`slack-channel: WebSocket error: ${err}
46673
+ process.stderr.write(`slack-channel: WebSocket error: ${describeWebSocketError(err)}
46477
46674
  `);
46675
+ if (currentWs !== ws) {
46676
+ try {
46677
+ ws.close();
46678
+ } catch {
46679
+ }
46680
+ return;
46681
+ }
46682
+ clearOpenWatchdogFor(ws);
46683
+ currentWs = null;
46684
+ try {
46685
+ ws.close();
46686
+ } catch {
46687
+ }
46688
+ if (isShuttingDown) return;
46689
+ scheduleReconnect("error");
46478
46690
  };
46479
46691
  }
46480
46692
  function shutdown(reason, exitCode = 0) {