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/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 GENERIC_IDLE_MS = 900;
25
+ const BELL = "\u0007";
26
+ const CTRL_C = "\u0003";
35
27
 
36
- function resolveSessionName(sessionOverride, currentPath = process.cwd()) {
37
- return sessionOverride || getDefaultSessionName(currentPath);
28
+ function resolveShell() {
29
+ const shell = String(process.env.SHELL || "").trim();
30
+ return shell || "/bin/bash";
38
31
  }
39
32
 
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`.");
33
+ function resolveShellArgs(shellPath) {
34
+ const base = path.basename(shellPath);
35
+ if (base === "bash" || base === "zsh") {
36
+ return ["-l"];
44
37
  }
45
- return resolved;
38
+ return [];
46
39
  }
47
40
 
48
- function formatCommand(commandTokens) {
49
- return commandTokens
50
- .map((token) => {
51
- if (/^[a-zA-Z0-9._/@:=+-]+$/.test(token)) {
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 `${compact.slice(0, maxLength - 3)}...`;
46
+ return "xterm-256color";
65
47
  }
66
48
 
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");
49
+ function resolveSessionName(currentPath = process.cwd()) {
50
+ return getDefaultSessionName(currentPath);
80
51
  }
81
52
 
82
- function resolveSessionFile(agentType, currentPath, processStartedAtMs, options = {}) {
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
- if (!this.active) {
113
- return;
68
+ const text = String(data || "");
69
+ if (!text || !this.active) {
70
+ return [];
114
71
  }
115
72
 
116
- this.buffer += String(data || "");
117
- this.lastOutputAt = Date.now();
118
-
119
- if (this.buffer.length > 24000) {
120
- this.buffer = this.buffer.slice(-24000);
121
- }
122
- }
123
-
124
- consumeReady() {
125
- if (!this.active || !this.lastOutputAt) {
126
- return null;
127
- }
128
-
129
- if (Date.now() - this.lastOutputAt < GENERIC_IDLE_MS) {
130
- return null;
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
- const fingerprint = hashText(`${this.lastInputText}\n${text}`);
139
- if (fingerprint === this.lastFingerprint) {
140
- return null;
91
+ this.buffer += char;
92
+ if (this.buffer.length > 24000) {
93
+ this.buffer = this.buffer.slice(-24000);
94
+ }
141
95
  }
142
96
 
143
- this.lastFingerprint = fingerprint;
144
- this.active = false;
145
- return text;
97
+ return answers;
146
98
  }
147
99
  }
148
100
 
149
- function extractGenericAnswer(rawText, lastInputText) {
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((block) => block.length > 0);
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 extractMarkedAnswer(content) {
182
- const lines = String(content || "").split("\n");
183
- const answerIndex = lines.findIndex((line) => line.trim().startsWith("(answer)"));
184
- if (answerIndex === -1) {
185
- return null;
186
- }
187
-
188
- const answerLines = lines.slice(answerIndex);
189
- answerLines[0] = answerLines[0].trim().replace(/^\(answer\)\s*/, "");
190
- return sanitizeRelayText(answerLines.join("\n"));
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 SeatProcess {
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.commandTokens = [...options.commandTokens];
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.startedAtMs = Date.now();
157
+ this.startedAt = new Date().toISOString();
211
158
  this.relayCount = 0;
212
- this.linked = false;
159
+ this.pendingInput = "";
213
160
  this.stopped = false;
214
161
  this.stdinCleanup = null;
215
162
  this.resizeCleanup = null;
216
- this.childToken = createId(16);
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
- childToken: this.childToken,
240
- agentType: this.agentType,
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
- installSignalHandlers() {
265
- const stop = () => {
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
- if (this.shouldUseGenericCapture() && /[\r\n]/.test(text)) {
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 || 80, process.stdout.rows || 24);
319
- } catch (error) {
320
- // Ignore resize failures while the child is exiting.
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
- launchChild() {
331
- resetDir(this.paths.dir);
332
-
333
- const [file, ...args] = this.commandTokens;
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
- this.childPid = this.child.pid;
348
- this.processStartedAtMs = Date.now();
349
- this.writeMeta();
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
- this.child.onData((data) => {
356
- process.stdout.write(data);
357
- if (this.shouldUseGenericCapture()) {
358
- this.genericTracker.append(data);
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
- partnerIsLive() {
372
- const partnerStatus = readJson(this.partnerPaths.statusPath, null);
373
- return Boolean(partnerStatus?.pid && isPidAlive(partnerStatus.pid));
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
- maybeMarkLinked() {
377
- if (this.linked || !this.partnerIsLive()) {
378
- return;
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
- shouldUseGenericCapture() {
386
- return !this.agentType;
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
- if (this.shouldUseGenericCapture()) {
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
- resolveStructuredLog() {
423
- if (!this.agentType || this.sessionState.file) {
424
- return;
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: entry.id || createId(12),
358
+ id: createId(12),
508
359
  type: "answer",
509
360
  seatId: this.seatId,
510
- origin: entry.origin || "unknown",
511
- text,
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(text)}`);
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.installSignalHandlers();
534
- this.launchChild();
377
+ this.installStopSignals();
378
+ this.launchShell();
535
379
  this.installStdinProxy();
536
380
  this.installResizeHandler();
537
381
 
538
- this.log(`${BRAND} seat ${this.seatId} started in session ${this.sessionName}.`);
539
- this.log(`Command: ${formatCommand(this.commandTokens)}`);
540
- this.log(`Stop everything from another terminal with: muuuuse stop`);
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
- if (this.child && !this.childExit) {
565
- try {
566
- this.child.kill("SIGTERM");
567
- } catch (error) {
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 stopSession(sessionName) {
587
- const results = [];
588
-
589
- for (const seatId of [1, 2]) {
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 listSessionNames() {
633
- const sessionsRoot = path.join(getStateRoot(), "sessions");
433
+ function signalPid(pid, signal) {
434
+ if (!Number.isInteger(pid) || pid <= 0 || !isPidAlive(pid)) {
435
+ return false;
436
+ }
634
437
  try {
635
- return fs.readdirSync(sessionsRoot, { withFileTypes: true })
636
- .filter((entry) => entry.isDirectory())
637
- .map((entry) => entry.name)
638
- .sort();
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
- async function stopSessions(sessionName = null) {
645
- const sessionNames = sessionName ? [sessionName] : listSessionNames();
646
- const sessionResults = sessionNames.map((name) => stopSession(name));
647
-
648
- await sleep(200);
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
- for (const sessionResult of sessionResults) {
651
- for (const seat of sessionResult.seats) {
652
- if (seat.wrapperPid && isPidAlive(seat.wrapperPid)) {
653
- try {
654
- process.kill(seat.wrapperPid, "SIGKILL");
655
- seat.wrapperForced = true;
656
- } catch (error) {
657
- seat.wrapperForced = false;
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 (seat.childPid && isPidAlive(seat.childPid)) {
664
- try {
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
- return {
677
- sessionName,
678
- sessions: sessionResults,
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
- status,
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 readAllSessionStatuses() {
697
- return listSessionNames().map((sessionName) => readSessionStatus(sessionName));
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
- SeatProcess,
702
- formatCommand,
703
- readAllSessionStatuses,
704
- readSessionStatus,
705
- resolveProgramTokens,
495
+ ArmedSeat,
706
496
  resolveSessionName,
707
- stopSession,
708
- stopSessions,
497
+ stopAllSessions,
709
498
  };