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