muuuuse 0.2.0 → 1.3.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 +43 -55
- package/bin/muuse.js +0 -0
- package/package.json +8 -3
- package/src/agents.js +29 -0
- package/src/cli.js +51 -124
- package/src/runtime.js +437 -498
- package/src/util.js +44 -47
package/src/runtime.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const path = require("node:path");
|
|
3
|
-
const
|
|
3
|
+
const pty = require("node-pty");
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
detectAgentTypeFromCommand,
|
|
7
|
+
expandPresetCommand,
|
|
8
8
|
readClaudeAnswers,
|
|
9
9
|
readCodexAnswers,
|
|
10
10
|
readGeminiAnswers,
|
|
@@ -12,14 +12,12 @@ const {
|
|
|
12
12
|
selectCodexSessionFile,
|
|
13
13
|
selectGeminiSessionFile,
|
|
14
14
|
} = require("./agents");
|
|
15
|
-
const { capturePaneText, getPaneChildProcesses, getPaneInfo, paneExists, sendTextAndEnter, setPaneTitle } = require("./tmux");
|
|
16
15
|
const {
|
|
17
16
|
BRAND,
|
|
18
|
-
CONTROLLER_WAIT_MS,
|
|
19
17
|
POLL_MS,
|
|
20
18
|
appendJsonl,
|
|
21
19
|
createId,
|
|
22
|
-
|
|
20
|
+
getDefaultSessionName,
|
|
23
21
|
getFileSize,
|
|
24
22
|
getSeatPaths,
|
|
25
23
|
hashText,
|
|
@@ -32,553 +30,439 @@ const {
|
|
|
32
30
|
writeJson,
|
|
33
31
|
} = require("./util");
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const daemon = readJson(daemonPath, null);
|
|
38
|
-
if (daemon?.pid && isPidAlive(daemon.pid)) {
|
|
39
|
-
try {
|
|
40
|
-
process.kill(daemon.pid, "SIGTERM");
|
|
41
|
-
} catch (error) {
|
|
42
|
-
// Ignore stale pid races.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
33
|
+
const GENERIC_IDLE_MS = 900;
|
|
34
|
+
const GENERIC_FALLBACK_DELAY_MS = 4000;
|
|
46
35
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
detached: true,
|
|
50
|
-
stdio: "ignore",
|
|
51
|
-
env: process.env,
|
|
52
|
-
});
|
|
53
|
-
child.unref();
|
|
36
|
+
function resolveSessionName(sessionOverride, currentPath = process.cwd()) {
|
|
37
|
+
return sessionOverride || getDefaultSessionName(currentPath);
|
|
54
38
|
}
|
|
55
39
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
seatId,
|
|
63
|
-
sessionName: paneInfo.sessionName,
|
|
64
|
-
paneId: paneInfo.paneId,
|
|
65
|
-
windowIndex: paneInfo.windowIndex,
|
|
66
|
-
windowName: paneInfo.windowName,
|
|
67
|
-
cwd: paneInfo.currentPath,
|
|
68
|
-
armedAt: new Date().toISOString(),
|
|
69
|
-
instanceId: createId(12),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
writeJson(seatPaths.metaPath, meta);
|
|
73
|
-
setPaneTitle(paneInfo.paneId, `muuuuse ${seatId}`);
|
|
74
|
-
spawnSeatDaemon(paneInfo.sessionName, seatId, binPath);
|
|
75
|
-
return meta;
|
|
40
|
+
function resolveProgramTokens(commandTokens, usePresets = true) {
|
|
41
|
+
const resolved = expandPresetCommand(commandTokens, usePresets);
|
|
42
|
+
if (resolved.length === 0) {
|
|
43
|
+
throw new Error("Seat commands now require a program. Example: `muuuuse 1 codex`.");
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
function
|
|
79
|
-
return
|
|
80
|
-
.map((
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!meta || !paneExists(meta.paneId)) {
|
|
84
|
-
return null;
|
|
48
|
+
function formatCommand(commandTokens) {
|
|
49
|
+
return commandTokens
|
|
50
|
+
.map((token) => {
|
|
51
|
+
if (/^[a-zA-Z0-9._/@:=+-]+$/.test(token)) {
|
|
52
|
+
return token;
|
|
85
53
|
}
|
|
86
|
-
return
|
|
54
|
+
return JSON.stringify(token);
|
|
87
55
|
})
|
|
88
|
-
.
|
|
56
|
+
.join(" ");
|
|
89
57
|
}
|
|
90
58
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
function configureScript({ sessionName, paneId, steps }) {
|
|
96
|
-
const seat = findSeatByPane(sessionName, paneId);
|
|
97
|
-
if (!seat) {
|
|
98
|
-
throw new Error("This pane is not armed. Run `muuuuse 1` or `muuuuse 2` first.");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const normalizedSteps = steps
|
|
102
|
-
.map((step) => sanitizeRelayText(step))
|
|
103
|
-
.filter((step) => step.length > 0);
|
|
104
|
-
|
|
105
|
-
if (normalizedSteps.length === 0) {
|
|
106
|
-
throw new Error("Script mode needs at least one non-empty step.");
|
|
59
|
+
function previewText(text, maxLength = 88) {
|
|
60
|
+
const compact = sanitizeRelayText(text).replace(/\s+/g, " ");
|
|
61
|
+
if (compact.length <= maxLength) {
|
|
62
|
+
return compact;
|
|
107
63
|
}
|
|
108
|
-
|
|
109
|
-
const seatPaths = getSeatPaths(sessionName, seat.seatId);
|
|
110
|
-
writeJson(seatPaths.scriptPath, {
|
|
111
|
-
mode: "script",
|
|
112
|
-
cursor: 0,
|
|
113
|
-
steps: normalizedSteps,
|
|
114
|
-
updatedAt: new Date().toISOString(),
|
|
115
|
-
});
|
|
116
|
-
setPaneTitle(paneId, `muuuuse ${seat.seatId} script`);
|
|
117
|
-
return {
|
|
118
|
-
seatId: seat.seatId,
|
|
119
|
-
steps: normalizedSteps,
|
|
120
|
-
};
|
|
64
|
+
return `${compact.slice(0, maxLength - 3)}...`;
|
|
121
65
|
}
|
|
122
66
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
67
|
+
function parseAnswerEntries(text) {
|
|
68
|
+
return String(text || "")
|
|
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");
|
|
133
80
|
}
|
|
134
81
|
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!payload) {
|
|
139
|
-
return null;
|
|
82
|
+
function resolveSessionFile(agentType, currentPath, processStartedAtMs) {
|
|
83
|
+
if (agentType === "codex") {
|
|
84
|
+
return selectCodexSessionFile(currentPath, processStartedAtMs);
|
|
140
85
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
appendJsonl(seatPaths.commandsPath, command);
|
|
150
|
-
return command;
|
|
86
|
+
if (agentType === "claude") {
|
|
87
|
+
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
88
|
+
}
|
|
89
|
+
if (agentType === "gemini") {
|
|
90
|
+
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
151
93
|
}
|
|
152
94
|
|
|
153
|
-
class
|
|
154
|
-
constructor(
|
|
155
|
-
this.
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.stopped = false;
|
|
161
|
-
this.offsets = { 1: 0, 2: 0 };
|
|
162
|
-
this.controllerPath = getControllerPath(sessionName);
|
|
163
|
-
this.seats = new Map();
|
|
95
|
+
class GenericAnswerTracker {
|
|
96
|
+
constructor() {
|
|
97
|
+
this.active = false;
|
|
98
|
+
this.buffer = "";
|
|
99
|
+
this.lastInputText = "";
|
|
100
|
+
this.lastOutputAt = 0;
|
|
101
|
+
this.lastFingerprint = null;
|
|
164
102
|
}
|
|
165
103
|
|
|
166
|
-
|
|
167
|
-
|
|
104
|
+
noteTurnStart(inputText = "") {
|
|
105
|
+
this.active = true;
|
|
106
|
+
this.buffer = "";
|
|
107
|
+
this.lastInputText = sanitizeRelayText(inputText);
|
|
108
|
+
this.lastOutputAt = 0;
|
|
168
109
|
}
|
|
169
110
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
process.once("SIGINT", stop);
|
|
175
|
-
process.once("SIGTERM", stop);
|
|
176
|
-
}
|
|
111
|
+
append(data) {
|
|
112
|
+
if (!this.active) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
177
115
|
|
|
178
|
-
|
|
179
|
-
this.
|
|
116
|
+
this.buffer += String(data || "");
|
|
117
|
+
this.lastOutputAt = Date.now();
|
|
180
118
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.seats = new Map(seats.map((seat) => [seat.seatId, seat]));
|
|
184
|
-
if (this.seats.has(1) && this.seats.has(2)) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
await sleep(CONTROLLER_WAIT_MS);
|
|
119
|
+
if (this.buffer.length > 24000) {
|
|
120
|
+
this.buffer = this.buffer.slice(-24000);
|
|
188
121
|
}
|
|
189
122
|
}
|
|
190
123
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.offsets[seatId] = getFileSize(eventsPath);
|
|
124
|
+
consumeReady() {
|
|
125
|
+
if (!this.active || !this.lastOutputAt) {
|
|
126
|
+
return null;
|
|
195
127
|
}
|
|
196
|
-
}
|
|
197
128
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
sessionName: this.sessionName,
|
|
202
|
-
seedSeat: this.seedSeat,
|
|
203
|
-
relays: this.relayCount,
|
|
204
|
-
startedAt: new Date().toISOString(),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
129
|
+
if (Date.now() - this.lastOutputAt < GENERIC_IDLE_MS) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
207
132
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
fs.rmSync(this.controllerPath, { force: true });
|
|
133
|
+
const text = extractGenericAnswer(this.buffer, this.lastInputText);
|
|
134
|
+
if (!text) {
|
|
135
|
+
return null;
|
|
212
136
|
}
|
|
213
|
-
}
|
|
214
137
|
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
if (this.stopped) {
|
|
219
|
-
return 0;
|
|
138
|
+
const fingerprint = hashText(`${this.lastInputText}\n${text}`);
|
|
139
|
+
if (fingerprint === this.lastFingerprint) {
|
|
140
|
+
return null;
|
|
220
141
|
}
|
|
221
142
|
|
|
222
|
-
this.
|
|
223
|
-
this.
|
|
143
|
+
this.lastFingerprint = fingerprint;
|
|
144
|
+
this.active = false;
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
224
148
|
|
|
225
|
-
|
|
226
|
-
|
|
149
|
+
function extractGenericAnswer(rawText, lastInputText) {
|
|
150
|
+
let candidate = sanitizeRelayText(rawText, 12000);
|
|
151
|
+
if (!candidate) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
227
154
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
232
|
-
this.print(`Kickoff -> seat ${this.seedSeat}: ${previewText(this.seedText)}`);
|
|
155
|
+
if (lastInputText) {
|
|
156
|
+
if (candidate === lastInputText) {
|
|
157
|
+
return null;
|
|
233
158
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
while (!this.stopped) {
|
|
237
|
-
await this.forwardNewAnswers();
|
|
238
|
-
if (this.relayCount >= this.maxRelays) {
|
|
239
|
-
this.print(`${BRAND} hit the relay cap (${this.maxRelays}).`);
|
|
240
|
-
return 0;
|
|
241
|
-
}
|
|
242
|
-
await sleep(POLL_MS);
|
|
243
|
-
}
|
|
244
|
-
return 0;
|
|
245
|
-
} finally {
|
|
246
|
-
this.removeState();
|
|
159
|
+
if (candidate.startsWith(`${lastInputText}\n`)) {
|
|
160
|
+
candidate = candidate.slice(lastInputText.length).trim();
|
|
247
161
|
}
|
|
248
162
|
}
|
|
249
163
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const { nextOffset, text } = readAppendedText(eventsPath, this.offsets[seatId]);
|
|
255
|
-
this.offsets[seatId] = nextOffset;
|
|
256
|
-
if (!text.trim()) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
164
|
+
const blocks = candidate
|
|
165
|
+
.split(/\n{2,}/)
|
|
166
|
+
.map((block) => block.trim())
|
|
167
|
+
.filter((block) => block.length > 0);
|
|
259
168
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.map((line) => line.trim())
|
|
263
|
-
.filter((line) => line.length > 0)
|
|
264
|
-
.map((line) => {
|
|
265
|
-
try {
|
|
266
|
-
return JSON.parse(line);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
272
|
-
|
|
273
|
-
for (const entry of entries) {
|
|
274
|
-
const queued = queueSeatCommand(this.sessionName, targetSeatId, entry.text, {
|
|
275
|
-
sourceSeat: seatId,
|
|
276
|
-
sourceEventId: entry.id,
|
|
277
|
-
});
|
|
278
|
-
if (!queued) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
this.relayCount += 1;
|
|
282
|
-
this.print(`[${seatId} -> ${targetSeatId}] ${previewText(entry.text)}`);
|
|
283
|
-
if (this.relayCount >= this.maxRelays) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
169
|
+
if (blocks.length === 0) {
|
|
170
|
+
return null;
|
|
288
171
|
}
|
|
172
|
+
|
|
173
|
+
return sanitizeRelayText(blocks[blocks.length - 1]);
|
|
289
174
|
}
|
|
290
175
|
|
|
291
|
-
class
|
|
292
|
-
constructor(
|
|
293
|
-
this.
|
|
294
|
-
this.
|
|
295
|
-
this.
|
|
296
|
-
this.
|
|
176
|
+
class SeatProcess {
|
|
177
|
+
constructor(options) {
|
|
178
|
+
this.seatId = options.seatId;
|
|
179
|
+
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
180
|
+
this.sessionName = options.sessionName;
|
|
181
|
+
this.cwd = options.cwd;
|
|
182
|
+
this.commandTokens = [...options.commandTokens];
|
|
183
|
+
this.agentType = detectAgentTypeFromCommand(this.commandTokens);
|
|
184
|
+
this.maxRelays = options.maxRelays;
|
|
185
|
+
|
|
186
|
+
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
187
|
+
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
188
|
+
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
189
|
+
|
|
190
|
+
this.child = null;
|
|
191
|
+
this.childPid = null;
|
|
192
|
+
this.childExit = null;
|
|
193
|
+
this.startedAtMs = Date.now();
|
|
194
|
+
this.relayCount = 0;
|
|
195
|
+
this.linked = false;
|
|
297
196
|
this.stopped = false;
|
|
298
|
-
this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
197
|
+
this.stdinCleanup = null;
|
|
198
|
+
this.resizeCleanup = null;
|
|
199
|
+
|
|
200
|
+
this.sessionState = {
|
|
201
|
+
file: null,
|
|
303
202
|
offset: 0,
|
|
304
203
|
lastMessageId: null,
|
|
305
|
-
processStartedAtMs: null,
|
|
306
|
-
};
|
|
307
|
-
this.paneState = {
|
|
308
|
-
text: "",
|
|
309
|
-
changedAt: 0,
|
|
310
|
-
lastCandidateHash: null,
|
|
311
204
|
};
|
|
205
|
+
|
|
206
|
+
this.genericTracker = new GenericAnswerTracker();
|
|
312
207
|
}
|
|
313
208
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
this.stopped = true;
|
|
317
|
-
};
|
|
318
|
-
process.once("SIGINT", stop);
|
|
319
|
-
process.once("SIGTERM", stop);
|
|
209
|
+
log(message) {
|
|
210
|
+
process.stderr.write(`${message}\n`);
|
|
320
211
|
}
|
|
321
212
|
|
|
322
|
-
|
|
323
|
-
writeJson(this.paths.
|
|
213
|
+
writeMeta(extra = {}) {
|
|
214
|
+
writeJson(this.paths.metaPath, {
|
|
215
|
+
seatId: this.seatId,
|
|
216
|
+
sessionName: this.sessionName,
|
|
217
|
+
cwd: this.cwd,
|
|
324
218
|
pid: process.pid,
|
|
219
|
+
childPid: this.childPid,
|
|
220
|
+
agentType: this.agentType,
|
|
221
|
+
command: this.commandTokens,
|
|
222
|
+
commandLine: formatCommand(this.commandTokens),
|
|
223
|
+
startedAt: new Date(this.startedAtMs).toISOString(),
|
|
224
|
+
...extra,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
writeStatus(extra = {}) {
|
|
229
|
+
writeJson(this.paths.statusPath, {
|
|
325
230
|
seatId: this.seatId,
|
|
326
231
|
sessionName: this.sessionName,
|
|
327
|
-
|
|
232
|
+
cwd: this.cwd,
|
|
233
|
+
pid: process.pid,
|
|
234
|
+
childPid: this.childPid,
|
|
235
|
+
agentType: this.agentType,
|
|
236
|
+
command: this.commandTokens,
|
|
237
|
+
relayCount: this.relayCount,
|
|
238
|
+
updatedAt: new Date().toISOString(),
|
|
239
|
+
...extra,
|
|
328
240
|
});
|
|
329
241
|
}
|
|
330
242
|
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
243
|
+
installSignalHandlers() {
|
|
244
|
+
const stop = () => {
|
|
245
|
+
this.stopped = true;
|
|
246
|
+
};
|
|
247
|
+
process.once("SIGINT", stop);
|
|
248
|
+
process.once("SIGTERM", stop);
|
|
336
249
|
}
|
|
337
250
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
251
|
+
installStdinProxy() {
|
|
252
|
+
const handleData = (chunk) => {
|
|
253
|
+
if (!this.child) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
341
256
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
257
|
+
const text = chunk.toString("utf8");
|
|
258
|
+
this.child.write(text);
|
|
259
|
+
if (this.shouldUseGenericCapture() && /[\r\n]/.test(text)) {
|
|
260
|
+
this.genericTracker.noteTurnStart("");
|
|
346
261
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
265
|
+
process.stdin.setRawMode(true);
|
|
350
266
|
}
|
|
267
|
+
process.stdin.resume();
|
|
268
|
+
process.stdin.on("data", handleData);
|
|
269
|
+
|
|
270
|
+
this.stdinCleanup = () => {
|
|
271
|
+
process.stdin.off("data", handleData);
|
|
272
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
273
|
+
process.stdin.setRawMode(false);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
351
276
|
}
|
|
352
277
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (!meta || !paneExists(meta.paneId)) {
|
|
356
|
-
this.writeStatus({ state: "waiting_for_pane" });
|
|
278
|
+
installResizeHandler() {
|
|
279
|
+
if (!process.stdout.isTTY) {
|
|
357
280
|
return;
|
|
358
281
|
}
|
|
359
282
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
283
|
+
const handleResize = () => {
|
|
284
|
+
if (!this.child) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
this.child.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Ignore resize failures while the child is exiting.
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
process.stdout.on("resize", handleResize);
|
|
296
|
+
this.resizeCleanup = () => {
|
|
297
|
+
process.stdout.off("resize", handleResize);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
launchChild() {
|
|
302
|
+
resetDir(this.paths.dir);
|
|
303
|
+
|
|
304
|
+
const [file, ...args] = this.commandTokens;
|
|
305
|
+
this.child = pty.spawn(file, args, {
|
|
306
|
+
cols: process.stdout.columns || 80,
|
|
307
|
+
cwd: this.cwd,
|
|
308
|
+
env: {
|
|
309
|
+
...process.env,
|
|
310
|
+
MUUUUSE_SEAT: String(this.seatId),
|
|
311
|
+
MUUUUSE_SESSION: this.sessionName,
|
|
312
|
+
},
|
|
313
|
+
name: process.env.TERM || "xterm-256color",
|
|
314
|
+
rows: process.stdout.rows || 24,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
this.childPid = this.child.pid;
|
|
318
|
+
this.writeMeta();
|
|
319
|
+
this.writeStatus({
|
|
320
|
+
partnerSeatId: this.partnerSeatId,
|
|
321
|
+
state: "running",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
this.child.onData((data) => {
|
|
325
|
+
process.stdout.write(data);
|
|
326
|
+
if (this.shouldUseGenericCapture()) {
|
|
327
|
+
this.genericTracker.append(data);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
this.child.onExit(({ exitCode, signal }) => {
|
|
332
|
+
this.childExit = {
|
|
333
|
+
exitCode,
|
|
334
|
+
signal: signal || null,
|
|
335
|
+
};
|
|
336
|
+
this.stopped = true;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
partnerIsLive() {
|
|
341
|
+
const partnerStatus = readJson(this.partnerPaths.statusPath, null);
|
|
342
|
+
return Boolean(partnerStatus?.pid && isPidAlive(partnerStatus.pid));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
maybeMarkLinked() {
|
|
346
|
+
if (this.linked || !this.partnerIsLive()) {
|
|
363
347
|
return;
|
|
364
348
|
}
|
|
365
349
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
cwd: paneInfo.currentPath,
|
|
370
|
-
windowName: paneInfo.windowName,
|
|
371
|
-
paneId: paneInfo.paneId,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
350
|
+
this.linked = true;
|
|
351
|
+
this.log(`${BRAND} seat ${this.seatId} linked with seat ${this.partnerSeatId} in session ${this.sessionName}.`);
|
|
352
|
+
}
|
|
374
353
|
|
|
375
|
-
|
|
376
|
-
this.
|
|
354
|
+
shouldUseGenericCapture() {
|
|
355
|
+
if (!this.agentType) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
377
358
|
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
state: "script",
|
|
381
|
-
scriptSteps: script.steps.length,
|
|
382
|
-
cursor: script.cursor || 0,
|
|
383
|
-
cwd: paneInfo.currentPath,
|
|
384
|
-
});
|
|
385
|
-
return;
|
|
359
|
+
if (this.sessionState.file) {
|
|
360
|
+
return false;
|
|
386
361
|
}
|
|
387
362
|
|
|
388
|
-
|
|
363
|
+
return Date.now() - this.startedAtMs >= GENERIC_FALLBACK_DELAY_MS;
|
|
389
364
|
}
|
|
390
365
|
|
|
391
|
-
|
|
392
|
-
const { nextOffset, text } = readAppendedText(this.
|
|
393
|
-
this.
|
|
366
|
+
pullPartnerEvents() {
|
|
367
|
+
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
368
|
+
this.partnerOffset = nextOffset;
|
|
394
369
|
if (!text.trim()) {
|
|
395
370
|
return;
|
|
396
371
|
}
|
|
397
372
|
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
} catch (error) {
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
.filter((entry) => entry && entry.type === "deliver" && typeof entry.text === "string");
|
|
410
|
-
|
|
411
|
-
for (const command of commands) {
|
|
412
|
-
if (script && Array.isArray(script.steps) && script.steps.length > 0) {
|
|
413
|
-
this.handleScriptTurn(meta, script, command);
|
|
373
|
+
const entries = parseAnswerEntries(text);
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
if (!this.child) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (Number.isFinite(this.maxRelays) && this.relayCount >= this.maxRelays) {
|
|
379
|
+
this.log(`${BRAND} seat ${this.seatId} hit the relay cap (${this.maxRelays}).`);
|
|
414
380
|
continue;
|
|
415
381
|
}
|
|
416
382
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
handleScriptTurn(meta, script, command) {
|
|
422
|
-
const steps = Array.isArray(script.steps) ? script.steps.filter((step) => step.length > 0) : [];
|
|
423
|
-
if (steps.length === 0) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
383
|
+
const payload = sanitizeRelayText(entry.text);
|
|
384
|
+
if (!payload) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
426
387
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
388
|
+
if (this.shouldUseGenericCapture()) {
|
|
389
|
+
this.genericTracker.noteTurnStart(payload);
|
|
390
|
+
}
|
|
430
391
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
writeJson(this.paths.scriptPath, nextScript);
|
|
437
|
-
this.emitAnswer({
|
|
438
|
-
id: createId(12),
|
|
439
|
-
origin: "script",
|
|
440
|
-
text: nextText,
|
|
441
|
-
createdAt: new Date().toISOString(),
|
|
442
|
-
});
|
|
392
|
+
this.child.write(payload.replace(/\n/g, "\r"));
|
|
393
|
+
this.child.write("\r");
|
|
394
|
+
this.relayCount += 1;
|
|
395
|
+
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
396
|
+
}
|
|
443
397
|
}
|
|
444
398
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!detectedAgent) {
|
|
448
|
-
this.liveState = {
|
|
449
|
-
type: null,
|
|
450
|
-
pid: null,
|
|
451
|
-
currentPath: paneInfo.currentPath,
|
|
452
|
-
sessionFile: null,
|
|
453
|
-
offset: 0,
|
|
454
|
-
lastMessageId: null,
|
|
455
|
-
processStartedAtMs: null,
|
|
456
|
-
};
|
|
457
|
-
this.writeStatus({
|
|
458
|
-
state: "armed",
|
|
459
|
-
cwd: paneInfo.currentPath,
|
|
460
|
-
agent: null,
|
|
461
|
-
});
|
|
399
|
+
resolveStructuredLog() {
|
|
400
|
+
if (!this.agentType || this.sessionState.file) {
|
|
462
401
|
return;
|
|
463
402
|
}
|
|
464
403
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
this.liveState.currentPath !== paneInfo.currentPath;
|
|
469
|
-
|
|
470
|
-
if (changed) {
|
|
471
|
-
this.liveState = {
|
|
472
|
-
type: detectedAgent.type,
|
|
473
|
-
pid: detectedAgent.pid,
|
|
474
|
-
currentPath: paneInfo.currentPath,
|
|
475
|
-
sessionFile: null,
|
|
476
|
-
offset: 0,
|
|
477
|
-
lastMessageId: null,
|
|
478
|
-
processStartedAtMs: detectedAgent.processStartedAtMs,
|
|
479
|
-
};
|
|
404
|
+
const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.startedAtMs);
|
|
405
|
+
if (!sessionFile) {
|
|
406
|
+
return;
|
|
480
407
|
}
|
|
481
408
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
if (this.liveState.sessionFile) {
|
|
490
|
-
if (detectedAgent.type === "gemini") {
|
|
491
|
-
const baseline = readGeminiAnswers(this.liveState.sessionFile, null);
|
|
492
|
-
this.liveState.lastMessageId = baseline.lastMessageId;
|
|
493
|
-
this.liveState.offset = baseline.fileSize;
|
|
494
|
-
} else {
|
|
495
|
-
this.liveState.offset = getFileSize(this.liveState.sessionFile);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
409
|
+
this.sessionState.file = sessionFile;
|
|
410
|
+
if (this.agentType === "gemini") {
|
|
411
|
+
const baseline = readGeminiAnswers(sessionFile, null);
|
|
412
|
+
this.sessionState.lastMessageId = baseline.lastMessageId;
|
|
413
|
+
this.sessionState.offset = baseline.fileSize;
|
|
414
|
+
} else {
|
|
415
|
+
this.sessionState.offset = getFileSize(sessionFile);
|
|
498
416
|
}
|
|
417
|
+
}
|
|
499
418
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
cwd: paneInfo.currentPath,
|
|
504
|
-
agent: detectedAgent.type,
|
|
505
|
-
log: "waiting_for_session_log",
|
|
506
|
-
});
|
|
419
|
+
collectStructuredAnswers() {
|
|
420
|
+
this.resolveStructuredLog();
|
|
421
|
+
if (!this.sessionState.file || !this.agentType) {
|
|
507
422
|
return;
|
|
508
423
|
}
|
|
509
424
|
|
|
510
425
|
const answers = [];
|
|
511
|
-
if (
|
|
512
|
-
const result = readCodexAnswers(this.
|
|
513
|
-
this.
|
|
426
|
+
if (this.agentType === "codex") {
|
|
427
|
+
const result = readCodexAnswers(this.sessionState.file, this.sessionState.offset);
|
|
428
|
+
this.sessionState.offset = result.nextOffset;
|
|
514
429
|
answers.push(...result.answers);
|
|
515
|
-
} else if (
|
|
516
|
-
const result = readClaudeAnswers(this.
|
|
517
|
-
this.
|
|
430
|
+
} else if (this.agentType === "claude") {
|
|
431
|
+
const result = readClaudeAnswers(this.sessionState.file, this.sessionState.offset);
|
|
432
|
+
this.sessionState.offset = result.nextOffset;
|
|
518
433
|
answers.push(...result.answers);
|
|
519
|
-
} else if (
|
|
520
|
-
const result = readGeminiAnswers(this.
|
|
521
|
-
this.
|
|
522
|
-
this.
|
|
434
|
+
} else if (this.agentType === "gemini") {
|
|
435
|
+
const result = readGeminiAnswers(this.sessionState.file, this.sessionState.lastMessageId);
|
|
436
|
+
this.sessionState.lastMessageId = result.lastMessageId;
|
|
437
|
+
this.sessionState.offset = result.fileSize;
|
|
523
438
|
answers.push(...result.answers);
|
|
524
439
|
}
|
|
525
440
|
|
|
526
441
|
for (const answer of answers) {
|
|
527
442
|
this.emitAnswer({
|
|
528
|
-
|
|
529
|
-
|
|
443
|
+
createdAt: answer.timestamp,
|
|
444
|
+
id: answer.id,
|
|
445
|
+
origin: this.agentType,
|
|
530
446
|
text: answer.text,
|
|
531
|
-
createdAt: answer.timestamp || new Date().toISOString(),
|
|
532
447
|
});
|
|
533
448
|
}
|
|
534
|
-
|
|
535
|
-
this.collectPaneFallback(meta, detectedAgent);
|
|
536
|
-
|
|
537
|
-
this.writeStatus({
|
|
538
|
-
state: "armed",
|
|
539
|
-
cwd: paneInfo.currentPath,
|
|
540
|
-
agent: detectedAgent.type,
|
|
541
|
-
log: this.liveState.sessionFile,
|
|
542
|
-
lastAnswerAt: answers.length > 0 ? answers[answers.length - 1].timestamp : undefined,
|
|
543
|
-
});
|
|
544
449
|
}
|
|
545
450
|
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const paneText = capturePaneText(meta.paneId, 240);
|
|
552
|
-
if (!paneText.trim()) {
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (paneText !== this.paneState.text) {
|
|
557
|
-
this.paneState.text = paneText;
|
|
558
|
-
this.paneState.changedAt = Date.now();
|
|
451
|
+
collectGenericAnswers() {
|
|
452
|
+
if (!this.shouldUseGenericCapture()) {
|
|
559
453
|
return;
|
|
560
454
|
}
|
|
561
455
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const candidate = extractCodexPaneAnswer(paneText);
|
|
567
|
-
if (!candidate) {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const candidateHash = hashText(`codex-pane:${candidate}:${paneText}`);
|
|
572
|
-
if (candidateHash === this.paneState.lastCandidateHash) {
|
|
456
|
+
const text = this.genericTracker.consumeReady();
|
|
457
|
+
if (!text) {
|
|
573
458
|
return;
|
|
574
459
|
}
|
|
575
460
|
|
|
576
|
-
this.paneState.lastCandidateHash = candidateHash;
|
|
577
461
|
this.emitAnswer({
|
|
578
|
-
id: createId(12),
|
|
579
|
-
origin: "codex_pane",
|
|
580
|
-
text: candidate,
|
|
581
462
|
createdAt: new Date().toISOString(),
|
|
463
|
+
id: createId(12),
|
|
464
|
+
origin: "generic",
|
|
465
|
+
text,
|
|
582
466
|
});
|
|
583
467
|
}
|
|
584
468
|
|
|
@@ -596,88 +480,143 @@ class SeatDaemon {
|
|
|
596
480
|
text,
|
|
597
481
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
598
482
|
});
|
|
483
|
+
|
|
484
|
+
this.log(`[${this.seatId}] ${previewText(text)}`);
|
|
599
485
|
}
|
|
600
486
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
487
|
+
async tick() {
|
|
488
|
+
this.maybeMarkLinked();
|
|
489
|
+
this.pullPartnerEvents();
|
|
490
|
+
this.collectStructuredAnswers();
|
|
491
|
+
this.collectGenericAnswers();
|
|
492
|
+
|
|
493
|
+
this.writeStatus({
|
|
494
|
+
partnerSeatId: this.partnerSeatId,
|
|
495
|
+
partnerLive: this.partnerIsLive(),
|
|
496
|
+
state: this.childExit ? "exited" : "running",
|
|
497
|
+
structuredLog: this.sessionState.file,
|
|
608
498
|
});
|
|
609
499
|
}
|
|
610
|
-
}
|
|
611
500
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
618
|
-
}
|
|
619
|
-
if (agentType === "gemini") {
|
|
620
|
-
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
621
|
-
}
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
501
|
+
async run() {
|
|
502
|
+
this.installSignalHandlers();
|
|
503
|
+
this.launchChild();
|
|
504
|
+
this.installStdinProxy();
|
|
505
|
+
this.installResizeHandler();
|
|
624
506
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
507
|
+
this.log(`${BRAND} seat ${this.seatId} started in session ${this.sessionName}.`);
|
|
508
|
+
this.log(`Command: ${formatCommand(this.commandTokens)}`);
|
|
509
|
+
this.log(`Stop both seats from another terminal with: muuuuse 3 stop`);
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
while (!this.stopped) {
|
|
513
|
+
await this.tick();
|
|
514
|
+
await sleep(POLL_MS);
|
|
515
|
+
}
|
|
516
|
+
} finally {
|
|
517
|
+
this.cleanup();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return this.childExit?.exitCode ?? 0;
|
|
629
521
|
}
|
|
630
|
-
return `${compact.slice(0, maxLength - 3)}...`;
|
|
631
|
-
}
|
|
632
522
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
523
|
+
cleanup() {
|
|
524
|
+
if (this.stdinCleanup) {
|
|
525
|
+
this.stdinCleanup();
|
|
526
|
+
this.stdinCleanup = null;
|
|
527
|
+
}
|
|
528
|
+
if (this.resizeCleanup) {
|
|
529
|
+
this.resizeCleanup();
|
|
530
|
+
this.resizeCleanup = null;
|
|
531
|
+
}
|
|
636
532
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
533
|
+
if (this.child && !this.childExit) {
|
|
534
|
+
try {
|
|
535
|
+
this.child.kill("SIGTERM");
|
|
536
|
+
} catch (error) {
|
|
537
|
+
// Ignore races during shutdown.
|
|
538
|
+
}
|
|
641
539
|
}
|
|
540
|
+
|
|
541
|
+
this.writeMeta({
|
|
542
|
+
childPid: this.childPid,
|
|
543
|
+
exitedAt: new Date().toISOString(),
|
|
544
|
+
});
|
|
545
|
+
this.writeStatus({
|
|
546
|
+
childPid: this.childPid,
|
|
547
|
+
exitCode: this.childExit?.exitCode ?? null,
|
|
548
|
+
exitedAt: new Date().toISOString(),
|
|
549
|
+
partnerSeatId: this.partnerSeatId,
|
|
550
|
+
state: "exited",
|
|
551
|
+
});
|
|
642
552
|
}
|
|
553
|
+
}
|
|
643
554
|
|
|
644
|
-
|
|
645
|
-
|
|
555
|
+
function stopSession(sessionName) {
|
|
556
|
+
const results = [];
|
|
557
|
+
|
|
558
|
+
for (const seatId of [1, 2]) {
|
|
559
|
+
const paths = getSeatPaths(sessionName, seatId);
|
|
560
|
+
const status = readJson(paths.statusPath, null);
|
|
561
|
+
const meta = readJson(paths.metaPath, null);
|
|
562
|
+
const wrapperPid = status?.pid || meta?.pid || null;
|
|
563
|
+
const childPid = status?.childPid || meta?.childPid || null;
|
|
564
|
+
|
|
565
|
+
let wrapperStopped = false;
|
|
566
|
+
let childStopped = false;
|
|
567
|
+
|
|
568
|
+
if (wrapperPid && isPidAlive(wrapperPid)) {
|
|
569
|
+
try {
|
|
570
|
+
process.kill(wrapperPid, "SIGTERM");
|
|
571
|
+
wrapperStopped = true;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
wrapperStopped = false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
646
576
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
577
|
+
if (childPid && isPidAlive(childPid)) {
|
|
578
|
+
try {
|
|
579
|
+
process.kill(childPid, "SIGTERM");
|
|
580
|
+
childStopped = true;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
childStopped = false;
|
|
583
|
+
}
|
|
651
584
|
}
|
|
652
|
-
}
|
|
653
585
|
|
|
654
|
-
|
|
655
|
-
|
|
586
|
+
results.push({
|
|
587
|
+
seatId,
|
|
588
|
+
childPid,
|
|
589
|
+
childStopped,
|
|
590
|
+
wrapperPid,
|
|
591
|
+
wrapperStopped,
|
|
592
|
+
});
|
|
656
593
|
}
|
|
657
594
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return "";
|
|
664
|
-
}
|
|
595
|
+
return {
|
|
596
|
+
sessionName,
|
|
597
|
+
seats: results,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
665
600
|
|
|
666
|
-
|
|
667
|
-
return
|
|
601
|
+
function readSessionStatus(sessionName) {
|
|
602
|
+
return {
|
|
603
|
+
sessionName,
|
|
604
|
+
seats: [1, 2].map((seatId) => {
|
|
605
|
+
const paths = getSeatPaths(sessionName, seatId);
|
|
606
|
+
const status = readJson(paths.statusPath, null);
|
|
607
|
+
return {
|
|
608
|
+
seatId,
|
|
609
|
+
status,
|
|
610
|
+
};
|
|
611
|
+
}),
|
|
612
|
+
};
|
|
668
613
|
}
|
|
669
614
|
|
|
670
615
|
module.exports = {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
enableLiveMode,
|
|
678
|
-
extractCodexPaneAnswer,
|
|
679
|
-
findSeatByPane,
|
|
680
|
-
listArmedSeats,
|
|
681
|
-
previewText,
|
|
682
|
-
queueSeatCommand,
|
|
616
|
+
SeatProcess,
|
|
617
|
+
formatCommand,
|
|
618
|
+
readSessionStatus,
|
|
619
|
+
resolveProgramTokens,
|
|
620
|
+
resolveSessionName,
|
|
621
|
+
stopSession,
|
|
683
622
|
};
|