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