@holon-run/agentinbox 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,9 +10,11 @@ const node_util_1 = require("node:util");
10
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  const node_os_1 = __importDefault(require("node:os"));
13
+ const logging_1 = require("./logging");
13
14
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
14
15
  const DEFAULT_ITERM2_SAMPLE_DELAY_MS = 900;
15
16
  const DEFAULT_TMUX_SAMPLE_DELAY_MS = 900;
17
+ const NORMALIZED_BUFFER_TAIL_LINES = 20;
16
18
  const ACTIVE_BUFFER_MARKERS = [
17
19
  "esc to interrupt",
18
20
  "Working (",
@@ -25,22 +27,36 @@ const TYPING_PROMPT_PREFIXES = [
25
27
  class DefaultActivationGate {
26
28
  runtimeProbes;
27
29
  terminalProbes;
28
- constructor(runtimeProbes = [
29
- new CodexRuntimePresenceProbe(),
30
- new ClaudeCodeRuntimePresenceProbe(),
31
- ], terminalProbes = [
32
- new CodexTerminalStateProbe(),
33
- new Iterm2TerminalStateProbe(),
34
- new ClaudeCodeTerminalStateProbe(),
35
- new TmuxTerminalStateProbe(),
36
- ]) {
37
- this.runtimeProbes = runtimeProbes;
38
- this.terminalProbes = terminalProbes;
30
+ logger;
31
+ constructor(runtimeProbes, terminalProbes, logger = new logging_1.NoopLogger()) {
32
+ this.logger = logger.child("activation_gate");
33
+ this.runtimeProbes = runtimeProbes ?? [
34
+ new CodexRuntimePresenceProbe(undefined, undefined, this.logger.child("codex_runtime")),
35
+ new ClaudeCodeRuntimePresenceProbe(undefined, undefined, this.logger.child("claude_runtime")),
36
+ ];
37
+ this.terminalProbes = terminalProbes ?? [
38
+ new CodexTerminalStateProbe(undefined, undefined, undefined, undefined, undefined, this.logger.child("codex_terminal")),
39
+ new Iterm2TerminalStateProbe(undefined, {}, this.logger.child("iterm2_terminal")),
40
+ new ClaudeCodeTerminalStateProbe(undefined, undefined, undefined, this.logger.child("claude_terminal")),
41
+ new TmuxTerminalStateProbe(undefined, {}, this.logger.child("tmux_terminal")),
42
+ ];
39
43
  }
40
44
  async evaluate(target) {
41
45
  const runtimeProbe = this.runtimeProbes.find((probe) => probe.supports(target));
42
46
  const runtimePresence = runtimeProbe ? await runtimeProbe.check(target) : "unknown";
43
47
  if (runtimePresence === "gone") {
48
+ this.logger.debug("gate_decision", {
49
+ targetId: target.targetId,
50
+ runtimeKind: target.runtimeKind,
51
+ backend: target.backend,
52
+ runtimeProbe: runtimeProbe?.constructor?.name ?? null,
53
+ runtimePresence,
54
+ terminalProbe: null,
55
+ terminalPresence: null,
56
+ terminalBusy: null,
57
+ outcome: "offline",
58
+ reason: "runtime_gone",
59
+ });
44
60
  return { outcome: "offline", reason: "runtime_gone" };
45
61
  }
46
62
  const terminalProbe = this.terminalProbes.find((probe) => probe.supports(target));
@@ -48,14 +64,62 @@ class DefaultActivationGate {
48
64
  ? await terminalProbe.check(target)
49
65
  : { presence: "unknown", busy: "unknown" };
50
66
  if (terminalState.presence === "gone") {
67
+ this.logger.debug("gate_decision", {
68
+ targetId: target.targetId,
69
+ runtimeKind: target.runtimeKind,
70
+ backend: target.backend,
71
+ runtimeProbe: runtimeProbe?.constructor?.name ?? null,
72
+ runtimePresence,
73
+ terminalProbe: terminalProbe?.constructor?.name ?? null,
74
+ terminalPresence: terminalState.presence,
75
+ terminalBusy: terminalState.busy,
76
+ outcome: "offline",
77
+ reason: "terminal_gone",
78
+ });
51
79
  return { outcome: "offline", reason: "terminal_gone" };
52
80
  }
53
81
  if (terminalState.busy === "busy") {
82
+ this.logger.debug("gate_decision", {
83
+ targetId: target.targetId,
84
+ runtimeKind: target.runtimeKind,
85
+ backend: target.backend,
86
+ runtimeProbe: runtimeProbe?.constructor?.name ?? null,
87
+ runtimePresence,
88
+ terminalProbe: terminalProbe?.constructor?.name ?? null,
89
+ terminalPresence: terminalState.presence,
90
+ terminalBusy: terminalState.busy,
91
+ outcome: "defer",
92
+ reason: "terminal_busy",
93
+ });
54
94
  return { outcome: "defer", reason: "terminal_busy" };
55
95
  }
56
96
  if (target.runtimeKind === "codex" && terminalState.busy === "idle") {
97
+ this.logger.debug("gate_decision", {
98
+ targetId: target.targetId,
99
+ runtimeKind: target.runtimeKind,
100
+ backend: target.backend,
101
+ runtimeProbe: runtimeProbe?.constructor?.name ?? null,
102
+ runtimePresence,
103
+ terminalProbe: terminalProbe?.constructor?.name ?? null,
104
+ terminalPresence: terminalState.presence,
105
+ terminalBusy: terminalState.busy,
106
+ outcome: "defer",
107
+ reason: "terminal_recently_active",
108
+ });
57
109
  return { outcome: "defer", reason: "terminal_recently_active" };
58
110
  }
111
+ this.logger.debug("gate_decision", {
112
+ targetId: target.targetId,
113
+ runtimeKind: target.runtimeKind,
114
+ backend: target.backend,
115
+ runtimeProbe: runtimeProbe?.constructor?.name ?? null,
116
+ runtimePresence,
117
+ terminalProbe: terminalProbe?.constructor?.name ?? null,
118
+ terminalPresence: terminalState.presence,
119
+ terminalBusy: terminalState.busy,
120
+ outcome: "inject",
121
+ reason: "gate_unknown_or_idle",
122
+ });
59
123
  return { outcome: "inject", reason: "gate_unknown_or_idle" };
60
124
  }
61
125
  }
@@ -63,36 +127,45 @@ exports.DefaultActivationGate = DefaultActivationGate;
63
127
  class CodexRuntimePresenceProbe {
64
128
  killFn;
65
129
  sessionFileReader;
66
- constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader) {
130
+ logger;
131
+ constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader, logger = new logging_1.NoopLogger()) {
67
132
  this.killFn = killFn;
68
133
  this.sessionFileReader = sessionFileReader;
134
+ this.logger = logger;
69
135
  }
70
136
  supports(target) {
71
137
  return target.runtimeKind === "codex" && Number.isInteger(target.runtimePid);
72
138
  }
73
139
  async check(target) {
74
140
  if (!Number.isInteger(target.runtimePid)) {
141
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_pid" });
75
142
  return "unknown";
76
143
  }
77
144
  try {
78
145
  this.killFn(target.runtimePid, 0);
79
146
  if (!target.runtimeSessionId) {
147
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_session_id" });
80
148
  return "unknown";
81
149
  }
82
150
  const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
83
151
  if (sessionInfo === "unknown") {
152
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "session_file_unknown" });
84
153
  return "unknown";
85
154
  }
86
155
  if (!sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
156
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_file_mismatch" });
87
157
  return "gone";
88
158
  }
159
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "alive" });
89
160
  return "alive";
90
161
  }
91
162
  catch (error) {
92
163
  const code = error instanceof Error && "code" in error ? String(error.code ?? "") : "";
93
164
  if (code === "ESRCH") {
165
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "process_missing" });
94
166
  return "gone";
95
167
  }
168
+ this.logger.debug("probe_error", { targetId: target.targetId, error, result: "unknown" });
96
169
  return "unknown";
97
170
  }
98
171
  }
@@ -104,12 +177,14 @@ class CodexTerminalStateProbe {
104
177
  sessionFileReader;
105
178
  statReader;
106
179
  nowFn;
107
- constructor(iTermProbe = new Iterm2TerminalStateProbe(), tmuxProbe = new TmuxTerminalStateProbe(), sessionFileReader, statReader, nowFn = () => Date.now()) {
180
+ logger;
181
+ constructor(iTermProbe = new Iterm2TerminalStateProbe(), tmuxProbe = new TmuxTerminalStateProbe(), sessionFileReader, statReader, nowFn = () => Date.now(), logger = new logging_1.NoopLogger()) {
108
182
  this.iTermProbe = iTermProbe;
109
183
  this.tmuxProbe = tmuxProbe;
110
184
  this.sessionFileReader = sessionFileReader;
111
185
  this.statReader = statReader;
112
186
  this.nowFn = nowFn;
187
+ this.logger = logger;
113
188
  }
114
189
  supports(target) {
115
190
  return target.runtimeKind === "codex";
@@ -117,26 +192,66 @@ class CodexTerminalStateProbe {
117
192
  async check(target) {
118
193
  const terminalState = await this.readTerminalState(target);
119
194
  if (!target.runtimeSessionId) {
195
+ this.logger.trace("probe_result", {
196
+ targetId: target.targetId,
197
+ result: terminalState,
198
+ reason: "missing_runtime_session_id",
199
+ });
120
200
  return terminalState;
121
201
  }
122
202
  const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
123
203
  if (sessionInfo === "unknown" || !sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
204
+ this.logger.trace("probe_result", {
205
+ targetId: target.targetId,
206
+ result: terminalState,
207
+ reason: "session_file_unavailable",
208
+ });
124
209
  return terminalState;
125
210
  }
126
211
  const stat = await (this.statReader ?? defaultFileStatReader)(sessionInfo.filePath);
127
212
  if (!stat) {
213
+ this.logger.trace("probe_result", {
214
+ targetId: target.targetId,
215
+ result: terminalState,
216
+ reason: "session_file_stat_missing",
217
+ });
128
218
  return terminalState;
129
219
  }
130
220
  if (terminalState.busy === "busy" || terminalState.presence === "gone") {
221
+ this.logger.trace("probe_result", {
222
+ targetId: target.targetId,
223
+ result: terminalState,
224
+ reason: "terminal_probe_decisive",
225
+ });
131
226
  return terminalState;
132
227
  }
133
228
  const ageMs = this.nowFn() - stat.mtimeMs;
134
229
  if (ageMs < 5000) {
135
- return { presence: terminalState.presence, busy: "busy" };
230
+ const result = { presence: terminalState.presence, busy: "busy" };
231
+ this.logger.trace("probe_result", {
232
+ targetId: target.targetId,
233
+ result,
234
+ reason: "session_file_recent_activity",
235
+ ageMs,
236
+ });
237
+ return result;
136
238
  }
137
239
  if (ageMs < 30000) {
138
- return { presence: terminalState.presence, busy: "idle" };
139
- }
240
+ const result = { presence: terminalState.presence, busy: "idle" };
241
+ this.logger.trace("probe_result", {
242
+ targetId: target.targetId,
243
+ result,
244
+ reason: "session_file_recently_active",
245
+ ageMs,
246
+ });
247
+ return result;
248
+ }
249
+ this.logger.trace("probe_result", {
250
+ targetId: target.targetId,
251
+ result: terminalState,
252
+ reason: "session_file_stale",
253
+ ageMs,
254
+ });
140
255
  return terminalState;
141
256
  }
142
257
  async readTerminalState(target) {
@@ -153,9 +268,11 @@ exports.CodexTerminalStateProbe = CodexTerminalStateProbe;
153
268
  class Iterm2TerminalStateProbe {
154
269
  execAsync;
155
270
  options;
156
- constructor(execAsync = execFileAsync, options = {}) {
271
+ logger;
272
+ constructor(execAsync = execFileAsync, options = {}, logger = new logging_1.NoopLogger()) {
157
273
  this.execAsync = execAsync;
158
274
  this.options = options;
275
+ this.logger = logger;
159
276
  }
160
277
  supports(target) {
161
278
  // Only support iTerm2 targets that are NOT Claude Code
@@ -168,17 +285,39 @@ class Iterm2TerminalStateProbe {
168
285
  async check(target) {
169
286
  const sessionId = target.itermSessionId?.trim();
170
287
  if (!sessionId) {
288
+ this.logger.trace("probe_result", {
289
+ targetId: target.targetId,
290
+ result: { presence: "unknown", busy: "unknown" },
291
+ reason: "missing_iterm_session_id",
292
+ });
171
293
  return { presence: "unknown", busy: "unknown" };
172
294
  }
173
295
  // Try Python API probe first (if enabled)
174
296
  if (this.options.enablePythonProbe !== false) {
175
297
  const pythonResult = await this.tryPythonApiProbe(sessionId, target.runtimeKind);
176
298
  if (pythonResult && pythonResult.presence !== "unknown") {
299
+ this.logger.debug("probe_result", {
300
+ targetId: target.targetId,
301
+ path: "python",
302
+ result: pythonResult,
303
+ });
177
304
  return pythonResult;
178
305
  }
306
+ this.logger.trace("probe_fallback", {
307
+ targetId: target.targetId,
308
+ from: "python",
309
+ to: "cli",
310
+ reason: pythonResult ? "python_presence_unknown" : "python_probe_unavailable",
311
+ });
179
312
  }
180
313
  // Fallback to command-line tools
181
- return this.checkWithCommandLineTools(sessionId);
314
+ const result = await this.checkWithCommandLineTools(sessionId, target.targetId);
315
+ this.logger.debug("probe_result", {
316
+ targetId: target.targetId,
317
+ path: "cli",
318
+ result,
319
+ });
320
+ return result;
182
321
  }
183
322
  async tryPythonApiProbe(sessionId, runtimeKind) {
184
323
  try {
@@ -188,12 +327,25 @@ class Iterm2TerminalStateProbe {
188
327
  });
189
328
  const data = JSON.parse(result.stdout);
190
329
  if (data.status === "gone") {
330
+ this.logger.trace("probe_python_result", {
331
+ sessionId,
332
+ result: { presence: "gone", busy: "unknown" },
333
+ reason: "session_gone",
334
+ });
191
335
  return { presence: "gone", busy: "unknown" };
192
336
  }
193
337
  if (data.status === "available") {
194
338
  // If we have cursor and lines data, use cursor-aware detection
195
- if (data.cursor && data.lines) {
196
- const bufferTail = data.lines.join("\n");
339
+ if (data.cursor && Array.isArray(data.lines)) {
340
+ const bufferTail = normalizeProbeTailFromLines(data.lines);
341
+ if (!bufferTail) {
342
+ this.logger.trace("probe_python_result", {
343
+ sessionId,
344
+ result: { presence: "available", busy: "unknown" },
345
+ reason: "empty_normalized_buffer",
346
+ });
347
+ return { presence: "available", busy: "unknown" };
348
+ }
197
349
  const cursorPosition = { x: data.cursor.x, y: data.cursor.y };
198
350
  const screenHeight = data.screen_height;
199
351
  const startLine = data.start_line ?? 0;
@@ -201,6 +353,11 @@ class Iterm2TerminalStateProbe {
201
353
  const busyStatus = evaluatePythonCursorAwareTypingPrompt(bufferTail, cursorPosition, screenHeight, startLine, runtimeKind);
202
354
  // If cursor-aware detection indicates busy, return immediately
203
355
  if (busyStatus === "busy") {
356
+ this.logger.trace("probe_python_result", {
357
+ sessionId,
358
+ result: { presence: "available", busy: "busy" },
359
+ reason: "cursor_aware_busy",
360
+ });
204
361
  return { presence: "available", busy: "busy" };
205
362
  }
206
363
  // If cursor-aware detection is certain it's not busy, don't apply prompt heuristics
@@ -211,17 +368,32 @@ class Iterm2TerminalStateProbe {
211
368
  env: { ...process.env, PYTHONIOENCODING: "utf-8" }
212
369
  });
213
370
  const secondData = JSON.parse(secondResult.stdout);
214
- if (secondData.status === "available" && secondData.lines) {
215
- const secondBufferTail = secondData.lines.join("\n");
216
- if (bufferTail !== secondBufferTail) {
371
+ if (secondData.status === "available" && Array.isArray(secondData.lines)) {
372
+ const secondBufferTail = normalizeProbeTailFromLines(secondData.lines);
373
+ if (secondBufferTail && bufferTail !== secondBufferTail) {
374
+ this.logger.trace("probe_python_result", {
375
+ sessionId,
376
+ result: { presence: "available", busy: "busy" },
377
+ reason: "buffer_changed",
378
+ });
217
379
  return { presence: "available", busy: "busy" };
218
380
  }
219
381
  }
220
382
  // Check for active buffer markers
221
383
  if (containsActiveBufferMarker(bufferTail)) {
384
+ this.logger.trace("probe_python_result", {
385
+ sessionId,
386
+ result: { presence: "available", busy: "busy" },
387
+ reason: "active_buffer_marker",
388
+ });
222
389
  return { presence: "available", busy: "busy" };
223
390
  }
224
- return { presence: "available", busy: "idle" };
391
+ this.logger.trace("probe_python_result", {
392
+ sessionId,
393
+ result: { presence: "available", busy: "unknown" },
394
+ reason: "cursor_aware_not_busy",
395
+ });
396
+ return { presence: "available", busy: "unknown" };
225
397
  }
226
398
  // If cursor-aware detection is uncertain, fall back to buffer change detection only
227
399
  // Do NOT use generic typing prompts to avoid false positives (see #116)
@@ -231,40 +403,85 @@ class Iterm2TerminalStateProbe {
231
403
  env: { ...process.env, PYTHONIOENCODING: "utf-8" }
232
404
  });
233
405
  const secondData = JSON.parse(secondResult.stdout);
234
- if (secondData.status === "available" && secondData.lines) {
235
- const secondBufferTail = secondData.lines.join("\n");
236
- if (bufferTail !== secondBufferTail) {
406
+ if (secondData.status === "available" && Array.isArray(secondData.lines)) {
407
+ const secondBufferTail = normalizeProbeTailFromLines(secondData.lines);
408
+ if (secondBufferTail && bufferTail !== secondBufferTail) {
409
+ this.logger.trace("probe_python_result", {
410
+ sessionId,
411
+ result: { presence: "available", busy: "busy" },
412
+ reason: "buffer_changed_after_unknown_cursor_state",
413
+ });
237
414
  return { presence: "available", busy: "busy" };
238
415
  }
239
416
  }
240
417
  // Check for active buffer markers
241
418
  if (containsActiveBufferMarker(bufferTail)) {
419
+ this.logger.trace("probe_python_result", {
420
+ sessionId,
421
+ result: { presence: "available", busy: "busy" },
422
+ reason: "active_buffer_marker_after_unknown_cursor_state",
423
+ });
242
424
  return { presence: "available", busy: "busy" };
243
425
  }
244
426
  // Return unknown when cursor-aware detection is uncertain
245
427
  // Do NOT fall back to generic typing prompts to avoid false positives
428
+ this.logger.trace("probe_python_result", {
429
+ sessionId,
430
+ result: { presence: "available", busy: "unknown" },
431
+ reason: "cursor_aware_unknown",
432
+ });
246
433
  return { presence: "available", busy: "unknown" };
247
434
  }
248
435
  // This should never be reached as all cases are handled above
436
+ this.logger.trace("probe_python_result", {
437
+ sessionId,
438
+ result: { presence: "available", busy: "unknown" },
439
+ reason: "cursor_probe_fell_through",
440
+ });
249
441
  return { presence: "available", busy: "unknown" };
250
442
  }
251
443
  // If we don't have cursor data, return unknown to trigger fallback
444
+ this.logger.trace("probe_python_result", {
445
+ sessionId,
446
+ result: null,
447
+ reason: "missing_cursor_data",
448
+ });
252
449
  return null;
253
450
  }
254
451
  // Any other status means we should fall back
452
+ this.logger.trace("probe_python_result", {
453
+ sessionId,
454
+ result: null,
455
+ reason: "unexpected_python_status",
456
+ status: data.status,
457
+ });
255
458
  return null;
256
459
  }
257
- catch {
460
+ catch (error) {
258
461
  // Python API probe failed, fall back to command-line tools
462
+ this.logger.trace("probe_fallback", {
463
+ sessionId,
464
+ from: "python",
465
+ to: "cli",
466
+ reason: "python_probe_error",
467
+ error,
468
+ });
259
469
  return null;
260
470
  }
261
471
  }
262
- async checkWithCommandLineTools(sessionId) {
472
+ async checkWithCommandLineTools(sessionId, targetId) {
263
473
  let it2api;
264
474
  try {
265
475
  it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
266
476
  }
267
- catch {
477
+ catch (error) {
478
+ this.logger.trace("probe_fallback", {
479
+ targetId,
480
+ from: "cli",
481
+ to: null,
482
+ reason: "it2api_unavailable",
483
+ error,
484
+ });
268
485
  return { presence: "unknown", busy: "unknown" };
269
486
  }
270
487
  const presence = await this.checkSessionPresence(it2api, sessionId);
@@ -282,12 +499,27 @@ class Iterm2TerminalStateProbe {
282
499
  return { presence: "available", busy: "unknown" };
283
500
  }
284
501
  if (first !== second) {
502
+ this.logger.trace("probe_cli_result", {
503
+ sessionId,
504
+ result: { presence: "available", busy: "busy" },
505
+ reason: "buffer_changed",
506
+ });
285
507
  return { presence: "available", busy: "busy" };
286
508
  }
287
509
  if (containsActiveBufferMarker(second)) {
510
+ this.logger.trace("probe_cli_result", {
511
+ sessionId,
512
+ result: { presence: "available", busy: "busy" },
513
+ reason: "active_buffer_marker",
514
+ });
288
515
  return { presence: "available", busy: "busy" };
289
516
  }
290
517
  // Do NOT use generic typing prompts to avoid false positives (see #116)
518
+ this.logger.trace("probe_cli_result", {
519
+ sessionId,
520
+ result: { presence: "available", busy: "unknown" },
521
+ reason: "stable_quiet_buffer",
522
+ });
291
523
  return { presence: "available", busy: "unknown" };
292
524
  }
293
525
  async checkSessionPresence(it2api, sessionId) {
@@ -296,7 +528,13 @@ class Iterm2TerminalStateProbe {
296
528
  const sessionIds = parseIterm2SessionIds(result.stdout);
297
529
  return sessionIds.includes(sessionId) ? "available" : "gone";
298
530
  }
299
- catch {
531
+ catch (error) {
532
+ this.logger.trace("probe_error", {
533
+ sessionId,
534
+ step: "list_sessions",
535
+ error,
536
+ result: "unknown",
537
+ });
300
538
  return "unknown";
301
539
  }
302
540
  }
@@ -322,9 +560,11 @@ exports.Iterm2TerminalStateProbe = Iterm2TerminalStateProbe;
322
560
  class TmuxTerminalStateProbe {
323
561
  execAsync;
324
562
  options;
325
- constructor(execAsync = execFileAsync, options = {}) {
563
+ logger;
564
+ constructor(execAsync = execFileAsync, options = {}, logger = new logging_1.NoopLogger()) {
326
565
  this.execAsync = execAsync;
327
566
  this.options = options;
567
+ this.logger = logger;
328
568
  }
329
569
  supports(target) {
330
570
  return target.backend === "tmux" && typeof target.tmuxPaneId === "string" && target.tmuxPaneId.trim().length > 0;
@@ -332,16 +572,39 @@ class TmuxTerminalStateProbe {
332
572
  async check(target) {
333
573
  const paneId = target.tmuxPaneId?.trim();
334
574
  if (!paneId) {
575
+ this.logger.trace("probe_result", {
576
+ targetId: target.targetId,
577
+ result: { presence: "unknown", busy: "unknown" },
578
+ reason: "missing_tmux_pane_id",
579
+ });
335
580
  return { presence: "unknown", busy: "unknown" };
336
581
  }
337
582
  const paneState = await this.readPaneState(paneId);
338
583
  if (paneState === "missing") {
584
+ this.logger.trace("probe_result", {
585
+ targetId: target.targetId,
586
+ paneId,
587
+ result: { presence: "gone", busy: "unknown" },
588
+ reason: "pane_missing",
589
+ });
339
590
  return { presence: "gone", busy: "unknown" };
340
591
  }
341
592
  if (paneState === "unknown") {
593
+ this.logger.trace("probe_result", {
594
+ targetId: target.targetId,
595
+ paneId,
596
+ result: { presence: "unknown", busy: "unknown" },
597
+ reason: "pane_state_unknown",
598
+ });
342
599
  return { presence: "unknown", busy: "unknown" };
343
600
  }
344
601
  if (paneState.dead) {
602
+ this.logger.trace("probe_result", {
603
+ targetId: target.targetId,
604
+ paneId,
605
+ result: { presence: "gone", busy: "unknown" },
606
+ reason: "pane_dead",
607
+ });
345
608
  return { presence: "gone", busy: "unknown" };
346
609
  }
347
610
  const first = await this.readBufferTail(paneId);
@@ -354,17 +617,41 @@ class TmuxTerminalStateProbe {
354
617
  return { presence: "available", busy: "unknown" };
355
618
  }
356
619
  if (first !== second) {
620
+ this.logger.trace("probe_result", {
621
+ targetId: target.targetId,
622
+ paneId,
623
+ result: { presence: "available", busy: "busy" },
624
+ reason: "buffer_changed",
625
+ });
357
626
  return { presence: "available", busy: "busy" };
358
627
  }
359
628
  if (containsActiveBufferMarker(second)) {
629
+ this.logger.trace("probe_result", {
630
+ targetId: target.targetId,
631
+ paneId,
632
+ result: { presence: "available", busy: "busy" },
633
+ reason: "active_buffer_marker",
634
+ });
360
635
  return { presence: "available", busy: "busy" };
361
636
  }
362
637
  // Use cursor-aware detection when available
363
638
  const cursorHint = evaluateCursorAwareTypingPrompt(target, paneState, second);
364
639
  if (cursorHint === "busy") {
640
+ this.logger.trace("probe_result", {
641
+ targetId: target.targetId,
642
+ paneId,
643
+ result: { presence: "available", busy: "busy" },
644
+ reason: "cursor_aware_busy",
645
+ });
365
646
  return { presence: "available", busy: "busy" };
366
647
  }
367
648
  // Do NOT fall back to generic typing prompts when cursorHint is unknown to avoid false positives (see #116)
649
+ this.logger.trace("probe_result", {
650
+ targetId: target.targetId,
651
+ paneId,
652
+ result: { presence: "available", busy: "unknown" },
653
+ reason: cursorHint === "not_busy" ? "cursor_aware_not_busy" : "cursor_aware_unknown",
654
+ });
368
655
  return { presence: "available", busy: "unknown" };
369
656
  }
370
657
  async readPaneState(paneId) {
@@ -397,7 +684,7 @@ class TmuxTerminalStateProbe {
397
684
  }
398
685
  async readBufferTail(paneId) {
399
686
  try {
400
- const result = await this.execAsync("tmux", ["capture-pane", "-p", "-t", paneId, "-S", "-20"]);
687
+ const result = await this.execAsync("tmux", ["capture-pane", "-p", "-t", paneId, "-S", `-${NORMALIZED_BUFFER_TAIL_LINES}`]);
401
688
  return normalizeBufferTail(result.stdout);
402
689
  }
403
690
  catch {
@@ -481,11 +768,14 @@ function normalizeBufferTail(buffer) {
481
768
  .replace(/\r/g, "")
482
769
  .split("\n")
483
770
  .map((line) => line.trimEnd())
484
- .slice(-20)
771
+ .slice(-NORMALIZED_BUFFER_TAIL_LINES)
485
772
  .join("\n")
486
773
  .trim();
487
774
  return normalized.length > 0 ? normalized : null;
488
775
  }
776
+ function normalizeProbeTailFromLines(lines) {
777
+ return normalizeBufferTail(lines.join("\n"));
778
+ }
489
779
  function resolveIterm2ApiPath(override) {
490
780
  if (override) {
491
781
  return override;
@@ -622,15 +912,18 @@ function isTmuxMissingPaneError(error) {
622
912
  class ClaudeCodeRuntimePresenceProbe {
623
913
  killFn;
624
914
  sessionFileReader;
625
- constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader) {
915
+ logger;
916
+ constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader, logger = new logging_1.NoopLogger()) {
626
917
  this.killFn = killFn;
627
918
  this.sessionFileReader = sessionFileReader;
919
+ this.logger = logger;
628
920
  }
629
921
  supports(target) {
630
922
  return target.runtimeKind === "claude_code" && Number.isInteger(target.runtimePid);
631
923
  }
632
924
  async check(target) {
633
925
  if (!Number.isInteger(target.runtimePid)) {
926
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_pid" });
634
927
  return "unknown";
635
928
  }
636
929
  try {
@@ -639,19 +932,24 @@ class ClaudeCodeRuntimePresenceProbe {
639
932
  // 2. Verify session file exists and is valid
640
933
  const sessionInfo = await (this.sessionFileReader ?? this.defaultSessionFileReader)(target.runtimePid);
641
934
  if (!sessionInfo) {
935
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_file_missing" });
642
936
  return "gone"; // Process exists but session file is invalid/missing
643
937
  }
644
938
  // 3. Verify session ID matches (prevent PID reuse)
645
939
  if (target.runtimeSessionId && sessionInfo.sessionId !== target.runtimeSessionId) {
940
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_id_mismatch" });
646
941
  return "gone";
647
942
  }
943
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "alive" });
648
944
  return "alive";
649
945
  }
650
946
  catch (error) {
651
947
  const code = error instanceof Error && "code" in error ? String(error.code ?? "") : "";
652
948
  if (code === "ESRCH") {
949
+ this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "process_missing" });
653
950
  return "gone"; // Process doesn't exist
654
951
  }
952
+ this.logger.debug("probe_error", { targetId: target.targetId, error, result: "unknown" });
655
953
  return "unknown";
656
954
  }
657
955
  }
@@ -678,26 +976,43 @@ class ClaudeCodeTerminalStateProbe {
678
976
  sessionFileReader;
679
977
  logFileStatReader;
680
978
  sleepFn;
681
- constructor(sessionFileReader, logFileStatReader, sleepFn = sleep) {
979
+ logger;
980
+ constructor(sessionFileReader, logFileStatReader, sleepFn = sleep, logger = new logging_1.NoopLogger()) {
682
981
  this.sessionFileReader = sessionFileReader;
683
982
  this.logFileStatReader = logFileStatReader;
684
983
  this.sleepFn = sleepFn;
984
+ this.logger = logger;
685
985
  }
686
986
  supports(target) {
687
987
  return target.runtimeKind === "claude_code";
688
988
  }
689
989
  async check(target) {
690
990
  if (!target.runtimePid || !target.runtimeSessionId) {
991
+ this.logger.trace("probe_result", {
992
+ targetId: target.targetId,
993
+ result: { presence: "unknown", busy: "unknown" },
994
+ reason: "missing_runtime_identity",
995
+ });
691
996
  return { presence: "unknown", busy: "unknown" };
692
997
  }
693
998
  try {
694
999
  // 1. Read session file to get cwd
695
1000
  const sessionData = await (this.sessionFileReader ?? this.defaultSessionFileReader)(target.runtimePid);
696
1001
  if (!sessionData) {
1002
+ this.logger.trace("probe_result", {
1003
+ targetId: target.targetId,
1004
+ result: { presence: "gone", busy: "unknown" },
1005
+ reason: "session_file_missing",
1006
+ });
697
1007
  return { presence: "gone", busy: "unknown" };
698
1008
  }
699
1009
  // 2. Verify session ID matches
700
1010
  if (sessionData.sessionId !== target.runtimeSessionId) {
1011
+ this.logger.trace("probe_result", {
1012
+ targetId: target.targetId,
1013
+ result: { presence: "gone", busy: "unknown" },
1014
+ reason: "session_id_mismatch",
1015
+ });
701
1016
  return { presence: "gone", busy: "unknown" };
702
1017
  }
703
1018
  // 3. Construct log file path using Claude Code's encoding rules
@@ -709,10 +1024,18 @@ class ClaudeCodeTerminalStateProbe {
709
1024
  const presence = target.backend === "iterm2" && target.itermSessionId
710
1025
  ? await this.checkIterm2Presence(target.itermSessionId)
711
1026
  : "available";
1027
+ this.logger.trace("probe_result", {
1028
+ targetId: target.targetId,
1029
+ result: { presence, busy },
1030
+ });
712
1031
  return { presence, busy };
713
1032
  }
714
1033
  catch (error) {
715
- console.warn('Claude Code state check failed:', error);
1034
+ this.logger.debug("probe_error", {
1035
+ targetId: target.targetId,
1036
+ error,
1037
+ result: { presence: "unknown", busy: "unknown" },
1038
+ });
716
1039
  return { presence: "unknown", busy: "unknown" };
717
1040
  }
718
1041
  }