@integrity-labs/agt-cli 0.28.1022 → 0.28.1024

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/bin/agt.js +5 -5
  2. package/dist/{chunk-QXJWRE64.js → chunk-5XPZF6FT.js} +30 -2
  3. package/dist/{chunk-QXJWRE64.js.map → chunk-5XPZF6FT.js.map} +1 -1
  4. package/dist/{chunk-X7SX25N6.js → chunk-BHKFVSIX.js} +10 -8
  5. package/dist/{chunk-X7SX25N6.js.map → chunk-BHKFVSIX.js.map} +1 -1
  6. package/dist/{chunk-5MUSHNVX.js → chunk-HVTL2N3I.js} +4 -4
  7. package/dist/{claude-pair-runtime-KKFNGRSA.js → claude-pair-runtime-IAO6B35F.js} +2 -2
  8. package/dist/lib/manager-worker.js +271 -15
  9. package/dist/lib/manager-worker.js.map +1 -1
  10. package/dist/mcp/direct-chat-channel.js +45 -11
  11. package/dist/mcp/index.js +9 -7
  12. package/dist/mcp/origami.js +9 -7
  13. package/dist/mcp/slack-channel.js +45 -11
  14. package/dist/mcp/telegram-channel.js +45 -11
  15. package/dist/{persistent-session-ZQDNCKQH.js → persistent-session-GZV4EXKD.js} +3 -3
  16. package/dist/{responsiveness-probe-Q27WU2MW.js → responsiveness-probe-WRHLHQCE.js} +3 -3
  17. package/dist/{session-auth-dead-JJUKS75B.js → session-auth-dead-ZKC2HH6T.js} +2 -2
  18. package/package.json +1 -1
  19. /package/dist/{chunk-5MUSHNVX.js.map → chunk-HVTL2N3I.js.map} +0 -0
  20. /package/dist/{claude-pair-runtime-KKFNGRSA.js.map → claude-pair-runtime-IAO6B35F.js.map} +0 -0
  21. /package/dist/{persistent-session-ZQDNCKQH.js.map → persistent-session-GZV4EXKD.js.map} +0 -0
  22. /package/dist/{responsiveness-probe-Q27WU2MW.js.map → responsiveness-probe-WRHLHQCE.js.map} +0 -0
  23. /package/dist/{session-auth-dead-JJUKS75B.js.map → session-auth-dead-ZKC2HH6T.js.map} +0 -0
@@ -15,7 +15,7 @@ import {
15
15
  parseEnvFileEntries,
16
16
  parseEnvIntegrations,
17
17
  shellQuote
18
- } from "./chunk-QXJWRE64.js";
18
+ } from "./chunk-5XPZF6FT.js";
19
19
  import {
20
20
  BIND_FAILURE_QUARANTINE_THRESHOLD,
21
21
  INTEGRATIONS_SECTION_END,
@@ -77,7 +77,7 @@ import {
77
77
  sessionTranscriptDir,
78
78
  worseConnectivityOutcome,
79
79
  wrapScheduledTaskPrompt
80
- } from "./chunk-X7SX25N6.js";
80
+ } from "./chunk-BHKFVSIX.js";
81
81
  import {
82
82
  classifyUnanswerablePane,
83
83
  summarizeUnanswerablePane
@@ -7140,7 +7140,7 @@ function exchangeFailureKind(err) {
7140
7140
  }
7141
7141
 
7142
7142
  // src/lib/api-client.ts
7143
- var agtCliVersion = true ? "0.28.1022" : "dev";
7143
+ var agtCliVersion = true ? "0.28.1024" : "dev";
7144
7144
  var lastConfigHash = null;
7145
7145
  function setConfigHash(hash) {
7146
7146
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -12094,4 +12094,4 @@ export {
12094
12094
  managerInstallSystemUnitCommand,
12095
12095
  managerUninstallSystemUnitCommand
12096
12096
  };
12097
- //# sourceMappingURL=chunk-5MUSHNVX.js.map
12097
+ //# sourceMappingURL=chunk-HVTL2N3I.js.map
@@ -266,7 +266,7 @@ async function spawnPairSession(session) {
266
266
  return { ok: true };
267
267
  } catch {
268
268
  }
269
- const { resolveClaudeBinary } = await import("./persistent-session-ZQDNCKQH.js");
269
+ const { resolveClaudeBinary } = await import("./persistent-session-GZV4EXKD.js");
270
270
  const claudeBin = resolveClaudeBinary();
271
271
  const pairEnv = {
272
272
  ...process.env,
@@ -734,4 +734,4 @@ export {
734
734
  startClaudePair,
735
735
  submitClaudePairCode
736
736
  };
737
- //# sourceMappingURL=claude-pair-runtime-KKFNGRSA.js.map
737
+ //# sourceMappingURL=claude-pair-runtime-IAO6B35F.js.map
@@ -66,7 +66,7 @@ import {
66
66
  safeWriteJsonAtomic,
67
67
  setConfigHash,
68
68
  tripClass
69
- } from "../chunk-5MUSHNVX.js";
69
+ } from "../chunk-HVTL2N3I.js";
70
70
  import {
71
71
  getProjectDir as getProjectDir2,
72
72
  getReadyTasks,
@@ -118,6 +118,8 @@ import {
118
118
  parseModelPolicy,
119
119
  policyResolvedModel,
120
120
  prepareForRespawn,
121
+ probeClaudeProcessInTmux,
122
+ probeTmuxSession,
121
123
  readChannelServerEnv,
122
124
  readDirectChatSessionState,
123
125
  readOpencodePaneLogTail,
@@ -147,7 +149,7 @@ import {
147
149
  takeZombieDetection,
148
150
  toOpencodeModel,
149
151
  writeEgressAllowlist
150
- } from "../chunk-QXJWRE64.js";
152
+ } from "../chunk-5XPZF6FT.js";
151
153
  import {
152
154
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
153
155
  AnchorSessionClient,
@@ -252,8 +254,10 @@ import {
252
254
  subagentActivityAgeSeconds,
253
255
  sumTranscriptUsageInWindow,
254
256
  transcriptActivityAgeSeconds
255
- } from "../chunk-X7SX25N6.js";
256
- import "../chunk-DHWNVVX4.js";
257
+ } from "../chunk-BHKFVSIX.js";
258
+ import {
259
+ classifyUnanswerablePane
260
+ } from "../chunk-DHWNVVX4.js";
257
261
  import {
258
262
  reapOrphanChannelMcps
259
263
  } from "../chunk-XWVM4KPK.js";
@@ -9384,6 +9388,136 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
9384
9388
  }
9385
9389
  }
9386
9390
 
9391
+ // src/lib/blocked-prompt-monitor.ts
9392
+ function observeBlockedPrompt(codeName, hasSession, capture) {
9393
+ if (!hasSession) return { state: "unknown", reason: "no-session" };
9394
+ let pane;
9395
+ try {
9396
+ pane = capture(codeName);
9397
+ } catch {
9398
+ return { state: "unknown", reason: "pane-unreadable" };
9399
+ }
9400
+ if (pane === null) return { state: "unknown", reason: "pane-unreadable" };
9401
+ const kind = classifyUnanswerablePane(pane);
9402
+ return kind ? { state: "blocked", kind } : { state: "clear" };
9403
+ }
9404
+ function carriedRecoveryFor(state9, pending2, codeName) {
9405
+ return state9 === "clear" ? pending2.get(codeName) : void 0;
9406
+ }
9407
+ function foldPendingRecovery(cost, carried) {
9408
+ const total = carried ? {
9409
+ tasksDeferred: cost.tasksDeferred + carried.tasksDeferred,
9410
+ messagesWithheld: cost.messagesWithheld + carried.messagesWithheld
9411
+ } : cost;
9412
+ return { total, hasCost: total.tasksDeferred > 0 || total.messagesWithheld > 0 };
9413
+ }
9414
+ function settlePendingRecovery(pending2, unreported, disposition) {
9415
+ const retain = disposition === "withheld";
9416
+ for (const [codeName, cost] of unreported) {
9417
+ if (retain) pending2.set(codeName, cost);
9418
+ else pending2.delete(codeName);
9419
+ }
9420
+ }
9421
+ var BlockedPromptCostLedger = class {
9422
+ /**
9423
+ * DISTINCT ITEMS, NOT A RUNNING SUM (obi-wan `0d2a571b2181` on #6329, MAJOR).
9424
+ *
9425
+ * This held two integers and added to them on every call. Both accrual sites
9426
+ * run once per ~10s poll over the SAME items:
9427
+ *
9428
+ * - a scheduled-task deferral deliberately does NOT advance `nextFireAt`
9429
+ * (that is what makes the task fire once the prompt is answered instead
9430
+ * of backlogging), so `getReadyTasks` returns the same task every tick;
9431
+ * - a withheld direct-chat message is left PENDING for durable replay, so
9432
+ * `/host/direct-chat/poll` returns the same message id every tick.
9433
+ *
9434
+ * So the counters were multiplied by the number of polls the block lasted.
9435
+ * One un-fired task on an agent blocked for the two days the ticket describes
9436
+ * yields 2 days / 10s = ~17,280, and the alert renders "17280 scheduled
9437
+ * task(s) not fired". The existing log lines are suppressed once-per-episode
9438
+ * precisely BECAUSE they repeat; the counters sat outside that suppression
9439
+ * and inherited the repetition instead.
9440
+ *
9441
+ * The whole argument for these numbers - "4 agents blocked, 9 scheduled tasks
9442
+ * not fired is what makes someone act" - depends on them being counts of
9443
+ * DISTINCT items. A number that grows with wall-clock time is not a cost, it
9444
+ * is a clock, and an operator who spotted that would stop trusting the rest
9445
+ * of the alert.
9446
+ *
9447
+ * Sets of ids make re-noting the same item idempotent, so correctness no
9448
+ * longer depends on the caller noticing it is in a poll loop.
9449
+ */
9450
+ byAgent = /* @__PURE__ */ new Map();
9451
+ episode(codeName) {
9452
+ let e = this.byAgent.get(codeName);
9453
+ if (!e) {
9454
+ e = { tasks: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Set() };
9455
+ this.byAgent.set(codeName, e);
9456
+ }
9457
+ return e;
9458
+ }
9459
+ /**
9460
+ * Record scheduled tasks this episode has declined to fire, by task id.
9461
+ *
9462
+ * Called with the whole ready set each poll; re-noting an id already in the
9463
+ * episode is a no-op, which is the point.
9464
+ */
9465
+ noteTasksDeferred(codeName, taskIds) {
9466
+ const e = this.episode(codeName);
9467
+ for (const id of taskIds) e.tasks.add(id);
9468
+ }
9469
+ /** Record one withheld direct-chat message, by message id. Idempotent. */
9470
+ noteMessageWithheld(codeName, messageId) {
9471
+ this.episode(codeName).messages.add(messageId);
9472
+ }
9473
+ costOf(codeName) {
9474
+ const e = this.byAgent.get(codeName);
9475
+ return { tasksDeferred: e?.tasks.size ?? 0, messagesWithheld: e?.messages.size ?? 0 };
9476
+ }
9477
+ /**
9478
+ * Settle this sweep: return the cost to report, and the observation to report
9479
+ * it WITH.
9480
+ *
9481
+ * A `clear` ends the episode, so the running total is cleared AFTER being
9482
+ * read — the poll that observes recovery still reports what the episode cost.
9483
+ * When that total is nonzero the observation is marked `recovering`, which is
9484
+ * what licenses the rollup to count those counters at all: a plain clear's
9485
+ * cost is ignored.
9486
+ *
9487
+ * IT IS NOT AN ANTI-TAMPERING GUARD, and this docblock used to say it was
9488
+ * (obi-wan `579d2c059673`). Both the core module and the route retract that
9489
+ * claim explicitly IN THIS SAME PR - `{state:'clear', recovering:true,
9490
+ * tasks_deferred:N}` is a shape any caller can send, so the flag distinguishes
9491
+ * a recovery from a plain clear and nothing more. I wrote the retraction
9492
+ * twice and left the retracted sentence standing here, which is how the next
9493
+ * reader ends up trusting a property that was withdrawn two files away.
9494
+ *
9495
+ * Returning the observation rather than having the caller decorate it keeps
9496
+ * that rule in one testable place instead of inline in the poll.
9497
+ */
9498
+ settle(codeName, observation) {
9499
+ const cost = this.costOf(codeName);
9500
+ if (observation.state !== "clear") return { observation, cost };
9501
+ this.byAgent.delete(codeName);
9502
+ const hadCost = cost.tasksDeferred > 0 || cost.messagesWithheld > 0;
9503
+ return { observation: hadCost ? { state: "clear", recovering: true } : observation, cost };
9504
+ }
9505
+ /**
9506
+ * Drop an agent's episode entirely — teardown, not recovery (CodeRabbit on
9507
+ * #6329). `clearAgentCaches` purges every other per-codeName map on a
9508
+ * handoff, decommission or unassign; without this, an agent that left DURING
9509
+ * a blocked episode would hand its `tasksDeferred` and `messagesWithheld` to
9510
+ * whatever agent next takes that code name.
9511
+ */
9512
+ forget(codeName) {
9513
+ this.byAgent.delete(codeName);
9514
+ }
9515
+ /** Current running cost without touching the episode. For tests and diagnostics. */
9516
+ peek(codeName) {
9517
+ return this.costOf(codeName);
9518
+ }
9519
+ };
9520
+
9387
9521
  // src/lib/occupancy-gate.ts
9388
9522
  import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync9, readSync as readSync2, statSync as statSync11 } from "fs";
9389
9523
  import { join as join38 } from "path";
@@ -13048,6 +13182,8 @@ function hasRevokedResiduals(state9) {
13048
13182
  }
13049
13183
  var pendingSessionRestarts = /* @__PURE__ */ new Map();
13050
13184
  var lastRestartWasExternal = /* @__PURE__ */ new Map();
13185
+ var blockedPromptCost = new BlockedPromptCostLedger();
13186
+ var pendingRecoveryCost = /* @__PURE__ */ new Map();
13051
13187
  var computerUseTunnels = new TunnelWatcher();
13052
13188
  var pendingRestartVerifications = /* @__PURE__ */ new Map();
13053
13189
  var agentCreatedAtMs = /* @__PURE__ */ new Map();
@@ -13790,7 +13926,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
13790
13926
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
13791
13927
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
13792
13928
  async function channelInboundActivityAgeSecondsFor(codeName) {
13793
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-Q27WU2MW.js");
13929
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-WRHLHQCE.js");
13794
13930
  const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
13795
13931
  if (newest === null) return null;
13796
13932
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -14488,6 +14624,8 @@ function resolveAgentFramework(codeName) {
14488
14624
  function clearAgentCaches(agentId, codeName) {
14489
14625
  const channelCacheMutated = clearAgentState(agentId, codeName);
14490
14626
  agentChannelTokens.delete(codeName);
14627
+ blockedPromptCost.forget(codeName);
14628
+ pendingRecoveryCost.delete(codeName);
14491
14629
  lastRefreshPayload.delete(agentId);
14492
14630
  boardTokenCache.forget(agentId);
14493
14631
  refreshFetchGate.forget(agentId);
@@ -14534,7 +14672,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
14534
14672
  var lastVersionCheckAt = 0;
14535
14673
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
14536
14674
  var lastResponsivenessProbeAt = 0;
14537
- var agtCliVersion = true ? "0.28.1022" : "dev";
14675
+ var agtCliVersion = true ? "0.28.1024" : "dev";
14538
14676
  function resolveBrewPath(execFileSync3) {
14539
14677
  try {
14540
14678
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -16169,7 +16307,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
16169
16307
  if (codeNames.length === 0) return;
16170
16308
  void (async () => {
16171
16309
  try {
16172
- const { collectDiagnostics } = await import("../persistent-session-ZQDNCKQH.js");
16310
+ const { collectDiagnostics } = await import("../persistent-session-GZV4EXKD.js");
16173
16311
  await api.post("/host/heartbeat", {
16174
16312
  host_id: hostId,
16175
16313
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -16311,7 +16449,7 @@ async function pollCycleInner() {
16311
16449
  }
16312
16450
  try {
16313
16451
  const { detectHostSecurity } = await import("../host-security-KQYYZZVW.js");
16314
- const { collectDiagnostics } = await import("../persistent-session-ZQDNCKQH.js");
16452
+ const { collectDiagnostics } = await import("../persistent-session-GZV4EXKD.js");
16315
16453
  const diagCodeNames = [...agentState.persistentSessionAgents];
16316
16454
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor) : void 0;
16317
16455
  let tailscaleHostname;
@@ -16487,7 +16625,7 @@ async function pollCycleInner() {
16487
16625
  collectPanelessActivityProbes,
16488
16626
  getResponsivenessIntervalMs,
16489
16627
  occupancyQualificationClassifications
16490
- } = await import("../responsiveness-probe-Q27WU2MW.js");
16628
+ } = await import("../responsiveness-probe-WRHLHQCE.js");
16491
16629
  const probeIntervalMs = getResponsivenessIntervalMs();
16492
16630
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
16493
16631
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -16604,7 +16742,7 @@ async function pollCycleInner() {
16604
16742
  collectResponsivenessProbes,
16605
16743
  livePendingInboundOldestAgeSeconds,
16606
16744
  parkPendingInbound
16607
- } = await import("../responsiveness-probe-Q27WU2MW.js");
16745
+ } = await import("../responsiveness-probe-WRHLHQCE.js");
16608
16746
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-M6AFZWNU.js");
16609
16747
  const wedgeNow = /* @__PURE__ */ new Date();
16610
16748
  const liveAgents = agentState.persistentSessionAgents;
@@ -16760,7 +16898,7 @@ async function pollCycleInner() {
16760
16898
  }
16761
16899
  try {
16762
16900
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-VR5QGP3J.js");
16763
- const { probeSessionAuth } = await import("../session-auth-dead-JJUKS75B.js");
16901
+ const { probeSessionAuth } = await import("../session-auth-dead-ZKC2HH6T.js");
16764
16902
  const observations = [];
16765
16903
  const pendingCacheCommits = [];
16766
16904
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -17022,6 +17160,107 @@ async function pollCycleInner() {
17022
17160
  }
17023
17161
  }
17024
17162
  }
17163
+ let sweepComplete = false;
17164
+ let omittedNoSession = 0;
17165
+ try {
17166
+ const unreportedRecovery = /* @__PURE__ */ new Map();
17167
+ const sweeps = [];
17168
+ for (const agent of agents) {
17169
+ if (agent.status !== "active") continue;
17170
+ const framework = agent.framework ?? agentFrameworkCache.get(agent.code_name) ?? DEFAULT_FRAMEWORK;
17171
+ if (framework !== "claude-code") continue;
17172
+ const tmuxProbe = probeTmuxSession(`agt-${agent.code_name}`);
17173
+ const claudeProbe = tmuxProbe === "alive" ? probeClaudeProcessInTmux(`agt-${agent.code_name}`) : "dead";
17174
+ if (tmuxProbe === "unknown" || claudeProbe === "unknown") {
17175
+ sweeps.push({
17176
+ agent_id: agent.agent_id,
17177
+ state: "unknown",
17178
+ unknown_reason: "pane-unreadable",
17179
+ tasks_deferred: 0,
17180
+ messages_withheld: 0
17181
+ });
17182
+ continue;
17183
+ }
17184
+ if (tmuxProbe !== "alive" || claudeProbe !== "alive") {
17185
+ omittedNoSession += 1;
17186
+ continue;
17187
+ }
17188
+ const observation = observeBlockedPrompt(agent.code_name, true, (codeName) => {
17189
+ try {
17190
+ return syncExecFile("tmux", ["capture-pane", "-t", `agt-${codeName}`, "-p"], {
17191
+ stdio: ["ignore", "pipe", "ignore"],
17192
+ timeout: 2e3
17193
+ }).toString();
17194
+ } catch {
17195
+ return null;
17196
+ }
17197
+ });
17198
+ const { observation: settled, cost } = blockedPromptCost.settle(
17199
+ agent.code_name,
17200
+ observation
17201
+ );
17202
+ const carried = carriedRecoveryFor(settled.state, pendingRecoveryCost, agent.code_name);
17203
+ const { total, hasCost } = foldPendingRecovery(cost, carried);
17204
+ const recovering = settled.state === "clear" && hasCost;
17205
+ if (recovering) unreportedRecovery.set(agent.code_name, total);
17206
+ sweeps.push({
17207
+ agent_id: agent.agent_id,
17208
+ state: settled.state,
17209
+ ...settled.state === "blocked" ? { variant: settled.kind } : {},
17210
+ ...settled.state === "unknown" ? { unknown_reason: settled.reason } : {},
17211
+ ...recovering ? { recovering: true } : {},
17212
+ tasks_deferred: total.tasksDeferred,
17213
+ messages_withheld: total.messagesWithheld
17214
+ });
17215
+ }
17216
+ sweepComplete = omittedNoSession === 0;
17217
+ if (!sweepComplete) {
17218
+ log(
17219
+ `[blocked-prompt-monitor] partial sweep: ${omittedNoSession} active agent(s) had no live session, so this batch may not close an open alert (ENG-10825)`
17220
+ );
17221
+ }
17222
+ if (sweeps.length === 0 && omittedNoSession > 0) {
17223
+ log(
17224
+ `[blocked-prompt-monitor] NOTHING REPORTED: all ${omittedNoSession} active claude-code agent(s) had no live session this poll, so no batch was sent and this host is unassessed. A wedged tmux now reads as 'unknown' rather than 'dead', so reaching here means either a genuine host-wide teardown or no tmux server at all (ENG-10825)`
17225
+ );
17226
+ }
17227
+ if (sweeps.length > 0) {
17228
+ const disposition = await api.post(
17229
+ "/host/blocked-prompt-report",
17230
+ {
17231
+ agents: sweeps,
17232
+ sweep_complete: sweepComplete,
17233
+ // WHY the sweep was partial, not just that it was (obi-wan
17234
+ // `82a0dd7ef253`). `sweep_complete: false` tells the route it may
17235
+ // not close an alert; it does not tell an operator that four agents
17236
+ // on this host have no session and that is why their critical will
17237
+ // not clear. The route writes this onto the alert payload.
17238
+ omitted_no_session: omittedNoSession
17239
+ }
17240
+ ).then((res) => {
17241
+ const dropped = res?.dropped_agent_ids ?? [];
17242
+ if (dropped.length > 0) {
17243
+ log(
17244
+ `[blocked-prompt-monitor] the route DROPPED ${dropped.length} reported agent(s): ${dropped.join(", ")} \u2014 they are not in host_agents for this host, so this sweep cannot be complete and an open alert cannot close until they stop being swept or the rows return (ENG-10825)`
17245
+ );
17246
+ }
17247
+ return res?.cost_disposition;
17248
+ }).catch((err) => {
17249
+ log(
17250
+ `[blocked-prompt-monitor] report failed (ENG-10825): ${err.message} \u2014 the sweep still ran locally; this poll's result did not reach the alert board`
17251
+ );
17252
+ return "withheld";
17253
+ });
17254
+ settlePendingRecovery(pendingRecoveryCost, unreportedRecovery, disposition);
17255
+ if (disposition === "withheld" && unreportedRecovery.size > 0) {
17256
+ log(
17257
+ `[blocked-prompt-monitor] holding ${unreportedRecovery.size} unreported recovery total(s) for the next poll \u2014 the route did not persist them (ENG-10825)`
17258
+ );
17259
+ }
17260
+ }
17261
+ } catch (err) {
17262
+ log(`[blocked-prompt-monitor] sweep threw (suppressed): ${err.message}`);
17263
+ }
17025
17264
  const frameworksThisCycle = new Set(agents.map((a) => a.framework).filter(Boolean));
17026
17265
  for (const fw of frameworksThisCycle) {
17027
17266
  await ensureFrameworkBinary(fw);
@@ -17252,6 +17491,21 @@ async function pollCycleInner() {
17252
17491
  // reason untouched. Merge-then-write rather than write-fresh: clobbering
17253
17492
  // the file each poll would reset gave_up_at and keep re-arming the
17254
17493
  // give-up notice for as long as the modal sat there.
17494
+ // ENG-10972 AC3: the wedged-on-a-prompt marker the channel servers read.
17495
+ // Its own file with nothing to preserve, so write-fresh is correct - unlike
17496
+ // watchdog-give-up.json, whose gave_up_at must survive a refresh.
17497
+ refreshBlockedPromptLiveness: (codeName, kind) => {
17498
+ const dir = getFramework("claude-code").getAgentDir(codeName);
17499
+ if (!existsSync19(dir)) return;
17500
+ atomicWriteFileSync(
17501
+ join42(dir, "blocked-prompt.json"),
17502
+ JSON.stringify({ kind, last_seen_at: (/* @__PURE__ */ new Date()).toISOString() })
17503
+ );
17504
+ },
17505
+ clearBlockedPromptLiveness: (codeName) => {
17506
+ const file = join42(getFramework("claude-code").getAgentDir(codeName), "blocked-prompt.json");
17507
+ if (existsSync19(file)) rmSync8(file, { force: true });
17508
+ },
17255
17509
  refreshUsageLimitLiveness: (codeName) => {
17256
17510
  const dir = getFramework("claude-code").getAgentDir(codeName);
17257
17511
  const file = join42(dir, "watchdog-give-up.json");
@@ -20917,6 +21171,7 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash20("sha256")
20917
21171
  const blockedKind = sessionBlockedOnPrompt(codeName);
20918
21172
  if (blockedKind) {
20919
21173
  deferReadyForBlockedPrompt = true;
21174
+ blockedPromptCost.noteTasksDeferred(codeName, ready.map((t) => t.taskId));
20920
21175
  if (shouldLogBlockedPrompt(codeName, "scheduled-firing")) {
20921
21176
  log(
20922
21177
  `[persistent-session] Deferring ${ready.length} ready task(s) for '${codeName}' \u2014 session is blocked on an interactive prompt (${blockedKind}) and cannot run a tool call; schedule NOT advanced, will fire once the prompt is answered (further deferrals this episode are not logged) [ENG-10335]`
@@ -21186,7 +21441,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
21186
21441
  try {
21187
21442
  const freshId = rotateSessionForWedge(codeName);
21188
21443
  consecutiveWedgeCycles.delete(codeName);
21189
- const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-Q27WU2MW.js");
21444
+ const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-WRHLHQCE.js");
21190
21445
  const { parked, deadLettered } = parkForRestart(codeName, /* @__PURE__ */ new Date());
21191
21446
  const parkNote = parked > 0 ? `, ${parked} inbound parked` : "";
21192
21447
  const deadNote = deadLettered > 0 ? `, ${deadLettered} undeliverable dead-lettered` : "";
@@ -21229,7 +21484,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
21229
21484
  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}`));
21230
21485
  void (async () => {
21231
21486
  try {
21232
- const { collectDiagnostics } = await import("../persistent-session-ZQDNCKQH.js");
21487
+ const { collectDiagnostics } = await import("../persistent-session-GZV4EXKD.js");
21233
21488
  await api.post("/host/heartbeat", {
21234
21489
  host_id: hostId,
21235
21490
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -21280,7 +21535,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
21280
21535
  }
21281
21536
  try {
21282
21537
  const hostId = await getHostId();
21283
- const { collectDiagnostics } = await import("../persistent-session-ZQDNCKQH.js");
21538
+ const { collectDiagnostics } = await import("../persistent-session-GZV4EXKD.js");
21284
21539
  await api.post("/host/heartbeat", {
21285
21540
  host_id: hostId,
21286
21541
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -21679,6 +21934,7 @@ async function processDirectChatMessage(agent, msg) {
21679
21934
  return;
21680
21935
  }
21681
21936
  const blockedOnPrompt = sessionBlockedOnPrompt(agent.codeName);
21937
+ if (blockedOnPrompt) blockedPromptCost.noteMessageWithheld(agent.codeName, msg.id);
21682
21938
  if (blockedOnPrompt && shouldLogBlockedPrompt(agent.codeName, "direct-chat")) {
21683
21939
  log(
21684
21940
  `[direct-chat] Not delivering to '${agent.codeName}' (msg=${msg.id}) \u2014 session is blocked on an interactive prompt (${blockedOnPrompt}), so nothing in-session can pull it; leaving pending for durable replay once the prompt is answered (further messages this episode are not logged) [ENG-10335]`
@@ -21951,7 +22207,7 @@ async function processClaudePairSessions(agents) {
21951
22207
  // cannot take it for itself — by the time it runs, the write it is looking
21952
22208
  // for may already have happened.
21953
22209
  snapshotPairConfig
21954
- } = await import("../claude-pair-runtime-KKFNGRSA.js");
22210
+ } = await import("../claude-pair-runtime-IAO6B35F.js");
21955
22211
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
21956
22212
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
21957
22213
  const killed = await killPairSession(pairTmuxSession(pairId));