muuuuse 1.3.2 → 1.4.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 +42 -62
- package/package.json +2 -3
- package/src/cli.js +17 -135
- package/src/runtime.js +208 -419
- package/src/util.js +28 -127
- package/src/agents.js +0 -539
- package/src/tmux.js +0 -170
package/src/runtime.js
CHANGED
|
@@ -2,151 +2,103 @@ const fs = require("node:fs");
|
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const pty = require("node-pty");
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
detectAgentTypeFromCommand,
|
|
7
|
-
expandPresetCommand,
|
|
8
|
-
readClaudeAnswers,
|
|
9
|
-
readCodexAnswers,
|
|
10
|
-
readGeminiAnswers,
|
|
11
|
-
selectClaudeSessionFile,
|
|
12
|
-
selectCodexSessionFile,
|
|
13
|
-
selectGeminiSessionFile,
|
|
14
|
-
} = require("./agents");
|
|
15
5
|
const {
|
|
16
6
|
BRAND,
|
|
17
7
|
POLL_MS,
|
|
18
8
|
appendJsonl,
|
|
19
9
|
createId,
|
|
10
|
+
ensureDir,
|
|
20
11
|
getDefaultSessionName,
|
|
21
12
|
getFileSize,
|
|
22
13
|
getSeatPaths,
|
|
23
14
|
getStateRoot,
|
|
24
15
|
hashText,
|
|
25
16
|
isPidAlive,
|
|
17
|
+
listSessionNames,
|
|
26
18
|
readAppendedText,
|
|
27
19
|
readJson,
|
|
28
|
-
resetDir,
|
|
29
20
|
sanitizeRelayText,
|
|
30
21
|
sleep,
|
|
31
22
|
writeJson,
|
|
32
23
|
} = require("./util");
|
|
33
24
|
|
|
34
|
-
const
|
|
25
|
+
const BELL = "\u0007";
|
|
26
|
+
const CTRL_C = "\u0003";
|
|
35
27
|
|
|
36
|
-
function
|
|
37
|
-
|
|
28
|
+
function resolveShell() {
|
|
29
|
+
const shell = String(process.env.SHELL || "").trim();
|
|
30
|
+
return shell || "/bin/bash";
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
33
|
+
function resolveShellArgs(shellPath) {
|
|
34
|
+
const base = path.basename(shellPath);
|
|
35
|
+
if (base === "bash" || base === "zsh") {
|
|
36
|
+
return ["-l"];
|
|
44
37
|
}
|
|
45
|
-
return
|
|
38
|
+
return [];
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return token;
|
|
53
|
-
}
|
|
54
|
-
return JSON.stringify(token);
|
|
55
|
-
})
|
|
56
|
-
.join(" ");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function previewText(text, maxLength = 88) {
|
|
60
|
-
const compact = sanitizeRelayText(text).replace(/\s+/g, " ");
|
|
61
|
-
if (compact.length <= maxLength) {
|
|
62
|
-
return compact;
|
|
41
|
+
function resolveChildTerm() {
|
|
42
|
+
const inherited = String(process.env.TERM || "").trim();
|
|
43
|
+
if (inherited && inherited.toLowerCase() !== "dumb") {
|
|
44
|
+
return inherited;
|
|
63
45
|
}
|
|
64
|
-
return
|
|
46
|
+
return "xterm-256color";
|
|
65
47
|
}
|
|
66
48
|
|
|
67
|
-
function
|
|
68
|
-
return
|
|
69
|
-
.split("\n")
|
|
70
|
-
.map((line) => line.trim())
|
|
71
|
-
.filter((line) => line.length > 0)
|
|
72
|
-
.map((line) => {
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(line);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
49
|
+
function resolveSessionName(currentPath = process.cwd()) {
|
|
50
|
+
return getDefaultSessionName(currentPath);
|
|
80
51
|
}
|
|
81
52
|
|
|
82
|
-
|
|
83
|
-
if (agentType === "codex") {
|
|
84
|
-
return selectCodexSessionFile(currentPath, processStartedAtMs, options);
|
|
85
|
-
}
|
|
86
|
-
if (agentType === "claude") {
|
|
87
|
-
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
88
|
-
}
|
|
89
|
-
if (agentType === "gemini") {
|
|
90
|
-
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
class GenericAnswerTracker {
|
|
53
|
+
class BellRelayTracker {
|
|
96
54
|
constructor() {
|
|
97
55
|
this.active = false;
|
|
98
56
|
this.buffer = "";
|
|
99
57
|
this.lastInputText = "";
|
|
100
|
-
this.lastOutputAt = 0;
|
|
101
58
|
this.lastFingerprint = null;
|
|
102
59
|
}
|
|
103
60
|
|
|
104
61
|
noteTurnStart(inputText = "") {
|
|
105
62
|
this.active = true;
|
|
106
63
|
this.buffer = "";
|
|
107
|
-
this.lastInputText = sanitizeRelayText(inputText);
|
|
108
|
-
this.lastOutputAt = 0;
|
|
64
|
+
this.lastInputText = sanitizeRelayText(inputText, 12000);
|
|
109
65
|
}
|
|
110
66
|
|
|
111
67
|
append(data) {
|
|
112
|
-
|
|
113
|
-
|
|
68
|
+
const text = String(data || "");
|
|
69
|
+
if (!text || !this.active) {
|
|
70
|
+
return [];
|
|
114
71
|
}
|
|
115
72
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const text = extractGenericAnswer(this.buffer, this.lastInputText);
|
|
134
|
-
if (!text) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
73
|
+
const answers = [];
|
|
74
|
+
for (const char of text) {
|
|
75
|
+
if (char === BELL) {
|
|
76
|
+
const answer = extractFinalBlock(this.buffer, this.lastInputText);
|
|
77
|
+
this.buffer = "";
|
|
78
|
+
this.active = false;
|
|
79
|
+
if (!answer) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const fingerprint = hashText(`${this.lastInputText}\n${answer}`);
|
|
83
|
+
if (fingerprint === this.lastFingerprint) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.lastFingerprint = fingerprint;
|
|
87
|
+
answers.push(answer);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
137
90
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
91
|
+
this.buffer += char;
|
|
92
|
+
if (this.buffer.length > 24000) {
|
|
93
|
+
this.buffer = this.buffer.slice(-24000);
|
|
94
|
+
}
|
|
141
95
|
}
|
|
142
96
|
|
|
143
|
-
|
|
144
|
-
this.active = false;
|
|
145
|
-
return text;
|
|
97
|
+
return answers;
|
|
146
98
|
}
|
|
147
99
|
}
|
|
148
100
|
|
|
149
|
-
function
|
|
101
|
+
function extractFinalBlock(rawText, lastInputText) {
|
|
150
102
|
let candidate = sanitizeRelayText(rawText, 12000);
|
|
151
103
|
if (!candidate) {
|
|
152
104
|
return null;
|
|
@@ -161,15 +113,10 @@ function extractGenericAnswer(rawText, lastInputText) {
|
|
|
161
113
|
}
|
|
162
114
|
}
|
|
163
115
|
|
|
164
|
-
const markerAnswer = extractMarkedAnswer(candidate);
|
|
165
|
-
if (markerAnswer) {
|
|
166
|
-
return markerAnswer;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
116
|
const blocks = candidate
|
|
170
117
|
.split(/\n{2,}/)
|
|
171
118
|
.map((block) => block.trim())
|
|
172
|
-
.filter(
|
|
119
|
+
.filter(Boolean);
|
|
173
120
|
|
|
174
121
|
if (blocks.length === 0) {
|
|
175
122
|
return null;
|
|
@@ -178,51 +125,42 @@ function extractGenericAnswer(rawText, lastInputText) {
|
|
|
178
125
|
return sanitizeRelayText(blocks[blocks.length - 1]);
|
|
179
126
|
}
|
|
180
127
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
128
|
+
function parseAnswerEntries(text) {
|
|
129
|
+
return String(text || "")
|
|
130
|
+
.split("\n")
|
|
131
|
+
.map((line) => line.trim())
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.map((line) => {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(line);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
191
141
|
}
|
|
192
142
|
|
|
193
|
-
class
|
|
143
|
+
class ArmedSeat {
|
|
194
144
|
constructor(options) {
|
|
195
145
|
this.seatId = options.seatId;
|
|
196
146
|
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
197
|
-
this.sessionName = options.sessionName;
|
|
198
147
|
this.cwd = options.cwd;
|
|
199
|
-
this.
|
|
200
|
-
this.agentType = detectAgentTypeFromCommand(this.commandTokens);
|
|
201
|
-
this.maxRelays = options.maxRelays;
|
|
202
|
-
|
|
148
|
+
this.sessionName = resolveSessionName(this.cwd);
|
|
203
149
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
204
150
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
205
151
|
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
206
152
|
|
|
207
153
|
this.child = null;
|
|
208
154
|
this.childPid = null;
|
|
155
|
+
this.childPgid = null;
|
|
209
156
|
this.childExit = null;
|
|
210
|
-
this.
|
|
157
|
+
this.startedAt = new Date().toISOString();
|
|
211
158
|
this.relayCount = 0;
|
|
212
|
-
this.
|
|
159
|
+
this.pendingInput = "";
|
|
213
160
|
this.stopped = false;
|
|
214
161
|
this.stdinCleanup = null;
|
|
215
162
|
this.resizeCleanup = null;
|
|
216
|
-
this.
|
|
217
|
-
this.processStartedAtMs = null;
|
|
218
|
-
|
|
219
|
-
this.sessionState = {
|
|
220
|
-
file: null,
|
|
221
|
-
offset: 0,
|
|
222
|
-
lastMessageId: null,
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
this.genericTracker = new GenericAnswerTracker();
|
|
163
|
+
this.tracker = new BellRelayTracker();
|
|
226
164
|
}
|
|
227
165
|
|
|
228
166
|
log(message) {
|
|
@@ -232,15 +170,13 @@ class SeatProcess {
|
|
|
232
170
|
writeMeta(extra = {}) {
|
|
233
171
|
writeJson(this.paths.metaPath, {
|
|
234
172
|
seatId: this.seatId,
|
|
173
|
+
partnerSeatId: this.partnerSeatId,
|
|
235
174
|
sessionName: this.sessionName,
|
|
236
175
|
cwd: this.cwd,
|
|
237
176
|
pid: process.pid,
|
|
238
177
|
childPid: this.childPid,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
command: this.commandTokens,
|
|
242
|
-
commandLine: formatCommand(this.commandTokens),
|
|
243
|
-
startedAt: new Date(this.startedAtMs).toISOString(),
|
|
178
|
+
command: [resolveShell(), ...resolveShellArgs(resolveShell())],
|
|
179
|
+
startedAt: this.startedAt,
|
|
244
180
|
...extra,
|
|
245
181
|
});
|
|
246
182
|
}
|
|
@@ -248,26 +184,54 @@ class SeatProcess {
|
|
|
248
184
|
writeStatus(extra = {}) {
|
|
249
185
|
writeJson(this.paths.statusPath, {
|
|
250
186
|
seatId: this.seatId,
|
|
187
|
+
partnerSeatId: this.partnerSeatId,
|
|
251
188
|
sessionName: this.sessionName,
|
|
252
189
|
cwd: this.cwd,
|
|
253
190
|
pid: process.pid,
|
|
254
191
|
childPid: this.childPid,
|
|
255
|
-
childToken: this.childToken,
|
|
256
|
-
agentType: this.agentType,
|
|
257
|
-
command: this.commandTokens,
|
|
258
192
|
relayCount: this.relayCount,
|
|
259
193
|
updatedAt: new Date().toISOString(),
|
|
260
194
|
...extra,
|
|
261
195
|
});
|
|
262
196
|
}
|
|
263
197
|
|
|
264
|
-
|
|
265
|
-
|
|
198
|
+
launchShell() {
|
|
199
|
+
ensureDir(this.paths.dir);
|
|
200
|
+
fs.rmSync(this.paths.pipePath, { force: true });
|
|
201
|
+
|
|
202
|
+
const shell = resolveShell();
|
|
203
|
+
const shellArgs = resolveShellArgs(shell);
|
|
204
|
+
this.child = pty.spawn(shell, shellArgs, {
|
|
205
|
+
cols: process.stdout.columns || 120,
|
|
206
|
+
rows: process.stdout.rows || 36,
|
|
207
|
+
cwd: this.cwd,
|
|
208
|
+
env: {
|
|
209
|
+
...process.env,
|
|
210
|
+
TERM: resolveChildTerm(),
|
|
211
|
+
MUUUUSE_SEAT: String(this.seatId),
|
|
212
|
+
MUUUUSE_SESSION: this.sessionName,
|
|
213
|
+
},
|
|
214
|
+
name: resolveChildTerm(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.childPid = this.child.pid;
|
|
218
|
+
this.childPgid = this.child.pid;
|
|
219
|
+
this.writeMeta();
|
|
220
|
+
this.writeStatus({ state: "running" });
|
|
221
|
+
|
|
222
|
+
this.child.onData((data) => {
|
|
223
|
+
fs.appendFileSync(this.paths.pipePath, data);
|
|
224
|
+
process.stdout.write(data);
|
|
225
|
+
const answers = this.tracker.append(data);
|
|
226
|
+
for (const answer of answers) {
|
|
227
|
+
this.emitAnswer(answer);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.child.onExit(({ exitCode, signal }) => {
|
|
232
|
+
this.childExit = { exitCode, signal: signal || null };
|
|
266
233
|
this.stopped = true;
|
|
267
|
-
};
|
|
268
|
-
process.once("SIGINT", stop);
|
|
269
|
-
process.once("SIGHUP", stop);
|
|
270
|
-
process.once("SIGTERM", stop);
|
|
234
|
+
});
|
|
271
235
|
}
|
|
272
236
|
|
|
273
237
|
installStdinProxy() {
|
|
@@ -278,10 +242,9 @@ class SeatProcess {
|
|
|
278
242
|
|
|
279
243
|
const text = chunk.toString("utf8");
|
|
280
244
|
this.child.write(text);
|
|
281
|
-
|
|
282
|
-
this.genericTracker.noteTurnStart("");
|
|
283
|
-
}
|
|
245
|
+
this.trackInput(text);
|
|
284
246
|
};
|
|
247
|
+
|
|
285
248
|
const handleEnd = () => {
|
|
286
249
|
this.stopped = true;
|
|
287
250
|
};
|
|
@@ -315,9 +278,9 @@ class SeatProcess {
|
|
|
315
278
|
}
|
|
316
279
|
|
|
317
280
|
try {
|
|
318
|
-
this.child.resize(process.stdout.columns ||
|
|
319
|
-
} catch
|
|
320
|
-
// Ignore resize
|
|
281
|
+
this.child.resize(process.stdout.columns || 120, process.stdout.rows || 36);
|
|
282
|
+
} catch {
|
|
283
|
+
// Ignore resize races while the child is exiting.
|
|
321
284
|
}
|
|
322
285
|
};
|
|
323
286
|
|
|
@@ -327,63 +290,40 @@ class SeatProcess {
|
|
|
327
290
|
};
|
|
328
291
|
}
|
|
329
292
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.child = pty.spawn(file, args, {
|
|
335
|
-
cols: process.stdout.columns || 80,
|
|
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
|
-
});
|
|
293
|
+
installStopSignals() {
|
|
294
|
+
const requestStop = () => {
|
|
295
|
+
this.stopped = true;
|
|
296
|
+
};
|
|
346
297
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
this.writeStatus({
|
|
351
|
-
partnerSeatId: this.partnerSeatId,
|
|
352
|
-
state: "running",
|
|
353
|
-
});
|
|
298
|
+
process.on("SIGTERM", requestStop);
|
|
299
|
+
process.on("SIGHUP", requestStop);
|
|
300
|
+
}
|
|
354
301
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
this.
|
|
302
|
+
trackInput(text) {
|
|
303
|
+
for (const char of String(text || "")) {
|
|
304
|
+
if (char === CTRL_C) {
|
|
305
|
+
this.pendingInput = "";
|
|
306
|
+
this.tracker.noteTurnStart("");
|
|
307
|
+
continue;
|
|
359
308
|
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
this.child.onExit(({ exitCode, signal }) => {
|
|
363
|
-
this.childExit = {
|
|
364
|
-
exitCode,
|
|
365
|
-
signal: signal || null,
|
|
366
|
-
};
|
|
367
|
-
this.stopped = true;
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
309
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
310
|
+
if (char === "\r" || char === "\n") {
|
|
311
|
+
const submitted = sanitizeRelayText(this.pendingInput, 12000);
|
|
312
|
+
this.pendingInput = "";
|
|
313
|
+
this.tracker.noteTurnStart(submitted);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
375
316
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
317
|
+
this.pendingInput += char;
|
|
318
|
+
if (this.pendingInput.length > 4000) {
|
|
319
|
+
this.pendingInput = this.pendingInput.slice(-4000);
|
|
320
|
+
}
|
|
379
321
|
}
|
|
380
|
-
|
|
381
|
-
this.linked = true;
|
|
382
|
-
this.log(`${BRAND} seat ${this.seatId} linked with seat ${this.partnerSeatId} in session ${this.sessionName}.`);
|
|
383
322
|
}
|
|
384
323
|
|
|
385
|
-
|
|
386
|
-
|
|
324
|
+
partnerIsLive() {
|
|
325
|
+
const partner = readJson(this.partnerPaths.statusPath, null);
|
|
326
|
+
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
387
327
|
}
|
|
388
328
|
|
|
389
329
|
pullPartnerEvents() {
|
|
@@ -395,23 +335,12 @@ class SeatProcess {
|
|
|
395
335
|
|
|
396
336
|
const entries = parseAnswerEntries(text);
|
|
397
337
|
for (const entry of entries) {
|
|
398
|
-
if (!this.child) {
|
|
399
|
-
continue;
|
|
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;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
338
|
const payload = sanitizeRelayText(entry.text);
|
|
407
|
-
if (!payload) {
|
|
339
|
+
if (!payload || !this.child) {
|
|
408
340
|
continue;
|
|
409
341
|
}
|
|
410
342
|
|
|
411
|
-
|
|
412
|
-
this.genericTracker.noteTurnStart(payload);
|
|
413
|
-
}
|
|
414
|
-
|
|
343
|
+
this.tracker.noteTurnStart(payload);
|
|
415
344
|
this.child.write(payload.replace(/\n/g, "\r"));
|
|
416
345
|
this.child.write("\r");
|
|
417
346
|
this.relayCount += 1;
|
|
@@ -419,125 +348,40 @@ class SeatProcess {
|
|
|
419
348
|
}
|
|
420
349
|
}
|
|
421
350
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.processStartedAtMs, {
|
|
428
|
-
snapshotEnv: this.agentType === "codex"
|
|
429
|
-
? {
|
|
430
|
-
MUUUUSE_CHILD_TOKEN: this.childToken,
|
|
431
|
-
MUUUUSE_SEAT: String(this.seatId),
|
|
432
|
-
MUUUUSE_SESSION: this.sessionName,
|
|
433
|
-
}
|
|
434
|
-
: null,
|
|
435
|
-
});
|
|
436
|
-
if (!sessionFile) {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
this.sessionState.file = sessionFile;
|
|
441
|
-
if (this.agentType === "gemini") {
|
|
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);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
collectStructuredAnswers() {
|
|
451
|
-
this.resolveStructuredLog();
|
|
452
|
-
if (!this.sessionState.file || !this.agentType) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const answers = [];
|
|
457
|
-
if (this.agentType === "codex") {
|
|
458
|
-
const result = readCodexAnswers(this.sessionState.file, this.sessionState.offset);
|
|
459
|
-
this.sessionState.offset = result.nextOffset;
|
|
460
|
-
answers.push(...result.answers);
|
|
461
|
-
} else if (this.agentType === "claude") {
|
|
462
|
-
const result = readClaudeAnswers(this.sessionState.file, this.sessionState.offset);
|
|
463
|
-
this.sessionState.offset = result.nextOffset;
|
|
464
|
-
answers.push(...result.answers);
|
|
465
|
-
} else if (this.agentType === "gemini") {
|
|
466
|
-
const result = readGeminiAnswers(this.sessionState.file, this.sessionState.lastMessageId);
|
|
467
|
-
this.sessionState.lastMessageId = result.lastMessageId;
|
|
468
|
-
this.sessionState.offset = result.fileSize;
|
|
469
|
-
answers.push(...result.answers);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
for (const answer of answers) {
|
|
473
|
-
this.emitAnswer({
|
|
474
|
-
createdAt: answer.timestamp,
|
|
475
|
-
id: answer.id,
|
|
476
|
-
origin: this.agentType,
|
|
477
|
-
text: answer.text,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
collectGenericAnswers() {
|
|
483
|
-
if (!this.shouldUseGenericCapture()) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const text = this.genericTracker.consumeReady();
|
|
488
|
-
if (!text) {
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
this.emitAnswer({
|
|
493
|
-
createdAt: new Date().toISOString(),
|
|
494
|
-
id: createId(12),
|
|
495
|
-
origin: "generic",
|
|
496
|
-
text,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
emitAnswer(entry) {
|
|
501
|
-
const text = sanitizeRelayText(entry.text);
|
|
502
|
-
if (!text) {
|
|
351
|
+
emitAnswer(text) {
|
|
352
|
+
const payload = sanitizeRelayText(text);
|
|
353
|
+
if (!payload) {
|
|
503
354
|
return;
|
|
504
355
|
}
|
|
505
356
|
|
|
506
357
|
appendJsonl(this.paths.eventsPath, {
|
|
507
|
-
id:
|
|
358
|
+
id: createId(12),
|
|
508
359
|
type: "answer",
|
|
509
360
|
seatId: this.seatId,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
createdAt: entry.createdAt || new Date().toISOString(),
|
|
361
|
+
text: payload,
|
|
362
|
+
createdAt: new Date().toISOString(),
|
|
513
363
|
});
|
|
514
364
|
|
|
515
|
-
this.log(`[${this.seatId}] ${previewText(
|
|
365
|
+
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
516
366
|
}
|
|
517
367
|
|
|
518
368
|
async tick() {
|
|
519
|
-
this.maybeMarkLinked();
|
|
520
369
|
this.pullPartnerEvents();
|
|
521
|
-
this.collectStructuredAnswers();
|
|
522
|
-
this.collectGenericAnswers();
|
|
523
|
-
|
|
524
370
|
this.writeStatus({
|
|
525
|
-
partnerSeatId: this.partnerSeatId,
|
|
526
371
|
partnerLive: this.partnerIsLive(),
|
|
527
372
|
state: this.childExit ? "exited" : "running",
|
|
528
|
-
structuredLog: this.sessionState.file,
|
|
529
373
|
});
|
|
530
374
|
}
|
|
531
375
|
|
|
532
376
|
async run() {
|
|
533
|
-
this.
|
|
534
|
-
this.
|
|
377
|
+
this.installStopSignals();
|
|
378
|
+
this.launchShell();
|
|
535
379
|
this.installStdinProxy();
|
|
536
380
|
this.installResizeHandler();
|
|
537
381
|
|
|
538
|
-
this.log(`${BRAND} seat ${this.seatId}
|
|
539
|
-
this.log(
|
|
540
|
-
this.log(`
|
|
382
|
+
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
383
|
+
this.log("Use this shell normally. When a program rings the terminal bell, the final block relays to the partner seat.");
|
|
384
|
+
this.log("Run `muuuuse stop` from any other shell to stop the loop.");
|
|
541
385
|
|
|
542
386
|
try {
|
|
543
387
|
while (!this.stopped) {
|
|
@@ -561,13 +405,10 @@ class SeatProcess {
|
|
|
561
405
|
this.resizeCleanup = null;
|
|
562
406
|
}
|
|
563
407
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// Ignore races during shutdown.
|
|
569
|
-
}
|
|
570
|
-
}
|
|
408
|
+
stopTrackedSeat({
|
|
409
|
+
childPid: this.childPid,
|
|
410
|
+
wrapperPid: process.pid,
|
|
411
|
+
});
|
|
571
412
|
|
|
572
413
|
this.writeMeta({
|
|
573
414
|
childPid: this.childPid,
|
|
@@ -575,135 +416,83 @@ class SeatProcess {
|
|
|
575
416
|
});
|
|
576
417
|
this.writeStatus({
|
|
577
418
|
childPid: this.childPid,
|
|
578
|
-
exitCode: this.childExit?.exitCode ?? null,
|
|
579
419
|
exitedAt: new Date().toISOString(),
|
|
580
|
-
partnerSeatId: this.partnerSeatId,
|
|
581
420
|
state: "exited",
|
|
582
421
|
});
|
|
583
422
|
}
|
|
584
423
|
}
|
|
585
424
|
|
|
586
|
-
function
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const paths = getSeatPaths(sessionName, seatId);
|
|
591
|
-
const status = readJson(paths.statusPath, null);
|
|
592
|
-
const meta = readJson(paths.metaPath, null);
|
|
593
|
-
const wrapperPid = status?.pid || meta?.pid || null;
|
|
594
|
-
const childPid = status?.childPid || meta?.childPid || null;
|
|
595
|
-
|
|
596
|
-
let wrapperStopped = false;
|
|
597
|
-
let childStopped = false;
|
|
598
|
-
|
|
599
|
-
if (wrapperPid && isPidAlive(wrapperPid)) {
|
|
600
|
-
try {
|
|
601
|
-
process.kill(wrapperPid, "SIGTERM");
|
|
602
|
-
wrapperStopped = true;
|
|
603
|
-
} catch (error) {
|
|
604
|
-
wrapperStopped = false;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (childPid && isPidAlive(childPid)) {
|
|
609
|
-
try {
|
|
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
|
-
});
|
|
425
|
+
function previewText(text, maxLength = 88) {
|
|
426
|
+
const compact = sanitizeRelayText(text).replace(/\s+/g, " ");
|
|
427
|
+
if (compact.length <= maxLength) {
|
|
428
|
+
return compact;
|
|
624
429
|
}
|
|
625
|
-
|
|
626
|
-
return {
|
|
627
|
-
sessionName,
|
|
628
|
-
seats: results,
|
|
629
|
-
};
|
|
430
|
+
return `${compact.slice(0, maxLength - 3)}...`;
|
|
630
431
|
}
|
|
631
432
|
|
|
632
|
-
function
|
|
633
|
-
|
|
433
|
+
function signalPid(pid, signal) {
|
|
434
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isPidAlive(pid)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
634
437
|
try {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
} catch (error) {
|
|
640
|
-
return [];
|
|
438
|
+
process.kill(pid, signal);
|
|
439
|
+
return true;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
641
442
|
}
|
|
642
443
|
}
|
|
643
444
|
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
445
|
+
function stopTrackedSeat({ wrapperPid = null, childPid = null }) {
|
|
446
|
+
const childStopped = signalPid(childPid, "SIGTERM");
|
|
447
|
+
const wrapperStopped = wrapperPid === process.pid ? false : signalPid(wrapperPid, "SIGTERM");
|
|
448
|
+
return { childStopped, wrapperStopped };
|
|
449
|
+
}
|
|
649
450
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
} else {
|
|
660
|
-
seat.wrapperForced = false;
|
|
661
|
-
}
|
|
451
|
+
function stopSession(sessionName) {
|
|
452
|
+
const seats = [1, 2]
|
|
453
|
+
.map((seatId) => {
|
|
454
|
+
const paths = getSeatPaths(sessionName, seatId);
|
|
455
|
+
const status = readJson(paths.statusPath, null);
|
|
456
|
+
const wrapperPid = status?.pid || null;
|
|
457
|
+
const childPid = status?.childPid || null;
|
|
458
|
+
const wrapperLive = isPidAlive(wrapperPid);
|
|
459
|
+
const childLive = isPidAlive(childPid);
|
|
662
460
|
|
|
663
|
-
if (
|
|
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;
|
|
461
|
+
if (!wrapperLive && !childLive) {
|
|
462
|
+
return null;
|
|
672
463
|
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
464
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
465
|
+
const result = stopTrackedSeat({ wrapperPid, childPid });
|
|
466
|
+
writeJson(paths.statusPath, {
|
|
467
|
+
...(status || {}),
|
|
468
|
+
state: "stopping",
|
|
469
|
+
updatedAt: new Date().toISOString(),
|
|
470
|
+
});
|
|
681
471
|
|
|
682
|
-
function readSessionStatus(sessionName) {
|
|
683
|
-
return {
|
|
684
|
-
sessionName,
|
|
685
|
-
seats: [1, 2].map((seatId) => {
|
|
686
|
-
const paths = getSeatPaths(sessionName, seatId);
|
|
687
|
-
const status = readJson(paths.statusPath, null);
|
|
688
472
|
return {
|
|
689
473
|
seatId,
|
|
690
|
-
|
|
474
|
+
wrapperStopped: result.wrapperStopped,
|
|
475
|
+
childStopped: result.childStopped,
|
|
691
476
|
};
|
|
692
|
-
})
|
|
693
|
-
|
|
477
|
+
})
|
|
478
|
+
.filter(Boolean);
|
|
479
|
+
|
|
480
|
+
if (seats.length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { sessionName, seats };
|
|
694
485
|
}
|
|
695
486
|
|
|
696
|
-
function
|
|
697
|
-
|
|
487
|
+
function stopAllSessions() {
|
|
488
|
+
const sessions = listSessionNames()
|
|
489
|
+
.map((sessionName) => stopSession(sessionName))
|
|
490
|
+
.filter(Boolean);
|
|
491
|
+
return { sessions };
|
|
698
492
|
}
|
|
699
493
|
|
|
700
494
|
module.exports = {
|
|
701
|
-
|
|
702
|
-
formatCommand,
|
|
703
|
-
readAllSessionStatuses,
|
|
704
|
-
readSessionStatus,
|
|
705
|
-
resolveProgramTokens,
|
|
495
|
+
ArmedSeat,
|
|
706
496
|
resolveSessionName,
|
|
707
|
-
|
|
708
|
-
stopSessions,
|
|
497
|
+
stopAllSessions,
|
|
709
498
|
};
|