muuuuse 5.5.4 → 7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "5.5.4",
4
- "description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
3
+ "version": "7.0.0",
4
+ "description": "🔌Muuuuse relay protocol for long-horizon zero-drift agentic code loops. Any seat relays to any other with per-target signed trust and flow control.",
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
@@ -468,7 +468,9 @@ function parseCodexAssistantLine(line, options = {}) {
468
468
  }
469
469
 
470
470
  const phase = String(entry.payload?.phase || "").trim().toLowerCase();
471
- const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
471
+ // Newer Codex sessions can omit `payload.phase` for final answers.
472
+ const normalizedPhase = phase === "commentary" ? "commentary" : "final_answer";
473
+ const relayablePhase = normalizedPhase === "final_answer" || (flowMode && normalizedPhase === "commentary");
472
474
  if (!relayablePhase) {
473
475
  return null;
474
476
  }
@@ -481,7 +483,7 @@ function parseCodexAssistantLine(line, options = {}) {
481
483
  return {
482
484
  id: entry.payload.id || hashText(line),
483
485
  text,
484
- phase: phase === "commentary" ? "commentary" : "final_answer",
486
+ phase: normalizedPhase,
485
487
  timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
486
488
  };
487
489
  } catch {
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,12 +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, continueSeatId, 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
- continueSeatId,
64
- flowMode,
65
63
  seatId,
66
64
  });
67
65
  const code = await seat.run();
@@ -75,15 +73,11 @@ function renderSeatStatus(seat) {
75
73
  const bits = [
76
74
  `seat ${seat.seatId}: ${seat.state}`,
77
75
  `agent ${seat.agent || "idle"}`,
78
- `flow ${seat.flowMode || "off"}`,
79
76
  `relays ${seat.relayCount}`,
80
77
  `wrapper ${seat.wrapperPid || "-"}`,
81
78
  `child ${seat.childPid || "-"}`,
82
79
  ];
83
80
 
84
- if (seat.partnerLive) {
85
- bits.push("peer live");
86
- }
87
81
  const renderedLinks = renderLinkTargets(seat);
88
82
  if (renderedLinks) {
89
83
  bits.push(`link ${renderedLinks}`);
@@ -106,74 +100,77 @@ function renderSeatStatus(seat) {
106
100
  }
107
101
 
108
102
  function renderLinkTargets(seat) {
109
- const targets = [];
110
- if (seat.partnerSeatId) {
111
- targets.push({
112
- targetSeatId: seat.partnerSeatId,
113
- flowMode: seat.flowMode || "off",
114
- });
115
- }
116
- for (const target of Array.isArray(seat.continueTargets) ? seat.continueTargets : []) {
117
- targets.push(target);
118
- }
119
-
103
+ const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
120
104
  return targets
121
- .map((target) => `${target.targetSeatId}:${target.flowMode}`)
105
+ .map((target) => `${target.targetSeatId} (flow ${target.flowMode})`)
122
106
  .join(", ");
123
107
  }
124
108
 
125
109
  function parseSeatOptions(command, args) {
126
110
  const seatId = normalizeSeatId(command);
127
- let flowMode = "off";
128
- let continueSeatId = null;
129
111
  let continueTargets = [];
130
112
  let index = 0;
113
+ let seatFlowMode = null;
114
+ let hasExplicitTarget = false;
115
+
116
+ const flowToken = String(args[index] || "").trim().toLowerCase();
117
+ if (flowToken === "flow") {
118
+ const parsedSeatFlow = parseFlowModeToken("flow", args[index + 1]);
119
+ if (!parsedSeatFlow) {
120
+ throw new Error(
121
+ `\`muuuuse ${command} flow\` requires \`on\` or \`off\`.`
122
+ );
123
+ }
124
+ seatFlowMode = parsedSeatFlow;
125
+ index += 2;
126
+ }
131
127
 
132
128
  while (index < args.length) {
133
129
  const token = String(args[index] || "").trim().toLowerCase();
134
130
 
135
- if (token === "flow") {
136
- const flowToken = String(args[index + 1] || "").trim().toLowerCase();
137
- if (flowToken === "on" || flowToken === "off") {
138
- flowMode = flowToken;
139
- index += 2;
140
- continue;
141
- }
142
- break;
143
- }
144
-
145
- if (token === "continue") {
146
- const parsedTargets = parseContinueTargets(args.slice(index + 1), flowMode);
147
- if (parsedTargets.targets.length > 0) {
148
- continueTargets = mergeTargets(continueTargets, parsedTargets.targets);
149
- continueSeatId = continueTargets[0].targetSeatId;
150
- index += 1 + parsedTargets.consumed;
151
- continue;
152
- }
153
- break;
154
- }
155
-
156
131
  if (token === "link") {
157
- const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId, flowMode);
132
+ const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
158
133
  if (parsedLinks.consumed > 0) {
159
- flowMode = parsedLinks.flowMode;
160
134
  continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
161
- continueSeatId = continueTargets[0]?.targetSeatId || null;
135
+ hasExplicitTarget = true;
162
136
  index += 1 + parsedLinks.consumed;
163
137
  continue;
164
138
  }
165
139
  break;
166
140
  }
167
141
 
142
+ if (token === "continue") {
143
+ const targetSeatId = normalizeSeatId(args[index + 1]);
144
+ if (!targetSeatId) {
145
+ break;
146
+ }
147
+ upsertTarget(continueTargets, {
148
+ targetSeatId,
149
+ flowMode: seatFlowMode || "on",
150
+ });
151
+ hasExplicitTarget = true;
152
+ index += 2;
153
+ continue;
154
+ }
155
+
168
156
  break;
169
157
  }
170
158
 
171
159
  if (index === args.length) {
172
- return { flowMode, continueSeatId, continueTargets };
160
+ if (seatFlowMode && !hasExplicitTarget) {
161
+ const partnerSeatId = seatId % 2 === 0 ? seatId - 1 : seatId + 1;
162
+ if (partnerSeatId > 0) {
163
+ upsertTarget(continueTargets, {
164
+ targetSeatId: partnerSeatId,
165
+ flowMode: seatFlowMode,
166
+ });
167
+ }
168
+ }
169
+ return { continueTargets };
173
170
  }
174
171
 
175
172
  throw new Error(
176
- `\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat>\`, or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
173
+ `\`muuuuse ${command}\` accepts no extra arguments, optional \`flow on/off\`, \`continue <seat>\`, or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
177
174
  );
178
175
  }
179
176
 
@@ -189,32 +186,8 @@ function mergeTargets(existingTargets, nextTargets) {
189
186
  return merged;
190
187
  }
191
188
 
192
- function parseContinueTargets(args, defaultFlowMode) {
193
- const targets = [];
194
- let consumed = 0;
195
-
196
- while (consumed < args.length) {
197
- const targetSeatId = normalizeSeatId(args[consumed]);
198
- if (!targetSeatId) {
199
- break;
200
- }
201
-
202
- const nextFlowMode = parseFlowModeToken(args[consumed + 1], args[consumed + 2]);
203
- const target = {
204
- targetSeatId,
205
- flowMode: nextFlowMode || defaultFlowMode,
206
- };
207
- upsertTarget(targets, target);
208
- consumed += nextFlowMode ? 3 : 1;
209
- }
210
-
211
- return { consumed, targets };
212
- }
213
-
214
- function parseLinkTargets(args, seatId, defaultFlowMode) {
215
- const partnerSeatId = seatId ? getPartnerSeatId(seatId) : null;
189
+ function parseLinkTargets(args, seatId) {
216
190
  const continueTargets = [];
217
- let flowMode = defaultFlowMode;
218
191
  let consumed = 0;
219
192
 
220
193
  while (consumed < args.length) {
@@ -228,19 +201,15 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
228
201
  break;
229
202
  }
230
203
 
231
- if (targetSeatId === partnerSeatId) {
232
- flowMode = targetFlowMode;
233
- } else {
234
- upsertTarget(continueTargets, {
235
- targetSeatId,
236
- flowMode: targetFlowMode,
237
- });
238
- }
204
+ upsertTarget(continueTargets, {
205
+ targetSeatId,
206
+ flowMode: targetFlowMode,
207
+ });
239
208
 
240
209
  consumed += 3;
241
210
  }
242
211
 
243
- return { consumed, continueTargets, flowMode };
212
+ return { consumed, continueTargets };
244
213
  }
245
214
 
246
215
  function parseFlowModeToken(flowToken, modeToken) {
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,
@@ -60,11 +58,6 @@ function normalizeFlowMode(flowMode) {
60
58
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
61
59
  }
62
60
 
63
- function normalizeContinueSeatId(value) {
64
- const seatId = normalizeSeatId(value);
65
- return seatId || null;
66
- }
67
-
68
61
  function resolveShell() {
69
62
  const shell = String(process.env.SHELL || "").trim();
70
63
  return shell || "/bin/bash";
@@ -134,6 +127,11 @@ function createSessionName(currentPath = process.cwd()) {
134
127
  return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
135
128
  }
136
129
 
130
+ function getAnchorSeatId(seatId = 1) {
131
+ const normalizedSeatId = normalizeSeatId(seatId) || 1;
132
+ return normalizedSeatId % 2 === 0 ? normalizedSeatId - 1 : normalizedSeatId;
133
+ }
134
+
137
135
  function sleepSync(ms) {
138
136
  if (!Number.isFinite(ms) || ms <= 0) {
139
137
  return;
@@ -142,45 +140,33 @@ function sleepSync(ms) {
142
140
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
143
141
  }
144
142
 
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
-
143
+ function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = null) {
144
+ const targetAnchorSeatId = normalizeSeatId(anchorSeatId) || null;
152
145
  const candidates = listSessionNames()
153
146
  .map((sessionName) => {
154
147
  const sessionPaths = getSessionPaths(sessionName);
155
148
  const controller = readJson(sessionPaths.controllerPath, 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);
162
- const stopRequest = readJson(sessionPaths.stopPath, null);
163
-
164
- const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
149
+ const cwd = controller?.cwd || null;
165
150
  if (!matchesWorkingPath(cwd, currentPath)) {
166
151
  return null;
167
152
  }
168
153
 
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);
154
+ const stopRequest = readJson(sessionPaths.stopPath, null);
175
155
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
176
- const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
156
+ const createdAtMs = Date.parse(controller?.createdAt || "");
177
157
 
178
- if (!anchorLive || seatLive) {
158
+ if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
179
159
  return null;
180
160
  }
181
161
 
182
- if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
183
- return null;
162
+ if (targetAnchorSeatId) {
163
+ const controllerAnchorSeatId = normalizeSeatId(controller?.anchorSeatId);
164
+ if (controllerAnchorSeatId && controllerAnchorSeatId !== targetAnchorSeatId) {
165
+ return null;
166
+ }
167
+ if (!controllerAnchorSeatId && !getSeatDirIfExists(sessionName, targetAnchorSeatId)) {
168
+ return null;
169
+ }
184
170
  }
185
171
 
186
172
  return {
@@ -194,10 +180,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
194
180
  return candidates[0]?.sessionName || null;
195
181
  }
196
182
 
197
- function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
183
+ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS, anchorSeatId = null) {
198
184
  const deadline = Date.now() + timeoutMs;
199
185
  while (Date.now() <= deadline) {
200
- const sessionName = findJoinableSessionName(currentPath, seatId);
186
+ const sessionName = findExistingSessionName(currentPath, anchorSeatId);
201
187
  if (sessionName) {
202
188
  return sessionName;
203
189
  }
@@ -208,11 +194,20 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
208
194
  }
209
195
 
210
196
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
211
- if (isAnchorSeat(seatId)) {
212
- return createSessionName(currentPath);
197
+ const anchorSeatId = getAnchorSeatId(seatId);
198
+ const existing = findExistingSessionName(currentPath, anchorSeatId);
199
+ if (existing) {
200
+ return existing;
201
+ }
202
+
203
+ if (seatId % 2 === 0) {
204
+ const waited = waitForExistingSessionName(currentPath, SEAT_JOIN_WAIT_MS, anchorSeatId);
205
+ if (waited) {
206
+ return waited;
207
+ }
213
208
  }
214
209
 
215
- return waitForJoinableSessionName(currentPath, seatId);
210
+ return createSessionName(currentPath);
216
211
  }
217
212
 
218
213
  function parseAnswerEntries(text) {
@@ -414,26 +409,6 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
414
409
  return null;
415
410
  }
416
411
 
417
- function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
418
- return JSON.stringify({
419
- type: "muuuuse_pair_claim",
420
- sessionName,
421
- challenge,
422
- seat1PublicKey,
423
- seat2PublicKey,
424
- });
425
- }
426
-
427
- function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
428
- return JSON.stringify({
429
- type: "muuuuse_pair_ack",
430
- sessionName,
431
- challenge,
432
- seat1PublicKey,
433
- seat2PublicKey,
434
- });
435
- }
436
-
437
412
  function buildAnswerSignaturePayload(sessionName, challenge, entry) {
438
413
  return JSON.stringify({
439
414
  type: "muuuuse_answer",
@@ -584,26 +559,52 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
584
559
  class ArmedSeat {
585
560
  constructor(options) {
586
561
  this.seatId = options.seatId;
587
- this.partnerSeatId = getPartnerSeatId(options.seatId);
588
- this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
589
- this.flowMode = normalizeFlowMode(options.flowMode);
590
- this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
562
+ this.anchorSeatId = getAnchorSeatId(this.seatId);
563
+ this.partnerSeatId = this.seatId % 2 === 0 ? this.seatId - 1 : this.seatId + 1;
591
564
  this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
592
565
  this.cwd = normalizeWorkingPath(options.cwd);
593
- if (this.continueSeatId === this.seatId || this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
594
- throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
566
+
567
+ // Auto-link adjacent partner seat for backwards compatibility (1↔2, 3↔4, ...).
568
+ if (this.continueTargets.length === 0) {
569
+ if (this.partnerSeatId > 0) {
570
+ this.continueTargets.push({ targetSeatId: this.partnerSeatId, flowMode: "on" });
571
+ }
572
+ }
573
+
574
+ if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
575
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
595
576
  }
596
577
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
597
578
  if (!this.sessionName) {
598
579
  throw new Error(
599
- `No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
580
+ `Failed to create or find session in ${this.cwd}.`
600
581
  );
601
582
  }
602
583
  this.sessionPaths = getSessionPaths(this.sessionName);
603
584
  this.paths = getSeatPaths(this.sessionName, this.seatId);
604
- this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
605
585
  this.continueOffset = getFileSize(this.paths.continuePath);
606
- this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
586
+
587
+ // Per-target trust state, paths, and event offsets for the signed relay channel.
588
+ // Only create directories for same-session targets (same anchor pair).
589
+ // Cross-session targets use the continuation channel and don't need local seat dirs.
590
+ const ownAnchor = getAnchorSeatId(this.seatId);
591
+ this.targetTrust = {};
592
+ this.targetPaths = {};
593
+ this.targetOffsets = {};
594
+ for (const t of this.continueTargets) {
595
+ const sameSession = getAnchorSeatId(t.targetSeatId) === ownAnchor;
596
+ this.targetTrust[t.targetSeatId] = {
597
+ challenge: null,
598
+ peerPublicKey: null,
599
+ phase: "initializing",
600
+ pairedAt: null,
601
+ sameSession,
602
+ };
603
+ this.targetPaths[t.targetSeatId] = sameSession
604
+ ? getSeatPaths(this.sessionName, t.targetSeatId)
605
+ : null;
606
+ this.targetOffsets[t.targetSeatId] = 0;
607
+ }
607
608
 
608
609
  this.child = null;
609
610
  this.childPid = null;
@@ -617,16 +618,11 @@ class ArmedSeat {
617
618
  this.resizeCleanup = null;
618
619
  this.forceKillTimer = null;
619
620
  this.identity = null;
621
+ this.ownChallenge = null;
620
622
  this.lastUserInputAtMs = 0;
621
623
  this.pendingInboundContext = null;
622
624
  this.recentInboundRelays = [];
623
625
  this.recentEmittedAnswers = [];
624
- this.trustState = {
625
- challenge: null,
626
- peerPublicKey: null,
627
- phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
628
- pairedAt: null,
629
- };
630
626
  this.liveState = {
631
627
  type: null,
632
628
  pid: null,
@@ -649,9 +645,6 @@ class ArmedSeat {
649
645
  updatedAt: new Date().toISOString(),
650
646
  anchorSeatId: this.anchorSeatId,
651
647
  partnerSeatId: this.partnerSeatId,
652
- anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
653
- partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
654
- pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
655
648
  ...extra,
656
649
  });
657
650
  }
@@ -663,10 +656,7 @@ class ArmedSeat {
663
656
  writeMeta(extra = {}) {
664
657
  writeJson(this.paths.metaPath, {
665
658
  seatId: this.seatId,
666
- partnerSeatId: this.partnerSeatId,
667
659
  sessionName: this.sessionName,
668
- flowMode: this.flowMode,
669
- continueSeatId: this.continueSeatId,
670
660
  continueTargets: this.continueTargets,
671
661
  cwd: this.cwd,
672
662
  pid: process.pid,
@@ -680,10 +670,7 @@ class ArmedSeat {
680
670
  writeStatus(extra = {}) {
681
671
  writeJson(this.paths.statusPath, {
682
672
  seatId: this.seatId,
683
- partnerSeatId: this.partnerSeatId,
684
673
  sessionName: this.sessionName,
685
- flowMode: this.flowMode,
686
- continueSeatId: this.continueSeatId,
687
674
  continueTargets: this.continueTargets,
688
675
  cwd: this.cwd,
689
676
  pid: process.pid,
@@ -696,168 +683,84 @@ class ArmedSeat {
696
683
 
697
684
  initializeTrustMaterial() {
698
685
  this.identity = loadOrCreateSeatIdentity(this.paths);
699
-
700
- if (!isAnchorSeat(this.seatId)) {
701
- return;
702
- }
703
-
686
+ const ownChallenge = createId(48);
704
687
  writeJson(this.paths.challengePath, {
705
688
  sessionName: this.sessionName,
706
- challenge: createId(48),
689
+ challenge: ownChallenge,
707
690
  publicKey: this.identity.publicKey,
708
691
  createdAt: new Date().toISOString(),
709
692
  });
710
- this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
711
- this.trustState.peerPublicKey = null;
712
- this.trustState.phase = "waiting_for_peer_signature";
713
- this.trustState.pairedAt = null;
714
- fs.rmSync(this.paths.ackPath, { force: true });
715
- fs.rmSync(this.partnerPaths.claimPath, { force: true });
693
+ this.ownChallenge = ownChallenge;
716
694
  }
717
695
 
718
- syncTrustState() {
696
+ syncTargetTrust() {
719
697
  if (!this.identity) {
720
698
  this.initializeTrustMaterial();
721
699
  }
722
700
 
723
- if (isAnchorSeat(this.seatId)) {
724
- this.syncSeatOneTrust();
725
- return;
701
+ for (const target of this.continueTargets) {
702
+ this.syncOneTargetTrust(target.targetSeatId);
726
703
  }
727
-
728
- this.syncSeatTwoTrust();
729
704
  }
730
705
 
731
- syncSeatOneTrust() {
732
- const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
733
- if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
734
- this.trustState = {
735
- challenge: null,
736
- peerPublicKey: null,
737
- phase: "waiting_for_peer_signature",
738
- pairedAt: null,
739
- };
706
+ syncOneTargetTrust(targetSeatId) {
707
+ const trust = this.targetTrust[targetSeatId];
708
+ if (!trust || trust.phase === "paired") {
740
709
  return;
741
710
  }
742
711
 
743
- this.trustState.challenge = challengeRecord.challenge;
744
- const claim = readJson(this.partnerPaths.claimPath, null);
745
- if (
746
- !claim ||
747
- claim.sessionName !== this.sessionName ||
748
- claim.challenge !== challengeRecord.challenge ||
749
- typeof claim.publicKey !== "string" ||
750
- typeof claim.signature !== "string" ||
751
- !verifyText(
752
- buildClaimMessage(
753
- this.sessionName,
754
- challengeRecord.challenge,
755
- this.identity.publicKey,
756
- claim.publicKey.trim()
757
- ),
758
- claim.signature,
759
- claim.publicKey
760
- )
761
- ) {
762
- this.trustState.peerPublicKey = null;
763
- this.trustState.phase = "waiting_for_peer_signature";
764
- this.trustState.pairedAt = null;
765
- fs.rmSync(this.paths.ackPath, { force: true });
712
+ // Cross-session target: relay goes through the continuation channel only.
713
+ if (!trust.sameSession) {
714
+ trust.phase = "paired";
715
+ trust.pairedAt = new Date().toISOString();
766
716
  return;
767
717
  }
768
718
 
769
- const peerPublicKey = claim.publicKey.trim();
770
- const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
771
- const currentAck = readJson(this.paths.ackPath, null);
772
- const ackIsValid = Boolean(
773
- currentAck &&
774
- currentAck.sessionName === this.sessionName &&
775
- currentAck.challenge === challengeRecord.challenge &&
776
- currentAck.publicKey === this.identity.publicKey &&
777
- currentAck.peerPublicKey === peerPublicKey &&
778
- typeof currentAck.signature === "string" &&
779
- verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
780
- );
781
- if (!ackIsValid) {
782
- writeJson(this.paths.ackPath, {
783
- sessionName: this.sessionName,
784
- challenge: challengeRecord.challenge,
785
- publicKey: this.identity.publicKey,
786
- peerPublicKey,
787
- signature: signText(ackMessage, this.identity.privateKey),
788
- signedAt: new Date().toISOString(),
789
- });
719
+ // Same-session target: read their challenge.json to get their public key
720
+ // and challenge. One-way trust — we just need to verify their events.
721
+ const targetPaths = this.targetPaths[targetSeatId];
722
+ if (!targetPaths) {
723
+ return;
790
724
  }
791
725
 
792
- const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
793
- this.trustState.peerPublicKey = peerPublicKey;
794
- this.trustState.phase = "paired";
795
- this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
796
- }
797
-
798
- syncSeatTwoTrust() {
799
- const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
800
- if (!challengeRecord) {
801
- this.trustState = {
802
- challenge: null,
803
- peerPublicKey: null,
804
- phase: "waiting_for_anchor_key",
805
- pairedAt: null,
806
- };
726
+ const targetChallenge = readSeatChallenge(targetPaths, this.sessionName);
727
+ if (!targetChallenge) {
728
+ trust.phase = "waiting_for_target";
807
729
  return;
808
730
  }
809
731
 
810
- const challenge = challengeRecord.challenge;
811
- const peerPublicKey = challengeRecord.publicKey;
812
- const claimPayload = {
813
- sessionName: this.sessionName,
814
- challenge,
815
- publicKey: this.identity.publicKey,
816
- };
817
- const claimSignature = signText(
818
- buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
819
- this.identity.privateKey
820
- );
821
- const currentClaim = readJson(this.paths.claimPath, null);
822
- if (
823
- !currentClaim ||
824
- currentClaim.sessionName !== claimPayload.sessionName ||
825
- currentClaim.challenge !== claimPayload.challenge ||
826
- currentClaim.publicKey !== claimPayload.publicKey ||
827
- currentClaim.signature !== claimSignature
828
- ) {
829
- writeJson(this.paths.claimPath, {
830
- ...claimPayload,
831
- signature: claimSignature,
832
- signedAt: new Date().toISOString(),
833
- });
834
- }
732
+ trust.challenge = targetChallenge.challenge;
733
+ trust.peerPublicKey = targetChallenge.publicKey;
734
+ trust.phase = "paired";
735
+ trust.pairedAt = new Date().toISOString();
835
736
 
836
- const ack = readJson(this.partnerPaths.ackPath, null);
837
- const paired = Boolean(
838
- ack &&
839
- ack.sessionName === this.sessionName &&
840
- ack.challenge === challenge &&
841
- ack.peerPublicKey === this.identity.publicKey &&
842
- ack.publicKey === peerPublicKey &&
843
- typeof ack.signature === "string" &&
844
- verifyText(
845
- buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
846
- ack.signature,
847
- peerPublicKey
848
- )
849
- );
737
+ // Initialize offset to current file size so we only read new events.
738
+ this.targetOffsets[targetSeatId] = getFileSize(targetPaths.eventsPath);
739
+ }
850
740
 
851
- this.trustState.challenge = challenge;
852
- this.trustState.peerPublicKey = peerPublicKey;
853
- this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
854
- this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
741
+ isTargetPaired(targetSeatId) {
742
+ const trust = this.targetTrust[targetSeatId];
743
+ return Boolean(trust && trust.phase === "paired");
855
744
  }
856
745
 
857
- isPaired() {
858
- return this.trustState.phase === "paired" &&
859
- typeof this.trustState.challenge === "string" &&
860
- typeof this.trustState.peerPublicKey === "string";
746
+ getOverallTrustPhase() {
747
+ const targets = this.continueTargets;
748
+ if (targets.length === 0) {
749
+ return "paired";
750
+ }
751
+ const allPaired = targets.every((t) => this.isTargetPaired(t.targetSeatId));
752
+ if (allPaired) {
753
+ return "paired";
754
+ }
755
+ const anyPaired = targets.some((t) => this.isTargetPaired(t.targetSeatId));
756
+ if (anyPaired) {
757
+ return "partial";
758
+ }
759
+ return "initializing";
760
+ }
761
+
762
+ hasAnyPairedTarget() {
763
+ return this.continueTargets.some((t) => this.isTargetPaired(t.targetSeatId));
861
764
  }
862
765
 
863
766
  launchShell() {
@@ -865,6 +768,7 @@ class ArmedSeat {
865
768
  fs.rmSync(this.paths.pipePath, { force: true });
866
769
  clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
867
770
  this.initializeTrustMaterial();
771
+ this.syncTargetTrust();
868
772
  this.writeController();
869
773
 
870
774
  const shell = resolveShell();
@@ -880,7 +784,7 @@ class ArmedSeat {
880
784
 
881
785
  this.childPid = this.child.pid;
882
786
  this.writeMeta();
883
- this.writeStatus({ state: "running", trust: this.trustState.phase });
787
+ this.writeStatus({ state: "running", trust: this.getOverallTrustPhase() });
884
788
 
885
789
  this.child.onData((data) => {
886
790
  fs.appendFileSync(this.paths.pipePath, data);
@@ -1014,11 +918,6 @@ class ArmedSeat {
1014
918
  }
1015
919
  }
1016
920
 
1017
- partnerIsLive() {
1018
- const partner = readJson(this.partnerPaths.statusPath, null);
1019
- return Boolean(partner?.pid && isPidAlive(partner.pid));
1020
- }
1021
-
1022
921
  stopRequested() {
1023
922
  const request = readJson(this.sessionPaths.stopPath, null);
1024
923
  if (!request?.requestedAt) {
@@ -1068,73 +967,6 @@ class ArmedSeat {
1068
967
  };
1069
968
  }
1070
969
 
1071
- async pullPartnerEvents() {
1072
- const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
1073
- this.partnerOffset = nextOffset;
1074
- if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
1075
- return;
1076
- }
1077
-
1078
- const entries = parseAnswerEntries(text);
1079
- for (const entry of entries) {
1080
- if (this.stopped || this.stopRequested()) {
1081
- this.requestStop("stop_requested");
1082
- return;
1083
- }
1084
-
1085
- if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1086
- continue;
1087
- }
1088
-
1089
- const payload = sanitizeRelayText(entry.text);
1090
- const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
1091
- chainId: entry.chainId || entry.id,
1092
- hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1093
- id: entry.id,
1094
- seatId: entry.seatId,
1095
- origin: entry.origin || "unknown",
1096
- phase: getRelayPhase(entry),
1097
- createdAt: entry.createdAt,
1098
- text: payload,
1099
- });
1100
- if (
1101
- !payload ||
1102
- entry.challenge !== this.trustState.challenge ||
1103
- entry.publicKey !== this.trustState.peerPublicKey ||
1104
- typeof entry.signature !== "string" ||
1105
- !verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
1106
- ) {
1107
- continue;
1108
- }
1109
-
1110
- const delivered = await sendTextAndEnter(
1111
- this.child,
1112
- payload,
1113
- () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1114
- );
1115
- if (!delivered) {
1116
- this.requestStop("relay_aborted");
1117
- return;
1118
- }
1119
-
1120
- if (this.stopped || this.stopRequested()) {
1121
- this.requestStop("stop_requested");
1122
- return;
1123
- }
1124
-
1125
- const deliveredAtMs = Date.now();
1126
- this.pendingInboundContext = {
1127
- chainId: entry.chainId || entry.id,
1128
- deliveredAtMs,
1129
- expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
1130
- hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1131
- };
1132
- this.relayCount += 1;
1133
- this.rememberInboundRelay(payload);
1134
- this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
1135
- }
1136
- }
1137
-
1138
970
  async pullContinuationEvents() {
1139
971
  const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
1140
972
  this.continueOffset = nextOffset;
@@ -1149,10 +981,6 @@ class ArmedSeat {
1149
981
  return;
1150
982
  }
1151
983
 
1152
- if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1153
- continue;
1154
- }
1155
-
1156
984
  const payload = sanitizeRelayText(entry.text);
1157
985
  if (!payload) {
1158
986
  continue;
@@ -1366,7 +1194,7 @@ class ArmedSeat {
1366
1194
  this.liveState.sessionFile,
1367
1195
  this.liveState.offset,
1368
1196
  this.liveState.captureSinceMs,
1369
- { flowMode: this.flowMode === "on" }
1197
+ { flowMode: true }
1370
1198
  );
1371
1199
  this.liveState.offset = result.nextOffset;
1372
1200
  answers.push(...result.answers);
@@ -1375,7 +1203,7 @@ class ArmedSeat {
1375
1203
  this.liveState.sessionFile,
1376
1204
  this.liveState.offset,
1377
1205
  this.liveState.captureSinceMs,
1378
- { flowMode: this.flowMode === "on" }
1206
+ { flowMode: true }
1379
1207
  );
1380
1208
  this.liveState.offset = result.nextOffset;
1381
1209
  answers.push(...result.answers);
@@ -1384,7 +1212,7 @@ class ArmedSeat {
1384
1212
  this.liveState.sessionFile,
1385
1213
  this.liveState.lastMessageId,
1386
1214
  this.liveState.captureSinceMs,
1387
- { flowMode: this.flowMode === "on" }
1215
+ { flowMode: true }
1388
1216
  );
1389
1217
  this.liveState.lastMessageId = result.lastMessageId;
1390
1218
  this.liveState.offset = result.fileSize;
@@ -1417,7 +1245,11 @@ class ArmedSeat {
1417
1245
  }
1418
1246
 
1419
1247
  const payload = sanitizeRelayText(entry.text);
1420
- if (!payload || !this.identity || !this.trustState.challenge) {
1248
+ if (!payload || !this.identity || !this.ownChallenge) {
1249
+ return;
1250
+ }
1251
+
1252
+ if (!this.hasAnyPairedTarget()) {
1421
1253
  return;
1422
1254
  }
1423
1255
 
@@ -1434,8 +1266,10 @@ class ArmedSeat {
1434
1266
  }
1435
1267
 
1436
1268
  const pendingInboundContext = this.getPendingInboundContext();
1437
-
1438
1269
  const entryId = entry.id || createId(12);
1270
+
1271
+ // Sign with OUR OWN challenge. Each reader verifies using our challenge
1272
+ // (which they obtained during the trust handshake as peerChallenge).
1439
1273
  const signedEntry = {
1440
1274
  id: entryId,
1441
1275
  type: "answer",
@@ -1446,61 +1280,67 @@ class ArmedSeat {
1446
1280
  createdAt: entry.createdAt || new Date().toISOString(),
1447
1281
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
1448
1282
  hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
1449
- challenge: this.trustState.challenge,
1283
+ challenge: this.ownChallenge,
1450
1284
  publicKey: this.identity.publicKey,
1451
1285
  };
1452
1286
  signedEntry.signature = signText(
1453
- buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1287
+ buildAnswerSignaturePayload(this.sessionName, this.ownChallenge, signedEntry),
1454
1288
  this.identity.privateKey
1455
1289
  );
1456
1290
  appendJsonl(this.paths.eventsPath, signedEntry);
1457
- this.forwardContinuation(signedEntry);
1458
- this.rememberEmittedAnswer(answerKey);
1459
1291
 
1292
+ // Forward via continuation channel for cross-session targets.
1293
+ for (const target of this.continueTargets) {
1294
+ this.forwardContinuation(signedEntry, target);
1295
+ }
1296
+
1297
+ this.rememberEmittedAnswer(answerKey);
1460
1298
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1461
1299
  }
1462
1300
 
1463
- forwardContinuation(signedEntry) {
1464
- // Route to legacy single continueSeatId if set
1465
- if (this.continueSeatId) {
1466
- const target = this.findContinuationTarget();
1467
- if (!target) {
1468
- this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
1469
- return;
1470
- }
1471
-
1472
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1473
- appendJsonl(target.paths.continuePath, continuationEntry);
1474
- this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1301
+ forwardContinuation(signedEntry, targetEntry) {
1302
+ if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1303
+ return;
1475
1304
  }
1476
1305
 
1477
- // Route to all continueTargets with per-target flow modes
1478
- for (const targetEntry of this.continueTargets) {
1479
- const target = this.findContinuationTarget(targetEntry.targetSeatId);
1480
- if (!target) {
1481
- this.log(`[${this.seatId}] target ${targetEntry.targetSeatId} unavailable`);
1482
- continue;
1306
+ // Same-session target: write directly to their continuation channel.
1307
+ const trust = this.targetTrust[targetEntry.targetSeatId];
1308
+ if (trust && trust.sameSession) {
1309
+ const targetPaths = this.targetPaths[targetEntry.targetSeatId];
1310
+ if (targetPaths) {
1311
+ const continuationEntry = buildContinuationEntry(this.sessionName, targetEntry.targetSeatId, signedEntry);
1312
+ appendJsonl(targetPaths.continuePath, continuationEntry);
1313
+ this.log(`[${this.seatId} => ${targetEntry.targetSeatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1483
1314
  }
1315
+ return;
1316
+ }
1484
1317
 
1485
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1486
- appendJsonl(target.paths.continuePath, continuationEntry);
1487
- this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1318
+ // Cross-session target: find the target across sessions.
1319
+ const target = this.findContinuationTarget(targetEntry.targetSeatId);
1320
+ if (!target) {
1321
+ return;
1488
1322
  }
1323
+
1324
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1325
+ appendJsonl(target.paths.continuePath, continuationEntry);
1326
+ this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1489
1327
  }
1490
1328
 
1491
1329
  async tick() {
1492
1330
  if (this.stopRequested()) {
1493
1331
  this.writeStatus({
1494
1332
  state: "stopping",
1495
- partnerLive: this.partnerIsLive(),
1496
- trust: this.trustState.phase,
1333
+ trust: this.getOverallTrustPhase(),
1497
1334
  });
1498
1335
  this.requestStop("stop_requested");
1499
1336
  return;
1500
1337
  }
1501
1338
 
1502
- this.syncTrustState();
1503
- await this.pullPartnerEvents();
1339
+ this.syncTargetTrust();
1340
+ if (this.stopped || this.stopRequested()) {
1341
+ this.requestStop("stop_requested");
1342
+ return;
1343
+ }
1504
1344
  await this.pullContinuationEvents();
1505
1345
  if (this.stopped || this.stopRequested()) {
1506
1346
  this.requestStop("stop_requested");
@@ -1515,13 +1355,11 @@ class ArmedSeat {
1515
1355
  this.writeStatus({
1516
1356
  state: live.state,
1517
1357
  agent: live.agent,
1518
- flowMode: this.flowMode,
1519
1358
  cwd: live.cwd,
1520
1359
  log: live.log,
1521
1360
  lastAnswerAt: live.lastAnswerAt,
1522
- partnerLive: this.partnerIsLive(),
1523
- trust: this.trustState.phase,
1524
- challengeReady: Boolean(this.trustState.challenge),
1361
+ trust: this.getOverallTrustPhase(),
1362
+ challengeReady: this.hasAnyPairedTarget(),
1525
1363
  });
1526
1364
  }
1527
1365
 
@@ -1533,18 +1371,9 @@ class ArmedSeat {
1533
1371
 
1534
1372
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1535
1373
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1536
- this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1537
- if (this.continueSeatId) {
1538
- this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1539
- }
1540
1374
  if (this.continueTargets.length > 0) {
1541
- const targets = this.continueTargets.map((t) => `${t.targetSeatId} (${t.flowMode})`).join(", ");
1542
- this.log(`Seat ${this.seatId} links to ${targets}.`);
1543
- }
1544
- if (isAnchorSeat(this.seatId)) {
1545
- this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
1546
- } else {
1547
- this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
1375
+ const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
1376
+ this.log(`Seat ${this.seatId} relays to ${targets}. Establishing trust.`);
1548
1377
  }
1549
1378
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1550
1379
 
@@ -1641,8 +1470,6 @@ function buildSeatReport(sessionName, seatId) {
1641
1470
  return {
1642
1471
  seatId,
1643
1472
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1644
- flowMode: status?.flowMode || meta?.flowMode || "off",
1645
- continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1646
1473
  continueTargets: status?.continueTargets || meta?.continueTargets || [],
1647
1474
  wrapperPid,
1648
1475
  childPid,
@@ -1657,7 +1484,6 @@ function buildSeatReport(sessionName, seatId) {
1657
1484
  trust: status?.trust || null,
1658
1485
  updatedAt: status?.updatedAt || null,
1659
1486
  lastAnswerAt: status?.lastAnswerAt || null,
1660
- partnerLive: Boolean(status?.partnerLive),
1661
1487
  };
1662
1488
  }
1663
1489
 
package/src/util.js CHANGED
@@ -9,7 +9,7 @@ const fs = require("node:fs");
9
9
  const os = require("node:os");
10
10
  const path = require("node:path");
11
11
 
12
- const BRAND = "🔌Muuuuse";
12
+ const BRAND = "🔌Muuuuse v7.0.0";
13
13
  const POLL_MS = 220;
14
14
  const MAX_RELAY_CHARS = 4000;
15
15
  const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
@@ -175,17 +175,6 @@ function normalizeSeatId(value) {
175
175
  return seatId;
176
176
  }
177
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
178
 
190
179
  function listSeatIds(sessionName) {
191
180
  const sessionDir = getSessionDir(sessionName);
@@ -282,36 +271,28 @@ function listSessionNames() {
282
271
 
283
272
  function usage() {
284
273
  return [
285
- `${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
274
+ `${BRAND} relay protocol for long-horizon zero-drift agentic code loops. agents relay output between terminals, converging to lucid conclusions.`,
286
275
  "",
287
276
  "Usage:",
288
277
  " muuuuse 1",
289
- " muuuuse 1 flow on",
290
- " muuuuse 1 flow off",
291
- " muuuuse 1 flow on continue 3",
278
+ " muuuuse 1 link 2 flow on",
279
+ " muuuuse 1 link 2 flow on 3 flow off",
292
280
  " muuuuse 2",
293
- " muuuuse 2 flow on",
294
- " muuuuse 2 flow off",
295
- " muuuuse 2 flow on continue 3",
281
+ " muuuuse 2 link 3 flow on",
296
282
  " muuuuse 3",
297
- " muuuuse 4",
298
- " muuuuse 4 flow on continue 1",
299
283
  " muuuuse stop",
300
284
  " muuuuse status",
301
285
  "",
302
286
  "Flow:",
303
- " 1. Run `muuuuse 1` in terminal one.",
304
- " 2. Run `muuuuse 2` in terminal two.",
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.",
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.",
312
292
  "",
313
293
  "Notes:",
314
- " - `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.",
315
296
  " - State lives under `~/.muuuuse`.",
316
297
  ].join("\n");
317
298
  }
@@ -325,9 +306,7 @@ module.exports = {
325
306
  ensureDir,
326
307
  getDefaultSessionName,
327
308
  getFileSize,
328
- getPartnerSeatId,
329
309
  loadOrCreateSeatIdentity,
330
- isAnchorSeat,
331
310
  getSeatPaths,
332
311
  getSessionPaths,
333
312
  getStateRoot,