muuuuse 5.5.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "5.5.4",
3
+ "version": "6.0.0",
4
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": {
@@ -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/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,17 +100,7 @@ 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
105
  .map((target) => `${target.targetSeatId}:${target.flowMode}`)
122
106
  .join(", ");
@@ -124,41 +108,16 @@ function renderLinkTargets(seat) {
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;
131
113
 
132
114
  while (index < args.length) {
133
115
  const token = String(args[index] || "").trim().toLowerCase();
134
116
 
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
117
  if (token === "link") {
157
- const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId, flowMode);
118
+ const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
158
119
  if (parsedLinks.consumed > 0) {
159
- flowMode = parsedLinks.flowMode;
160
120
  continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
161
- continueSeatId = continueTargets[0]?.targetSeatId || null;
162
121
  index += 1 + parsedLinks.consumed;
163
122
  continue;
164
123
  }
@@ -169,11 +128,11 @@ function parseSeatOptions(command, args) {
169
128
  }
170
129
 
171
130
  if (index === args.length) {
172
- return { flowMode, continueSeatId, continueTargets };
131
+ return { continueTargets };
173
132
  }
174
133
 
175
134
  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.`
135
+ `\`muuuuse ${command}\` accepts no extra arguments or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
177
136
  );
178
137
  }
179
138
 
@@ -189,32 +148,8 @@ function mergeTargets(existingTargets, nextTargets) {
189
148
  return merged;
190
149
  }
191
150
 
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;
151
+ function parseLinkTargets(args, seatId) {
216
152
  const continueTargets = [];
217
- let flowMode = defaultFlowMode;
218
153
  let consumed = 0;
219
154
 
220
155
  while (consumed < args.length) {
@@ -228,19 +163,15 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
228
163
  break;
229
164
  }
230
165
 
231
- if (targetSeatId === partnerSeatId) {
232
- flowMode = targetFlowMode;
233
- } else {
234
- upsertTarget(continueTargets, {
235
- targetSeatId,
236
- flowMode: targetFlowMode,
237
- });
238
- }
166
+ upsertTarget(continueTargets, {
167
+ targetSeatId,
168
+ flowMode: targetFlowMode,
169
+ });
239
170
 
240
171
  consumed += 3;
241
172
  }
242
173
 
243
- return { consumed, continueTargets, flowMode };
174
+ return { consumed, continueTargets };
244
175
  }
245
176
 
246
177
  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,
@@ -142,42 +140,19 @@ 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()) {
152
144
  const candidates = listSessionNames()
153
145
  .map((sessionName) => {
154
146
  const sessionPaths = getSessionPaths(sessionName);
155
147
  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;
148
+ const cwd = controller?.cwd || null;
165
149
  if (!matchesWorkingPath(cwd, currentPath)) {
166
150
  return null;
167
151
  }
168
152
 
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);
153
+ const stopRequest = readJson(sessionPaths.stopPath, null);
175
154
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
176
- const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
177
-
178
- if (!anchorLive || seatLive) {
179
- return null;
180
- }
155
+ const createdAtMs = Date.parse(controller?.createdAt || "");
181
156
 
182
157
  if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
183
158
  return null;
@@ -194,10 +169,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
194
169
  return candidates[0]?.sessionName || null;
195
170
  }
196
171
 
197
- function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
172
+ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
198
173
  const deadline = Date.now() + timeoutMs;
199
174
  while (Date.now() <= deadline) {
200
- const sessionName = findJoinableSessionName(currentPath, seatId);
175
+ const sessionName = findExistingSessionName(currentPath);
201
176
  if (sessionName) {
202
177
  return sessionName;
203
178
  }
@@ -208,11 +183,12 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
208
183
  }
209
184
 
210
185
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
211
- if (isAnchorSeat(seatId)) {
212
- return createSessionName(currentPath);
186
+ const existing = findExistingSessionName(currentPath);
187
+ if (existing) {
188
+ return existing;
213
189
  }
214
190
 
215
- return waitForJoinableSessionName(currentPath, seatId);
191
+ return createSessionName(currentPath);
216
192
  }
217
193
 
218
194
  function parseAnswerEntries(text) {
@@ -414,26 +390,6 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
414
390
  return null;
415
391
  }
416
392
 
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
393
  function buildAnswerSignaturePayload(sessionName, challenge, entry) {
438
394
  return JSON.stringify({
439
395
  type: "muuuuse_answer",
@@ -584,26 +540,35 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
584
540
  class ArmedSeat {
585
541
  constructor(options) {
586
542
  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);
591
543
  this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
592
544
  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.`);
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.`);
595
561
  }
596
- this.sessionName = resolveSessionName(this.cwd, this.seatId);
562
+ this.sessionName = resolveSessionName(this.cwd);
597
563
  if (!this.sessionName) {
598
564
  throw new Error(
599
- `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}.`
600
566
  );
601
567
  }
602
568
  this.sessionPaths = getSessionPaths(this.sessionName);
603
569
  this.paths = getSeatPaths(this.sessionName, this.seatId);
604
- this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
605
570
  this.continueOffset = getFileSize(this.paths.continuePath);
606
- this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
571
+ this.relayTargets = {};
607
572
 
608
573
  this.child = null;
609
574
  this.childPid = null;
@@ -623,9 +588,8 @@ class ArmedSeat {
623
588
  this.recentEmittedAnswers = [];
624
589
  this.trustState = {
625
590
  challenge: null,
626
- peerPublicKey: null,
627
- phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
628
- pairedAt: null,
591
+ phase: "initialized",
592
+ createdAt: null,
629
593
  };
630
594
  this.liveState = {
631
595
  type: null,
@@ -647,11 +611,6 @@ class ArmedSeat {
647
611
  cwd: this.cwd,
648
612
  createdAt: current.createdAt || this.startedAt,
649
613
  updatedAt: new Date().toISOString(),
650
- anchorSeatId: this.anchorSeatId,
651
- 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
614
  ...extra,
656
615
  });
657
616
  }
@@ -663,10 +622,7 @@ class ArmedSeat {
663
622
  writeMeta(extra = {}) {
664
623
  writeJson(this.paths.metaPath, {
665
624
  seatId: this.seatId,
666
- partnerSeatId: this.partnerSeatId,
667
625
  sessionName: this.sessionName,
668
- flowMode: this.flowMode,
669
- continueSeatId: this.continueSeatId,
670
626
  continueTargets: this.continueTargets,
671
627
  cwd: this.cwd,
672
628
  pid: process.pid,
@@ -680,10 +636,7 @@ class ArmedSeat {
680
636
  writeStatus(extra = {}) {
681
637
  writeJson(this.paths.statusPath, {
682
638
  seatId: this.seatId,
683
- partnerSeatId: this.partnerSeatId,
684
639
  sessionName: this.sessionName,
685
- flowMode: this.flowMode,
686
- continueSeatId: this.continueSeatId,
687
640
  continueTargets: this.continueTargets,
688
641
  cwd: this.cwd,
689
642
  pid: process.pid,
@@ -696,168 +649,19 @@ class ArmedSeat {
696
649
 
697
650
  initializeTrustMaterial() {
698
651
  this.identity = loadOrCreateSeatIdentity(this.paths);
699
-
700
- if (!isAnchorSeat(this.seatId)) {
701
- return;
702
- }
703
-
704
652
  writeJson(this.paths.challengePath, {
705
653
  sessionName: this.sessionName,
706
- challenge: createId(48),
707
654
  publicKey: this.identity.publicKey,
708
655
  createdAt: new Date().toISOString(),
709
656
  });
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 });
657
+ this.trustState.phase = "initialized";
658
+ this.trustState.createdAt = new Date().toISOString();
716
659
  }
717
660
 
718
661
  syncTrustState() {
719
662
  if (!this.identity) {
720
663
  this.initializeTrustMaterial();
721
664
  }
722
-
723
- if (isAnchorSeat(this.seatId)) {
724
- this.syncSeatOneTrust();
725
- return;
726
- }
727
-
728
- this.syncSeatTwoTrust();
729
- }
730
-
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
- };
740
- return;
741
- }
742
-
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 });
766
- return;
767
- }
768
-
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
- });
790
- }
791
-
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
- };
807
- return;
808
- }
809
-
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
- }
835
-
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
- );
850
-
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;
855
- }
856
-
857
- isPaired() {
858
- return this.trustState.phase === "paired" &&
859
- typeof this.trustState.challenge === "string" &&
860
- typeof this.trustState.peerPublicKey === "string";
861
665
  }
862
666
 
863
667
  launchShell() {
@@ -1014,11 +818,6 @@ class ArmedSeat {
1014
818
  }
1015
819
  }
1016
820
 
1017
- partnerIsLive() {
1018
- const partner = readJson(this.partnerPaths.statusPath, null);
1019
- return Boolean(partner?.pid && isPidAlive(partner.pid));
1020
- }
1021
-
1022
821
  stopRequested() {
1023
822
  const request = readJson(this.sessionPaths.stopPath, null);
1024
823
  if (!request?.requestedAt) {
@@ -1068,73 +867,6 @@ class ArmedSeat {
1068
867
  };
1069
868
  }
1070
869
 
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
870
  async pullContinuationEvents() {
1139
871
  const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
1140
872
  this.continueOffset = nextOffset;
@@ -1149,10 +881,6 @@ class ArmedSeat {
1149
881
  return;
1150
882
  }
1151
883
 
1152
- if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1153
- continue;
1154
- }
1155
-
1156
884
  const payload = sanitizeRelayText(entry.text);
1157
885
  if (!payload) {
1158
886
  continue;
@@ -1366,7 +1094,7 @@ class ArmedSeat {
1366
1094
  this.liveState.sessionFile,
1367
1095
  this.liveState.offset,
1368
1096
  this.liveState.captureSinceMs,
1369
- { flowMode: this.flowMode === "on" }
1097
+ { flowMode: true }
1370
1098
  );
1371
1099
  this.liveState.offset = result.nextOffset;
1372
1100
  answers.push(...result.answers);
@@ -1375,7 +1103,7 @@ class ArmedSeat {
1375
1103
  this.liveState.sessionFile,
1376
1104
  this.liveState.offset,
1377
1105
  this.liveState.captureSinceMs,
1378
- { flowMode: this.flowMode === "on" }
1106
+ { flowMode: true }
1379
1107
  );
1380
1108
  this.liveState.offset = result.nextOffset;
1381
1109
  answers.push(...result.answers);
@@ -1384,7 +1112,7 @@ class ArmedSeat {
1384
1112
  this.liveState.sessionFile,
1385
1113
  this.liveState.lastMessageId,
1386
1114
  this.liveState.captureSinceMs,
1387
- { flowMode: this.flowMode === "on" }
1115
+ { flowMode: true }
1388
1116
  );
1389
1117
  this.liveState.lastMessageId = result.lastMessageId;
1390
1118
  this.liveState.offset = result.fileSize;
@@ -1461,21 +1189,13 @@ class ArmedSeat {
1461
1189
  }
1462
1190
 
1463
1191
  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)}`);
1475
- }
1476
-
1477
1192
  // Route to all continueTargets with per-target flow modes
1478
1193
  for (const targetEntry of this.continueTargets) {
1194
+ // Skip entries that don't match the target's flowMode
1195
+ if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1196
+ continue;
1197
+ }
1198
+
1479
1199
  const target = this.findContinuationTarget(targetEntry.targetSeatId);
1480
1200
  if (!target) {
1481
1201
  this.log(`[${this.seatId}] target ${targetEntry.targetSeatId} unavailable`);
@@ -1492,7 +1212,6 @@ class ArmedSeat {
1492
1212
  if (this.stopRequested()) {
1493
1213
  this.writeStatus({
1494
1214
  state: "stopping",
1495
- partnerLive: this.partnerIsLive(),
1496
1215
  trust: this.trustState.phase,
1497
1216
  });
1498
1217
  this.requestStop("stop_requested");
@@ -1500,7 +1219,6 @@ class ArmedSeat {
1500
1219
  }
1501
1220
 
1502
1221
  this.syncTrustState();
1503
- await this.pullPartnerEvents();
1504
1222
  await this.pullContinuationEvents();
1505
1223
  if (this.stopped || this.stopRequested()) {
1506
1224
  this.requestStop("stop_requested");
@@ -1515,11 +1233,9 @@ class ArmedSeat {
1515
1233
  this.writeStatus({
1516
1234
  state: live.state,
1517
1235
  agent: live.agent,
1518
- flowMode: this.flowMode,
1519
1236
  cwd: live.cwd,
1520
1237
  log: live.log,
1521
1238
  lastAnswerAt: live.lastAnswerAt,
1522
- partnerLive: this.partnerIsLive(),
1523
1239
  trust: this.trustState.phase,
1524
1240
  challengeReady: Boolean(this.trustState.challenge),
1525
1241
  });
@@ -1533,18 +1249,9 @@ class ArmedSeat {
1533
1249
 
1534
1250
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1535
1251
  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
1252
  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.`);
1253
+ const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
1254
+ this.log(`Seat ${this.seatId} relays to ${targets}.`);
1548
1255
  }
1549
1256
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1550
1257
 
@@ -1641,8 +1348,6 @@ function buildSeatReport(sessionName, seatId) {
1641
1348
  return {
1642
1349
  seatId,
1643
1350
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1644
- flowMode: status?.flowMode || meta?.flowMode || "off",
1645
- continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1646
1351
  continueTargets: status?.continueTargets || meta?.continueTargets || [],
1647
1352
  wrapperPid,
1648
1353
  childPid,
@@ -1657,7 +1362,6 @@ function buildSeatReport(sessionName, seatId) {
1657
1362
  trust: status?.trust || null,
1658
1363
  updatedAt: status?.updatedAt || null,
1659
1364
  lastAnswerAt: status?.lastAnswerAt || null,
1660
- partnerLive: Boolean(status?.partnerLive),
1661
1365
  };
1662
1366
  }
1663
1367
 
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 v6.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,