muuuuse 5.0.2 → 6.0.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
@@ -1,44 +1,56 @@
1
1
  # 🔌Muuuuse
2
2
 
3
- `🔌Muuuuse` is a tiny terminal relay with flexible routing.
3
+ `🔌Muuuuse` is a tiny terminal relay.
4
4
 
5
5
  It does one job:
6
- - arm any number of terminals with `muuuuse <N>`
7
- - each seat can route to any other seats with individual flow modes
6
+ - arm terminal one with `muuuuse 1`
7
+ - arm terminal two with `muuuuse 2`
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`, ...
10
+ - choose per-seat relay mode with `flow on` or `flow off`
8
11
  - watch Codex, Claude, or Gemini for local assistant output
9
- - relay that output to configured targets with per-target flow control
12
+ - inject that output into the other armed terminal
10
13
  - keep looping until you stop it
11
14
 
12
15
  The whole surface is:
13
16
 
14
17
  ```bash
15
18
  muuuuse 1
19
+ muuuuse 1 flow on
20
+ muuuuse 1 flow off
21
+ muuuuse 1 flow off continue 5
16
22
  muuuuse 2
17
- muuuuse 3 link 1 flow on 2 flow off
18
- muuuuse 4 link 3 flow on
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
19
29
  muuuuse status
20
30
  muuuuse stop
21
31
  ```
22
32
 
23
- ## Routing
33
+ ## Flow
24
34
 
25
- Any seat can route to any targets. Syntax:
35
+ Terminal 1:
26
36
 
27
37
  ```bash
28
- muuuuse <seat> link <target1> flow <on|off> [<target2> flow <on|off> ...]
38
+ muuuuse 1 flow on
29
39
  ```
30
40
 
31
- Example: Seat 3 routes to seat 1 with full output (flow on) and seat 4 with final answers only (flow off):
41
+ Terminal 2:
32
42
 
33
43
  ```bash
34
- muuuuse 3 link 1 flow on 4 flow off
44
+ muuuuse 2 flow off
35
45
  ```
36
46
 
37
- Each seat defines its own routing targets. When that seat gets an answer from Claude/Codex/Gemini, it sends to all configured targets with their respective flow modes.
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.
38
48
 
39
- `flow on` means relay both thinking/commentary and final answers. `flow off` means relay final answers only.
49
+ `flow on` means that seat relays commentary and final answers. `flow off` means that seat relays and accepts final answers only. Mixed calibration is allowed per seat.
40
50
 
41
- If you want Codex in one and Gemini in another, start them inside the armed shells:
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
+
53
+ If you want Codex in one and Gemini in the other, start them inside the armed shells:
42
54
 
43
55
  ```bash
44
56
  codex
@@ -48,7 +60,7 @@ codex
48
60
  gemini
49
61
  ```
50
62
 
51
- `🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each target's flow mode, types that output into the target seat, and then sends Enter as a separate keystroke.
63
+ `🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each seat's flow mode, types that output into the other seat, and then sends Enter as a separate keystroke.
52
64
 
53
65
  Check the live state from any terminal:
54
66
 
@@ -65,9 +77,8 @@ muuuuse stop
65
77
  ## Notes
66
78
 
67
79
  - state lives under `~/.muuuuse`
68
- - each seat defines its own routing targets independently
69
- - any seat can route to any other seat number
70
- - flow modes are per-target (not per-seat)
80
+ - only the signed armed pair can exchange relay events
81
+ - `continue <seat>` is a separate local forwarding lane and can target any armed seat number
71
82
  - supported relay detection is built for Codex, Claude, and Gemini
72
83
 
73
84
  ## Install
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "5.0.2",
4
- "description": "🔌Muuuuse arms terminals with flexible routing to any targets - pure message relay graph.",
3
+ "version": "6.0.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"
@@ -40,8 +40,7 @@
40
40
  ],
41
41
  "scripts": {
42
42
  "test": "node test/cli.test.js",
43
- "pack:local": "npm pack",
44
- "prepublishOnly": "npm test"
43
+ "pack:local": "npm pack"
45
44
  },
46
45
  "dependencies": {
47
46
  "node-pty": "^1.1.0"
package/src/agents.js CHANGED
@@ -69,44 +69,6 @@ function detectAgent(processes) {
69
69
  return buildDetectedAgent("gemini", process);
70
70
  }
71
71
  }
72
-
73
- // Fallback: if no agent process found, check for Claude Code running in a separate terminal
74
- // (Claude Code's session updates even if it's not a child process of this shell)
75
- if (processes.length > 0) {
76
- // Use the bash/shell process as reference for timing
77
- const shellProcess = processes[0];
78
- const now = Date.now();
79
- const recentThreshold = now - 2 * 60 * 1000; // Last 2 minutes
80
-
81
- const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
82
- if (fs.existsSync(CLAUDE_ROOT)) {
83
- try {
84
- const sessionFiles = walkFiles(CLAUDE_ROOT, (f) => f.endsWith(".jsonl"))
85
- .map(filePath => {
86
- try {
87
- const stat = fs.statSync(filePath);
88
- return { filePath, mtimeMs: stat.mtimeMs };
89
- } catch {
90
- return null;
91
- }
92
- })
93
- .filter(e => e && e.mtimeMs > recentThreshold)
94
- .sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
95
-
96
- if (sessionFiles) {
97
- return {
98
- type: "claude",
99
- pid: process.pid || shellProcess.pid,
100
- args: "claude-code-terminal-session",
101
- cwd: shellProcess.cwd || null,
102
- elapsedSeconds: Math.round((Date.now() - shellProcess.startedAtMs) / 1000),
103
- processStartedAtMs: Date.now() - (shellProcess.elapsedSeconds ?? 0) * 1000,
104
- };
105
- }
106
- } catch {}
107
- }
108
- }
109
-
110
72
  return null;
111
73
  }
112
74
 
@@ -593,7 +555,8 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
593
555
  return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
594
556
  }
595
557
 
596
- function extractClaudeAssistantText(content) {
558
+ function extractClaudeAssistantText(content, options = {}) {
559
+ const flowMode = options.flowMode === true;
597
560
  if (!Array.isArray(content)) {
598
561
  return "";
599
562
  }
@@ -606,23 +569,7 @@ function extractClaudeAssistantText(content) {
606
569
  if (item.type === "text" && typeof item.text === "string") {
607
570
  return [item.text.trim()];
608
571
  }
609
- return [];
610
- })
611
- .filter((text) => text.length > 0)
612
- .join("\n");
613
- }
614
-
615
- function extractClaudeThinkingText(content) {
616
- if (!Array.isArray(content)) {
617
- return "";
618
- }
619
-
620
- return content
621
- .flatMap((item) => {
622
- if (!item || typeof item !== "object") {
623
- return [];
624
- }
625
- if (item.type === "thinking" && typeof item.thinking === "string") {
572
+ if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
626
573
  return [item.thinking.trim()];
627
574
  }
628
575
  return [];
@@ -639,46 +586,28 @@ function parseClaudeAssistantLine(line, options = {}) {
639
586
  return null;
640
587
  }
641
588
 
642
- const isFinal = entry.message?.stop_reason === "end_turn";
643
- if (!flowMode && !isFinal) {
589
+ if (!flowMode && entry.message?.stop_reason !== "end_turn") {
644
590
  return null;
645
591
  }
646
592
 
647
- const id = entry.uuid || entry.message.id || hashText(line);
648
- const timestamp = entry.timestamp || new Date().toISOString();
649
- const results = [];
650
-
651
- if (flowMode) {
652
- const thinking = sanitizeRelayText(extractClaudeThinkingText(entry.message.content));
653
- if (thinking) {
654
- results.push({
655
- id: `${id}-thinking`,
656
- text: thinking,
657
- phase: "commentary",
658
- timestamp,
659
- });
660
- }
661
- }
662
-
663
- const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
664
- if (text) {
665
- results.push({
666
- id,
667
- text,
668
- phase: isFinal ? "final_answer" : "commentary",
669
- timestamp,
670
- });
593
+ const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
594
+ if (!text) {
595
+ return null;
671
596
  }
672
597
 
673
- return results.length > 0 ? results : null;
598
+ return {
599
+ id: entry.uuid || entry.message.id || hashText(line),
600
+ text,
601
+ phase: flowMode && entry.message?.stop_reason !== "end_turn" ? "commentary" : "final_answer",
602
+ timestamp: entry.timestamp || new Date().toISOString(),
603
+ };
674
604
  } catch {
675
605
  return null;
676
606
  }
677
607
  }
678
608
 
679
609
  function parseClaudeFinalLine(line) {
680
- const results = parseClaudeAssistantLine(line, { flowMode: false });
681
- return Array.isArray(results) ? results[0] || null : results;
610
+ return parseClaudeAssistantLine(line, { flowMode: false });
682
611
  }
683
612
 
684
613
  function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
@@ -687,10 +616,8 @@ function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
687
616
  .split("\n")
688
617
  .map((line) => line.trim())
689
618
  .filter((line) => line.length > 0)
690
- .flatMap((line) => {
691
- const result = parseClaudeAssistantLine(line, options);
692
- return Array.isArray(result) ? result : result ? [result] : [];
693
- })
619
+ .map((line) => parseClaudeAssistantLine(line, options))
620
+ .filter((entry) => entry !== null)
694
621
  .filter((entry) => isAnswerNewEnough(entry, sinceMs));
695
622
 
696
623
  return { nextOffset, answers };
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- const { BRAND, getPartnerSeatId, normalizeSeatId, 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)) {
@@ -56,11 +56,10 @@ async function main(argv = process.argv.slice(2)) {
56
56
 
57
57
  const seatId = normalizeSeatId(command);
58
58
  if (seatId) {
59
- const { flowMode, continueTargets } = parseSeatOptions(command, argv.slice(1));
59
+ const { continueTargets } = parseSeatOptions(command, argv.slice(1));
60
60
  const seat = new ArmedSeat({
61
61
  cwd: process.cwd(),
62
62
  continueTargets,
63
- flowMode,
64
63
  seatId,
65
64
  });
66
65
  const code = await seat.run();
@@ -74,15 +73,11 @@ function renderSeatStatus(seat) {
74
73
  const bits = [
75
74
  `seat ${seat.seatId}: ${seat.state}`,
76
75
  `agent ${seat.agent || "idle"}`,
77
- `flow ${seat.flowMode || "off"}`,
78
76
  `relays ${seat.relayCount}`,
79
77
  `wrapper ${seat.wrapperPid || "-"}`,
80
78
  `child ${seat.childPid || "-"}`,
81
79
  ];
82
80
 
83
- if (seat.partnerLive) {
84
- bits.push("peer live");
85
- }
86
81
  const renderedLinks = renderLinkTargets(seat);
87
82
  if (renderedLinks) {
88
83
  bits.push(`link ${renderedLinks}`);
@@ -106,48 +101,34 @@ function renderSeatStatus(seat) {
106
101
 
107
102
  function renderLinkTargets(seat) {
108
103
  const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
109
- if (targets.length === 0) {
110
- return "";
111
- }
112
- return targets.map((target) => `${target.targetSeatId}:${target.flowMode}`).join(", ");
104
+ return targets
105
+ .map((target) => `${target.targetSeatId}:${target.flowMode}`)
106
+ .join(", ");
113
107
  }
114
108
 
115
109
  function parseSeatOptions(command, args) {
116
110
  const seatId = normalizeSeatId(command);
117
- let flowMode = "off";
118
111
  let continueTargets = [];
119
- if (args.length === 0) {
120
- return { flowMode, continueTargets };
121
- }
112
+ let index = 0;
122
113
 
123
- if (String(args[0] || "").trim().toLowerCase() === "link") {
124
- const parsedLinks = parseLinkTargets(args.slice(1), seatId, flowMode);
125
- if (parsedLinks.consumed === args.length - 1 && parsedLinks.consumed > 0) {
126
- return {
127
- flowMode: parsedLinks.flowMode,
128
- continueTargets: parsedLinks.continueTargets,
129
- };
130
- }
131
- } else {
132
- let index = 0;
133
-
134
- const flowToken = String(args[index] || "").trim().toLowerCase();
135
- const flowModeToken = String(args[index + 1] || "").trim().toLowerCase();
136
- if (flowToken === "flow" && (flowModeToken === "on" || flowModeToken === "off")) {
137
- flowMode = flowModeToken;
138
- index += 2;
139
- }
114
+ while (index < args.length) {
115
+ const token = String(args[index] || "").trim().toLowerCase();
140
116
 
141
- const continueToken = String(args[index] || "").trim().toLowerCase();
142
- const targetSeatId = normalizeSeatId(args[index + 1]);
143
- if (continueToken === "continue" && targetSeatId) {
144
- continueTargets = [{ targetSeatId, flowMode }];
145
- index += 2;
117
+ if (token === "link") {
118
+ const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
119
+ if (parsedLinks.consumed > 0) {
120
+ continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
121
+ index += 1 + parsedLinks.consumed;
122
+ continue;
123
+ }
124
+ break;
146
125
  }
147
126
 
148
- if (index === args.length) {
149
- return { flowMode, continueTargets };
150
- }
127
+ break;
128
+ }
129
+
130
+ if (index === args.length) {
131
+ return { continueTargets };
151
132
  }
152
133
 
153
134
  throw new Error(
@@ -155,8 +136,20 @@ function parseSeatOptions(command, args) {
155
136
  );
156
137
  }
157
138
 
158
- function parseLinkTargets(args, seatId, defaultFlowMode) {
159
- const targets = [];
139
+ function mergeTargets(existingTargets, nextTargets) {
140
+ const merged = [];
141
+ for (const target of Array.isArray(existingTargets) ? existingTargets : []) {
142
+ upsertTarget(merged, target);
143
+ }
144
+ for (const target of Array.isArray(nextTargets) ? nextTargets : []) {
145
+ upsertTarget(merged, target);
146
+ }
147
+
148
+ return merged;
149
+ }
150
+
151
+ function parseLinkTargets(args, seatId) {
152
+ const continueTargets = [];
160
153
  let consumed = 0;
161
154
 
162
155
  while (consumed < args.length) {
@@ -170,7 +163,7 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
170
163
  break;
171
164
  }
172
165
 
173
- upsertTarget(targets, {
166
+ upsertTarget(continueTargets, {
174
167
  targetSeatId,
175
168
  flowMode: targetFlowMode,
176
169
  });
@@ -178,7 +171,7 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
178
171
  consumed += 3;
179
172
  }
180
173
 
181
- return { consumed, continueTargets: targets, flowMode: defaultFlowMode };
174
+ return { consumed, continueTargets };
182
175
  }
183
176
 
184
177
  function parseFlowModeToken(flowToken, modeToken) {
@@ -201,5 +194,4 @@ function upsertTarget(targets, nextTarget) {
201
194
 
202
195
  module.exports = {
203
196
  main,
204
- parseSeatOptions,
205
197
  };
package/src/runtime.js CHANGED
@@ -20,12 +20,10 @@ const {
20
20
  ensureDir,
21
21
  getDefaultSessionName,
22
22
  getFileSize,
23
- getPartnerSeatId,
24
23
  getSeatPaths,
25
24
  getSessionPaths,
26
25
  getStateRoot,
27
26
  hashText,
28
- isAnchorSeat,
29
27
  isPidAlive,
30
28
  listSeatIds,
31
29
  loadOrCreateSeatIdentity,
@@ -42,7 +40,6 @@ const {
42
40
 
43
41
  const TYPE_CHUNK_DELAY_MS = 18;
44
42
  const TYPE_CHUNK_SIZE = 24;
45
- const TYPE_SUBMIT_DELAY_MS = 60;
46
43
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
47
44
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
48
45
  const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
@@ -57,32 +54,6 @@ const CHILD_ENV_DROP_KEYS = [
57
54
  "CODEX_THREAD_ID",
58
55
  ];
59
56
 
60
- function bestEffortEnableChildEcho(child) {
61
- const ptsName = String(child?.ptsName || "").trim();
62
- if (!ptsName || process.platform === "win32") {
63
- return;
64
- }
65
-
66
- try {
67
- execFileSync("stty", [
68
- "-F",
69
- ptsName,
70
- "echo",
71
- "icanon",
72
- "isig",
73
- "iexten",
74
- "echoe",
75
- "echok",
76
- "echoke",
77
- "echoctl",
78
- ], {
79
- stdio: "ignore",
80
- });
81
- } catch {
82
- // Best effort only. The shell or child app may later change its own tty mode.
83
- }
84
- }
85
-
86
57
  function normalizeFlowMode(flowMode) {
87
58
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
88
59
  }
@@ -92,26 +63,6 @@ function normalizeContinueSeatId(value) {
92
63
  return seatId || null;
93
64
  }
94
65
 
95
- function normalizeContinueTargets(targets, defaultFlowMode = "off") {
96
- const normalized = [];
97
- const seen = new Set();
98
-
99
- for (const entry of Array.isArray(targets) ? targets : []) {
100
- const targetSeatId = normalizeSeatId(entry?.targetSeatId ?? entry?.seatId ?? entry);
101
- if (!targetSeatId || seen.has(targetSeatId)) {
102
- continue;
103
- }
104
-
105
- seen.add(targetSeatId);
106
- normalized.push({
107
- targetSeatId,
108
- flowMode: normalizeFlowMode(entry?.flowMode ?? entry?.flow ?? defaultFlowMode),
109
- });
110
- }
111
-
112
- return normalized;
113
- }
114
-
115
66
  function resolveShell() {
116
67
  const shell = String(process.env.SHELL || "").trim();
117
68
  return shell || "/bin/bash";
@@ -189,47 +140,19 @@ function sleepSync(ms) {
189
140
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
190
141
  }
191
142
 
192
- function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
193
- const normalizedSeatId = normalizeSeatId(seatId);
194
- const anchorSeatId = getPartnerSeatId(normalizedSeatId);
195
- if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
196
- return null;
197
- }
198
-
143
+ function findExistingSessionName(currentPath = process.cwd()) {
199
144
  const candidates = listSessionNames()
200
145
  .map((sessionName) => {
201
146
  const sessionPaths = getSessionPaths(sessionName);
202
147
  const controller = readJson(sessionPaths.controllerPath, null);
203
- const anchorPaths = getSeatPaths(sessionName, anchorSeatId);
204
- const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
205
- const anchorMeta = readJson(anchorPaths.metaPath, null);
206
- const anchorStatus = readJson(anchorPaths.statusPath, null);
207
- const seatMeta = readJson(seatPaths.metaPath, null);
208
- const seatStatus = readJson(seatPaths.statusPath, null);
209
- const stopRequest = readJson(sessionPaths.stopPath, null);
210
-
211
- const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
148
+ const cwd = controller?.cwd || null;
212
149
  if (!matchesWorkingPath(cwd, currentPath)) {
213
150
  return null;
214
151
  }
215
152
 
216
- const anchorWrapperPid = anchorStatus?.pid || anchorMeta?.pid || null;
217
- const anchorChildPid = anchorStatus?.childPid || anchorMeta?.childPid || null;
218
- const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
219
- const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
220
- const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
221
- const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
153
+ const stopRequest = readJson(sessionPaths.stopPath, null);
222
154
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
223
- const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
224
-
225
- // Accept session if: controller exists AND (anchor is live OR anchor has written meta/status files).
226
- // This handles the startup race: anchor might not have a live PID yet, but if it wrote files, it's initialized.
227
- const controllerExists = controller !== null;
228
- const anchorInitialized = anchorMeta !== null || anchorStatus !== null;
229
- const anchorReady = anchorLive || anchorInitialized;
230
- if (!controllerExists || !anchorReady || seatLive) {
231
- return null;
232
- }
155
+ const createdAtMs = Date.parse(controller?.createdAt || "");
233
156
 
234
157
  if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
235
158
  return null;
@@ -246,10 +169,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
246
169
  return candidates[0]?.sessionName || null;
247
170
  }
248
171
 
249
- function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
172
+ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
250
173
  const deadline = Date.now() + timeoutMs;
251
174
  while (Date.now() <= deadline) {
252
- const sessionName = findJoinableSessionName(currentPath, seatId);
175
+ const sessionName = findExistingSessionName(currentPath);
253
176
  if (sessionName) {
254
177
  return sessionName;
255
178
  }
@@ -260,11 +183,12 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
260
183
  }
261
184
 
262
185
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
263
- if (isAnchorSeat(seatId)) {
264
- return createSessionName(currentPath);
186
+ const existing = findExistingSessionName(currentPath);
187
+ if (existing) {
188
+ return existing;
265
189
  }
266
190
 
267
- return waitForJoinableSessionName(currentPath, seatId);
191
+ return createSessionName(currentPath);
268
192
  }
269
193
 
270
194
  function parseAnswerEntries(text) {
@@ -466,26 +390,6 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
466
390
  return null;
467
391
  }
468
392
 
469
- function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
470
- return JSON.stringify({
471
- type: "muuuuse_pair_claim",
472
- sessionName,
473
- challenge,
474
- seat1PublicKey,
475
- seat2PublicKey,
476
- });
477
- }
478
-
479
- function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
480
- return JSON.stringify({
481
- type: "muuuuse_pair_ack",
482
- sessionName,
483
- challenge,
484
- seat1PublicKey,
485
- seat2PublicKey,
486
- });
487
- }
488
-
489
393
  function buildAnswerSignaturePayload(sessionName, challenge, entry) {
490
394
  return JSON.stringify({
491
395
  type: "muuuuse_answer",
@@ -497,7 +401,6 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
497
401
  seatId: entry.seatId,
498
402
  origin: entry.origin,
499
403
  phase: entry.phase || "final_answer",
500
- flowMode: entry.flowMode || "off",
501
404
  createdAt: entry.createdAt,
502
405
  text: entry.text,
503
406
  });
@@ -517,7 +420,6 @@ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
517
420
  chainId: entry.chainId,
518
421
  hop: entry.hop,
519
422
  sourceAnswerId: entry.id,
520
- flowMode: entry.flowMode || null,
521
423
  publicKey: entry.publicKey || null,
522
424
  signature: entry.signature || null,
523
425
  };
@@ -626,14 +528,6 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
626
528
  return false;
627
529
  }
628
530
 
629
- // Some TUIs treat fast, chunked relay typing as paste input and suppress an
630
- // immediate Enter. A short settle delay keeps submit behavior reliable.
631
- await sleep(TYPE_SUBMIT_DELAY_MS);
632
-
633
- if (shouldAbort() || !child) {
634
- return false;
635
- }
636
-
637
531
  try {
638
532
  child.write("\r");
639
533
  } catch {
@@ -646,30 +540,35 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
646
540
  class ArmedSeat {
647
541
  constructor(options) {
648
542
  this.seatId = options.seatId;
649
- this.partnerSeatId = getPartnerSeatId(options.seatId);
650
- this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
651
- this.flowMode = normalizeFlowMode(options.flowMode);
652
- this.continueTargets = normalizeContinueTargets(
653
- options.continueTargets || (
654
- options.continueSeatId ? [{ targetSeatId: options.continueSeatId, flowMode: options.flowMode }] : []
655
- ),
656
- this.flowMode
657
- );
543
+ this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
658
544
  this.cwd = normalizeWorkingPath(options.cwd);
659
- if (this.continueTargets.some((target) => target.targetSeatId === this.seatId)) {
660
- throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
545
+
546
+ // Auto-link partner seat for backwards compatibility (seat 1→2, seat 2→1)
547
+ // This preserves v5 behavior where odd seats (anchor) initiate relay to even (partner)
548
+ if (this.continueTargets.length === 0) {
549
+ if (this.seatId === 1) {
550
+ // Seat 1 (odd/anchor) relays to seat 2
551
+ this.continueTargets.push({ targetSeatId: 2, flowMode: "on" });
552
+ } else if (this.seatId === 3) {
553
+ // Seat 3 relays to seat 4
554
+ this.continueTargets.push({ targetSeatId: 4, flowMode: "on" });
555
+ }
556
+ // Even seats don't auto-link (they receive from odd partners)
557
+ }
558
+
559
+ if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
560
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
661
561
  }
662
- this.sessionName = resolveSessionName(this.cwd, this.seatId);
562
+ this.sessionName = resolveSessionName(this.cwd);
663
563
  if (!this.sessionName) {
664
564
  throw new Error(
665
- `No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
565
+ `Failed to create or find session in ${this.cwd}.`
666
566
  );
667
567
  }
668
568
  this.sessionPaths = getSessionPaths(this.sessionName);
669
569
  this.paths = getSeatPaths(this.sessionName, this.seatId);
670
- this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
671
570
  this.continueOffset = getFileSize(this.paths.continuePath);
672
- this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
571
+ this.relayTargets = {};
673
572
 
674
573
  this.child = null;
675
574
  this.childPid = null;
@@ -689,9 +588,8 @@ class ArmedSeat {
689
588
  this.recentEmittedAnswers = [];
690
589
  this.trustState = {
691
590
  challenge: null,
692
- peerPublicKey: null,
693
- phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
694
- pairedAt: null,
591
+ phase: "initialized",
592
+ createdAt: null,
695
593
  };
696
594
  this.liveState = {
697
595
  type: null,
@@ -713,11 +611,6 @@ class ArmedSeat {
713
611
  cwd: this.cwd,
714
612
  createdAt: current.createdAt || this.startedAt,
715
613
  updatedAt: new Date().toISOString(),
716
- anchorSeatId: this.anchorSeatId,
717
- partnerSeatId: this.partnerSeatId,
718
- anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
719
- partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
720
- pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
721
614
  ...extra,
722
615
  });
723
616
  }
@@ -729,9 +622,7 @@ class ArmedSeat {
729
622
  writeMeta(extra = {}) {
730
623
  writeJson(this.paths.metaPath, {
731
624
  seatId: this.seatId,
732
- partnerSeatId: this.partnerSeatId,
733
625
  sessionName: this.sessionName,
734
- flowMode: this.flowMode,
735
626
  continueTargets: this.continueTargets,
736
627
  cwd: this.cwd,
737
628
  pid: process.pid,
@@ -745,9 +636,7 @@ class ArmedSeat {
745
636
  writeStatus(extra = {}) {
746
637
  writeJson(this.paths.statusPath, {
747
638
  seatId: this.seatId,
748
- partnerSeatId: this.partnerSeatId,
749
639
  sessionName: this.sessionName,
750
- flowMode: this.flowMode,
751
640
  continueTargets: this.continueTargets,
752
641
  cwd: this.cwd,
753
642
  pid: process.pid,
@@ -760,168 +649,19 @@ class ArmedSeat {
760
649
 
761
650
  initializeTrustMaterial() {
762
651
  this.identity = loadOrCreateSeatIdentity(this.paths);
763
-
764
- if (!isAnchorSeat(this.seatId)) {
765
- return;
766
- }
767
-
768
652
  writeJson(this.paths.challengePath, {
769
653
  sessionName: this.sessionName,
770
- challenge: createId(48),
771
654
  publicKey: this.identity.publicKey,
772
655
  createdAt: new Date().toISOString(),
773
656
  });
774
- this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
775
- this.trustState.peerPublicKey = null;
776
- this.trustState.phase = "waiting_for_peer_signature";
777
- this.trustState.pairedAt = null;
778
- fs.rmSync(this.paths.ackPath, { force: true });
779
- fs.rmSync(this.partnerPaths.claimPath, { force: true });
657
+ this.trustState.phase = "initialized";
658
+ this.trustState.createdAt = new Date().toISOString();
780
659
  }
781
660
 
782
661
  syncTrustState() {
783
662
  if (!this.identity) {
784
663
  this.initializeTrustMaterial();
785
664
  }
786
-
787
- if (isAnchorSeat(this.seatId)) {
788
- this.syncSeatOneTrust();
789
- return;
790
- }
791
-
792
- this.syncSeatTwoTrust();
793
- }
794
-
795
- syncSeatOneTrust() {
796
- const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
797
- if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
798
- this.trustState = {
799
- challenge: null,
800
- peerPublicKey: null,
801
- phase: "waiting_for_peer_signature",
802
- pairedAt: null,
803
- };
804
- return;
805
- }
806
-
807
- this.trustState.challenge = challengeRecord.challenge;
808
- const claim = readJson(this.partnerPaths.claimPath, null);
809
- if (
810
- !claim ||
811
- claim.sessionName !== this.sessionName ||
812
- claim.challenge !== challengeRecord.challenge ||
813
- typeof claim.publicKey !== "string" ||
814
- typeof claim.signature !== "string" ||
815
- !verifyText(
816
- buildClaimMessage(
817
- this.sessionName,
818
- challengeRecord.challenge,
819
- this.identity.publicKey,
820
- claim.publicKey.trim()
821
- ),
822
- claim.signature,
823
- claim.publicKey
824
- )
825
- ) {
826
- this.trustState.peerPublicKey = null;
827
- this.trustState.phase = "waiting_for_peer_signature";
828
- this.trustState.pairedAt = null;
829
- fs.rmSync(this.paths.ackPath, { force: true });
830
- return;
831
- }
832
-
833
- const peerPublicKey = claim.publicKey.trim();
834
- const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
835
- const currentAck = readJson(this.paths.ackPath, null);
836
- const ackIsValid = Boolean(
837
- currentAck &&
838
- currentAck.sessionName === this.sessionName &&
839
- currentAck.challenge === challengeRecord.challenge &&
840
- currentAck.publicKey === this.identity.publicKey &&
841
- currentAck.peerPublicKey === peerPublicKey &&
842
- typeof currentAck.signature === "string" &&
843
- verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
844
- );
845
- if (!ackIsValid) {
846
- writeJson(this.paths.ackPath, {
847
- sessionName: this.sessionName,
848
- challenge: challengeRecord.challenge,
849
- publicKey: this.identity.publicKey,
850
- peerPublicKey,
851
- signature: signText(ackMessage, this.identity.privateKey),
852
- signedAt: new Date().toISOString(),
853
- });
854
- }
855
-
856
- const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
857
- this.trustState.peerPublicKey = peerPublicKey;
858
- this.trustState.phase = "paired";
859
- this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
860
- }
861
-
862
- syncSeatTwoTrust() {
863
- const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
864
- if (!challengeRecord) {
865
- this.trustState = {
866
- challenge: null,
867
- peerPublicKey: null,
868
- phase: "waiting_for_anchor_key",
869
- pairedAt: null,
870
- };
871
- return;
872
- }
873
-
874
- const challenge = challengeRecord.challenge;
875
- const peerPublicKey = challengeRecord.publicKey;
876
- const claimPayload = {
877
- sessionName: this.sessionName,
878
- challenge,
879
- publicKey: this.identity.publicKey,
880
- };
881
- const claimSignature = signText(
882
- buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
883
- this.identity.privateKey
884
- );
885
- const currentClaim = readJson(this.paths.claimPath, null);
886
- if (
887
- !currentClaim ||
888
- currentClaim.sessionName !== claimPayload.sessionName ||
889
- currentClaim.challenge !== claimPayload.challenge ||
890
- currentClaim.publicKey !== claimPayload.publicKey ||
891
- currentClaim.signature !== claimSignature
892
- ) {
893
- writeJson(this.paths.claimPath, {
894
- ...claimPayload,
895
- signature: claimSignature,
896
- signedAt: new Date().toISOString(),
897
- });
898
- }
899
-
900
- const ack = readJson(this.partnerPaths.ackPath, null);
901
- const paired = Boolean(
902
- ack &&
903
- ack.sessionName === this.sessionName &&
904
- ack.challenge === challenge &&
905
- ack.peerPublicKey === this.identity.publicKey &&
906
- ack.publicKey === peerPublicKey &&
907
- typeof ack.signature === "string" &&
908
- verifyText(
909
- buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
910
- ack.signature,
911
- peerPublicKey
912
- )
913
- );
914
-
915
- this.trustState.challenge = challenge;
916
- this.trustState.peerPublicKey = peerPublicKey;
917
- this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
918
- this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
919
- }
920
-
921
- isPaired() {
922
- return this.trustState.phase === "paired" &&
923
- typeof this.trustState.challenge === "string" &&
924
- typeof this.trustState.peerPublicKey === "string";
925
665
  }
926
666
 
927
667
  launchShell() {
@@ -941,7 +681,6 @@ class ArmedSeat {
941
681
  env: childEnv,
942
682
  name: childEnv.TERM,
943
683
  });
944
- bestEffortEnableChildEcho(this.child);
945
684
 
946
685
  this.childPid = this.child.pid;
947
686
  this.writeMeta();
@@ -1079,11 +818,6 @@ class ArmedSeat {
1079
818
  }
1080
819
  }
1081
820
 
1082
- partnerIsLive() {
1083
- const partner = readJson(this.partnerPaths.statusPath, null);
1084
- return Boolean(partner?.pid && isPidAlive(partner.pid));
1085
- }
1086
-
1087
821
  stopRequested() {
1088
822
  const request = readJson(this.sessionPaths.stopPath, null);
1089
823
  if (!request?.requestedAt) {
@@ -1094,23 +828,19 @@ class ArmedSeat {
1094
828
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
1095
829
  }
1096
830
 
1097
- shouldCaptureCommentary() {
1098
- return this.flowMode === "on" || this.continueTargets.some((target) => target.flowMode === "on");
1099
- }
1100
-
1101
- findContinuationTarget(targetSeatId) {
1102
- const normalizedTargetSeatId = normalizeSeatId(targetSeatId);
1103
- if (!normalizedTargetSeatId) {
831
+ findContinuationTarget(targetSeatId = null) {
832
+ const seatIdToFind = targetSeatId || this.continueSeatId;
833
+ if (!seatIdToFind) {
1104
834
  return null;
1105
835
  }
1106
836
 
1107
837
  const candidates = listSessionNames()
1108
838
  .map((sessionName) => {
1109
- if (!getSeatDirIfExists(sessionName, normalizedTargetSeatId)) {
839
+ if (!getSeatDirIfExists(sessionName, seatIdToFind)) {
1110
840
  return null;
1111
841
  }
1112
842
 
1113
- const seat = buildSeatReport(sessionName, normalizedTargetSeatId);
843
+ const seat = buildSeatReport(sessionName, seatIdToFind);
1114
844
  if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1115
845
  return null;
1116
846
  }
@@ -1137,75 +867,6 @@ class ArmedSeat {
1137
867
  };
1138
868
  }
1139
869
 
1140
- async pullPartnerEvents() {
1141
- const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
1142
- this.partnerOffset = nextOffset;
1143
- if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
1144
- return;
1145
- }
1146
-
1147
- const entries = parseAnswerEntries(text);
1148
- for (const entry of entries) {
1149
- if (this.stopped || this.stopRequested()) {
1150
- this.requestStop("stop_requested");
1151
- return;
1152
- }
1153
-
1154
- const inboundFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1155
- if (!shouldAcceptInboundEntry(inboundFlowMode, entry)) {
1156
- continue;
1157
- }
1158
-
1159
- const payload = sanitizeRelayText(entry.text);
1160
- const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
1161
- chainId: entry.chainId || entry.id,
1162
- hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1163
- id: entry.id,
1164
- seatId: entry.seatId,
1165
- origin: entry.origin || "unknown",
1166
- phase: getRelayPhase(entry),
1167
- flowMode: inboundFlowMode,
1168
- createdAt: entry.createdAt,
1169
- text: payload,
1170
- });
1171
- if (
1172
- !payload ||
1173
- entry.challenge !== this.trustState.challenge ||
1174
- entry.publicKey !== this.trustState.peerPublicKey ||
1175
- typeof entry.signature !== "string" ||
1176
- !verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
1177
- ) {
1178
- continue;
1179
- }
1180
-
1181
- const delivered = await sendTextAndEnter(
1182
- this.child,
1183
- payload,
1184
- () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1185
- );
1186
- if (!delivered) {
1187
- this.requestStop("relay_aborted");
1188
- return;
1189
- }
1190
-
1191
- if (this.stopped || this.stopRequested()) {
1192
- this.requestStop("stop_requested");
1193
- return;
1194
- }
1195
-
1196
- const deliveredAtMs = Date.now();
1197
- this.pendingInboundContext = {
1198
- chainId: entry.chainId || entry.id,
1199
- deliveredAtMs,
1200
- expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
1201
- hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1202
- };
1203
- this.relayCount += 1;
1204
- this.rememberInboundRelay(payload);
1205
- this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
1206
- }
1207
- }
1208
-
1209
870
  async pullContinuationEvents() {
1210
871
  const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
1211
872
  this.continueOffset = nextOffset;
@@ -1220,11 +881,6 @@ class ArmedSeat {
1220
881
  return;
1221
882
  }
1222
883
 
1223
- const continueFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1224
- if (!shouldAcceptInboundEntry(continueFlowMode, entry)) {
1225
- continue;
1226
- }
1227
-
1228
884
  const payload = sanitizeRelayText(entry.text);
1229
885
  if (!payload) {
1230
886
  continue;
@@ -1433,13 +1089,12 @@ class ArmedSeat {
1433
1089
  }
1434
1090
 
1435
1091
  const answers = [];
1436
- const captureCommentary = this.shouldCaptureCommentary();
1437
1092
  if (detectedAgent.type === "codex") {
1438
1093
  const result = readCodexAnswers(
1439
1094
  this.liveState.sessionFile,
1440
1095
  this.liveState.offset,
1441
1096
  this.liveState.captureSinceMs,
1442
- { flowMode: captureCommentary }
1097
+ { flowMode: true }
1443
1098
  );
1444
1099
  this.liveState.offset = result.nextOffset;
1445
1100
  answers.push(...result.answers);
@@ -1448,7 +1103,7 @@ class ArmedSeat {
1448
1103
  this.liveState.sessionFile,
1449
1104
  this.liveState.offset,
1450
1105
  this.liveState.captureSinceMs,
1451
- { flowMode: captureCommentary }
1106
+ { flowMode: true }
1452
1107
  );
1453
1108
  this.liveState.offset = result.nextOffset;
1454
1109
  answers.push(...result.answers);
@@ -1457,7 +1112,7 @@ class ArmedSeat {
1457
1112
  this.liveState.sessionFile,
1458
1113
  this.liveState.lastMessageId,
1459
1114
  this.liveState.captureSinceMs,
1460
- { flowMode: captureCommentary }
1115
+ { flowMode: true }
1461
1116
  );
1462
1117
  this.liveState.lastMessageId = result.lastMessageId;
1463
1118
  this.liveState.offset = result.fileSize;
@@ -1515,7 +1170,6 @@ class ArmedSeat {
1515
1170
  seatId: this.seatId,
1516
1171
  origin: entry.origin || "unknown",
1517
1172
  phase: entry.phase || "final_answer",
1518
- flowMode: this.flowMode,
1519
1173
  text: payload,
1520
1174
  createdAt: entry.createdAt || new Date().toISOString(),
1521
1175
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
@@ -1527,19 +1181,17 @@ class ArmedSeat {
1527
1181
  buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1528
1182
  this.identity.privateKey
1529
1183
  );
1530
-
1531
1184
  appendJsonl(this.paths.eventsPath, signedEntry);
1532
- this.routeToTargets(signedEntry);
1185
+ this.forwardContinuation(signedEntry);
1533
1186
  this.rememberEmittedAnswer(answerKey);
1187
+
1534
1188
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1535
1189
  }
1536
1190
 
1537
- routeToTargets(signedEntry) {
1538
- if (this.continueTargets.length === 0) {
1539
- return;
1540
- }
1541
-
1191
+ forwardContinuation(signedEntry) {
1192
+ // Route to all continueTargets with per-target flow modes
1542
1193
  for (const targetEntry of this.continueTargets) {
1194
+ // Skip entries that don't match the target's flowMode
1543
1195
  if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1544
1196
  continue;
1545
1197
  }
@@ -1550,12 +1202,9 @@ class ArmedSeat {
1550
1202
  continue;
1551
1203
  }
1552
1204
 
1553
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, {
1554
- ...signedEntry,
1555
- flowMode: targetEntry.flowMode,
1556
- });
1205
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1557
1206
  appendJsonl(target.paths.continuePath, continuationEntry);
1558
- this.log(`[${this.seatId} => ${target.seatId} ${targetEntry.flowMode}] ${previewText(continuationEntry.text)}`);
1207
+ this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1559
1208
  }
1560
1209
  }
1561
1210
 
@@ -1563,7 +1212,6 @@ class ArmedSeat {
1563
1212
  if (this.stopRequested()) {
1564
1213
  this.writeStatus({
1565
1214
  state: "stopping",
1566
- partnerLive: this.partnerIsLive(),
1567
1215
  trust: this.trustState.phase,
1568
1216
  });
1569
1217
  this.requestStop("stop_requested");
@@ -1571,7 +1219,6 @@ class ArmedSeat {
1571
1219
  }
1572
1220
 
1573
1221
  this.syncTrustState();
1574
- await this.pullPartnerEvents();
1575
1222
  await this.pullContinuationEvents();
1576
1223
  if (this.stopped || this.stopRequested()) {
1577
1224
  this.requestStop("stop_requested");
@@ -1586,11 +1233,9 @@ class ArmedSeat {
1586
1233
  this.writeStatus({
1587
1234
  state: live.state,
1588
1235
  agent: live.agent,
1589
- flowMode: this.flowMode,
1590
1236
  cwd: live.cwd,
1591
1237
  log: live.log,
1592
1238
  lastAnswerAt: live.lastAnswerAt,
1593
- partnerLive: this.partnerIsLive(),
1594
1239
  trust: this.trustState.phase,
1595
1240
  challengeReady: Boolean(this.trustState.challenge),
1596
1241
  });
@@ -1604,16 +1249,9 @@ class ArmedSeat {
1604
1249
 
1605
1250
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1606
1251
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1607
- this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1608
1252
  if (this.continueTargets.length > 0) {
1609
- this.log(
1610
- `Seat ${this.seatId} continues to ${this.continueTargets.map((target) => `seat ${target.targetSeatId} (${target.flowMode})`).join(", ")}.`
1611
- );
1612
- }
1613
- if (isAnchorSeat(this.seatId)) {
1614
- this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
1615
- } else {
1616
- this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
1253
+ const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
1254
+ this.log(`Seat ${this.seatId} relays to ${targets}.`);
1617
1255
  }
1618
1256
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1619
1257
 
@@ -1709,17 +1347,8 @@ function buildSeatReport(sessionName, seatId) {
1709
1347
 
1710
1348
  return {
1711
1349
  seatId,
1712
- partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
1713
1350
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1714
- flowMode: status?.flowMode || meta?.flowMode || "off",
1715
- continueTargets: normalizeContinueTargets(
1716
- status?.continueTargets || meta?.continueTargets || (
1717
- (status?.continueSeatId || meta?.continueSeatId)
1718
- ? [{ targetSeatId: status?.continueSeatId || meta?.continueSeatId, flowMode: status?.flowMode || meta?.flowMode || "off" }]
1719
- : []
1720
- ),
1721
- status?.flowMode || meta?.flowMode || "off"
1722
- ),
1351
+ continueTargets: status?.continueTargets || meta?.continueTargets || [],
1723
1352
  wrapperPid,
1724
1353
  childPid,
1725
1354
  wrapperLive,
@@ -1733,7 +1362,6 @@ function buildSeatReport(sessionName, seatId) {
1733
1362
  trust: status?.trust || null,
1734
1363
  updatedAt: status?.updatedAt || null,
1735
1364
  lastAnswerAt: status?.lastAnswerAt || null,
1736
- partnerLive: Boolean(status?.partnerLive),
1737
1365
  };
1738
1366
  }
1739
1367
 
@@ -1795,13 +1423,6 @@ function stopAllSessions() {
1795
1423
  signalPid(seat.wrapperPid, "SIGTERM");
1796
1424
  }
1797
1425
  }
1798
-
1799
- // Clean up session directory so stale data doesn't bleed into next session.
1800
- try {
1801
- fs.rmSync(sessionPaths.dir, { recursive: true, force: true });
1802
- } catch {
1803
- // Best-effort cleanup.
1804
- }
1805
1426
  }
1806
1427
 
1807
1428
  return {
package/src/util.js CHANGED
@@ -9,16 +9,7 @@ const fs = require("node:fs");
9
9
  const os = require("node:os");
10
10
  const path = require("node:path");
11
11
 
12
- function getBrand() {
13
- try {
14
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
15
- return `🔌Muuuuse v${pkg.version}`;
16
- } catch {
17
- return "🔌Muuuuse";
18
- }
19
- }
20
-
21
- const BRAND = getBrand();
12
+ const BRAND = "🔌Muuuuse v6.0.0";
22
13
  const POLL_MS = 220;
23
14
  const MAX_RELAY_CHARS = 4000;
24
15
  const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
@@ -184,17 +175,6 @@ function normalizeSeatId(value) {
184
175
  return seatId;
185
176
  }
186
177
 
187
- function isAnchorSeat(seatId) {
188
- return normalizeSeatId(seatId) % 2 === 1;
189
- }
190
-
191
- function getPartnerSeatId(seatId) {
192
- const normalized = normalizeSeatId(seatId);
193
- if (!normalized) {
194
- return null;
195
- }
196
- return isAnchorSeat(normalized) ? normalized + 1 : normalized - 1;
197
- }
198
178
 
199
179
  function listSeatIds(sessionName) {
200
180
  const sessionDir = getSessionDir(sessionName);
@@ -291,34 +271,28 @@ function listSessionNames() {
291
271
 
292
272
  function usage() {
293
273
  return [
294
- `${BRAND} arms terminals with flexible routing to any targets - pure message relay graph.`,
274
+ `${BRAND} relay protocol for long-horizon zero-drift agentic code loops. agents relay output between terminals, converging to lucid conclusions.`,
295
275
  "",
296
276
  "Usage:",
297
- " muuuuse",
298
277
  " muuuuse 1",
299
- " muuuuse 1 link 2 flow off",
300
278
  " muuuuse 1 link 2 flow on",
301
279
  " muuuuse 1 link 2 flow on 3 flow off",
302
280
  " muuuuse 2",
281
+ " muuuuse 2 link 3 flow on",
303
282
  " muuuuse 3",
304
- " muuuuse 4",
305
- " muuuuse 4 link 3 flow off 1 flow on",
306
283
  " muuuuse stop",
307
284
  " muuuuse status",
308
285
  "",
309
286
  "Flow:",
310
- " 1. Run `muuuuse 1` in terminal one, then `muuuuse 2` in terminal two.",
311
- " 2. Bare seats default to final-only pair relay.",
312
- " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
313
- " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
314
- " 5. Use `link <seat> flow on [<seat> flow off ...]` to set each outbound route.",
315
- " 6. Include the odd/even partner in `link` to set normal pair flow.",
316
- " 7. Any extra linked seats receive routed copies with their own flow mode.",
317
- " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
318
- " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
287
+ " 1. Run `muuuuse <seat>` in each terminal to arm it (any seat number, any count).",
288
+ " 2. Optionally add `link <target> flow on/off [<target> flow on/off ...]` to relay output to other seats.",
289
+ " 3. Use those armed shells normally. Codex, Claude, and Gemini relay automatically from their local session logs.",
290
+ " 4. `flow off` sends final answers only. `flow on` sends both commentary and final answers.",
291
+ " 5. Run `muuuuse status` or `muuuuse stop` from any terminal.",
319
292
  "",
320
293
  "Notes:",
321
- " - `muuuuse stop` and `muuuuse status` work from another terminal or the same one.",
294
+ " - Any seat can relay to any other seat independently.",
295
+ " - `muuuuse stop` and `muuuuse status` work from any terminal.",
322
296
  " - State lives under `~/.muuuuse`.",
323
297
  ].join("\n");
324
298
  }
@@ -332,9 +306,7 @@ module.exports = {
332
306
  ensureDir,
333
307
  getDefaultSessionName,
334
308
  getFileSize,
335
- getPartnerSeatId,
336
309
  loadOrCreateSeatIdentity,
337
- isAnchorSeat,
338
310
  getSeatPaths,
339
311
  getSessionPaths,
340
312
  getStateRoot,