@link-assistant/hive-mind 2.11.13 → 2.12.1

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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -18,7 +18,6 @@
18
18
  * @see https://github.com/link-assistant/hive-mind/issues/380
19
19
  * @see https://github.com/link-assistant/hive-mind/issues/1927
20
20
  */
21
-
22
21
  import { exec as execCallback } from 'child_process';
23
22
  import fs from 'fs/promises';
24
23
  import { promisify } from 'util';
@@ -27,20 +26,16 @@ import { notifySubscribers, getSubscriberCount } from './telegram-subscribers.li
27
26
  import { classifyExitStatus, normalizeExitCode } from './session-status.lib.mjs';
28
27
  import { readLastSessionIdFromLog, buildResumeCommand, formatResumeSection } from './session-resume.lib.mjs';
29
28
  import { resolveFailedSessionPullRequestState } from './github-pr-state.lib.mjs';
30
- // Issue #2117: a docker terminal failure that no anchored log footer corroborates
31
- // may be an exit code start-command fabricated from the command's own output.
29
+ // Issue #2117: a docker terminal failure that no anchored log footer corroborates may be an exit code start-command fabricated from the command's own output.
32
30
  import { clearUnverifiedDockerTerminalMarker as clearUnverifiedDockerTerminalMarkerImpl, shouldDeferUnverifiedDockerTerminal as shouldDeferUnverifiedDockerTerminalImpl } from './session-monitor.docker-terminal.lib.mjs';
33
31
  import { isDockerIsolation, sessionStartMs, resolveOomKilledState, resolveStaleExecutingState as resolveStaleExecutingStateImpl } from './session-monitor.stale-executing.lib.mjs';
34
32
  // Issue #2134: kill-cause diagnostics + the matching pull-request notice.
35
33
  import { buildKillCompletionSections, announceKillOnPullRequest } from './session-monitor.kill-sections.lib.mjs';
36
34
  import { runKillRecoveryForCompletion } from './session-kill-resume.lib.mjs';
37
-
38
35
  export { formatSessionCompletionMessage, getSessionCompletionExitCode } from './work-session-formatting.lib.mjs';
39
36
  export { DOCKER_TERMINAL_FOOTER_GRACE_MS } from './session-monitor.docker-terminal.lib.mjs';
40
37
  export { STALE_EXECUTING_MIN_AGE_MS, DOCKER_BACKEND_GONE_GRACE_MS } from './session-monitor.stale-executing.lib.mjs';
41
-
42
38
  const exec = promisify(execCallback);
43
-
44
39
  // Lazy import for isolation runner (only when needed)
45
40
  let _isolationRunner = null;
46
41
  async function getIsolationRunner() {
@@ -51,14 +46,9 @@ async function getIsolationRunner() {
51
46
  }
52
47
  // In-memory session store
53
48
  const activeSessions = new Map();
54
-
55
- // Issue #1927: optional durable mirror of the in-memory registry. When set (by
56
- // the bot at startup via setSessionStore), every track/complete is persisted so
57
- // a restart can reload and keep monitoring detached sessions. Left null in unit
58
- // tests and one-off CLI paths, where in-memory tracking is sufficient.
49
+ // Issue #1927: optional durable mirror of the in-memory registry. When set (by the bot at startup via setSessionStore), every track/complete is persisted so a restart can reload and keep monitoring detached sessions. Left null in unit tests and one-off CLI paths, where in-memory tracking is sufficient.
59
50
  let sessionStore = null;
60
51
  let sessionLogger = null;
61
-
62
52
  /**
63
53
  * Attach a durable session store (see session-store.lib.mjs) so tracked sessions
64
54
  * survive a bot restart. Passing null disconnects the store (used by tests).
@@ -67,7 +57,6 @@ let sessionLogger = null;
67
57
  export function setSessionStore(store) {
68
58
  sessionStore = store || null;
69
59
  }
70
-
71
60
  /**
72
61
  * Attach a structured logger (see bot-logger.lib.mjs) so session lifecycle
73
62
  * transitions are recorded with timestamps. Optional; console is used otherwise.
@@ -76,19 +65,16 @@ export function setSessionStore(store) {
76
65
  export function setSessionLogger(logger) {
77
66
  sessionLogger = logger || null;
78
67
  }
79
-
80
68
  function logEvent(type, data) {
81
69
  if (sessionLogger && typeof sessionLogger.event === 'function') {
82
70
  sessionLogger.event(type, data);
83
71
  }
84
72
  }
85
-
86
73
  export function resetSessionMonitorForTests() {
87
74
  activeSessions.clear();
88
75
  sessionStore = null;
89
76
  sessionLogger = null;
90
77
  }
91
-
92
78
  /**
93
79
  * Inject a stub isolation runner so tests can drive getIsolationSessionState
94
80
  * without spawning real `$ --status` / docker probes. Pass `null` to restore the
@@ -97,7 +83,6 @@ export function resetSessionMonitorForTests() {
97
83
  export function __setIsolationRunnerForTests(runner) {
98
84
  _isolationRunner = runner;
99
85
  }
100
-
101
86
  /**
102
87
  * Test-only accessor for getIsolationSessionState (otherwise module-private).
103
88
  * Used by tests/test-issue-1939-docker-isolation.mjs to verify that an ambiguous
@@ -106,7 +91,6 @@ export function __setIsolationRunnerForTests(runner) {
106
91
  export function getIsolationSessionStateForTests(sessionName, sessionInfo, options = {}) {
107
92
  return getIsolationSessionState(sessionName, sessionInfo, options);
108
93
  }
109
-
110
94
  /**
111
95
  * Issue #1586: Timeout for non-isolation sessions.
112
96
  * Non-isolation (plain start-screen) sessions cannot reliably detect completion
@@ -121,7 +105,6 @@ export function getIsolationSessionStateForTests(sessionName, sessionInfo, optio
121
105
  export const NON_ISOLATION_SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
122
106
  export const DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY = 'on-failure';
123
107
  export const DOCKER_TASK_CONTAINER_KEEP_POLICIES = ['always', 'on-failure', 'never'];
124
-
125
108
  export function resolveDockerTaskContainerKeepPolicy({ env = process.env, verbose = false } = {}) {
126
109
  const raw = String(env?.HIVE_MIND_KEEP_TASK_CONTAINER || '')
127
110
  .trim()
@@ -133,7 +116,6 @@ export function resolveDockerTaskContainerKeepPolicy({ env = process.env, verbos
133
116
  }
134
117
  return DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY;
135
118
  }
136
-
137
119
  /**
138
120
  * Check if a screen session exists
139
121
  * @param {string} sessionName - Name of the screen session to check
@@ -148,7 +130,6 @@ export async function checkScreenSessionExists(sessionName) {
148
130
  return false;
149
131
  }
150
132
  }
151
-
152
133
  /**
153
134
  * Track a new session for completion monitoring
154
135
  *
@@ -169,12 +150,7 @@ export function trackSession(sessionName, sessionInfo, verbose = false) {
169
150
  if (verbose) {
170
151
  console.log(`[VERBOSE] Session ${sessionName} tracked in memory (mode: ${mode})`);
171
152
  }
172
- // Issue #1927: mirror to the durable store so a restart can resume monitoring.
173
- // Only isolation-backed sessions are persisted — they are the ones tracked in
174
- // `$` (start-command) with a reliable status record (requirement #2). Plain
175
- // screen sessions are timeout-based best-effort; resuming them after a restart
176
- // could fabricate a "finished" message with no real exit code, so they stay
177
- // in-memory only.
153
+ // Issue #1927: mirror to the durable store so a restart can resume monitoring. Only isolation-backed sessions are persisted — they are the ones tracked in `$` (start-command) with a reliable status record (requirement #2). Plain screen sessions are timeout-based best-effort; resuming them after a restart could fabricate a "finished" message with no real exit code, so they stay in-memory only.
178
154
  if (sessionStore && isPersistableSession(sessionInfo)) {
179
155
  try {
180
156
  sessionStore.persist(sessionName, sessionInfo);
@@ -191,7 +167,6 @@ export function trackSession(sessionName, sessionInfo, verbose = false) {
191
167
  startTime: sessionInfo.startTime instanceof Date ? sessionInfo.startTime.toISOString() : sessionInfo.startTime || null,
192
168
  });
193
169
  }
194
-
195
170
  /**
196
171
  * Whether a session should be mirrored to the durable store. Only isolation
197
172
  * sessions with a start-command UUID qualify (see trackSession rationale).
@@ -201,7 +176,6 @@ export function trackSession(sessionName, sessionInfo, verbose = false) {
201
176
  function isPersistableSession(sessionInfo) {
202
177
  return Boolean(sessionInfo?.isolationBackend && sessionInfo?.sessionId);
203
178
  }
204
-
205
179
  function persistSessionSnapshot(sessionName, sessionInfo) {
206
180
  if (!sessionStore || !isPersistableSession(sessionInfo)) return;
207
181
  try {
@@ -210,7 +184,6 @@ function persistSessionSnapshot(sessionName, sessionInfo) {
210
184
  /* best effort — persistence must never break monitoring */
211
185
  }
212
186
  }
213
-
214
187
  /**
215
188
  * Look up the in-memory record for a session id (UUID for isolation sessions
216
189
  * or the screen session name for non-isolation sessions). Returns null when no
@@ -225,7 +198,6 @@ export function getTrackedSessionInfo(sessionName) {
225
198
  if (!sessionName) return null;
226
199
  return activeSessions.get(sessionName) || null;
227
200
  }
228
-
229
201
  /**
230
202
  * Issue #2052: record that an operator explicitly requested a session stop
231
203
  * (e.g. Telegram `/stop <uuid>`). The subsequent SIGTERM/SIGKILL exit (143/137,
@@ -252,7 +224,6 @@ export function markSessionStopRequested(sessionId, { requestedBy = null, verbos
252
224
  logEvent('session_stop_requested', { sessionName: key, sessionId, requestedBy: requestedBy || null });
253
225
  return true;
254
226
  }
255
-
256
227
  /**
257
228
  * Stop tracking a session that was registered optimistically but never actually
258
229
  * started (e.g. the start-command launch failed). Removes it from the in-memory
@@ -278,7 +249,6 @@ export function untrackSession(sessionName, verbose = false) {
278
249
  }
279
250
  logEvent('session_untracked', { sessionName });
280
251
  }
281
-
282
252
  /**
283
253
  * Get the number of active sessions being tracked
284
254
  * @param {boolean} verbose - Whether to log verbose output
@@ -290,7 +260,6 @@ export function getActiveSessionCount(verbose = false) {
290
260
  }
291
261
  return activeSessions.size;
292
262
  }
293
-
294
263
  /**
295
264
  * Get all active sessions
296
265
  * @param {boolean} verbose - Whether to log verbose output
@@ -306,7 +275,6 @@ function getActiveSessions(verbose = false) {
306
275
  }
307
276
  return sessions;
308
277
  }
309
-
310
278
  /**
311
279
  * Remove a session from tracking
312
280
  * @param {string} sessionName - Name of the session to remove
@@ -318,8 +286,7 @@ function completeSession(sessionName, exitCode = 0, verbose = false, status = nu
318
286
  if (verbose) {
319
287
  console.log(`[VERBOSE] Session ${sessionName} removed from tracking (exit: ${exitCode}${status ? `, status: ${status}` : ''})`);
320
288
  }
321
- // Issue #1927: drop from the durable snapshot (and append a `complete` audit
322
- // event recording how it ended) so a later restart does not try to resume it.
289
+ // Issue #1927: drop from the durable snapshot (and append a `complete` audit event recording how it ended) so a later restart does not try to resume it.
323
290
  if (sessionStore && isPersistableSession(sessionInfo)) {
324
291
  try {
325
292
  sessionStore.remove(sessionName, { status, exitCode });
@@ -329,30 +296,21 @@ function completeSession(sessionName, exitCode = 0, verbose = false, status = nu
329
296
  }
330
297
  logEvent('session_completed', { sessionName, exitCode: exitCode ?? null, status: status || null });
331
298
  }
332
-
333
299
  function isMessageAlreadyUpdatedError(error) {
334
300
  const message = String(error?.message || '').toLowerCase();
335
301
  return message.includes('message is not modified');
336
302
  }
337
-
338
303
  function normalizeSessionUrl(url) {
339
- // Strip the fragment first, then any trailing slashes, so URLs that carry a
340
- // fragment after a trailing slash (e.g. `.../issues/18/#comment`) normalize to
341
- // the same value as the bare `.../issues/18`. Doing it in the other order
342
- // would leave a dangling trailing slash. (Issue #1871.)
304
+ // Strip the fragment first, then any trailing slashes, so URLs that carry a fragment after a trailing slash (e.g. `.../issues/18/#comment`) normalize to the same value as the bare `.../issues/18`. Doing it in the other order would leave a dangling trailing slash. (Issue #1871.)
343
305
  return url.replace(/#.*$/, '').replace(/\/+$/, '').toLowerCase();
344
306
  }
345
-
346
307
  const GITHUB_PULL_REQUEST_URL_RE = /https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([0-9]+)/g;
347
-
348
308
  export function extractPullRequestUrlFromText(text, { owner = null, repo = null } = {}) {
349
309
  if (!text) return null;
350
-
351
310
  const expectedOwner = owner ? String(owner).toLowerCase() : null;
352
311
  const expectedRepo = repo ? String(repo).toLowerCase() : null;
353
312
  const value = String(text);
354
313
  GITHUB_PULL_REQUEST_URL_RE.lastIndex = 0;
355
-
356
314
  let match;
357
315
  while ((match = GITHUB_PULL_REQUEST_URL_RE.exec(value)) !== null) {
358
316
  const [, matchOwner, matchRepo, pullNumber] = match;
@@ -360,13 +318,10 @@ export function extractPullRequestUrlFromText(text, { owner = null, repo = null
360
318
  if (expectedRepo && matchRepo.toLowerCase() !== expectedRepo) continue;
361
319
  return `https://github.com/${matchOwner}/${matchRepo}/pull/${pullNumber}`;
362
320
  }
363
-
364
321
  return null;
365
322
  }
366
-
367
323
  async function resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose = false, readFile = fs.readFile } = {}) {
368
324
  if (!logPath) return null;
369
-
370
325
  try {
371
326
  const logText = await readFile(logPath, 'utf8');
372
327
  const pullRequestUrl = extractPullRequestUrlFromText(logText, { owner: ctx.owner, repo: ctx.repo });
@@ -381,7 +336,6 @@ async function resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose = fal
381
336
  return null;
382
337
  }
383
338
  }
384
-
385
339
  /**
386
340
  * Issue #1945/#1988: Parse `📊 [DISK]` repository-size checkpoint markers out
387
341
  * of the captured solve log, optionally add docker writable-layer sizes, and
@@ -416,7 +370,6 @@ export async function buildDiskDiagnosticsExtraSection(logPath, { verbose = fals
416
370
  return '';
417
371
  }
418
372
  }
419
-
420
373
  async function getDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose = false, sizeProvider = null } = {}) {
421
374
  if (sessionInfo?.isolationBackend !== 'docker') return null;
422
375
  const containerName = sessionInfo.sessionId || sessionName;
@@ -436,7 +389,6 @@ async function getDockerContainerFilesystemSizeForSession(sessionName, sessionIn
436
389
  return null;
437
390
  }
438
391
  }
439
-
440
392
  async function refreshDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose = false, sizeProvider = null } = {}) {
441
393
  const bytes = await getDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose, sizeProvider });
442
394
  if (!Number.isFinite(bytes)) return null;
@@ -445,40 +397,32 @@ async function refreshDockerContainerFilesystemSizeForSession(sessionName, sessi
445
397
  persistSessionSnapshot(sessionName, sessionInfo);
446
398
  return bytes;
447
399
  }
448
-
449
400
  function getLastKnownDockerContainerFilesystemSize(sessionInfo) {
450
401
  return Number.isFinite(sessionInfo?.containerFilesystemLastBytes) ? sessionInfo.containerFilesystemLastBytes : null;
451
402
  }
452
-
453
403
  function isSuccessfulTaskCompletion({ exitCode = null, status = null } = {}) {
454
404
  const outcome = classifySessionOutcome({ exitCode, status });
455
405
  if (outcome.failed) return false;
456
406
  if (exitCode === 0) return true;
457
-
458
407
  const normalizedStatus = String(status || '')
459
408
  .trim()
460
409
  .toLowerCase();
461
410
  return exitCode === null && (normalizedStatus === 'executed' || normalizedStatus === 'completed');
462
411
  }
463
-
464
412
  function formatDockerTaskContainerKeptSection({ containerName, keepPolicy }) {
465
413
  return ['*Docker container kept*', `Container: \`${containerName}\``, `Policy: \`HIVE_MIND_KEEP_TASK_CONTAINER=${keepPolicy}\``, `Inspect: \`docker start -ai ${containerName}\``, `Shell: \`docker exec -it ${containerName} sh\``, `Remove when done: \`docker rm -f ${containerName}\``].join('\n');
466
414
  }
467
-
468
415
  export function buildDockerTaskContainerCompletionAction({ sessionName, sessionInfo, exitCode = null, status = null, env = process.env, verbose = false } = {}) {
469
416
  if (sessionInfo?.isolationBackend !== 'docker') {
470
417
  return { applies: false, containerName: null, keepPolicy: null, shouldRemove: false, extraSection: '' };
471
418
  }
472
-
473
419
  const containerName = sessionInfo.sessionId || sessionName || null;
474
420
  if (!containerName) {
475
421
  return { applies: false, containerName: null, keepPolicy: null, shouldRemove: false, extraSection: '' };
476
422
  }
477
-
478
423
  const keepPolicy = resolveDockerTaskContainerKeepPolicy({ env, verbose });
479
424
  const successful = isSuccessfulTaskCompletion({ exitCode, status });
480
425
  const shouldKeep = keepPolicy === 'always' || (keepPolicy === 'on-failure' && !successful);
481
-
482
426
  return {
483
427
  applies: true,
484
428
  containerName,
@@ -488,10 +432,8 @@ export function buildDockerTaskContainerCompletionAction({ sessionName, sessionI
488
432
  extraSection: shouldKeep ? formatDockerTaskContainerKeptSection({ containerName, keepPolicy }) : '',
489
433
  };
490
434
  }
491
-
492
435
  async function applyDockerTaskContainerCompletionAction(action, { verbose = false, removeDockerContainer = null } = {}) {
493
436
  if (!action?.applies || !action.shouldRemove || !action.containerName) return;
494
-
495
437
  try {
496
438
  const removeFn =
497
439
  removeDockerContainer ||
@@ -513,7 +455,6 @@ async function applyDockerTaskContainerCompletionAction(action, { verbose = fals
513
455
  }
514
456
  }
515
457
  }
516
-
517
458
  function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false) {
518
459
  const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
519
460
  const elapsed = Date.now() - startTime.getTime();
@@ -530,32 +471,24 @@ function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false)
530
471
  }
531
472
  return true;
532
473
  }
533
-
534
474
  function clearUnverifiedDockerTerminalMarker(sessionName, sessionInfo) {
535
475
  clearUnverifiedDockerTerminalMarkerImpl(sessionInfo, () => persistSessionSnapshot(sessionName, sessionInfo));
536
476
  }
537
-
538
477
  function shouldDeferUnverifiedDockerTerminal(sessionName, sessionInfo, { exitCode, endTime, verbose }) {
539
478
  return shouldDeferUnverifiedDockerTerminalImpl(sessionName, sessionInfo, { exitCode, endTime, verbose, persistSnapshot: () => persistSessionSnapshot(sessionName, sessionInfo) });
540
479
  }
541
-
542
480
  function resolveStaleExecutingState(sessionName, sessionInfo, statusResult, options) {
543
481
  return resolveStaleExecutingStateImpl(sessionName, sessionInfo, statusResult, { ...options, persistSnapshot: () => persistSessionSnapshot(sessionName, sessionInfo) });
544
482
  }
545
-
546
483
  async function getIsolationSessionState(sessionName, sessionInfo, options = {}) {
547
484
  const { verbose = false, statusProvider = null, exitFromLog = null, backendAlive = null, sessionRunning = null } = options;
548
485
  const sessionId = sessionInfo.sessionId || sessionName;
549
-
550
486
  try {
551
487
  const runner = await getIsolationRunner();
552
488
  const statusResult = statusProvider ? await statusProvider(sessionId, sessionInfo) : await runner.querySessionStatus(sessionId, verbose);
553
-
554
489
  if (statusResult?.exists && statusResult.status) {
555
490
  if (statusResult.oomKilled === true) {
556
- // Issue #2134: `oomKilled` is a *container* flag — the kernel sets it when
557
- // any process in the cgroup is OOM-killed — so it is verified against the
558
- // log footer and container liveness before a kill is announced.
491
+ // Issue #2134: `oomKilled` is a *container* flag — the kernel sets it when any process in the cgroup is OOM-killed — so it is verified against the log footer and container liveness before a kill is announced.
559
492
  return await resolveOomKilledState(sessionName, sessionInfo, statusResult, {
560
493
  verbose,
561
494
  runner,
@@ -565,34 +498,25 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
565
498
  });
566
499
  }
567
500
  if (runner.isExecutingSessionStatus(statusResult.status)) {
568
- // Issue #1927: an `executing` status is not trusted blindly — verify the
569
- // process is really alive. start-command can keep reporting `executing`
570
- // after a kill, which is exactly how an OOM-killed /solve went unreported.
501
+ // Issue #1927: an `executing` status is not trusted blindly — verify the process is really alive. start-command can keep reporting `executing` after a kill, which is exactly how an OOM-killed /solve went unreported.
571
502
  const stale = await resolveStaleExecutingState(sessionName, sessionInfo, statusResult, { verbose, runner, exitFromLog, backendAlive });
572
503
  if (stale) {
573
504
  if (verbose) {
574
505
  console.log(`[VERBOSE] Session ${sessionName} reported '${statusResult.status}' but is actually terminated (${stale.reason}); treating as ${stale.status} (exit ${stale.exitCode})`);
575
506
  }
576
- // Rewrite the status payload so downstream completion formatting sees
577
- // the real terminal status/exit code instead of the stale `executing`.
507
+ // Rewrite the status payload so downstream completion formatting sees the real terminal status/exit code instead of the stale `executing`.
578
508
  const correctedStatus = stale.status || 'killed';
579
509
  const corrected = { ...statusResult, status: correctedStatus, exitCode: stale.exitCode, endTime: statusResult.endTime || stale.endTime || null };
580
510
  return { running: false, exitCode: stale.exitCode, status: correctedStatus, statusResult: corrected, stale: true };
581
511
  }
582
- // Back to a plain `executing` report: any earlier unverified terminal
583
- // failure was provisional and is now moot (issue #2117).
512
+ // Back to a plain `executing` report: any earlier unverified terminal failure was provisional and is now moot (issue #2117).
584
513
  clearUnverifiedDockerTerminalMarker(sessionName, sessionInfo);
585
514
  return { running: true, exitCode: null, status: statusResult.status, statusResult };
586
515
  }
587
516
  if (runner.isTerminalSessionStatus(statusResult.status)) {
588
517
  const exitCode = statusResult.exitCode !== undefined ? statusResult.exitCode : null;
589
518
  const logPath = statusResult.logPath || sessionInfo?.logPath || null;
590
- // The log FOOTER is the authoritative terminal result. It is anchored on
591
- // the `=====` separator (see parseSessionExitFooter), so — unlike the
592
- // exit code `$ --status` derives from an unanchored full-log scan — it
593
- // cannot be forged by output the wrapped command printed (issue #2117).
594
- // Prefer it whenever it exists: that both recovers a real code from a
595
- // missing/sentinel status (issue #1927) and overrides a fabricated one.
519
+ // The log FOOTER is the authoritative terminal result. It is anchored on the `=====` separator (see parseSessionExitFooter), so — unlike the exit code `$ --status` derives from an unanchored full-log scan — it cannot be forged by output the wrapped command printed (issue #2117). Prefer it whenever it exists: that both recovers a real code from a missing/sentinel status (issue #1927) and overrides a fabricated one.
596
520
  const readFooter = exitFromLog || runner.readSessionExitFromLog;
597
521
  const footer = logPath && readFooter ? readFooter(logPath, { verbose }) : null;
598
522
  if (footer?.finished) {
@@ -604,29 +528,16 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
604
528
  clearUnverifiedDockerTerminalMarker(sessionName, sessionInfo);
605
529
  return { running: false, exitCode: footerExitCode, status: correctedStatus, statusResult: { ...statusResult, status: correctedStatus, exitCode: footerExitCode } };
606
530
  }
607
- // Issue #1939: a native docker session can report a terminal status
608
- // ("executed") with the unknown exit-code sentinel (-1) while the
609
- // container is still running. When the log footer above did not recover
610
- // a real terminal exit, such a status is provisional — fall through to
611
- // isSessionRunning() below, which cross-checks the live container via
612
- // `docker inspect` before we notify the user the work finished.
531
+ // Issue #1939: a native docker session can report a terminal status ("executed") with the unknown exit-code sentinel (-1) while the container is still running. When the log footer above did not recover a real terminal exit, such a status is provisional — fall through to isSessionRunning() below, which cross-checks the live container via `docker inspect` before we notify the user the work finished.
613
532
  const dockerSession = isDockerIsolation(sessionInfo, statusResult);
614
533
  const ambiguousDockerTerminal = dockerSession && typeof runner.isUnknownDockerExitCode === 'function' && runner.isUnknownDockerExitCode(exitCode);
615
- // Issue #2117: a docker terminal FAILURE with no corroborating footer is
616
- // provisional too — start-command can fabricate that exit code from the
617
- // command's own output. Give the real footer a moment to appear instead
618
- // of announcing a failure the run never had. Only a *freshly* reported
619
- // end time can still be in that race, so an older terminal record is
620
- // still reported without delay.
534
+ // Issue #2117: a docker terminal FAILURE with no corroborating footer is provisional too — start-command can fabricate that exit code from the command's own output. Give the real footer a moment to appear instead of announcing a failure the run never had. Only a *freshly* reported end time can still be in that race, so an older terminal record is still reported without delay.
621
535
  const normalizedExitCode = normalizeExitCode(exitCode);
622
536
  const unverifiedDockerFailure = dockerSession && !ambiguousDockerTerminal && normalizedExitCode !== null && normalizedExitCode !== 0;
623
537
  if (unverifiedDockerFailure && shouldDeferUnverifiedDockerTerminal(sessionName, sessionInfo, { exitCode, endTime: statusResult.endTime || null, verbose })) {
624
538
  return { running: true, exitCode: null, status: statusResult.status, statusResult, deferred: true };
625
539
  }
626
- // Issue #2134: even after the grace window, a container that is verifiably
627
- // still alive cannot have produced a terminal failure — the same liveness
628
- // ladder used for `oomKilled` applies here, so no kill is announced while
629
- // the working session keeps running (that is exactly what #2134 reported).
540
+ // Issue #2134: even after the grace window, a container that is verifiably still alive cannot have produced a terminal failure — the same liveness ladder used for `oomKilled` applies here, so no kill is announced while the working session keeps running (that is exactly what #2134 reported).
630
541
  if (unverifiedDockerFailure) {
631
542
  const probe = backendAlive || runner.checkBackendSessionAlive;
632
543
  let alive = null;
@@ -650,9 +561,7 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
650
561
  }
651
562
  }
652
563
  }
653
-
654
- // The status record is unavailable (no `exists`/`status`). Fall back to a
655
- // direct backend liveness check. `sessionRunning` is injectable purely so
564
+ // The status record is unavailable (no `exists`/`status`). Fall back to a direct backend liveness check. `sessionRunning` is injectable purely so
656
565
  // this path is testable without the real `$`/`screen` binaries; production
657
566
  // always uses the runner's real check.
658
567
  const checkRunning = sessionRunning || runner.isSessionRunning;
@@ -692,7 +601,6 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
692
601
  return { running: false, exitCode: null, status: null, statusResult: null };
693
602
  }
694
603
  }
695
-
696
604
  /**
697
605
  * Monitor active sessions and send notifications when they complete
698
606
  * @param {Object} bot - Telegraf bot instance for sending messages
@@ -700,22 +608,18 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
700
608
  */
701
609
  export async function monitorSessions(bot, verbose = false, options = {}) {
702
610
  const sessions = getActiveSessions(verbose);
703
-
704
611
  if (sessions.length === 0) {
705
612
  return;
706
613
  }
707
-
708
614
  if (verbose) {
709
615
  console.log(`[VERBOSE] Checking ${sessions.length} active session(s)...`);
710
616
  }
711
-
712
617
  for (const { sessionName, sessionInfo } of sessions) {
713
618
  let stillRunning;
714
619
  let exitCode = null;
715
620
  let statusResult = null;
716
621
  let resolvedStatus = null;
717
622
  let observedContainerFilesystemBytes = null;
718
-
719
623
  if (sessionInfo.isolationBackend && sessionInfo.sessionId) {
720
624
  // Isolation mode: use $ --status, with screen -ls only as a fallback
721
625
  // when the status record is unavailable. Terminal $ statuses are
@@ -761,17 +665,14 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
761
665
  }
762
666
  }
763
667
  }
764
-
765
668
  if (sessionInfo?.isolationBackend === 'docker') {
766
669
  observedContainerFilesystemBytes = await refreshDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, {
767
670
  verbose,
768
671
  sizeProvider: options.dockerContainerSizeProvider,
769
672
  });
770
673
  }
771
-
772
674
  if (!stillRunning) {
773
675
  console.log(`Session ${sessionName} has finished. Sending notification to chat ${sessionInfo.chatId}`);
774
-
775
676
  let dockerTaskContainerAction = null;
776
677
  try {
777
678
  const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
@@ -783,7 +684,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
783
684
  env: options.env || process.env,
784
685
  verbose,
785
686
  });
786
-
787
687
  // Issue #1688/#1905: Resolve the created PR from GitHub or, when its
788
688
  // linked-issue API lags, from the completed solve log.
789
689
  let pullRequestUrl = null;
@@ -799,7 +699,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
799
699
  console.log(`[VERBOSE] Pull request lookup failed for ${sessionName}: ${lookupError?.message || lookupError}`);
800
700
  }
801
701
  }
802
-
803
702
  let pullRequestState = null;
804
703
  const completionOutcome = classifySessionOutcome({ exitCode: finalExitCode, status: resolvedStatus });
805
704
  try {
@@ -816,7 +715,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
816
715
  } catch (stateError) {
817
716
  if (verbose) console.log(`[VERBOSE] Pull request state resolution failed for ${sessionName}: ${stateError?.message || stateError}`);
818
717
  }
819
-
820
718
  // Issue #594: append an end-of-task limits snapshot/delta. Cached
821
719
  // helpers prevent parallel sessions from stampeding the upstream API.
822
720
  const limitsExtraSections = [];
@@ -846,7 +744,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
846
744
  }
847
745
  }
848
746
  }
849
-
850
747
  // Issue #1927: for a killed /solve, offer a command using the last tool
851
748
  // session ID in the log. Do not auto-relaunch work that may reliably OOM.
852
749
  const resumeExtraSections = [];
@@ -878,7 +775,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
878
775
  console.log(`[VERBOSE] Could not build resume section for ${sessionName}: ${resumeError?.message || resumeError}`);
879
776
  }
880
777
  }
881
-
882
778
  // Issue #1945/#1988: append a "💾 Disk usage" block from repository
883
779
  // size markers and, for docker isolation, the container writable layer.
884
780
  const diskExtraSections = [];
@@ -899,7 +795,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
899
795
  }
900
796
  }
901
797
  const dockerTaskContainerExtraSections = dockerTaskContainerAction?.extraSection ? [dockerTaskContainerAction.extraSection] : [];
902
-
903
798
  // Issue #2134: say exactly WHY a session was killed, and warn when a
904
799
  // session merely survived a kill event instead of reporting a plain
905
800
  // success. The pull request gets the very same report below.
@@ -913,7 +808,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
913
808
  readFile: options.readFile,
914
809
  env: options.env || process.env,
915
810
  });
916
-
917
811
  // Issue #2134: `--on-session-kill=resume` must actually start a new
918
812
  // working session, and both surfaces must say so. Done before the
919
813
  // message is built so the Telegram report and the pull-request notice
@@ -935,7 +829,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
935
829
  killRecovery = recovered.recovery;
936
830
  if (recovered.section) killReport.sections.push(recovered.section);
937
831
  }
938
-
939
832
  const message = formatSessionCompletionMessage({
940
833
  sessionName,
941
834
  sessionInfo,
@@ -947,7 +840,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
947
840
  pullRequestState,
948
841
  extraSections: [...limitsExtraSections, ...killReport.sections, ...resumeExtraSections, ...diskExtraSections, ...dockerTaskContainerExtraSections],
949
842
  });
950
-
951
843
  if (killReport.killed || killReport.recovered) {
952
844
  const notice = await announceKillOnPullRequest({
953
845
  pullRequestUrl,
@@ -971,7 +863,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
971
863
  console.log(`[VERBOSE] Killed-session notice not posted for ${sessionName}: ${notice.skipped || 'unknown reason'}`);
972
864
  }
973
865
  }
974
-
975
866
  // Update the original reply message if messageId is available, otherwise send new message
976
867
  let notifyFromChatId = null;
977
868
  let notifyMessageId = null;
@@ -984,7 +875,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
984
875
  notifyFromChatId = sent?.chat?.id || sessionInfo.chatId;
985
876
  notifyMessageId = sent?.message_id || null;
986
877
  }
987
-
988
878
  // Issue #1688: forward the same completion message to every /subscribe-d user
989
879
  // in their private chat with the bot. Failures are logged but don't block
990
880
  // completion of the parent session.
@@ -1008,7 +898,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
1008
898
  console.error(`[session-monitor] notifySubscribers failed for ${sessionName}:`, notifyError);
1009
899
  }
1010
900
  }
1011
-
1012
901
  await applyDockerTaskContainerCompletionAction(dockerTaskContainerAction, {
1013
902
  verbose,
1014
903
  removeDockerContainer: options.removeDockerContainer,
@@ -1034,7 +923,6 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
1034
923
  }
1035
924
  }
1036
925
  }
1037
-
1038
926
  /**
1039
927
  * Look up the URL of a pull request linked to the issue this session worked on.
1040
928
  * Returns null when the session was already operating on a PR, the URL context
@@ -1059,7 +947,6 @@ async function resolvePullRequestUrlForSession(sessionInfo, { verbose = false, l
1059
947
  if (!ctx || ctx.type !== 'issue' || !ctx.owner || !ctx.repo || !ctx.number) {
1060
948
  return null;
1061
949
  }
1062
-
1063
950
  if (typeof lookupLinkedPullRequest === 'function') {
1064
951
  const linkedPullRequestUrl = await lookupLinkedPullRequest(ctx);
1065
952
  if (linkedPullRequestUrl) return linkedPullRequestUrl;
@@ -1080,20 +967,16 @@ async function resolvePullRequestUrlForSession(sessionInfo, { verbose = false, l
1080
967
  }
1081
968
  }
1082
969
  }
1083
-
1084
970
  const logPath = statusResult?.logPath || sessionInfo?.logPath || null;
1085
971
  const pullRequestUrlFromLog = await resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose, readFile });
1086
972
  if (pullRequestUrlFromLog) return pullRequestUrlFromLog;
1087
-
1088
973
  if (verbose && logPath) {
1089
974
  console.log(`[VERBOSE] No PR URL found for issue ${ctx.owner}/${ctx.repo}#${ctx.number} in session log ${logPath}`);
1090
975
  } else if (verbose) {
1091
976
  console.log(`[VERBOSE] No session log path available for PR URL fallback for issue ${ctx.owner}/${ctx.repo}#${ctx.number}`);
1092
977
  }
1093
-
1094
978
  return null;
1095
979
  }
1096
-
1097
980
  /**
1098
981
  * Start the session monitoring interval
1099
982
  * @param {Object} bot - Telegraf bot instance for sending messages
@@ -1113,7 +996,6 @@ export function startSessionMonitoring(bot, verbose = false, intervalMs = 30000,
1113
996
  console.log(`📊 Session monitoring started (checking every ${intervalMs / 1000} seconds, storage: ${storage})`);
1114
997
  return timer;
1115
998
  }
1116
-
1117
999
  /**
1118
1000
  * Issue #1927 (requirements #2 and #4): after a bot restart, reload the sessions
1119
1001
  * that were still being tracked when the previous process died and re-register
@@ -1140,12 +1022,10 @@ export async function resumeTrackedSessions(options = {}) {
1140
1022
  const { store = sessionStore, verbose = false, botStartTime = Math.floor(Date.now() / 1000) } = options;
1141
1023
  const resumed = [];
1142
1024
  const skipped = [];
1143
-
1144
1025
  if (!store) {
1145
1026
  if (verbose) console.log('[VERBOSE] resumeTrackedSessions: no durable session store configured, nothing to resume');
1146
1027
  return { resumed, skipped };
1147
1028
  }
1148
-
1149
1029
  let persisted;
1150
1030
  try {
1151
1031
  persisted = store.load();
@@ -1153,7 +1033,6 @@ export async function resumeTrackedSessions(options = {}) {
1153
1033
  console.error(`[session-monitor] resumeTrackedSessions: could not load persisted sessions: ${error.message}`);
1154
1034
  return { resumed, skipped };
1155
1035
  }
1156
-
1157
1036
  for (const { sessionName, sessionInfo } of persisted) {
1158
1037
  if (activeSessions.has(sessionName)) {
1159
1038
  skipped.push({ sessionName, reason: 'already-tracked' });
@@ -1167,7 +1046,6 @@ export async function resumeTrackedSessions(options = {}) {
1167
1046
  if (verbose) console.log(`[VERBOSE] Skipping resume of ${sessionName}: started after bot start`);
1168
1047
  continue;
1169
1048
  }
1170
-
1171
1049
  activeSessions.set(sessionName, sessionInfo);
1172
1050
  resumed.push({ sessionName, sessionInfo });
1173
1051
  logEvent('session_resumed', {
@@ -1181,16 +1059,13 @@ export async function resumeTrackedSessions(options = {}) {
1181
1059
  console.log(`[VERBOSE] Resumed tracking of session ${sessionName} (url: ${sessionInfo.url || 'n/a'}, command: ${sessionInfo.command || 'n/a'}, backend: ${sessionInfo.isolationBackend || 'screen'})`);
1182
1060
  }
1183
1061
  }
1184
-
1185
1062
  if (resumed.length > 0) {
1186
1063
  console.log(`♻️ Resumed monitoring of ${resumed.length} session(s) from durable store after restart`);
1187
1064
  } else if (verbose) {
1188
1065
  console.log('[VERBOSE] resumeTrackedSessions: no eligible sessions to resume');
1189
1066
  }
1190
-
1191
1067
  return { resumed, skipped };
1192
1068
  }
1193
-
1194
1069
  /**
1195
1070
  * Issue #1567: Check if there's an active session for a given URL.
1196
1071
  * This prevents concurrent sessions on the same PR/issue, which causes
@@ -1212,10 +1087,8 @@ export async function resumeTrackedSessions(options = {}) {
1212
1087
  */
1213
1088
  export function hasActiveSessionForUrl(url, verbose = false) {
1214
1089
  if (!url) return { isActive: false, sessionName: null };
1215
-
1216
1090
  // Normalize the URL for comparison (remove trailing slashes, fragments, etc.)
1217
1091
  const normalizedUrl = normalizeSessionUrl(url);
1218
-
1219
1092
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
1220
1093
  // Issue #1586: Auto-expire non-isolation sessions after timeout
1221
1094
  if (!sessionInfo.isolationBackend && !isNonIsolationSessionActive(sessionName, sessionInfo, verbose)) {
@@ -1229,15 +1102,12 @@ export function hasActiveSessionForUrl(url, verbose = false) {
1229
1102
  return { isActive: true, sessionName };
1230
1103
  }
1231
1104
  }
1232
-
1233
1105
  if (verbose) {
1234
1106
  console.log(`[VERBOSE] No active session found for URL ${url}`);
1235
1107
  }
1236
1108
  return { isActive: false, sessionName: null };
1237
1109
  }
1238
-
1239
1110
  const SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1240
-
1241
1111
  /**
1242
1112
  * Issue #1871: Find a tracked, still-running session for a GitHub issue/PR URL
1243
1113
  * and report whether it can be stopped by forwarding CTRL+C to the
@@ -1264,9 +1134,7 @@ const SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
1264
1134
  */
1265
1135
  export function findStoppableSessionByUrl(url, verbose = false) {
1266
1136
  if (!url) return null;
1267
-
1268
1137
  const normalizedUrl = normalizeSessionUrl(url);
1269
-
1270
1138
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
1271
1139
  if (!sessionInfo.url || normalizeSessionUrl(sessionInfo.url) !== normalizedUrl) {
1272
1140
  continue;
@@ -1275,19 +1143,16 @@ export function findStoppableSessionByUrl(url, verbose = false) {
1275
1143
  if (!sessionInfo.isolationBackend && !isNonIsolationSessionActive(sessionName, sessionInfo, verbose)) {
1276
1144
  continue;
1277
1145
  }
1278
-
1279
1146
  // The UUID `$ --stop` expects is the start-command session id. For
1280
1147
  // isolation sessions it is tracked either as sessionInfo.sessionId or as
1281
1148
  // the (UUID-shaped) session key itself.
1282
1149
  const candidateId = sessionInfo.sessionId || sessionName;
1283
1150
  const sessionId = SESSION_UUID_RE.test(candidateId) ? candidateId : null;
1284
1151
  const stoppable = Boolean(sessionInfo.isolationBackend && sessionId);
1285
-
1286
1152
  if (verbose) {
1287
1153
  const mode = sessionInfo.isolationBackend ? `isolation:${sessionInfo.isolationBackend}` : 'non-isolation';
1288
1154
  console.log(`[VERBOSE] findStoppableSessionByUrl: matched ${sessionName} for ${url} (${mode}, stoppable=${stoppable})`);
1289
1155
  }
1290
-
1291
1156
  return {
1292
1157
  sessionName,
1293
1158
  sessionId,
@@ -1296,13 +1161,11 @@ export function findStoppableSessionByUrl(url, verbose = false) {
1296
1161
  stoppable,
1297
1162
  };
1298
1163
  }
1299
-
1300
1164
  if (verbose) {
1301
1165
  console.log(`[VERBOSE] findStoppableSessionByUrl: no tracked session for ${url}`);
1302
1166
  }
1303
1167
  return null;
1304
1168
  }
1305
-
1306
1169
  /**
1307
1170
  * Async active-session check for command handlers.
1308
1171
  *
@@ -1318,21 +1181,17 @@ export function findStoppableSessionByUrl(url, verbose = false) {
1318
1181
  */
1319
1182
  export async function hasActiveSessionForUrlAsync(url, verbose = false, options = {}) {
1320
1183
  if (!url) return { isActive: false, sessionName: null };
1321
-
1322
1184
  const normalizedUrl = normalizeSessionUrl(url);
1323
-
1324
1185
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
1325
1186
  if (!sessionInfo.url || normalizeSessionUrl(sessionInfo.url) !== normalizedUrl) {
1326
1187
  continue;
1327
1188
  }
1328
-
1329
1189
  if (!sessionInfo.isolationBackend) {
1330
1190
  if (isNonIsolationSessionActive(sessionName, sessionInfo, verbose)) {
1331
1191
  return { isActive: true, sessionName, status: null };
1332
1192
  }
1333
1193
  continue;
1334
1194
  }
1335
-
1336
1195
  const state = await getIsolationSessionState(sessionName, sessionInfo, {
1337
1196
  verbose,
1338
1197
  statusProvider: options.statusProvider,
@@ -1343,20 +1202,17 @@ export async function hasActiveSessionForUrlAsync(url, verbose = false, options
1343
1202
  }
1344
1203
  return { isActive: true, sessionName, status: state.status || null };
1345
1204
  }
1346
-
1347
1205
  if (verbose) {
1348
1206
  console.log(`[VERBOSE] Isolated session ${sessionName} for URL ${url} is no longer running (status: ${state.status || 'unknown'}), allowing retry while monitor sends completion`);
1349
1207
  }
1350
1208
  sessionInfo.lastKnownStatus = state.status || null;
1351
1209
  sessionInfo.lastKnownExitCode = state.exitCode ?? null;
1352
1210
  }
1353
-
1354
1211
  if (verbose) {
1355
1212
  console.log(`[VERBOSE] No active session found for URL ${url}`);
1356
1213
  }
1357
1214
  return { isActive: false, sessionName: null };
1358
1215
  }
1359
-
1360
1216
  /**
1361
1217
  * Refresh tracked isolation sessions and count only those that are executing.
1362
1218
  *
@@ -1368,31 +1224,25 @@ export async function hasActiveSessionForUrlAsync(url, verbose = false, options
1368
1224
  export async function getRunningTrackedIsolationSessions(verbose = false, options = {}) {
1369
1225
  const sessions = [];
1370
1226
  const byTool = {};
1371
-
1372
1227
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
1373
1228
  if (!sessionInfo.isolationBackend) {
1374
1229
  continue;
1375
1230
  }
1376
-
1377
1231
  const state = await getIsolationSessionState(sessionName, sessionInfo, {
1378
1232
  verbose,
1379
1233
  statusProvider: options.statusProvider,
1380
1234
  });
1381
-
1382
1235
  if (!state.running) {
1383
1236
  sessionInfo.lastKnownStatus = state.status || null;
1384
1237
  sessionInfo.lastKnownExitCode = state.exitCode ?? null;
1385
1238
  continue;
1386
1239
  }
1387
-
1388
1240
  const tool = sessionInfo.tool || 'claude';
1389
1241
  sessions.push(sessionName);
1390
1242
  byTool[tool] = (byTool[tool] || 0) + 1;
1391
1243
  }
1392
-
1393
1244
  return { count: sessions.length, sessions, byTool };
1394
1245
  }
1395
-
1396
1246
  /**
1397
1247
  * Return the currently-executing tracked sessions with the details needed to
1398
1248
  * render them as a clickable list in `/queue`: the issue/PR
@@ -1416,11 +1266,9 @@ export async function getRunningTrackedIsolationSessions(verbose = false, option
1416
1266
  export async function getRunningSessionItems(verbose = false, options = {}) {
1417
1267
  const items = [];
1418
1268
  const screenChecker = options.screenChecker || checkScreenSessionExists;
1419
-
1420
1269
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
1421
1270
  let running;
1422
1271
  let status = null;
1423
-
1424
1272
  if (sessionInfo.isolationBackend) {
1425
1273
  // Forward every injectable seam so the listing applies the same #1927
1426
1274
  // stale-`executing` reconciliation the monitor does — a session that
@@ -1455,7 +1303,6 @@ export async function getRunningSessionItems(verbose = false, options = {}) {
1455
1303
  continue;
1456
1304
  }
1457
1305
  }
1458
-
1459
1306
  items.push({
1460
1307
  sessionName,
1461
1308
  url: sessionInfo.url || null,
@@ -1465,14 +1312,11 @@ export async function getRunningSessionItems(verbose = false, options = {}) {
1465
1312
  isolationBackend: sessionInfo.isolationBackend || null,
1466
1313
  });
1467
1314
  }
1468
-
1469
1315
  if (verbose) {
1470
1316
  console.log(`[VERBOSE] getRunningSessionItems found ${items.length} running session(s)`);
1471
1317
  }
1472
-
1473
1318
  return items;
1474
1319
  }
1475
-
1476
1320
  /**
1477
1321
  * Get statistics about session tracking
1478
1322
  * @param {boolean} verbose - Whether to log verbose output
@@ -1481,11 +1325,9 @@ export async function getRunningSessionItems(verbose = false, options = {}) {
1481
1325
  export function getSessionStats(verbose = false) {
1482
1326
  const sessions = Array.from(activeSessions.values());
1483
1327
  const isolated = sessions.filter(s => s.isolationBackend);
1484
-
1485
1328
  if (verbose) {
1486
1329
  console.log(`[VERBOSE] Session stats: ${sessions.length} total, ${isolated.length} isolated`);
1487
1330
  }
1488
-
1489
1331
  return {
1490
1332
  total: activeSessions.size,
1491
1333
  executing: activeSessions.size,