muuuuse 1.4.0 → 1.4.2
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 +25 -38
- package/package.json +2 -2
- package/src/agents.js +391 -0
- package/src/cli.js +51 -5
- package/src/runtime.js +603 -157
- package/src/util.js +22 -3
package/src/runtime.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
|
-
const
|
|
2
|
+
const { execFileSync } = require("node:child_process");
|
|
3
3
|
const pty = require("node-pty");
|
|
4
4
|
|
|
5
|
+
const {
|
|
6
|
+
detectAgent,
|
|
7
|
+
readClaudeAnswers,
|
|
8
|
+
readCodexAnswers,
|
|
9
|
+
readGeminiAnswers,
|
|
10
|
+
selectClaudeSessionFile,
|
|
11
|
+
selectCodexSessionFile,
|
|
12
|
+
selectGeminiSessionFile,
|
|
13
|
+
} = require("./agents");
|
|
5
14
|
const {
|
|
6
15
|
BRAND,
|
|
7
16
|
POLL_MS,
|
|
@@ -11,7 +20,7 @@ const {
|
|
|
11
20
|
getDefaultSessionName,
|
|
12
21
|
getFileSize,
|
|
13
22
|
getSeatPaths,
|
|
14
|
-
|
|
23
|
+
getSessionPaths,
|
|
15
24
|
hashText,
|
|
16
25
|
isPidAlive,
|
|
17
26
|
listSessionNames,
|
|
@@ -22,8 +31,10 @@ const {
|
|
|
22
31
|
writeJson,
|
|
23
32
|
} = require("./util");
|
|
24
33
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
34
|
+
const TYPE_DELAY_MS = 70;
|
|
35
|
+
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
36
|
+
const MAX_RECENT_INBOUND_RELAYS = 12;
|
|
37
|
+
const STOP_FORCE_KILL_MS = 1200;
|
|
27
38
|
|
|
28
39
|
function resolveShell() {
|
|
29
40
|
const shell = String(process.env.SHELL || "").trim();
|
|
@@ -31,9 +42,9 @@ function resolveShell() {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
function resolveShellArgs(shellPath) {
|
|
34
|
-
const base =
|
|
35
|
-
if (base === "bash" || base === "zsh") {
|
|
36
|
-
return ["-
|
|
45
|
+
const base = shellPath.split("/").pop();
|
|
46
|
+
if (base === "bash" || base === "zsh" || base === "fish") {
|
|
47
|
+
return ["-i"];
|
|
37
48
|
}
|
|
38
49
|
return [];
|
|
39
50
|
}
|
|
@@ -50,94 +61,147 @@ function resolveSessionName(currentPath = process.cwd()) {
|
|
|
50
61
|
return getDefaultSessionName(currentPath);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
function parseAnswerEntries(text) {
|
|
65
|
+
return String(text || "")
|
|
66
|
+
.split("\n")
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map((line) => {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(line);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readProcessCwd(pid) {
|
|
80
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
81
|
+
return null;
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
try {
|
|
85
|
+
return fs.realpathSync(`/proc/${pid}/cwd`);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
65
88
|
}
|
|
89
|
+
}
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
91
|
+
function getChildProcesses(rootPid) {
|
|
92
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
try {
|
|
97
|
+
const output = execFileSync("ps", ["-axo", "pid=,ppid=,etimes=,command="], {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const processes = output
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((line) => line.trim())
|
|
104
|
+
.filter((line) => line.length > 0)
|
|
105
|
+
.map((line) => {
|
|
106
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
|
|
107
|
+
if (!match) {
|
|
108
|
+
return null;
|
|
81
109
|
}
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
pid: Number.parseInt(match[1], 10),
|
|
113
|
+
ppid: Number.parseInt(match[2], 10),
|
|
114
|
+
elapsedSeconds: Number.parseInt(match[3], 10),
|
|
115
|
+
args: match[4],
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.filter((entry) => entry !== null);
|
|
119
|
+
|
|
120
|
+
const descendants = [];
|
|
121
|
+
const queue = [rootPid];
|
|
122
|
+
const seen = new Set(queue);
|
|
123
|
+
|
|
124
|
+
while (queue.length > 0) {
|
|
125
|
+
const parentPid = queue.shift();
|
|
126
|
+
for (const process of processes) {
|
|
127
|
+
if (process.ppid !== parentPid || seen.has(process.pid)) {
|
|
84
128
|
continue;
|
|
85
129
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (this.buffer.length > 24000) {
|
|
93
|
-
this.buffer = this.buffer.slice(-24000);
|
|
130
|
+
seen.add(process.pid);
|
|
131
|
+
queue.push(process.pid);
|
|
132
|
+
descendants.push({
|
|
133
|
+
...process,
|
|
134
|
+
cwd: readProcessCwd(process.pid),
|
|
135
|
+
});
|
|
94
136
|
}
|
|
95
137
|
}
|
|
96
138
|
|
|
97
|
-
return
|
|
139
|
+
return descendants.sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
98
142
|
}
|
|
99
143
|
}
|
|
100
144
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
if (!candidate) {
|
|
145
|
+
function resolveSessionFile(agentType, currentPath, processStartedAtMs) {
|
|
146
|
+
if (!currentPath) {
|
|
104
147
|
return null;
|
|
105
148
|
}
|
|
106
149
|
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
if (candidate.startsWith(`${lastInputText}\n`)) {
|
|
112
|
-
candidate = candidate.slice(lastInputText.length).trim();
|
|
113
|
-
}
|
|
150
|
+
if (agentType === "codex") {
|
|
151
|
+
return selectCodexSessionFile(currentPath, processStartedAtMs);
|
|
114
152
|
}
|
|
153
|
+
if (agentType === "claude") {
|
|
154
|
+
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
155
|
+
}
|
|
156
|
+
if (agentType === "gemini") {
|
|
157
|
+
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.map((block) => block.trim())
|
|
119
|
-
.filter(Boolean);
|
|
162
|
+
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
163
|
+
const lines = String(text || "").replace(/\r/g, "").split("\n");
|
|
120
164
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
166
|
+
if (shouldAbort() || !child) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
124
169
|
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
const line = lines[index];
|
|
171
|
+
if (line.length > 0) {
|
|
172
|
+
try {
|
|
173
|
+
child.write(line);
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
await sleep(TYPE_DELAY_MS);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (index < lines.length - 1) {
|
|
181
|
+
if (shouldAbort()) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
127
184
|
|
|
128
|
-
function parseAnswerEntries(text) {
|
|
129
|
-
return String(text || "")
|
|
130
|
-
.split("\n")
|
|
131
|
-
.map((line) => line.trim())
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
.map((line) => {
|
|
134
185
|
try {
|
|
135
|
-
|
|
186
|
+
child.write("\r");
|
|
136
187
|
} catch {
|
|
137
|
-
return
|
|
188
|
+
return false;
|
|
138
189
|
}
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
await sleep(TYPE_DELAY_MS);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (shouldAbort() || !child) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
child.write("\r");
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return true;
|
|
141
205
|
}
|
|
142
206
|
|
|
143
207
|
class ArmedSeat {
|
|
@@ -146,21 +210,34 @@ class ArmedSeat {
|
|
|
146
210
|
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
147
211
|
this.cwd = options.cwd;
|
|
148
212
|
this.sessionName = resolveSessionName(this.cwd);
|
|
213
|
+
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
149
214
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
150
215
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
151
216
|
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
152
217
|
|
|
153
218
|
this.child = null;
|
|
154
219
|
this.childPid = null;
|
|
155
|
-
this.childPgid = null;
|
|
156
220
|
this.childExit = null;
|
|
157
221
|
this.startedAt = new Date().toISOString();
|
|
222
|
+
this.startedAtMs = Date.now();
|
|
158
223
|
this.relayCount = 0;
|
|
159
|
-
this.pendingInput = "";
|
|
160
224
|
this.stopped = false;
|
|
225
|
+
this.stopReason = null;
|
|
161
226
|
this.stdinCleanup = null;
|
|
162
227
|
this.resizeCleanup = null;
|
|
163
|
-
this.
|
|
228
|
+
this.forceKillTimer = null;
|
|
229
|
+
this.recentInboundRelays = [];
|
|
230
|
+
this.liveState = {
|
|
231
|
+
type: null,
|
|
232
|
+
pid: null,
|
|
233
|
+
currentPath: null,
|
|
234
|
+
sessionFile: null,
|
|
235
|
+
offset: 0,
|
|
236
|
+
lastMessageId: null,
|
|
237
|
+
processStartedAtMs: null,
|
|
238
|
+
captureSinceMs: this.startedAtMs,
|
|
239
|
+
lastAnswerAt: null,
|
|
240
|
+
};
|
|
164
241
|
}
|
|
165
242
|
|
|
166
243
|
log(message) {
|
|
@@ -198,6 +275,7 @@ class ArmedSeat {
|
|
|
198
275
|
launchShell() {
|
|
199
276
|
ensureDir(this.paths.dir);
|
|
200
277
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
278
|
+
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
201
279
|
|
|
202
280
|
const shell = resolveShell();
|
|
203
281
|
const shellArgs = resolveShellArgs(shell);
|
|
@@ -215,16 +293,16 @@ class ArmedSeat {
|
|
|
215
293
|
});
|
|
216
294
|
|
|
217
295
|
this.childPid = this.child.pid;
|
|
218
|
-
this.childPgid = this.child.pid;
|
|
219
296
|
this.writeMeta();
|
|
220
297
|
this.writeStatus({ state: "running" });
|
|
221
298
|
|
|
222
299
|
this.child.onData((data) => {
|
|
223
300
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
301
|
+
if (!this.stopped) {
|
|
302
|
+
const visibleData = String(data).replace(/\u0007/g, "");
|
|
303
|
+
if (visibleData) {
|
|
304
|
+
process.stdout.write(visibleData);
|
|
305
|
+
}
|
|
228
306
|
}
|
|
229
307
|
});
|
|
230
308
|
|
|
@@ -239,10 +317,7 @@ class ArmedSeat {
|
|
|
239
317
|
if (!this.child) {
|
|
240
318
|
return;
|
|
241
319
|
}
|
|
242
|
-
|
|
243
|
-
const text = chunk.toString("utf8");
|
|
244
|
-
this.child.write(text);
|
|
245
|
-
this.trackInput(text);
|
|
320
|
+
this.child.write(chunk.toString("utf8"));
|
|
246
321
|
};
|
|
247
322
|
|
|
248
323
|
const handleEnd = () => {
|
|
@@ -292,32 +367,59 @@ class ArmedSeat {
|
|
|
292
367
|
|
|
293
368
|
installStopSignals() {
|
|
294
369
|
const requestStop = () => {
|
|
295
|
-
this.
|
|
370
|
+
this.requestStop("signal");
|
|
296
371
|
};
|
|
297
372
|
|
|
298
373
|
process.on("SIGTERM", requestStop);
|
|
299
374
|
process.on("SIGHUP", requestStop);
|
|
300
375
|
}
|
|
301
376
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
377
|
+
requestStop(reason = "stop_requested") {
|
|
378
|
+
if (this.stopped) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.stopped = true;
|
|
383
|
+
this.stopReason = reason;
|
|
384
|
+
|
|
385
|
+
if (this.childPid) {
|
|
386
|
+
signalProcessFamily(this.childPid, "SIGHUP");
|
|
387
|
+
signalProcessFamily(this.childPid, "SIGTERM");
|
|
388
|
+
this.scheduleForcedKill();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (this.child && !this.childExit) {
|
|
392
|
+
try {
|
|
393
|
+
this.child.kill();
|
|
394
|
+
} catch {
|
|
395
|
+
// best effort shutdown
|
|
308
396
|
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
309
399
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
400
|
+
scheduleForcedKill() {
|
|
401
|
+
if (this.forceKillTimer || !this.childPid) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.forceKillTimer = setTimeout(() => {
|
|
406
|
+
this.forceKillTimer = null;
|
|
407
|
+
if (!this.childPid || this.childExit) {
|
|
408
|
+
return;
|
|
315
409
|
}
|
|
316
410
|
|
|
317
|
-
this.
|
|
318
|
-
if (this.
|
|
319
|
-
|
|
411
|
+
signalProcessFamily(this.childPid, "SIGKILL");
|
|
412
|
+
if (this.child && !this.childExit) {
|
|
413
|
+
try {
|
|
414
|
+
this.child.kill();
|
|
415
|
+
} catch {
|
|
416
|
+
// best effort hard shutdown
|
|
417
|
+
}
|
|
320
418
|
}
|
|
419
|
+
}, STOP_FORCE_KILL_MS);
|
|
420
|
+
|
|
421
|
+
if (typeof this.forceKillTimer.unref === "function") {
|
|
422
|
+
this.forceKillTimer.unref();
|
|
321
423
|
}
|
|
322
424
|
}
|
|
323
425
|
|
|
@@ -326,50 +428,271 @@ class ArmedSeat {
|
|
|
326
428
|
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
327
429
|
}
|
|
328
430
|
|
|
329
|
-
|
|
431
|
+
stopRequested() {
|
|
432
|
+
const request = readJson(this.sessionPaths.stopPath, null);
|
|
433
|
+
if (!request?.requestedAt) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const requestedAtMs = Date.parse(request.requestedAt);
|
|
438
|
+
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async pullPartnerEvents() {
|
|
330
442
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
331
443
|
this.partnerOffset = nextOffset;
|
|
332
|
-
if (!text.trim()) {
|
|
444
|
+
if (!text.trim() || !this.child || this.stopped) {
|
|
333
445
|
return;
|
|
334
446
|
}
|
|
335
447
|
|
|
336
448
|
const entries = parseAnswerEntries(text);
|
|
337
449
|
for (const entry of entries) {
|
|
450
|
+
if (this.stopped || this.stopRequested()) {
|
|
451
|
+
this.requestStop("stop_requested");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
338
455
|
const payload = sanitizeRelayText(entry.text);
|
|
339
|
-
if (!payload
|
|
456
|
+
if (!payload) {
|
|
340
457
|
continue;
|
|
341
458
|
}
|
|
342
459
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
460
|
+
const delivered = await sendTextAndEnter(
|
|
461
|
+
this.child,
|
|
462
|
+
payload,
|
|
463
|
+
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
464
|
+
);
|
|
465
|
+
if (!delivered) {
|
|
466
|
+
this.requestStop("relay_aborted");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (this.stopped || this.stopRequested()) {
|
|
471
|
+
this.requestStop("stop_requested");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
346
475
|
this.relayCount += 1;
|
|
476
|
+
this.rememberInboundRelay(payload);
|
|
347
477
|
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
348
478
|
}
|
|
349
479
|
}
|
|
350
480
|
|
|
351
|
-
|
|
481
|
+
rememberInboundRelay(text) {
|
|
352
482
|
const payload = sanitizeRelayText(text);
|
|
353
483
|
if (!payload) {
|
|
354
484
|
return;
|
|
355
485
|
}
|
|
356
486
|
|
|
487
|
+
const now = Date.now();
|
|
488
|
+
this.pruneRecentInboundRelays(now);
|
|
489
|
+
this.recentInboundRelays.push({
|
|
490
|
+
hash: hashText(payload),
|
|
491
|
+
text: payload,
|
|
492
|
+
timestampMs: now,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (this.recentInboundRelays.length > MAX_RECENT_INBOUND_RELAYS) {
|
|
496
|
+
this.recentInboundRelays = this.recentInboundRelays.slice(-MAX_RECENT_INBOUND_RELAYS);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
pruneRecentInboundRelays(now = Date.now()) {
|
|
501
|
+
this.recentInboundRelays = this.recentInboundRelays.filter(
|
|
502
|
+
(entry) => now - entry.timestampMs <= MIRROR_SUPPRESSION_WINDOW_MS
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
takeMirroredInboundRelay(payload) {
|
|
507
|
+
const normalized = sanitizeRelayText(payload);
|
|
508
|
+
if (!normalized) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
this.pruneRecentInboundRelays();
|
|
513
|
+
const payloadHash = hashText(normalized);
|
|
514
|
+
const matchIndex = this.recentInboundRelays.findIndex((entry) => entry.hash === payloadHash);
|
|
515
|
+
if (matchIndex === -1) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const [match] = this.recentInboundRelays.splice(matchIndex, 1);
|
|
520
|
+
return match;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
collectLiveAnswers() {
|
|
524
|
+
const detectedAgent = detectAgent(getChildProcesses(this.childPid));
|
|
525
|
+
if (!detectedAgent) {
|
|
526
|
+
this.liveState = {
|
|
527
|
+
type: null,
|
|
528
|
+
pid: null,
|
|
529
|
+
currentPath: null,
|
|
530
|
+
sessionFile: null,
|
|
531
|
+
offset: 0,
|
|
532
|
+
lastMessageId: null,
|
|
533
|
+
processStartedAtMs: null,
|
|
534
|
+
captureSinceMs: this.startedAtMs,
|
|
535
|
+
lastAnswerAt: null,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
state: this.childExit ? "exited" : "running",
|
|
540
|
+
agent: null,
|
|
541
|
+
cwd: this.cwd,
|
|
542
|
+
log: null,
|
|
543
|
+
lastAnswerAt: null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const currentPath = detectedAgent.cwd || this.cwd;
|
|
548
|
+
const changed =
|
|
549
|
+
this.liveState.type !== detectedAgent.type ||
|
|
550
|
+
this.liveState.pid !== detectedAgent.pid ||
|
|
551
|
+
this.liveState.currentPath !== currentPath;
|
|
552
|
+
|
|
553
|
+
if (changed) {
|
|
554
|
+
this.liveState = {
|
|
555
|
+
type: detectedAgent.type,
|
|
556
|
+
pid: detectedAgent.pid,
|
|
557
|
+
currentPath,
|
|
558
|
+
sessionFile: null,
|
|
559
|
+
offset: 0,
|
|
560
|
+
lastMessageId: null,
|
|
561
|
+
processStartedAtMs: detectedAgent.processStartedAtMs,
|
|
562
|
+
captureSinceMs: Math.max(
|
|
563
|
+
this.startedAtMs,
|
|
564
|
+
Number.isFinite(detectedAgent.processStartedAtMs) ? detectedAgent.processStartedAtMs : 0
|
|
565
|
+
),
|
|
566
|
+
lastAnswerAt: null,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!this.liveState.sessionFile) {
|
|
571
|
+
this.liveState.sessionFile = resolveSessionFile(
|
|
572
|
+
detectedAgent.type,
|
|
573
|
+
currentPath,
|
|
574
|
+
detectedAgent.processStartedAtMs
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
if (this.liveState.sessionFile) {
|
|
578
|
+
this.liveState.offset = 0;
|
|
579
|
+
this.liveState.lastMessageId = null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!this.liveState.sessionFile) {
|
|
584
|
+
return {
|
|
585
|
+
state: "running",
|
|
586
|
+
agent: detectedAgent.type,
|
|
587
|
+
cwd: currentPath,
|
|
588
|
+
log: "waiting_for_session_log",
|
|
589
|
+
lastAnswerAt: this.liveState.lastAnswerAt,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const answers = [];
|
|
594
|
+
if (detectedAgent.type === "codex") {
|
|
595
|
+
const result = readCodexAnswers(
|
|
596
|
+
this.liveState.sessionFile,
|
|
597
|
+
this.liveState.offset,
|
|
598
|
+
this.liveState.captureSinceMs
|
|
599
|
+
);
|
|
600
|
+
this.liveState.offset = result.nextOffset;
|
|
601
|
+
answers.push(...result.answers);
|
|
602
|
+
} else if (detectedAgent.type === "claude") {
|
|
603
|
+
const result = readClaudeAnswers(
|
|
604
|
+
this.liveState.sessionFile,
|
|
605
|
+
this.liveState.offset,
|
|
606
|
+
this.liveState.captureSinceMs
|
|
607
|
+
);
|
|
608
|
+
this.liveState.offset = result.nextOffset;
|
|
609
|
+
answers.push(...result.answers);
|
|
610
|
+
} else if (detectedAgent.type === "gemini") {
|
|
611
|
+
const result = readGeminiAnswers(
|
|
612
|
+
this.liveState.sessionFile,
|
|
613
|
+
this.liveState.lastMessageId,
|
|
614
|
+
this.liveState.captureSinceMs
|
|
615
|
+
);
|
|
616
|
+
this.liveState.lastMessageId = result.lastMessageId;
|
|
617
|
+
this.liveState.offset = result.fileSize;
|
|
618
|
+
answers.push(...result.answers);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
for (const answer of answers) {
|
|
622
|
+
this.emitAnswer({
|
|
623
|
+
id: answer.id || createId(12),
|
|
624
|
+
origin: detectedAgent.type,
|
|
625
|
+
text: answer.text,
|
|
626
|
+
createdAt: answer.timestamp || new Date().toISOString(),
|
|
627
|
+
});
|
|
628
|
+
this.liveState.lastAnswerAt = answer.timestamp || new Date().toISOString();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
state: "running",
|
|
633
|
+
agent: detectedAgent.type,
|
|
634
|
+
cwd: currentPath,
|
|
635
|
+
log: this.liveState.sessionFile,
|
|
636
|
+
lastAnswerAt: this.liveState.lastAnswerAt,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
emitAnswer(entry) {
|
|
641
|
+
if (this.stopped) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const payload = sanitizeRelayText(entry.text);
|
|
646
|
+
if (!payload) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const mirroredInbound = this.takeMirroredInboundRelay(payload);
|
|
651
|
+
if (mirroredInbound) {
|
|
652
|
+
this.log(`[${this.seatId}] suppressed mirrored relay: ${previewText(payload)}`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
357
656
|
appendJsonl(this.paths.eventsPath, {
|
|
358
|
-
id: createId(12),
|
|
657
|
+
id: entry.id || createId(12),
|
|
359
658
|
type: "answer",
|
|
360
659
|
seatId: this.seatId,
|
|
660
|
+
origin: entry.origin || "unknown",
|
|
361
661
|
text: payload,
|
|
362
|
-
createdAt: new Date().toISOString(),
|
|
662
|
+
createdAt: entry.createdAt || new Date().toISOString(),
|
|
363
663
|
});
|
|
364
664
|
|
|
365
665
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
366
666
|
}
|
|
367
667
|
|
|
368
668
|
async tick() {
|
|
369
|
-
this.
|
|
669
|
+
if (this.stopRequested()) {
|
|
670
|
+
this.writeStatus({
|
|
671
|
+
state: "stopping",
|
|
672
|
+
partnerLive: this.partnerIsLive(),
|
|
673
|
+
});
|
|
674
|
+
this.requestStop("stop_requested");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
await this.pullPartnerEvents();
|
|
679
|
+
if (this.stopped || this.stopRequested()) {
|
|
680
|
+
this.requestStop("stop_requested");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const live = this.collectLiveAnswers();
|
|
685
|
+
if (this.stopped) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
370
689
|
this.writeStatus({
|
|
690
|
+
state: live.state,
|
|
691
|
+
agent: live.agent,
|
|
692
|
+
cwd: live.cwd,
|
|
693
|
+
log: live.log,
|
|
694
|
+
lastAnswerAt: live.lastAnswerAt,
|
|
371
695
|
partnerLive: this.partnerIsLive(),
|
|
372
|
-
state: this.childExit ? "exited" : "running",
|
|
373
696
|
});
|
|
374
697
|
}
|
|
375
698
|
|
|
@@ -380,8 +703,8 @@ class ArmedSeat {
|
|
|
380
703
|
this.installResizeHandler();
|
|
381
704
|
|
|
382
705
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
383
|
-
this.log("Use this shell normally.
|
|
384
|
-
this.log("Run `muuuuse stop` from any
|
|
706
|
+
this.log("Use this shell normally. Codex, Claude, and Gemini final answers relay automatically from their local session logs.");
|
|
707
|
+
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
385
708
|
|
|
386
709
|
try {
|
|
387
710
|
while (!this.stopped) {
|
|
@@ -396,6 +719,11 @@ class ArmedSeat {
|
|
|
396
719
|
}
|
|
397
720
|
|
|
398
721
|
cleanup() {
|
|
722
|
+
if (this.forceKillTimer) {
|
|
723
|
+
clearTimeout(this.forceKillTimer);
|
|
724
|
+
this.forceKillTimer = null;
|
|
725
|
+
}
|
|
726
|
+
|
|
399
727
|
if (this.stdinCleanup) {
|
|
400
728
|
this.stdinCleanup();
|
|
401
729
|
this.stdinCleanup = null;
|
|
@@ -405,10 +733,16 @@ class ArmedSeat {
|
|
|
405
733
|
this.resizeCleanup = null;
|
|
406
734
|
}
|
|
407
735
|
|
|
408
|
-
|
|
409
|
-
childPid
|
|
410
|
-
|
|
411
|
-
|
|
736
|
+
if (this.child && !this.childExit) {
|
|
737
|
+
if (this.childPid && isPidAlive(this.childPid)) {
|
|
738
|
+
signalProcessFamily(this.childPid, "SIGKILL");
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
this.child.kill();
|
|
742
|
+
} catch {
|
|
743
|
+
// best effort
|
|
744
|
+
}
|
|
745
|
+
}
|
|
412
746
|
|
|
413
747
|
this.writeMeta({
|
|
414
748
|
childPid: this.childPid,
|
|
@@ -430,69 +764,181 @@ function previewText(text, maxLength = 88) {
|
|
|
430
764
|
return `${compact.slice(0, maxLength - 3)}...`;
|
|
431
765
|
}
|
|
432
766
|
|
|
433
|
-
function
|
|
434
|
-
|
|
435
|
-
|
|
767
|
+
function buildSeatReport(sessionName, seatId) {
|
|
768
|
+
const paths = getSeatPaths(sessionName, seatId);
|
|
769
|
+
const daemon = readJson(paths.daemonPath, null);
|
|
770
|
+
const status = readJson(paths.statusPath, null);
|
|
771
|
+
const meta = readJson(paths.metaPath, null);
|
|
772
|
+
|
|
773
|
+
if (!status && !meta && !daemon) {
|
|
774
|
+
return null;
|
|
436
775
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
776
|
+
|
|
777
|
+
const legacyTmux = Boolean(daemon?.pid || meta?.paneId);
|
|
778
|
+
const wrapperPid = status?.pid || daemon?.pid || meta?.pid || null;
|
|
779
|
+
const childPid = status?.childPid || meta?.childPid || null;
|
|
780
|
+
const wrapperLive = isPidAlive(wrapperPid);
|
|
781
|
+
const childLive = isPidAlive(childPid);
|
|
782
|
+
|
|
783
|
+
if (!wrapperLive && !childLive) {
|
|
784
|
+
return null;
|
|
442
785
|
}
|
|
443
|
-
}
|
|
444
786
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
787
|
+
return {
|
|
788
|
+
seatId,
|
|
789
|
+
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
790
|
+
wrapperPid,
|
|
791
|
+
childPid,
|
|
792
|
+
wrapperLive,
|
|
793
|
+
childLive,
|
|
794
|
+
legacyTmux,
|
|
795
|
+
agent: status?.agent || null,
|
|
796
|
+
cwd: status?.cwd || meta?.cwd || null,
|
|
797
|
+
relayCount: status?.relayCount || 0,
|
|
798
|
+
log: status?.log || null,
|
|
799
|
+
startedAt: meta?.startedAt || null,
|
|
800
|
+
updatedAt: status?.updatedAt || null,
|
|
801
|
+
lastAnswerAt: status?.lastAnswerAt || null,
|
|
802
|
+
partnerLive: Boolean(status?.partnerLive),
|
|
803
|
+
};
|
|
449
804
|
}
|
|
450
805
|
|
|
451
|
-
function
|
|
452
|
-
const
|
|
453
|
-
.map((
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
806
|
+
function getStatusReport() {
|
|
807
|
+
const sessions = listSessionNames()
|
|
808
|
+
.map((sessionName) => {
|
|
809
|
+
const sessionPaths = getSessionPaths(sessionName);
|
|
810
|
+
const controller = readJson(sessionPaths.controllerPath, null);
|
|
811
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
812
|
+
const seats = [1, 2]
|
|
813
|
+
.map((seatId) => buildSeatReport(sessionName, seatId))
|
|
814
|
+
.filter((entry) => entry !== null);
|
|
815
|
+
|
|
816
|
+
const controllerPid = controller?.pid || null;
|
|
817
|
+
const controllerLive = isPidAlive(controllerPid);
|
|
818
|
+
|
|
819
|
+
if (seats.length === 0 && !controllerLive) {
|
|
462
820
|
return null;
|
|
463
821
|
}
|
|
464
822
|
|
|
465
|
-
const
|
|
466
|
-
writeJson(paths.statusPath, {
|
|
467
|
-
...(status || {}),
|
|
468
|
-
state: "stopping",
|
|
469
|
-
updatedAt: new Date().toISOString(),
|
|
470
|
-
});
|
|
823
|
+
const stopRequestedAt = selectVisibleStopRequest(stopRequest?.requestedAt, seats);
|
|
471
824
|
|
|
472
825
|
return {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
826
|
+
sessionName,
|
|
827
|
+
controllerPid,
|
|
828
|
+
controllerLive,
|
|
829
|
+
stopRequestedAt,
|
|
830
|
+
seats,
|
|
476
831
|
};
|
|
477
832
|
})
|
|
478
|
-
.filter(
|
|
479
|
-
|
|
480
|
-
if (seats.length === 0) {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
833
|
+
.filter((entry) => entry !== null);
|
|
483
834
|
|
|
484
|
-
return {
|
|
835
|
+
return { sessions };
|
|
485
836
|
}
|
|
486
837
|
|
|
487
838
|
function stopAllSessions() {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
839
|
+
const report = getStatusReport();
|
|
840
|
+
const requestedAt = new Date().toISOString();
|
|
841
|
+
|
|
842
|
+
for (const session of report.sessions) {
|
|
843
|
+
const sessionPaths = getSessionPaths(session.sessionName);
|
|
844
|
+
writeJson(sessionPaths.stopPath, {
|
|
845
|
+
requestId: createId(12),
|
|
846
|
+
requestedAt,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
if (session.controllerLive) {
|
|
850
|
+
signalPid(session.controllerPid, "SIGTERM");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
for (const seat of session.seats) {
|
|
854
|
+
if (seat.childLive) {
|
|
855
|
+
signalProcessFamily(seat.childPid, "SIGHUP");
|
|
856
|
+
signalProcessFamily(seat.childPid, "SIGTERM");
|
|
857
|
+
signalProcessFamily(seat.childPid, "SIGKILL");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (seat.wrapperLive) {
|
|
861
|
+
signalPid(seat.wrapperPid, "SIGTERM");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
requestedAt,
|
|
868
|
+
sessions: report.sessions,
|
|
869
|
+
};
|
|
492
870
|
}
|
|
493
871
|
|
|
494
872
|
module.exports = {
|
|
495
873
|
ArmedSeat,
|
|
874
|
+
getStatusReport,
|
|
496
875
|
resolveSessionName,
|
|
497
876
|
stopAllSessions,
|
|
498
877
|
};
|
|
878
|
+
|
|
879
|
+
function signalPid(pid, signal) {
|
|
880
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isPidAlive(pid)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
process.kill(pid, signal);
|
|
886
|
+
return true;
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function signalProcessTree(rootPid, signal) {
|
|
893
|
+
const descendants = getChildProcesses(rootPid);
|
|
894
|
+
let delivered = 0;
|
|
895
|
+
for (const process of descendants) {
|
|
896
|
+
if (signalPid(process.pid, signal)) {
|
|
897
|
+
delivered += 1;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (signalPid(rootPid, signal)) {
|
|
902
|
+
delivered += 1;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return delivered;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function signalProcessFamily(rootPid, signal) {
|
|
909
|
+
return signalProcessTree(rootPid, signal);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function clearStaleStopRequest(stopPath, startedAtMs) {
|
|
913
|
+
const request = readJson(stopPath, null);
|
|
914
|
+
if (!request?.requestedAt) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const requestedAtMs = Date.parse(request.requestedAt);
|
|
919
|
+
if (Number.isFinite(requestedAtMs) && requestedAtMs <= startedAtMs) {
|
|
920
|
+
fs.rmSync(stopPath, { force: true });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function selectVisibleStopRequest(requestedAt, seats) {
|
|
925
|
+
if (!requestedAt) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const requestedAtMs = Date.parse(requestedAt);
|
|
930
|
+
if (!Number.isFinite(requestedAtMs)) {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const newestStartedAtMs = seats
|
|
935
|
+
.map((seat) => Date.parse(seat.startedAt || ""))
|
|
936
|
+
.filter((value) => Number.isFinite(value))
|
|
937
|
+
.sort((left, right) => right - left)[0];
|
|
938
|
+
|
|
939
|
+
if (Number.isFinite(newestStartedAtMs) && requestedAtMs <= newestStartedAtMs) {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return requestedAt;
|
|
944
|
+
}
|