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