@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.
- package/dist/src/adapters.js +6 -6
- package/dist/src/cli.js +18 -5
- package/dist/src/daemon.js +36 -2
- package/dist/src/http.js +1 -0
- package/dist/src/logging.js +112 -0
- package/dist/src/paths.js +1 -0
- package/dist/src/runtime_gate.js +360 -37
- package/dist/src/service.js +60 -3
- package/dist/src/source_resolution.js +20 -20
- package/dist/src/source_schema.js +2 -2
- package/dist/src/sources/remote.js +23 -23
- package/dist/src/sources/remote_modules.js +418 -0
- package/dist/src/sources/remote_profiles.js +9 -414
- package/package.json +1 -1
- package/scripts/iterm2_cursor_probe.py +8 -3
package/dist/src/runtime_gate.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
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(-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|