muuuuse 2.2.5 → 2.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 CHANGED
@@ -6,6 +6,7 @@ It does one job:
6
6
  - arm terminal one with `muuuuse 1`
7
7
  - arm terminal two with `muuuuse 2`
8
8
  - have seat 1 generate a session key and seat 2 sign it
9
+ - additional isolated pairs work the same way: `3/4`, `5/6`, `7/8`, ...
9
10
  - choose per-seat relay mode with `flow on` or `flow off`
10
11
  - watch Codex, Claude, or Gemini for local assistant output
11
12
  - inject that output into the other armed terminal
@@ -16,8 +17,15 @@ The whole surface is:
16
17
  ```bash
17
18
  muuuuse 1
18
19
  muuuuse 1 flow on
20
+ muuuuse 1 flow off
21
+ muuuuse 1 flow off continue 5
19
22
  muuuuse 2
20
23
  muuuuse 2 flow off
24
+ muuuuse 2 flow on continue 3
25
+ muuuuse 3
26
+ muuuuse 3 flow on
27
+ muuuuse 4
28
+ muuuuse 4 flow off continue 1
21
29
  muuuuse status
22
30
  muuuuse stop
23
31
  ```
@@ -36,10 +44,12 @@ Terminal 2:
36
44
  muuuuse 2 flow off
37
45
  ```
38
46
 
39
- Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Use those shells normally.
47
+ Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Every odd/even adjacent pair works the same way in parallel: `3/4`, `5/6`, `7/8`, and so on. Use those shells normally.
40
48
 
41
49
  `flow on` means that seat sends commentary and final answers. `flow off` means that seat waits for final answers only. Each seat decides what it sends out, so mixed calibration is allowed.
42
50
 
51
+ `continue <seat>` forwards that seat's relayed output into another armed seat without changing the signed odd/even pair law. This lets you build local loops like `1 -> 2 -> 3 -> 4 -> 1` while every adjacent pair still keeps its own session keypair.
52
+
43
53
  If you want Codex in one and Gemini in the other, start them inside the armed shells:
44
54
 
45
55
  ```bash
@@ -69,6 +79,7 @@ muuuuse stop
69
79
  - no tmux
70
80
  - state lives under `~/.muuuuse`
71
81
  - only the signed armed pair can exchange relay events
82
+ - `continue <seat>` is a separate local forwarding lane and can target any armed seat number
72
83
  - supported relay detection is built for Codex, Claude, and Gemini
73
84
  - `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
74
85
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "2.2.5",
4
- "description": "🔌Muuuuse arms two regular terminals and relays assistant output between them.",
3
+ "version": "2.3.0",
4
+ "description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "muuuuse": "bin/muuse.js"
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- const { BRAND, usage } = require("./util");
1
+ const { BRAND, normalizeSeatId, usage } = require("./util");
2
2
  const { ArmedSeat, getStatusReport, stopAllSessions } = require("./runtime");
3
3
 
4
4
  async function main(argv = process.argv.slice(2)) {
@@ -54,12 +54,14 @@ async function main(argv = process.argv.slice(2)) {
54
54
  return;
55
55
  }
56
56
 
57
- if (command === "1" || command === "2") {
58
- const flowMode = parseSeatFlowMode(command, argv.slice(1));
57
+ const seatId = normalizeSeatId(command);
58
+ if (seatId) {
59
+ const { flowMode, continueSeatId } = parseSeatOptions(command, argv.slice(1));
59
60
  const seat = new ArmedSeat({
60
61
  cwd: process.cwd(),
62
+ continueSeatId,
61
63
  flowMode,
62
- seatId: Number(command),
64
+ seatId,
63
65
  });
64
66
  const code = await seat.run();
65
67
  process.exit(code);
@@ -81,6 +83,9 @@ function renderSeatStatus(seat) {
81
83
  if (seat.partnerLive) {
82
84
  bits.push("peer live");
83
85
  }
86
+ if (seat.continueSeatId) {
87
+ bits.push(`continue ${seat.continueSeatId}`);
88
+ }
84
89
  if (seat.trust) {
85
90
  bits.push(`trust ${seat.trust}`);
86
91
  }
@@ -98,23 +103,57 @@ function renderSeatStatus(seat) {
98
103
  return output;
99
104
  }
100
105
 
101
- function parseSeatFlowMode(command, args) {
102
- if (args.length === 0) {
103
- return "off";
104
- }
106
+ function parseSeatOptions(command, args) {
107
+ let flowMode = "off";
108
+ let continueSeatId = null;
105
109
 
106
- if (args.length === 2 && String(args[0]).trim().toLowerCase() === "flow") {
107
- const flowMode = String(args[1]).trim().toLowerCase();
108
- if (flowMode === "on" || flowMode === "off") {
109
- return flowMode;
110
+ for (let index = 0; index < args.length;) {
111
+ const token = String(args[index] || "").trim().toLowerCase();
112
+
113
+ if (token === "flow") {
114
+ const flowToken = String(args[index + 1] || "").trim().toLowerCase();
115
+ if (flowToken === "on" || flowToken === "off") {
116
+ flowMode = flowToken;
117
+ index += 2;
118
+ continue;
119
+ }
120
+ break;
121
+ }
122
+
123
+ if (token === "continue") {
124
+ const targetSeatId = normalizeSeatId(args[index + 1]);
125
+ if (targetSeatId) {
126
+ continueSeatId = targetSeatId;
127
+ index += 2;
128
+ continue;
129
+ }
130
+ break;
110
131
  }
132
+
133
+ break;
134
+ }
135
+
136
+ if (args.length === 0 || (flowMode || continueSeatId !== null) && consumedAllArgs(args, flowMode, continueSeatId)) {
137
+ return { flowMode, continueSeatId };
111
138
  }
112
139
 
113
140
  throw new Error(
114
- `\`muuuuse ${command}\` accepts either no extra arguments or \`flow on\` / \`flow off\`. Run it directly in the terminal you want to arm.`
141
+ `\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat>\`, or both in sequence. Run it directly in the terminal you want to arm.`
115
142
  );
116
143
  }
117
144
 
145
+ function consumedAllArgs(args, flowMode, continueSeatId) {
146
+ const expected = [];
147
+ if (flowMode !== "off" || args.includes("flow")) {
148
+ expected.push("flow", flowMode);
149
+ }
150
+ if (continueSeatId !== null || args.includes("continue")) {
151
+ expected.push("continue", String(continueSeatId));
152
+ }
153
+ return expected.length === args.length &&
154
+ expected.every((value, index) => String(args[index]).trim().toLowerCase() === String(value).trim().toLowerCase());
155
+ }
156
+
118
157
  module.exports = {
119
158
  main,
120
159
  };
package/src/runtime.js CHANGED
@@ -20,12 +20,17 @@ const {
20
20
  ensureDir,
21
21
  getDefaultSessionName,
22
22
  getFileSize,
23
+ getPartnerSeatId,
23
24
  getSeatPaths,
24
25
  getSessionPaths,
26
+ getStateRoot,
25
27
  hashText,
28
+ isAnchorSeat,
26
29
  isPidAlive,
30
+ listSeatIds,
27
31
  loadOrCreateSeatIdentity,
28
32
  listSessionNames,
33
+ normalizeSeatId,
29
34
  readAppendedText,
30
35
  readJson,
31
36
  sanitizeRelayText,
@@ -55,6 +60,11 @@ function normalizeFlowMode(flowMode) {
55
60
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
56
61
  }
57
62
 
63
+ function normalizeContinueSeatId(value) {
64
+ const seatId = normalizeSeatId(value);
65
+ return seatId || null;
66
+ }
67
+
58
68
  function resolveShell() {
59
69
  const shell = String(process.env.SHELL || "").trim();
60
70
  return shell || "/bin/bash";
@@ -132,34 +142,40 @@ function sleepSync(ms) {
132
142
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
133
143
  }
134
144
 
135
- function findJoinableSessionName(currentPath = process.cwd()) {
145
+ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
146
+ const normalizedSeatId = normalizeSeatId(seatId);
147
+ const anchorSeatId = getPartnerSeatId(normalizedSeatId);
148
+ if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
149
+ return null;
150
+ }
151
+
136
152
  const candidates = listSessionNames()
137
153
  .map((sessionName) => {
138
154
  const sessionPaths = getSessionPaths(sessionName);
139
155
  const controller = readJson(sessionPaths.controllerPath, null);
140
- const seat1Paths = getSeatPaths(sessionName, 1);
141
- const seat2Paths = getSeatPaths(sessionName, 2);
142
- const seat1Meta = readJson(seat1Paths.metaPath, null);
143
- const seat1Status = readJson(seat1Paths.statusPath, null);
144
- const seat2Meta = readJson(seat2Paths.metaPath, null);
145
- const seat2Status = readJson(seat2Paths.statusPath, null);
156
+ const anchorPaths = getSeatPaths(sessionName, anchorSeatId);
157
+ const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
158
+ const anchorMeta = readJson(anchorPaths.metaPath, null);
159
+ const anchorStatus = readJson(anchorPaths.statusPath, null);
160
+ const seatMeta = readJson(seatPaths.metaPath, null);
161
+ const seatStatus = readJson(seatPaths.statusPath, null);
146
162
  const stopRequest = readJson(sessionPaths.stopPath, null);
147
163
 
148
- const cwd = controller?.cwd || seat1Status?.cwd || seat1Meta?.cwd || seat2Status?.cwd || seat2Meta?.cwd || null;
164
+ const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
149
165
  if (!matchesWorkingPath(cwd, currentPath)) {
150
166
  return null;
151
167
  }
152
168
 
153
- const seat1WrapperPid = seat1Status?.pid || seat1Meta?.pid || null;
154
- const seat1ChildPid = seat1Status?.childPid || seat1Meta?.childPid || null;
155
- const seat2WrapperPid = seat2Status?.pid || seat2Meta?.pid || null;
156
- const seat2ChildPid = seat2Status?.childPid || seat2Meta?.childPid || null;
157
- const seat1Live = isPidAlive(seat1WrapperPid) || isPidAlive(seat1ChildPid);
158
- const seat2Live = isPidAlive(seat2WrapperPid) || isPidAlive(seat2ChildPid);
169
+ const anchorWrapperPid = anchorStatus?.pid || anchorMeta?.pid || null;
170
+ const anchorChildPid = anchorStatus?.childPid || anchorMeta?.childPid || null;
171
+ const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
172
+ const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
173
+ const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
174
+ const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
159
175
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
160
- const createdAtMs = Date.parse(controller?.createdAt || seat1Meta?.startedAt || seat1Status?.updatedAt || "");
176
+ const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
161
177
 
162
- if (!seat1Live || seat2Live) {
178
+ if (!anchorLive || seatLive) {
163
179
  return null;
164
180
  }
165
181
 
@@ -178,10 +194,10 @@ function findJoinableSessionName(currentPath = process.cwd()) {
178
194
  return candidates[0]?.sessionName || null;
179
195
  }
180
196
 
181
- function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
197
+ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
182
198
  const deadline = Date.now() + timeoutMs;
183
199
  while (Date.now() <= deadline) {
184
- const sessionName = findJoinableSessionName(currentPath);
200
+ const sessionName = findJoinableSessionName(currentPath, seatId);
185
201
  if (sessionName) {
186
202
  return sessionName;
187
203
  }
@@ -192,15 +208,11 @@ function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEA
192
208
  }
193
209
 
194
210
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
195
- if (seatId === 1) {
211
+ if (isAnchorSeat(seatId)) {
196
212
  return createSessionName(currentPath);
197
213
  }
198
214
 
199
- if (seatId === 2) {
200
- return waitForJoinableSessionName(currentPath);
201
- }
202
-
203
- return createSessionName(currentPath);
215
+ return waitForJoinableSessionName(currentPath, seatId);
204
216
  }
205
217
 
206
218
  function parseAnswerEntries(text) {
@@ -218,6 +230,26 @@ function parseAnswerEntries(text) {
218
230
  .filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
219
231
  }
220
232
 
233
+ function parseContinueEntries(text, targetSeatId) {
234
+ return String(text || "")
235
+ .split("\n")
236
+ .map((line) => line.trim())
237
+ .filter(Boolean)
238
+ .map((line) => {
239
+ try {
240
+ return JSON.parse(line);
241
+ } catch {
242
+ return null;
243
+ }
244
+ })
245
+ .filter((entry) => (
246
+ entry &&
247
+ entry.type === "continue" &&
248
+ typeof entry.text === "string" &&
249
+ normalizeSeatId(entry.targetSeatId) === targetSeatId
250
+ ));
251
+ }
252
+
221
253
  function readSessionHeaderText(filePath, maxBytes = 16384) {
222
254
  try {
223
255
  const fd = fs.openSync(filePath, "r");
@@ -417,6 +449,36 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
417
449
  });
418
450
  }
419
451
 
452
+ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
453
+ return {
454
+ id: createId(12),
455
+ type: "continue",
456
+ sourceSessionName,
457
+ sourceSeatId: entry.seatId,
458
+ targetSeatId,
459
+ origin: entry.origin || "unknown",
460
+ text: entry.text,
461
+ createdAt: entry.createdAt || new Date().toISOString(),
462
+ chainId: entry.chainId,
463
+ hop: entry.hop,
464
+ sourceAnswerId: entry.id,
465
+ publicKey: entry.publicKey || null,
466
+ signature: entry.signature || null,
467
+ };
468
+ }
469
+
470
+ function getSeatDirIfExists(sessionName, seatId) {
471
+ const dir = path.join(getStateRoot(), "sessions", sessionName, `seat-${seatId}`);
472
+ try {
473
+ if (fs.statSync(dir).isDirectory()) {
474
+ return dir;
475
+ }
476
+ } catch {
477
+ return null;
478
+ }
479
+ return null;
480
+ }
481
+
420
482
  function readSeatChallenge(paths, sessionName) {
421
483
  const record = readJson(paths.challengePath, null);
422
484
  if (
@@ -511,16 +573,24 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
511
573
  class ArmedSeat {
512
574
  constructor(options) {
513
575
  this.seatId = options.seatId;
514
- this.partnerSeatId = options.seatId === 1 ? 2 : 1;
576
+ this.partnerSeatId = getPartnerSeatId(options.seatId);
577
+ this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
515
578
  this.flowMode = normalizeFlowMode(options.flowMode);
579
+ this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
516
580
  this.cwd = normalizeWorkingPath(options.cwd);
581
+ if (this.continueSeatId === this.seatId) {
582
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
583
+ }
517
584
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
518
585
  if (!this.sessionName) {
519
- throw new Error("No armed `muuuuse 1` seat is waiting in this cwd. Run `muuuuse 1` first.");
586
+ throw new Error(
587
+ `No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
588
+ );
520
589
  }
521
590
  this.sessionPaths = getSessionPaths(this.sessionName);
522
591
  this.paths = getSeatPaths(this.sessionName, this.seatId);
523
592
  this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
593
+ this.continueOffset = getFileSize(this.paths.continuePath);
524
594
  this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
525
595
 
526
596
  this.child = null;
@@ -542,7 +612,7 @@ class ArmedSeat {
542
612
  this.trustState = {
543
613
  challenge: null,
544
614
  peerPublicKey: null,
545
- phase: this.seatId === 1 ? "waiting_for_peer_signature" : "waiting_for_seat1_key",
615
+ phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
546
616
  pairedAt: null,
547
617
  };
548
618
  this.liveState = {
@@ -565,9 +635,11 @@ class ArmedSeat {
565
635
  cwd: this.cwd,
566
636
  createdAt: current.createdAt || this.startedAt,
567
637
  updatedAt: new Date().toISOString(),
568
- seat1Pid: this.seatId === 1 ? process.pid : current.seat1Pid || null,
569
- seat2Pid: this.seatId === 2 ? process.pid : current.seat2Pid || null,
570
- pid: this.seatId === 1 ? process.pid : current.pid || null,
638
+ anchorSeatId: this.anchorSeatId,
639
+ partnerSeatId: this.partnerSeatId,
640
+ anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
641
+ partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
642
+ pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
571
643
  ...extra,
572
644
  });
573
645
  }
@@ -582,6 +654,7 @@ class ArmedSeat {
582
654
  partnerSeatId: this.partnerSeatId,
583
655
  sessionName: this.sessionName,
584
656
  flowMode: this.flowMode,
657
+ continueSeatId: this.continueSeatId,
585
658
  cwd: this.cwd,
586
659
  pid: process.pid,
587
660
  childPid: this.childPid,
@@ -597,6 +670,7 @@ class ArmedSeat {
597
670
  partnerSeatId: this.partnerSeatId,
598
671
  sessionName: this.sessionName,
599
672
  flowMode: this.flowMode,
673
+ continueSeatId: this.continueSeatId,
600
674
  cwd: this.cwd,
601
675
  pid: process.pid,
602
676
  childPid: this.childPid,
@@ -609,7 +683,7 @@ class ArmedSeat {
609
683
  initializeTrustMaterial() {
610
684
  this.identity = loadOrCreateSeatIdentity(this.paths);
611
685
 
612
- if (this.seatId !== 1) {
686
+ if (!isAnchorSeat(this.seatId)) {
613
687
  return;
614
688
  }
615
689
 
@@ -632,7 +706,7 @@ class ArmedSeat {
632
706
  this.initializeTrustMaterial();
633
707
  }
634
708
 
635
- if (this.seatId === 1) {
709
+ if (isAnchorSeat(this.seatId)) {
636
710
  this.syncSeatOneTrust();
637
711
  return;
638
712
  }
@@ -713,7 +787,7 @@ class ArmedSeat {
713
787
  this.trustState = {
714
788
  challenge: null,
715
789
  peerPublicKey: null,
716
- phase: "waiting_for_seat1_key",
790
+ phase: "waiting_for_anchor_key",
717
791
  pairedAt: null,
718
792
  };
719
793
  return;
@@ -941,6 +1015,44 @@ class ArmedSeat {
941
1015
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
942
1016
  }
943
1017
 
1018
+ findContinuationTarget() {
1019
+ if (!this.continueSeatId) {
1020
+ return null;
1021
+ }
1022
+
1023
+ const candidates = listSessionNames()
1024
+ .map((sessionName) => {
1025
+ if (!getSeatDirIfExists(sessionName, this.continueSeatId)) {
1026
+ return null;
1027
+ }
1028
+
1029
+ const seat = buildSeatReport(sessionName, this.continueSeatId);
1030
+ if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1031
+ return null;
1032
+ }
1033
+
1034
+ const updatedAtMs = Date.parse(seat.updatedAt || seat.startedAt || "");
1035
+ return {
1036
+ seat,
1037
+ sessionName,
1038
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
1039
+ };
1040
+ })
1041
+ .filter((entry) => entry !== null)
1042
+ .sort((left, right) => right.updatedAtMs - left.updatedAtMs);
1043
+
1044
+ if (candidates.length === 0) {
1045
+ return null;
1046
+ }
1047
+
1048
+ const target = candidates[0];
1049
+ return {
1050
+ seatId: target.seat.seatId,
1051
+ sessionName: target.sessionName,
1052
+ paths: getSeatPaths(target.sessionName, target.seat.seatId),
1053
+ };
1054
+ }
1055
+
944
1056
  async pullPartnerEvents() {
945
1057
  const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
946
1058
  this.partnerOffset = nextOffset;
@@ -1003,6 +1115,53 @@ class ArmedSeat {
1003
1115
  }
1004
1116
  }
1005
1117
 
1118
+ async pullContinuationEvents() {
1119
+ const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
1120
+ this.continueOffset = nextOffset;
1121
+ if (!text.trim() || !this.child || this.stopped) {
1122
+ return;
1123
+ }
1124
+
1125
+ const entries = parseContinueEntries(text, this.seatId);
1126
+ for (const entry of entries) {
1127
+ if (this.stopped || this.stopRequested()) {
1128
+ this.requestStop("stop_requested");
1129
+ return;
1130
+ }
1131
+
1132
+ const payload = sanitizeRelayText(entry.text);
1133
+ if (!payload) {
1134
+ continue;
1135
+ }
1136
+
1137
+ const delivered = await sendTextAndEnter(
1138
+ this.child,
1139
+ payload,
1140
+ () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1141
+ );
1142
+ if (!delivered) {
1143
+ this.requestStop("relay_aborted");
1144
+ return;
1145
+ }
1146
+
1147
+ if (this.stopped || this.stopRequested()) {
1148
+ this.requestStop("stop_requested");
1149
+ return;
1150
+ }
1151
+
1152
+ const deliveredAtMs = Date.now();
1153
+ this.pendingInboundContext = {
1154
+ chainId: entry.chainId || entry.sourceAnswerId || entry.id,
1155
+ deliveredAtMs,
1156
+ expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
1157
+ hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1158
+ };
1159
+ this.relayCount += 1;
1160
+ this.rememberInboundRelay(payload);
1161
+ this.log(`[${entry.sourceSeatId} => ${this.seatId}] ${previewText(payload)}`);
1162
+ }
1163
+ }
1164
+
1006
1165
  rememberInboundRelay(text) {
1007
1166
  const payload = sanitizeRelayText(text);
1008
1167
  if (!payload) {
@@ -1269,11 +1428,28 @@ class ArmedSeat {
1269
1428
  this.identity.privateKey
1270
1429
  );
1271
1430
  appendJsonl(this.paths.eventsPath, signedEntry);
1431
+ this.forwardContinuation(signedEntry);
1272
1432
  this.rememberEmittedAnswer(answerKey);
1273
1433
 
1274
1434
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1275
1435
  }
1276
1436
 
1437
+ forwardContinuation(signedEntry) {
1438
+ if (!this.continueSeatId) {
1439
+ return;
1440
+ }
1441
+
1442
+ const target = this.findContinuationTarget();
1443
+ if (!target) {
1444
+ this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
1445
+ return;
1446
+ }
1447
+
1448
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1449
+ appendJsonl(target.paths.continuePath, continuationEntry);
1450
+ this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1451
+ }
1452
+
1277
1453
  async tick() {
1278
1454
  if (this.stopRequested()) {
1279
1455
  this.writeStatus({
@@ -1287,6 +1463,7 @@ class ArmedSeat {
1287
1463
 
1288
1464
  this.syncTrustState();
1289
1465
  await this.pullPartnerEvents();
1466
+ await this.pullContinuationEvents();
1290
1467
  if (this.stopped || this.stopRequested()) {
1291
1468
  this.requestStop("stop_requested");
1292
1469
  return;
@@ -1319,10 +1496,13 @@ class ArmedSeat {
1319
1496
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1320
1497
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1321
1498
  this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1322
- if (this.seatId === 1) {
1323
- this.log("Seat 1 generated the session key and is waiting for seat 2 to sign it.");
1499
+ if (this.continueSeatId) {
1500
+ this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1501
+ }
1502
+ if (isAnchorSeat(this.seatId)) {
1503
+ this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
1324
1504
  } else {
1325
- this.log("Seat 2 will sign the session key from seat 1, then relay goes live.");
1505
+ this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
1326
1506
  }
1327
1507
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1328
1508
 
@@ -1420,6 +1600,7 @@ function buildSeatReport(sessionName, seatId) {
1420
1600
  seatId,
1421
1601
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1422
1602
  flowMode: status?.flowMode || meta?.flowMode || "off",
1603
+ continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1423
1604
  wrapperPid,
1424
1605
  childPid,
1425
1606
  wrapperLive,
@@ -1443,7 +1624,7 @@ function getStatusReport() {
1443
1624
  const sessionPaths = getSessionPaths(sessionName);
1444
1625
  const controller = readJson(sessionPaths.controllerPath, null);
1445
1626
  const stopRequest = readJson(sessionPaths.stopPath, null);
1446
- const seats = [1, 2]
1627
+ const seats = listSeatIds(sessionName)
1447
1628
  .map((seatId) => buildSeatReport(sessionName, seatId))
1448
1629
  .filter((entry) => entry !== null);
1449
1630
 
package/src/util.js CHANGED
@@ -167,6 +167,42 @@ function getSessionPaths(sessionName) {
167
167
  };
168
168
  }
169
169
 
170
+ function normalizeSeatId(value) {
171
+ const seatId = Number.parseInt(String(value || "").trim(), 10);
172
+ if (!Number.isInteger(seatId) || seatId <= 0) {
173
+ return null;
174
+ }
175
+ return seatId;
176
+ }
177
+
178
+ function isAnchorSeat(seatId) {
179
+ return normalizeSeatId(seatId) % 2 === 1;
180
+ }
181
+
182
+ function getPartnerSeatId(seatId) {
183
+ const normalized = normalizeSeatId(seatId);
184
+ if (!normalized) {
185
+ return null;
186
+ }
187
+ return isAnchorSeat(normalized) ? normalized + 1 : normalized - 1;
188
+ }
189
+
190
+ function listSeatIds(sessionName) {
191
+ const sessionDir = getSessionDir(sessionName);
192
+ try {
193
+ return fs.readdirSync(sessionDir, { withFileTypes: true })
194
+ .filter((entry) => entry.isDirectory())
195
+ .map((entry) => {
196
+ const match = entry.name.match(/^seat-(\d+)$/);
197
+ return match ? Number.parseInt(match[1], 10) : null;
198
+ })
199
+ .filter((seatId) => Number.isInteger(seatId))
200
+ .sort((left, right) => left - right);
201
+ } catch {
202
+ return [];
203
+ }
204
+ }
205
+
170
206
  function getSeatDir(sessionName, seatId) {
171
207
  return ensureDir(path.join(getSessionDir(sessionName), `seat-${seatId}`));
172
208
  }
@@ -177,6 +213,7 @@ function getSeatPaths(sessionName, seatId) {
177
213
  ackPath: path.join(dir, "ack.json"),
178
214
  challengePath: path.join(dir, "challenge.json"),
179
215
  claimPath: path.join(dir, "claim.json"),
216
+ continuePath: path.join(dir, "continue.jsonl"),
180
217
  dir,
181
218
  daemonPath: path.join(dir, "daemon.json"),
182
219
  eventsPath: path.join(dir, "events.jsonl"),
@@ -245,26 +282,33 @@ function listSessionNames() {
245
282
 
246
283
  function usage() {
247
284
  return [
248
- `${BRAND} arms two regular terminals and relays assistant output between them.`,
285
+ `${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
249
286
  "",
250
287
  "Usage:",
251
288
  " muuuuse 1",
252
289
  " muuuuse 1 flow on",
253
290
  " muuuuse 1 flow off",
291
+ " muuuuse 1 flow on continue 3",
254
292
  " muuuuse 2",
255
293
  " muuuuse 2 flow on",
256
294
  " muuuuse 2 flow off",
295
+ " muuuuse 2 flow on continue 3",
296
+ " muuuuse 3",
297
+ " muuuuse 4",
298
+ " muuuuse 4 flow on continue 1",
257
299
  " muuuuse stop",
258
300
  " muuuuse status",
259
301
  "",
260
302
  "Flow:",
261
303
  " 1. Run `muuuuse 1` in terminal one.",
262
304
  " 2. Run `muuuuse 2` in terminal two.",
263
- " 3. Seat 1 generates the session key and seat 2 signs it automatically.",
264
- " 4. Optional: arm each seat with `flow on` or `flow off`.",
265
- " 5. Use those armed shells normally.",
266
- " 6. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
267
- " 7. Run `muuuuse status` or `muuuuse stop` from any shell.",
305
+ " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
306
+ " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
307
+ " 5. Optional: arm each seat with `flow on` or `flow off`.",
308
+ " 6. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
309
+ " 7. Use those armed shells normally.",
310
+ " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
311
+ " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
268
312
  "",
269
313
  "Notes:",
270
314
  " - No tmux.",
@@ -282,13 +326,17 @@ module.exports = {
282
326
  ensureDir,
283
327
  getDefaultSessionName,
284
328
  getFileSize,
329
+ getPartnerSeatId,
285
330
  loadOrCreateSeatIdentity,
331
+ isAnchorSeat,
286
332
  getSeatPaths,
287
333
  getSessionPaths,
288
334
  getStateRoot,
289
335
  hashText,
290
336
  isPidAlive,
337
+ listSeatIds,
291
338
  listSessionNames,
339
+ normalizeSeatId,
292
340
  readAppendedText,
293
341
  readJson,
294
342
  sanitizeRelayText,