@integrity-labs/agt-cli 0.28.612 → 0.28.614

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.
@@ -33932,7 +33932,14 @@ var AGENT_DIRECTED_NOTICE_KINDS = [
33932
33932
  // written for the machine. The user learns the same fact from the connect card
33933
33933
  // in that conversation flipping to its connected state, so showing this row as
33934
33934
  // well would say it twice, the second time in prose aimed at an agent.
33935
- "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"
33936
33943
  ];
33937
33944
  var AGENT_DIRECTED_KIND_SET = new Set(AGENT_DIRECTED_NOTICE_KINDS);
33938
33945
 
@@ -34082,6 +34089,17 @@ var CONDITIONAL_DELIVERY_BODY = [
34082
34089
  var FRESH_HEARTBEAT_THRESHOLD_MS = 2 * 60 * 1e3;
34083
34090
 
34084
34091
  // ../core/dist/claude-code-usage/banner-parser.js
34092
+ var SESSION_QUALIFIER = /^(?:session|\d{1,2}\s*-?\s*hours?|\d{1,2}h)$/i;
34093
+ function classifyLimitScope(qualifier) {
34094
+ const q = (qualifier ?? "").trim();
34095
+ if (!q)
34096
+ return "weekly";
34097
+ if (SESSION_QUALIFIER.test(q))
34098
+ return "session";
34099
+ if (/^weekly$/i.test(q))
34100
+ return "weekly";
34101
+ return "unknown";
34102
+ }
34085
34103
  var SEP = /[\s·\-–—]+/.source;
34086
34104
  var SUBJECT = /(?:You(?:['’]ve|\s+have)?\s+)?/.source;
34087
34105
  var TIME_OF_DAY = /\d{1,2}(?::\d{2})?\s*(?:am|pm)(?:\s*\(?UTC\)?)?/.source;
@@ -34095,7 +34113,27 @@ var BANNER_PATTERNS = [
34095
34113
  // Wrapped to keep the per-pattern shape consistent with the percentage form:
34096
34114
  // the parser checks group 1 for a digit string and treats a missing one as
34097
34115
  // pct=100.
34098
- new RegExp(`${SUBJECT}hit\\s+your\\s+(?:[a-z0-9-]+\\s+)?limit${SEP}resets\\s+(${RESET_DATE})`, "i")
34116
+ // ENG-9007: the qualifier is CAPTURED now, by name. It used to be
34117
+ // `(?:[a-z0-9-]+\s+)?` - permissive and non-capturing - so the parser
34118
+ // recognised "session" and "5-hour", matched on them, and threw the word away.
34119
+ // Named groups keep the numbered capture indices stable for the percentage
34120
+ // form above, which still reads groups 1 and 2 positionally.
34121
+ new RegExp(
34122
+ // CodeRabbit on #4621: the qualifier was ONE token, so `classifyLimitScope`
34123
+ // accepted "5 hours" while this pattern could never deliver it — the whole
34124
+ // banner failed to match, no observation was recorded, and the agent went
34125
+ // dark rather than merely being misclassified. Multi-token qualifiers now
34126
+ // match the duration shapes SESSION_QUALIFIER already accepts.
34127
+ //
34128
+ // Deliberately NOT the naive `[a-z0-9]+(?:\s*-?\s*[a-z0-9]+)*`: the
34129
+ // separator there can match empty, which reduces to `(a+)+` and backtracks
34130
+ // catastrophically on a long alphanumeric run that is never followed by
34131
+ // "limit". This parser runs over agent-produced pane.log tails, so that is
34132
+ // reachable input. Requiring at least one separator per extra token, and
34133
+ // bounding the count, keeps it linear.
34134
+ `${SUBJECT}hit\\s+your\\s+(?<qualifier>[a-z0-9]+(?:[\\s-]+[a-z0-9]+){0,3}\\s+)?limit${SEP}resets\\s+(?<reset>${RESET_DATE})`,
34135
+ "i"
34136
+ )
34099
34137
  ];
34100
34138
  function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34101
34139
  let bestIndex = -1;
@@ -34108,24 +34146,33 @@ function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34108
34146
  pattern.lastIndex++;
34109
34147
  let pct;
34110
34148
  let resetStr;
34149
+ let qualifier;
34111
34150
  if (i === 0) {
34112
34151
  pct = Number.parseInt(match[1], 10);
34113
34152
  resetStr = match[2];
34153
+ qualifier = "weekly";
34114
34154
  } else {
34115
34155
  pct = 100;
34116
- resetStr = match[1];
34156
+ resetStr = match.groups?.["reset"] ?? "";
34157
+ qualifier = match.groups?.["qualifier"]?.trim() || null;
34117
34158
  }
34118
34159
  if (!Number.isFinite(pct) || pct < 0 || pct > 100)
34119
34160
  continue;
34120
34161
  const nextChar = text[match.index + match[0].length];
34121
34162
  if (nextChar !== void 0 && /[A-Za-z0-9]/.test(nextChar))
34122
34163
  continue;
34123
- const weekResetsAt = parseResetDateTime(resetStr, now);
34124
- if (!weekResetsAt)
34164
+ const reset = parseResetDateTime(resetStr, now);
34165
+ if (!reset)
34125
34166
  continue;
34126
34167
  if (match.index >= bestIndex) {
34127
34168
  bestIndex = match.index;
34128
- best = { pct, weekResetsAt };
34169
+ best = {
34170
+ pct,
34171
+ weekResetsAt: reset.at,
34172
+ resetPrecision: reset.precision,
34173
+ limitScope: classifyLimitScope(qualifier),
34174
+ limitQualifier: qualifier
34175
+ };
34129
34176
  }
34130
34177
  }
34131
34178
  }
@@ -34169,7 +34216,10 @@ function parseResetDateTime(humanDate, now) {
34169
34216
  if (!hm)
34170
34217
  return null;
34171
34218
  const todayAt = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hm.hour, hm.minute);
34172
- return new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt);
34219
+ return {
34220
+ at: new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt),
34221
+ precision: "hour_only"
34222
+ };
34173
34223
  }
34174
34224
  const timeMatch = trimmed.match(TIME_TAIL);
34175
34225
  const dateOnly = timeMatch ? trimmed.slice(0, timeMatch.index).trim() : trimmed;
@@ -34202,7 +34252,7 @@ function parseResetDateTime(humanDate, now) {
34202
34252
  resolved = candidate;
34203
34253
  }
34204
34254
  }
34205
- return resolved;
34255
+ return resolved ? { at: resolved, precision: "dated" } : null;
34206
34256
  }
34207
34257
 
34208
34258
  // ../core/dist/claude-code-usage/run-marker.js
@@ -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
 
@@ -40373,7 +40380,27 @@ var BANNER_PATTERNS = [
40373
40380
  // Wrapped to keep the per-pattern shape consistent with the percentage form:
40374
40381
  // the parser checks group 1 for a digit string and treats a missing one as
40375
40382
  // pct=100.
40376
- new RegExp(`${SUBJECT}hit\\s+your\\s+(?:[a-z0-9-]+\\s+)?limit${SEP}resets\\s+(${RESET_DATE})`, "i")
40383
+ // ENG-9007: the qualifier is CAPTURED now, by name. It used to be
40384
+ // `(?:[a-z0-9-]+\s+)?` - permissive and non-capturing - so the parser
40385
+ // recognised "session" and "5-hour", matched on them, and threw the word away.
40386
+ // Named groups keep the numbered capture indices stable for the percentage
40387
+ // form above, which still reads groups 1 and 2 positionally.
40388
+ new RegExp(
40389
+ // CodeRabbit on #4621: the qualifier was ONE token, so `classifyLimitScope`
40390
+ // accepted "5 hours" while this pattern could never deliver it — the whole
40391
+ // banner failed to match, no observation was recorded, and the agent went
40392
+ // dark rather than merely being misclassified. Multi-token qualifiers now
40393
+ // match the duration shapes SESSION_QUALIFIER already accepts.
40394
+ //
40395
+ // Deliberately NOT the naive `[a-z0-9]+(?:\s*-?\s*[a-z0-9]+)*`: the
40396
+ // separator there can match empty, which reduces to `(a+)+` and backtracks
40397
+ // catastrophically on a long alphanumeric run that is never followed by
40398
+ // "limit". This parser runs over agent-produced pane.log tails, so that is
40399
+ // reachable input. Requiring at least one separator per extra token, and
40400
+ // bounding the count, keeps it linear.
40401
+ `${SUBJECT}hit\\s+your\\s+(?<qualifier>[a-z0-9]+(?:[\\s-]+[a-z0-9]+){0,3}\\s+)?limit${SEP}resets\\s+(?<reset>${RESET_DATE})`,
40402
+ "i"
40403
+ )
40377
40404
  ];
40378
40405
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
40379
40406
 
@@ -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
 
@@ -34411,6 +34418,17 @@ var CONDITIONAL_DELIVERY_BODY = [
34411
34418
  var FRESH_HEARTBEAT_THRESHOLD_MS = 2 * 60 * 1e3;
34412
34419
 
34413
34420
  // ../core/dist/claude-code-usage/banner-parser.js
34421
+ var SESSION_QUALIFIER = /^(?:session|\d{1,2}\s*-?\s*hours?|\d{1,2}h)$/i;
34422
+ function classifyLimitScope(qualifier) {
34423
+ const q = (qualifier ?? "").trim();
34424
+ if (!q)
34425
+ return "weekly";
34426
+ if (SESSION_QUALIFIER.test(q))
34427
+ return "session";
34428
+ if (/^weekly$/i.test(q))
34429
+ return "weekly";
34430
+ return "unknown";
34431
+ }
34414
34432
  var SEP = /[\s·\-–—]+/.source;
34415
34433
  var SUBJECT = /(?:You(?:['’]ve|\s+have)?\s+)?/.source;
34416
34434
  var TIME_OF_DAY = /\d{1,2}(?::\d{2})?\s*(?:am|pm)(?:\s*\(?UTC\)?)?/.source;
@@ -34424,7 +34442,27 @@ var BANNER_PATTERNS = [
34424
34442
  // Wrapped to keep the per-pattern shape consistent with the percentage form:
34425
34443
  // the parser checks group 1 for a digit string and treats a missing one as
34426
34444
  // pct=100.
34427
- new RegExp(`${SUBJECT}hit\\s+your\\s+(?:[a-z0-9-]+\\s+)?limit${SEP}resets\\s+(${RESET_DATE})`, "i")
34445
+ // ENG-9007: the qualifier is CAPTURED now, by name. It used to be
34446
+ // `(?:[a-z0-9-]+\s+)?` - permissive and non-capturing - so the parser
34447
+ // recognised "session" and "5-hour", matched on them, and threw the word away.
34448
+ // Named groups keep the numbered capture indices stable for the percentage
34449
+ // form above, which still reads groups 1 and 2 positionally.
34450
+ new RegExp(
34451
+ // CodeRabbit on #4621: the qualifier was ONE token, so `classifyLimitScope`
34452
+ // accepted "5 hours" while this pattern could never deliver it — the whole
34453
+ // banner failed to match, no observation was recorded, and the agent went
34454
+ // dark rather than merely being misclassified. Multi-token qualifiers now
34455
+ // match the duration shapes SESSION_QUALIFIER already accepts.
34456
+ //
34457
+ // Deliberately NOT the naive `[a-z0-9]+(?:\s*-?\s*[a-z0-9]+)*`: the
34458
+ // separator there can match empty, which reduces to `(a+)+` and backtracks
34459
+ // catastrophically on a long alphanumeric run that is never followed by
34460
+ // "limit". This parser runs over agent-produced pane.log tails, so that is
34461
+ // reachable input. Requiring at least one separator per extra token, and
34462
+ // bounding the count, keeps it linear.
34463
+ `${SUBJECT}hit\\s+your\\s+(?<qualifier>[a-z0-9]+(?:[\\s-]+[a-z0-9]+){0,3}\\s+)?limit${SEP}resets\\s+(?<reset>${RESET_DATE})`,
34464
+ "i"
34465
+ )
34428
34466
  ];
34429
34467
  function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34430
34468
  let bestIndex = -1;
@@ -34437,24 +34475,33 @@ function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34437
34475
  pattern.lastIndex++;
34438
34476
  let pct;
34439
34477
  let resetStr;
34478
+ let qualifier;
34440
34479
  if (i === 0) {
34441
34480
  pct = Number.parseInt(match[1], 10);
34442
34481
  resetStr = match[2];
34482
+ qualifier = "weekly";
34443
34483
  } else {
34444
34484
  pct = 100;
34445
- resetStr = match[1];
34485
+ resetStr = match.groups?.["reset"] ?? "";
34486
+ qualifier = match.groups?.["qualifier"]?.trim() || null;
34446
34487
  }
34447
34488
  if (!Number.isFinite(pct) || pct < 0 || pct > 100)
34448
34489
  continue;
34449
34490
  const nextChar = text[match.index + match[0].length];
34450
34491
  if (nextChar !== void 0 && /[A-Za-z0-9]/.test(nextChar))
34451
34492
  continue;
34452
- const weekResetsAt = parseResetDateTime(resetStr, now);
34453
- if (!weekResetsAt)
34493
+ const reset = parseResetDateTime(resetStr, now);
34494
+ if (!reset)
34454
34495
  continue;
34455
34496
  if (match.index >= bestIndex) {
34456
34497
  bestIndex = match.index;
34457
- best = { pct, weekResetsAt };
34498
+ best = {
34499
+ pct,
34500
+ weekResetsAt: reset.at,
34501
+ resetPrecision: reset.precision,
34502
+ limitScope: classifyLimitScope(qualifier),
34503
+ limitQualifier: qualifier
34504
+ };
34458
34505
  }
34459
34506
  }
34460
34507
  }
@@ -34498,7 +34545,10 @@ function parseResetDateTime(humanDate, now) {
34498
34545
  if (!hm)
34499
34546
  return null;
34500
34547
  const todayAt = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hm.hour, hm.minute);
34501
- return new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt);
34548
+ return {
34549
+ at: new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt),
34550
+ precision: "hour_only"
34551
+ };
34502
34552
  }
34503
34553
  const timeMatch = trimmed.match(TIME_TAIL);
34504
34554
  const dateOnly = timeMatch ? trimmed.slice(0, timeMatch.index).trim() : trimmed;
@@ -34531,7 +34581,7 @@ function parseResetDateTime(humanDate, now) {
34531
34581
  resolved = candidate;
34532
34582
  }
34533
34583
  }
34534
- return resolved;
34584
+ return resolved ? { at: resolved, precision: "dated" } : null;
34535
34585
  }
34536
34586
 
34537
34587
  // ../core/dist/claude-code-usage/run-marker.js
@@ -39394,6 +39444,91 @@ async function runOrRetry(fn, opts) {
39394
39444
  }
39395
39445
  }
39396
39446
 
39447
+ // src/slack-socket-reconnect.ts
39448
+ var RECONNECT_BASE_DELAY_MS = 1e3;
39449
+ var RECONNECT_MAX_DELAY_MS = 3e4;
39450
+ var SOCKET_OPEN_TIMEOUT_MS = 3e4;
39451
+ var CONNECT_FETCH_TIMEOUT_MS = 15e3;
39452
+ var SOCKET_DOWN_ALERT_AFTER_MS = 3 * 6e4;
39453
+ function reconnectDelayMs(attempt, opts = {}) {
39454
+ const base = opts.baseMs ?? RECONNECT_BASE_DELAY_MS;
39455
+ const max = opts.maxMs ?? RECONNECT_MAX_DELAY_MS;
39456
+ const random = opts.random ?? Math.random;
39457
+ const safeAttempt = Math.max(1, Math.min(Math.floor(attempt), 30));
39458
+ const ceiling = Math.min(max, base * 2 ** (safeAttempt - 1));
39459
+ const floor = Math.min(base, ceiling);
39460
+ return Math.round(floor + random() * (ceiling - floor));
39461
+ }
39462
+ function redactSocketUrl(text) {
39463
+ return text.replace(
39464
+ /([?&])(ticket|token|app_id_key)=[^&\s"']*/gi,
39465
+ (_m, sep2, key2) => `${sep2}${key2}=<redacted>`
39466
+ );
39467
+ }
39468
+ function describeWebSocketError(err) {
39469
+ const parts = [];
39470
+ const e = err ?? {};
39471
+ if (typeof e.message === "string" && e.message) parts.push(e.message);
39472
+ const cause = e.error;
39473
+ if (cause && typeof cause === "object") {
39474
+ if (typeof cause.message === "string" && cause.message && !parts.includes(cause.message)) {
39475
+ parts.push(cause.message);
39476
+ }
39477
+ if (typeof cause.code === "string" && cause.code) parts.push(`errno=${cause.code}`);
39478
+ }
39479
+ if (typeof e.code === "number") parts.push(`code=${e.code}`);
39480
+ if (typeof e.reason === "string" && e.reason) parts.push(`reason=${e.reason}`);
39481
+ if (typeof e.wasClean === "boolean") parts.push(`wasClean=${e.wasClean}`);
39482
+ if (parts.length === 0) {
39483
+ const type = typeof e.type === "string" && e.type ? e.type : typeof err;
39484
+ const ctor = err && typeof err === "object" ? err.constructor?.name ?? "Object" : typeof err;
39485
+ return redactSocketUrl(`no detail available (${ctor}, type=${type})`);
39486
+ }
39487
+ return redactSocketUrl(parts.join(" "));
39488
+ }
39489
+ function isWebSocketOpen(ws) {
39490
+ return ws?.readyState === 1;
39491
+ }
39492
+ function createReconnectScheduler(deps) {
39493
+ const now = deps.now ?? Date.now;
39494
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
39495
+ let pending = false;
39496
+ let attempt = 0;
39497
+ let downSince = now();
39498
+ return {
39499
+ schedule(reason) {
39500
+ if (deps.shouldStop() || pending) return;
39501
+ pending = true;
39502
+ if (downSince === null) downSince = now();
39503
+ attempt += 1;
39504
+ const opts = {};
39505
+ if (deps.baseMs !== void 0) opts.baseMs = deps.baseMs;
39506
+ if (deps.maxMs !== void 0) opts.maxMs = deps.maxMs;
39507
+ if (deps.random !== void 0) opts.random = deps.random;
39508
+ const delay = reconnectDelayMs(attempt, opts);
39509
+ deps.log(
39510
+ `slack-channel: Socket Mode reconnecting in ${delay}ms (attempt ${attempt}, ${reason})`
39511
+ );
39512
+ const timer = setTimer(() => {
39513
+ pending = false;
39514
+ if (deps.shouldStop()) return;
39515
+ deps.connect();
39516
+ }, delay);
39517
+ timer.unref?.();
39518
+ },
39519
+ noteOpen() {
39520
+ attempt = 0;
39521
+ downSince = null;
39522
+ },
39523
+ downForMs(connected) {
39524
+ if (connected) return null;
39525
+ return downSince === null ? null : now() - downSince;
39526
+ },
39527
+ attempts: () => attempt,
39528
+ isPending: () => pending
39529
+ };
39530
+ }
39531
+
39397
39532
  // src/turn-initiator-marker.ts
39398
39533
  import { writeFileSync as writeFileSync7, readFileSync as readFileSync9, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
39399
39534
  import { dirname as dirname4, join as join12 } from "path";
@@ -43163,10 +43298,12 @@ function formatLastActivity() {
43163
43298
  return `${kindLabel} ${ago}`;
43164
43299
  }
43165
43300
  function buildAgentStatusReply(codeName) {
43166
- const connected = currentWs != null && !isShuttingDown;
43301
+ const connected = isSocketConnected();
43167
43302
  const dot = connected ? "\u{1F7E2}" : "\u{1F534}";
43168
43303
  const state = connected ? "online" : "offline";
43169
- const connectivityLine = `${dot} *${state}* \u2014 Socket Mode ${connected ? "connected" : "disconnected"}. Last activity: ${formatLastActivity()}.`;
43304
+ const downMs = socketDownForMs();
43305
+ const downSuffix = !connected && downMs !== null ? ` for ${Math.floor(downMs / 6e4)}m \u2014 inbound is dead` : "";
43306
+ const connectivityLine = `${dot} *${state}* \u2014 Socket Mode ${connected ? "connected" : `disconnected${downSuffix}`}. Last activity: ${formatLastActivity()}.`;
43170
43307
  const sessionState = readAgentSessionState(SLACK_AGENT_DIR);
43171
43308
  return buildAgentConfigReport({ codeName, connectivityLine, state: sessionState });
43172
43309
  }
@@ -43177,9 +43314,16 @@ function statusRuntimeConfig() {
43177
43314
  }
43178
43315
  async function computeSlackOwnChannelLiveStatus() {
43179
43316
  const checked_at = (/* @__PURE__ */ new Date()).toISOString();
43180
- const connected = currentWs != null && !isShuttingDown;
43317
+ const connected = isSocketConnected();
43181
43318
  if (!connected) {
43182
- return { level: "down", checked_at, reason: "Socket Mode disconnected" };
43319
+ const downMs = socketDownForMs();
43320
+ const forSuffix = downMs === null ? "" : ` for ${Math.floor(downMs / 1e3)}s`;
43321
+ const sustained = downMs !== null && downMs >= SOCKET_DOWN_ALERT_AFTER_MS;
43322
+ return {
43323
+ level: "down",
43324
+ checked_at,
43325
+ reason: `Socket Mode disconnected${forSuffix}${sustained ? " - inbound Slack is DEAD (outbound still works, so this is invisible from outside)" : ""}`
43326
+ };
43183
43327
  }
43184
43328
  const { id, authFailed } = await getBotUserId();
43185
43329
  if (id) {
@@ -43706,7 +43850,7 @@ async function handleSlashCommandEnvelope(payload) {
43706
43850
  );
43707
43851
  return;
43708
43852
  }
43709
- const connected = currentWs != null && !isShuttingDown;
43853
+ const connected = isSocketConnected();
43710
43854
  const dot = connected ? "\u{1F7E2}" : "\u{1F534}";
43711
43855
  const state = connected ? "online" : "offline";
43712
43856
  const pong = `${dot} pong - \`${codeName}\` is ${state} (manager \u2022 ${(/* @__PURE__ */ new Date()).toISOString()}). Last activity: ${formatLastActivity()}.`;
@@ -45778,6 +45922,47 @@ async function resolveUserEmail(userId) {
45778
45922
  }
45779
45923
  var currentWs = null;
45780
45924
  var isShuttingDown = false;
45925
+ var clearOpenWatchdog = null;
45926
+ function clearOpenWatchdogFor(ws) {
45927
+ if (currentWs !== ws) return;
45928
+ clearOpenWatchdog?.();
45929
+ clearOpenWatchdog = null;
45930
+ }
45931
+ var reconnectScheduler = createReconnectScheduler({
45932
+ connect: () => connectSocketModeSafely(),
45933
+ log: (line) => process.stderr.write(`${line}
45934
+ `),
45935
+ shouldStop: () => isShuttingDown
45936
+ });
45937
+ function isSocketConnected() {
45938
+ return !isShuttingDown && isWebSocketOpen(currentWs);
45939
+ }
45940
+ function scheduleReconnect(reason) {
45941
+ reconnectScheduler.schedule(reason);
45942
+ }
45943
+ function armOpenWatchdog(ws) {
45944
+ clearOpenWatchdog?.();
45945
+ const timer = setTimeout(() => {
45946
+ if (currentWs !== ws) return;
45947
+ if (isShuttingDown || isWebSocketOpen(ws)) return;
45948
+ process.stderr.write(
45949
+ `slack-channel: Socket Mode did not open within ${SOCKET_OPEN_TIMEOUT_MS}ms - abandoning it
45950
+ `
45951
+ );
45952
+ currentWs = null;
45953
+ clearOpenWatchdog = null;
45954
+ try {
45955
+ ws.close();
45956
+ } catch {
45957
+ }
45958
+ scheduleReconnect("open timeout");
45959
+ }, SOCKET_OPEN_TIMEOUT_MS);
45960
+ timer.unref?.();
45961
+ clearOpenWatchdog = () => clearTimeout(timer);
45962
+ }
45963
+ function socketDownForMs() {
45964
+ return reconnectScheduler.downForMs(isSocketConnected());
45965
+ }
45781
45966
  var acquiredLockPath = null;
45782
45967
  var stopLockHeartbeat = null;
45783
45968
  async function seedMutedChannelsFromApi() {
@@ -45828,14 +46013,27 @@ async function connectSocketMode() {
45828
46013
  if (isShuttingDown) return;
45829
46014
  const res = await fetch("https://slack.com/api/apps.connections.open", {
45830
46015
  method: "POST",
45831
- headers: { Authorization: `Bearer ${APP_TOKEN}` }
46016
+ headers: { Authorization: `Bearer ${APP_TOKEN}` },
46017
+ // CodeRabbit, #4655. Without a deadline a hanging fetch is the one failure
46018
+ // this whole change does not cover: it never rejects, so the retry chain is
46019
+ // never entered, and no socket exists yet so the open watchdog does not
46020
+ // apply either. The process ends up with nothing pending and no attempt in
46021
+ // flight — the incident's shape, reached a different way. A timeout turns
46022
+ // the hang into a rejection, which the retry chain already handles.
46023
+ signal: AbortSignal.timeout(CONNECT_FETCH_TIMEOUT_MS)
46024
+ // CodeRabbit, #4655. Without a deadline a hanging fetch is the one failure
46025
+ // this whole change does not cover: it never rejects, so the retry chain is
46026
+ // never entered, and no socket exists yet so the open watchdog does not
46027
+ // apply either. The process ends up with nothing pending and no attempt in
46028
+ // flight — the incident's shape, reached a different way. A timeout turns
46029
+ // the hang into a rejection, which the retry chain already handles.
45832
46030
  });
45833
46031
  const data = await res.json();
45834
46032
  if (isShuttingDown) return;
45835
46033
  if (!data.ok || !data.url) {
45836
46034
  process.stderr.write(`slack-channel: Socket Mode connection failed: ${data.error}
45837
46035
  `);
45838
- if (!isShuttingDown) setTimeout(connectSocketModeSafely, 1e4).unref?.();
46036
+ scheduleReconnect(`apps.connections.open ${data.error ?? "failed"}`);
45839
46037
  return;
45840
46038
  }
45841
46039
  const ws = new WebSocket(data.url);
@@ -45847,7 +46045,17 @@ async function connectSocketMode() {
45847
46045
  return;
45848
46046
  }
45849
46047
  currentWs = ws;
46048
+ armOpenWatchdog(ws);
45850
46049
  ws.onopen = () => {
46050
+ if (currentWs !== ws) {
46051
+ try {
46052
+ ws.close();
46053
+ } catch {
46054
+ }
46055
+ return;
46056
+ }
46057
+ clearOpenWatchdogFor(ws);
46058
+ reconnectScheduler.noteOpen();
45851
46059
  process.stderr.write("slack-channel: Socket Mode connected\n");
45852
46060
  recordActivity("connect");
45853
46061
  void setBotStatus(":large_green_circle:", "Active");
@@ -46493,15 +46701,35 @@ async function connectSocketMode() {
46493
46701
  `);
46494
46702
  }
46495
46703
  };
46496
- ws.onclose = () => {
46497
- if (currentWs === ws) currentWs = null;
46704
+ ws.onclose = (evt) => {
46705
+ if (currentWs !== ws) return;
46706
+ clearOpenWatchdogFor(ws);
46707
+ currentWs = null;
46498
46708
  if (isShuttingDown) return;
46499
- process.stderr.write("slack-channel: Socket Mode disconnected, reconnecting...\n");
46500
- setTimeout(connectSocketModeSafely, 5e3).unref?.();
46709
+ process.stderr.write(
46710
+ `slack-channel: Socket Mode disconnected (${describeWebSocketError(evt)})
46711
+ `
46712
+ );
46713
+ scheduleReconnect("close");
46501
46714
  };
46502
46715
  ws.onerror = (err) => {
46503
- process.stderr.write(`slack-channel: WebSocket error: ${err}
46716
+ process.stderr.write(`slack-channel: WebSocket error: ${describeWebSocketError(err)}
46504
46717
  `);
46718
+ if (currentWs !== ws) {
46719
+ try {
46720
+ ws.close();
46721
+ } catch {
46722
+ }
46723
+ return;
46724
+ }
46725
+ clearOpenWatchdogFor(ws);
46726
+ currentWs = null;
46727
+ try {
46728
+ ws.close();
46729
+ } catch {
46730
+ }
46731
+ if (isShuttingDown) return;
46732
+ scheduleReconnect("error");
46505
46733
  };
46506
46734
  }
46507
46735
  function shutdown(reason, exitCode = 0) {
@@ -34476,7 +34476,14 @@ var AGENT_DIRECTED_NOTICE_KINDS = [
34476
34476
  // written for the machine. The user learns the same fact from the connect card
34477
34477
  // in that conversation flipping to its connected state, so showing this row as
34478
34478
  // well would say it twice, the second time in prose aimed at an agent.
34479
- "integration_connected"
34479
+ "integration_connected",
34480
+ // ENG-8745. The offer RESULT receipt (not the offer card — that one is
34481
+ // written for the human and is how they decided). Its content opens "Your
34482
+ // offer was ACCEPTED", carries an `[offer_result]` machine tag, and closes by
34483
+ // instructing the agent what not to do ("do not re-raise it yourself"). The
34484
+ // human needs none of it: they are the person who tapped the button, and the
34485
+ // card swapped under their finger to say so.
34486
+ "offer_result"
34480
34487
  ];
34481
34488
  var AGENT_DIRECTED_KIND_SET = new Set(AGENT_DIRECTED_NOTICE_KINDS);
34482
34489
 
@@ -34570,6 +34577,17 @@ var CONDITIONAL_DELIVERY_BODY = [
34570
34577
  var FRESH_HEARTBEAT_THRESHOLD_MS = 2 * 60 * 1e3;
34571
34578
 
34572
34579
  // ../core/dist/claude-code-usage/banner-parser.js
34580
+ var SESSION_QUALIFIER = /^(?:session|\d{1,2}\s*-?\s*hours?|\d{1,2}h)$/i;
34581
+ function classifyLimitScope(qualifier) {
34582
+ const q = (qualifier ?? "").trim();
34583
+ if (!q)
34584
+ return "weekly";
34585
+ if (SESSION_QUALIFIER.test(q))
34586
+ return "session";
34587
+ if (/^weekly$/i.test(q))
34588
+ return "weekly";
34589
+ return "unknown";
34590
+ }
34573
34591
  var SEP = /[\s·\-–—]+/.source;
34574
34592
  var SUBJECT = /(?:You(?:['’]ve|\s+have)?\s+)?/.source;
34575
34593
  var TIME_OF_DAY = /\d{1,2}(?::\d{2})?\s*(?:am|pm)(?:\s*\(?UTC\)?)?/.source;
@@ -34583,7 +34601,27 @@ var BANNER_PATTERNS = [
34583
34601
  // Wrapped to keep the per-pattern shape consistent with the percentage form:
34584
34602
  // the parser checks group 1 for a digit string and treats a missing one as
34585
34603
  // pct=100.
34586
- new RegExp(`${SUBJECT}hit\\s+your\\s+(?:[a-z0-9-]+\\s+)?limit${SEP}resets\\s+(${RESET_DATE})`, "i")
34604
+ // ENG-9007: the qualifier is CAPTURED now, by name. It used to be
34605
+ // `(?:[a-z0-9-]+\s+)?` - permissive and non-capturing - so the parser
34606
+ // recognised "session" and "5-hour", matched on them, and threw the word away.
34607
+ // Named groups keep the numbered capture indices stable for the percentage
34608
+ // form above, which still reads groups 1 and 2 positionally.
34609
+ new RegExp(
34610
+ // CodeRabbit on #4621: the qualifier was ONE token, so `classifyLimitScope`
34611
+ // accepted "5 hours" while this pattern could never deliver it — the whole
34612
+ // banner failed to match, no observation was recorded, and the agent went
34613
+ // dark rather than merely being misclassified. Multi-token qualifiers now
34614
+ // match the duration shapes SESSION_QUALIFIER already accepts.
34615
+ //
34616
+ // Deliberately NOT the naive `[a-z0-9]+(?:\s*-?\s*[a-z0-9]+)*`: the
34617
+ // separator there can match empty, which reduces to `(a+)+` and backtracks
34618
+ // catastrophically on a long alphanumeric run that is never followed by
34619
+ // "limit". This parser runs over agent-produced pane.log tails, so that is
34620
+ // reachable input. Requiring at least one separator per extra token, and
34621
+ // bounding the count, keeps it linear.
34622
+ `${SUBJECT}hit\\s+your\\s+(?<qualifier>[a-z0-9]+(?:[\\s-]+[a-z0-9]+){0,3}\\s+)?limit${SEP}resets\\s+(?<reset>${RESET_DATE})`,
34623
+ "i"
34624
+ )
34587
34625
  ];
34588
34626
  function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34589
34627
  let bestIndex = -1;
@@ -34596,24 +34634,33 @@ function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34596
34634
  pattern.lastIndex++;
34597
34635
  let pct;
34598
34636
  let resetStr;
34637
+ let qualifier;
34599
34638
  if (i === 0) {
34600
34639
  pct = Number.parseInt(match[1], 10);
34601
34640
  resetStr = match[2];
34641
+ qualifier = "weekly";
34602
34642
  } else {
34603
34643
  pct = 100;
34604
- resetStr = match[1];
34644
+ resetStr = match.groups?.["reset"] ?? "";
34645
+ qualifier = match.groups?.["qualifier"]?.trim() || null;
34605
34646
  }
34606
34647
  if (!Number.isFinite(pct) || pct < 0 || pct > 100)
34607
34648
  continue;
34608
34649
  const nextChar = text[match.index + match[0].length];
34609
34650
  if (nextChar !== void 0 && /[A-Za-z0-9]/.test(nextChar))
34610
34651
  continue;
34611
- const weekResetsAt = parseResetDateTime(resetStr, now);
34612
- if (!weekResetsAt)
34652
+ const reset = parseResetDateTime(resetStr, now);
34653
+ if (!reset)
34613
34654
  continue;
34614
34655
  if (match.index >= bestIndex) {
34615
34656
  bestIndex = match.index;
34616
- best = { pct, weekResetsAt };
34657
+ best = {
34658
+ pct,
34659
+ weekResetsAt: reset.at,
34660
+ resetPrecision: reset.precision,
34661
+ limitScope: classifyLimitScope(qualifier),
34662
+ limitQualifier: qualifier
34663
+ };
34617
34664
  }
34618
34665
  }
34619
34666
  }
@@ -34657,7 +34704,10 @@ function parseResetDateTime(humanDate, now) {
34657
34704
  if (!hm)
34658
34705
  return null;
34659
34706
  const todayAt = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hm.hour, hm.minute);
34660
- return new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt);
34707
+ return {
34708
+ at: new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt),
34709
+ precision: "hour_only"
34710
+ };
34661
34711
  }
34662
34712
  const timeMatch = trimmed.match(TIME_TAIL);
34663
34713
  const dateOnly = timeMatch ? trimmed.slice(0, timeMatch.index).trim() : trimmed;
@@ -34690,7 +34740,7 @@ function parseResetDateTime(humanDate, now) {
34690
34740
  resolved = candidate;
34691
34741
  }
34692
34742
  }
34693
- return resolved;
34743
+ return resolved ? { at: resolved, precision: "dated" } : null;
34694
34744
  }
34695
34745
 
34696
34746
  // ../core/dist/claude-code-usage/run-marker.js