muuuuse 3.1.1 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runtime.js CHANGED
@@ -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,
@@ -40,9 +38,9 @@ const {
40
38
  writeJson,
41
39
  } = require("./util");
42
40
 
43
- const TYPE_CHUNK_DELAY_MS = 18;
41
+ // A short settle delay keeps interactive CLIs from treating submit as another newline.
42
+ const TYPE_CHUNK_DELAY_MS = 45;
44
43
  const TYPE_CHUNK_SIZE = 24;
45
- const TYPE_SUBMIT_DELAY_MS = 60;
46
44
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
47
45
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
48
46
  const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
@@ -92,24 +90,24 @@ function normalizeContinueSeatId(value) {
92
90
  return seatId || null;
93
91
  }
94
92
 
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
- });
93
+ function normalizeContinueTargets(value) {
94
+ if (!Array.isArray(value)) {
95
+ return [];
110
96
  }
111
97
 
112
- return normalized;
98
+ return value
99
+ .map((entry) => {
100
+ const seatId = normalizeSeatId(entry?.seatId);
101
+ if (!seatId) {
102
+ return null;
103
+ }
104
+
105
+ return {
106
+ seatId,
107
+ flowMode: normalizeFlowMode(entry?.flowMode),
108
+ };
109
+ })
110
+ .filter((entry) => entry !== null);
113
111
  }
114
112
 
115
113
  function resolveShell() {
@@ -189,50 +187,37 @@ function sleepSync(ms) {
189
187
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
190
188
  }
191
189
 
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
-
190
+ function findExistingSessionName(currentPath = process.cwd()) {
199
191
  const candidates = listSessionNames()
200
192
  .map((sessionName) => {
201
193
  const sessionPaths = getSessionPaths(sessionName);
202
194
  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);
195
+ const seats = listSeatIds(sessionName)
196
+ .map((seatId) => buildSeatReport(sessionName, seatId))
197
+ .filter((entry) => entry !== null);
210
198
 
211
- const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
199
+ const cwd = controller?.cwd || seats[0]?.cwd || null;
212
200
  if (!matchesWorkingPath(cwd, currentPath)) {
213
201
  return null;
214
202
  }
215
203
 
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);
222
- const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
223
- const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
224
-
225
- if (!anchorLive || seatLive) {
204
+ const controllerPid = controller?.pid || null;
205
+ const controllerLive = isPidAlive(controllerPid);
206
+ if (seats.length === 0 && !controllerLive) {
226
207
  return null;
227
208
  }
228
209
 
229
- if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
230
- return null;
231
- }
210
+ const createdAtMs = Date.parse(
211
+ controller?.createdAt ||
212
+ seats
213
+ .map((seat) => seat.startedAt || seat.updatedAt || "")
214
+ .find((value) => value) ||
215
+ ""
216
+ );
232
217
 
233
218
  return {
234
219
  sessionName,
235
- createdAtMs,
220
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
236
221
  };
237
222
  })
238
223
  .filter((entry) => entry !== null)
@@ -241,10 +226,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
241
226
  return candidates[0]?.sessionName || null;
242
227
  }
243
228
 
244
- function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
229
+ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
245
230
  const deadline = Date.now() + timeoutMs;
246
231
  while (Date.now() <= deadline) {
247
- const sessionName = findJoinableSessionName(currentPath, seatId);
232
+ const sessionName = findExistingSessionName(currentPath);
248
233
  if (sessionName) {
249
234
  return sessionName;
250
235
  }
@@ -255,11 +240,31 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
255
240
  }
256
241
 
257
242
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
258
- if (isAnchorSeat(seatId)) {
259
- return createSessionName(currentPath);
243
+ const existingSessionName = findExistingSessionName(currentPath);
244
+ if (!existingSessionName) {
245
+ const normalizedSeatId = normalizeSeatId(seatId) || 1;
246
+ const joinWaitMs = Math.min(1000, Math.max(0, normalizedSeatId - 1) * 250);
247
+ const waitedSessionName = waitForExistingSessionName(currentPath, joinWaitMs);
248
+ if (!waitedSessionName) {
249
+ return createSessionName(currentPath);
250
+ }
251
+ const conflictingWaitedSeat = buildSeatReport(waitedSessionName, seatId);
252
+ if (conflictingWaitedSeat) {
253
+ throw new Error(
254
+ `Seat ${seatId} is already armed in this cwd. Stop it first or choose another seat number.`
255
+ );
256
+ }
257
+ return waitedSessionName;
260
258
  }
261
259
 
262
- return waitForJoinableSessionName(currentPath, seatId);
260
+ const conflictingSeat = buildSeatReport(existingSessionName, seatId);
261
+ if (conflictingSeat) {
262
+ throw new Error(
263
+ `Seat ${seatId} is already armed in this cwd. Stop it first or choose another seat number.`
264
+ );
265
+ }
266
+
267
+ return existingSessionName;
263
268
  }
264
269
 
265
270
  function parseAnswerEntries(text) {
@@ -461,50 +466,30 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
461
466
  return null;
462
467
  }
463
468
 
464
- function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
465
- return JSON.stringify({
466
- type: "muuuuse_pair_claim",
467
- sessionName,
468
- challenge,
469
- seat1PublicKey,
470
- seat2PublicKey,
471
- });
472
- }
473
-
474
- function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
475
- return JSON.stringify({
476
- type: "muuuuse_pair_ack",
477
- sessionName,
478
- challenge,
479
- seat1PublicKey,
480
- seat2PublicKey,
481
- });
482
- }
483
-
484
469
  function buildAnswerSignaturePayload(sessionName, challenge, entry) {
485
470
  return JSON.stringify({
486
- type: "muuuuse_answer",
471
+ type: "muuuuse_relay",
487
472
  sessionName,
488
- challenge,
489
473
  chainId: entry.chainId,
490
474
  hop: entry.hop,
491
475
  id: entry.id,
492
- seatId: entry.seatId,
476
+ sourceSeatId: normalizeSeatId(entry.sourceSeatId || entry.seatId),
477
+ targetSeatId: normalizeSeatId(entry.targetSeatId),
493
478
  origin: entry.origin,
494
- phase: entry.phase || "final_answer",
495
- flowMode: entry.flowMode || "off",
479
+ phase: getRelayPhase(entry),
496
480
  createdAt: entry.createdAt,
497
481
  text: entry.text,
498
482
  });
499
483
  }
500
484
 
501
- function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
485
+ function buildContinuationEntry(sourceSessionName, targetSeatId, entry, targetFlowMode = null) {
502
486
  return {
503
487
  id: createId(12),
504
488
  type: "continue",
505
489
  sourceSessionName,
506
490
  sourceSeatId: entry.seatId,
507
491
  targetSeatId,
492
+ targetFlowMode: normalizeFlowMode(targetFlowMode),
508
493
  origin: entry.origin || "unknown",
509
494
  phase: entry.phase || "final_answer",
510
495
  text: entry.text,
@@ -512,7 +497,6 @@ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
512
497
  chainId: entry.chainId,
513
498
  hop: entry.hop,
514
499
  sourceAnswerId: entry.id,
515
- flowMode: entry.flowMode || null,
516
500
  publicKey: entry.publicKey || null,
517
501
  signature: entry.signature || null,
518
502
  };
@@ -539,24 +523,6 @@ function getSeatDirIfExists(sessionName, seatId) {
539
523
  return null;
540
524
  }
541
525
 
542
- function readSeatChallenge(paths, sessionName) {
543
- const record = readJson(paths.challengePath, null);
544
- if (
545
- !record ||
546
- record.sessionName !== sessionName ||
547
- typeof record.challenge !== "string" ||
548
- typeof record.publicKey !== "string"
549
- ) {
550
- return null;
551
- }
552
-
553
- return {
554
- challenge: record.challenge,
555
- publicKey: record.publicKey.trim(),
556
- createdAt: record.createdAt || null,
557
- };
558
- }
559
-
560
526
  function normalizeRelayPayloadForTyping(text) {
561
527
  return String(text || "")
562
528
  .replace(/\r/g, "")
@@ -621,14 +587,6 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
621
587
  return false;
622
588
  }
623
589
 
624
- // Some TUIs treat fast, chunked relay typing as paste input and suppress an
625
- // immediate Enter. A short settle delay keeps submit behavior reliable.
626
- await sleep(TYPE_SUBMIT_DELAY_MS);
627
-
628
- if (shouldAbort() || !child) {
629
- return false;
630
- }
631
-
632
590
  try {
633
591
  child.write("\r");
634
592
  } catch {
@@ -641,31 +599,20 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
641
599
  class ArmedSeat {
642
600
  constructor(options) {
643
601
  this.seatId = options.seatId;
644
- this.partnerSeatId = getPartnerSeatId(options.seatId);
645
- this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
646
602
  this.flowMode = normalizeFlowMode(options.flowMode);
647
- this.continueTargets = normalizeContinueTargets(
648
- options.continueTargets || (
649
- options.continueSeatId ? [{ targetSeatId: options.continueSeatId, flowMode: options.flowMode }] : []
650
- ),
651
- this.flowMode
652
- );
653
- this.continueSeatId = this.continueTargets[0]?.targetSeatId || normalizeContinueSeatId(options.continueSeatId);
603
+ this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
604
+ this.continueTargets = normalizeContinueTargets(options.continueTargets);
654
605
  this.cwd = normalizeWorkingPath(options.cwd);
655
- if (this.continueTargets.some((target) => target.targetSeatId === this.seatId)) {
606
+ if (this.continueSeatId === this.seatId) {
656
607
  throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
657
608
  }
658
- this.sessionName = resolveSessionName(this.cwd, this.seatId);
659
- if (!this.sessionName) {
660
- throw new Error(
661
- `No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
662
- );
609
+ if (this.continueTargets.some((target) => target.seatId === this.seatId)) {
610
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot link to itself.`);
663
611
  }
612
+ this.sessionName = resolveSessionName(this.cwd, this.seatId);
664
613
  this.sessionPaths = getSessionPaths(this.sessionName);
665
614
  this.paths = getSeatPaths(this.sessionName, this.seatId);
666
- this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
667
615
  this.continueOffset = getFileSize(this.paths.continuePath);
668
- this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
669
616
 
670
617
  this.child = null;
671
618
  this.childPid = null;
@@ -678,17 +625,11 @@ class ArmedSeat {
678
625
  this.stdinCleanup = null;
679
626
  this.resizeCleanup = null;
680
627
  this.forceKillTimer = null;
681
- this.identity = null;
628
+ this.identity = loadOrCreateSeatIdentity(this.paths);
682
629
  this.lastUserInputAtMs = 0;
683
630
  this.pendingInboundContext = null;
684
631
  this.recentInboundRelays = [];
685
632
  this.recentEmittedAnswers = [];
686
- this.trustState = {
687
- challenge: null,
688
- peerPublicKey: null,
689
- phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
690
- pairedAt: null,
691
- };
692
633
  this.liveState = {
693
634
  type: null,
694
635
  pid: null,
@@ -709,11 +650,7 @@ class ArmedSeat {
709
650
  cwd: this.cwd,
710
651
  createdAt: current.createdAt || this.startedAt,
711
652
  updatedAt: new Date().toISOString(),
712
- anchorSeatId: this.anchorSeatId,
713
- partnerSeatId: this.partnerSeatId,
714
- anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
715
- partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
716
- pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
653
+ pid: process.pid,
717
654
  ...extra,
718
655
  });
719
656
  }
@@ -725,7 +662,6 @@ class ArmedSeat {
725
662
  writeMeta(extra = {}) {
726
663
  writeJson(this.paths.metaPath, {
727
664
  seatId: this.seatId,
728
- partnerSeatId: this.partnerSeatId,
729
665
  sessionName: this.sessionName,
730
666
  flowMode: this.flowMode,
731
667
  continueSeatId: this.continueSeatId,
@@ -733,6 +669,7 @@ class ArmedSeat {
733
669
  cwd: this.cwd,
734
670
  pid: process.pid,
735
671
  childPid: this.childPid,
672
+ publicKey: this.identity?.publicKey || null,
736
673
  command: [resolveShell(), ...resolveShellArgs(resolveShell())],
737
674
  startedAt: this.startedAt,
738
675
  ...extra,
@@ -742,7 +679,6 @@ class ArmedSeat {
742
679
  writeStatus(extra = {}) {
743
680
  writeJson(this.paths.statusPath, {
744
681
  seatId: this.seatId,
745
- partnerSeatId: this.partnerSeatId,
746
682
  sessionName: this.sessionName,
747
683
  flowMode: this.flowMode,
748
684
  continueSeatId: this.continueSeatId,
@@ -750,183 +686,17 @@ class ArmedSeat {
750
686
  cwd: this.cwd,
751
687
  pid: process.pid,
752
688
  childPid: this.childPid,
689
+ publicKey: this.identity?.publicKey || null,
753
690
  relayCount: this.relayCount,
754
691
  updatedAt: new Date().toISOString(),
755
692
  ...extra,
756
693
  });
757
694
  }
758
695
 
759
- initializeTrustMaterial() {
760
- this.identity = loadOrCreateSeatIdentity(this.paths);
761
-
762
- if (!isAnchorSeat(this.seatId)) {
763
- return;
764
- }
765
-
766
- writeJson(this.paths.challengePath, {
767
- sessionName: this.sessionName,
768
- challenge: createId(48),
769
- publicKey: this.identity.publicKey,
770
- createdAt: new Date().toISOString(),
771
- });
772
- this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
773
- this.trustState.peerPublicKey = null;
774
- this.trustState.phase = "waiting_for_peer_signature";
775
- this.trustState.pairedAt = null;
776
- fs.rmSync(this.paths.ackPath, { force: true });
777
- fs.rmSync(this.partnerPaths.claimPath, { force: true });
778
- }
779
-
780
- syncTrustState() {
781
- if (!this.identity) {
782
- this.initializeTrustMaterial();
783
- }
784
-
785
- if (isAnchorSeat(this.seatId)) {
786
- this.syncSeatOneTrust();
787
- return;
788
- }
789
-
790
- this.syncSeatTwoTrust();
791
- }
792
-
793
- syncSeatOneTrust() {
794
- const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
795
- if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
796
- this.trustState = {
797
- challenge: null,
798
- peerPublicKey: null,
799
- phase: "waiting_for_peer_signature",
800
- pairedAt: null,
801
- };
802
- return;
803
- }
804
-
805
- this.trustState.challenge = challengeRecord.challenge;
806
- const claim = readJson(this.partnerPaths.claimPath, null);
807
- if (
808
- !claim ||
809
- claim.sessionName !== this.sessionName ||
810
- claim.challenge !== challengeRecord.challenge ||
811
- typeof claim.publicKey !== "string" ||
812
- typeof claim.signature !== "string" ||
813
- !verifyText(
814
- buildClaimMessage(
815
- this.sessionName,
816
- challengeRecord.challenge,
817
- this.identity.publicKey,
818
- claim.publicKey.trim()
819
- ),
820
- claim.signature,
821
- claim.publicKey
822
- )
823
- ) {
824
- this.trustState.peerPublicKey = null;
825
- this.trustState.phase = "waiting_for_peer_signature";
826
- this.trustState.pairedAt = null;
827
- fs.rmSync(this.paths.ackPath, { force: true });
828
- return;
829
- }
830
-
831
- const peerPublicKey = claim.publicKey.trim();
832
- const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
833
- const currentAck = readJson(this.paths.ackPath, null);
834
- const ackIsValid = Boolean(
835
- currentAck &&
836
- currentAck.sessionName === this.sessionName &&
837
- currentAck.challenge === challengeRecord.challenge &&
838
- currentAck.publicKey === this.identity.publicKey &&
839
- currentAck.peerPublicKey === peerPublicKey &&
840
- typeof currentAck.signature === "string" &&
841
- verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
842
- );
843
- if (!ackIsValid) {
844
- writeJson(this.paths.ackPath, {
845
- sessionName: this.sessionName,
846
- challenge: challengeRecord.challenge,
847
- publicKey: this.identity.publicKey,
848
- peerPublicKey,
849
- signature: signText(ackMessage, this.identity.privateKey),
850
- signedAt: new Date().toISOString(),
851
- });
852
- }
853
-
854
- const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
855
- this.trustState.peerPublicKey = peerPublicKey;
856
- this.trustState.phase = "paired";
857
- this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
858
- }
859
-
860
- syncSeatTwoTrust() {
861
- const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
862
- if (!challengeRecord) {
863
- this.trustState = {
864
- challenge: null,
865
- peerPublicKey: null,
866
- phase: "waiting_for_anchor_key",
867
- pairedAt: null,
868
- };
869
- return;
870
- }
871
-
872
- const challenge = challengeRecord.challenge;
873
- const peerPublicKey = challengeRecord.publicKey;
874
- const claimPayload = {
875
- sessionName: this.sessionName,
876
- challenge,
877
- publicKey: this.identity.publicKey,
878
- };
879
- const claimSignature = signText(
880
- buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
881
- this.identity.privateKey
882
- );
883
- const currentClaim = readJson(this.paths.claimPath, null);
884
- if (
885
- !currentClaim ||
886
- currentClaim.sessionName !== claimPayload.sessionName ||
887
- currentClaim.challenge !== claimPayload.challenge ||
888
- currentClaim.publicKey !== claimPayload.publicKey ||
889
- currentClaim.signature !== claimSignature
890
- ) {
891
- writeJson(this.paths.claimPath, {
892
- ...claimPayload,
893
- signature: claimSignature,
894
- signedAt: new Date().toISOString(),
895
- });
896
- }
897
-
898
- const ack = readJson(this.partnerPaths.ackPath, null);
899
- const paired = Boolean(
900
- ack &&
901
- ack.sessionName === this.sessionName &&
902
- ack.challenge === challenge &&
903
- ack.peerPublicKey === this.identity.publicKey &&
904
- ack.publicKey === peerPublicKey &&
905
- typeof ack.signature === "string" &&
906
- verifyText(
907
- buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
908
- ack.signature,
909
- peerPublicKey
910
- )
911
- );
912
-
913
- this.trustState.challenge = challenge;
914
- this.trustState.peerPublicKey = peerPublicKey;
915
- this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
916
- this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
917
- }
918
-
919
- isPaired() {
920
- return this.trustState.phase === "paired" &&
921
- typeof this.trustState.challenge === "string" &&
922
- typeof this.trustState.peerPublicKey === "string";
923
- }
924
-
925
696
  launchShell() {
926
697
  ensureDir(this.paths.dir);
927
698
  fs.rmSync(this.paths.pipePath, { force: true });
928
699
  clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
929
- this.initializeTrustMaterial();
930
700
  this.writeController();
931
701
 
932
702
  const shell = resolveShell();
@@ -943,7 +713,7 @@ class ArmedSeat {
943
713
 
944
714
  this.childPid = this.child.pid;
945
715
  this.writeMeta();
946
- this.writeStatus({ state: "running", trust: this.trustState.phase });
716
+ this.writeStatus({ state: "running" });
947
717
 
948
718
  this.child.onData((data) => {
949
719
  fs.appendFileSync(this.paths.pipePath, data);
@@ -1077,9 +847,19 @@ class ArmedSeat {
1077
847
  }
1078
848
  }
1079
849
 
1080
- partnerIsLive() {
1081
- const partner = readJson(this.partnerPaths.statusPath, null);
1082
- return Boolean(partner?.pid && isPidAlive(partner.pid));
850
+ getConfiguredTargets() {
851
+ const targets = [...this.continueTargets];
852
+ if (this.continueSeatId && !targets.some((target) => target.seatId === this.continueSeatId)) {
853
+ targets.push({
854
+ seatId: this.continueSeatId,
855
+ flowMode: this.flowMode,
856
+ });
857
+ }
858
+ return targets;
859
+ }
860
+
861
+ shouldCaptureCommentary() {
862
+ return this.getConfiguredTargets().some((target) => target.flowMode === "on");
1083
863
  }
1084
864
 
1085
865
  stopRequested() {
@@ -1092,116 +872,100 @@ class ArmedSeat {
1092
872
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
1093
873
  }
1094
874
 
1095
- shouldCaptureCommentary() {
1096
- return this.flowMode === "on" || this.continueTargets.some((target) => target.flowMode === "on");
875
+ sourceLinksToTarget(sourceSeatId, targetSeatId = this.seatId) {
876
+ const desiredSeatId = normalizeSeatId(sourceSeatId);
877
+ const desiredTargetSeatId = normalizeSeatId(targetSeatId);
878
+ if (!desiredSeatId || !desiredTargetSeatId) {
879
+ return false;
880
+ }
881
+
882
+ const sourcePaths = getSeatPaths(this.sessionName, desiredSeatId);
883
+ const sourceStatus = readJson(sourcePaths.statusPath, null);
884
+ const sourceMeta = readJson(sourcePaths.metaPath, null);
885
+ const sourceContinueSeatId = sourceStatus?.continueSeatId || sourceMeta?.continueSeatId || null;
886
+ const sourceContinueTargets = normalizeContinueTargets(
887
+ sourceStatus?.continueTargets || sourceMeta?.continueTargets
888
+ );
889
+
890
+ const configuredTargets = [...sourceContinueTargets];
891
+ if (
892
+ sourceContinueSeatId &&
893
+ !configuredTargets.some((target) => target.seatId === normalizeSeatId(sourceContinueSeatId))
894
+ ) {
895
+ configuredTargets.push({
896
+ seatId: normalizeSeatId(sourceContinueSeatId),
897
+ flowMode: normalizeFlowMode(sourceStatus?.flowMode || sourceMeta?.flowMode),
898
+ });
899
+ }
900
+
901
+ return configuredTargets.some((target) => target.seatId === desiredTargetSeatId);
1097
902
  }
1098
903
 
1099
- findContinuationTarget(targetSeatId) {
1100
- const normalizedTargetSeatId = normalizeSeatId(targetSeatId);
1101
- if (!normalizedTargetSeatId) {
904
+ readSourcePublicKey(sourceSeatId) {
905
+ const desiredSeatId = normalizeSeatId(sourceSeatId);
906
+ if (!desiredSeatId) {
1102
907
  return null;
1103
908
  }
1104
909
 
1105
- const candidates = listSessionNames()
1106
- .map((sessionName) => {
1107
- if (!getSeatDirIfExists(sessionName, normalizedTargetSeatId)) {
1108
- return null;
1109
- }
910
+ const sourcePaths = getSeatPaths(this.sessionName, desiredSeatId);
911
+ const sourceMeta = readJson(sourcePaths.metaPath, null);
912
+ if (typeof sourceMeta?.publicKey === "string" && sourceMeta.publicKey.trim()) {
913
+ return sourceMeta.publicKey.trim();
914
+ }
1110
915
 
1111
- const seat = buildSeatReport(sessionName, normalizedTargetSeatId);
1112
- if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1113
- return null;
1114
- }
916
+ try {
917
+ const key = fs.readFileSync(sourcePaths.publicKeyPath, "utf8").trim();
918
+ return key || null;
919
+ } catch {
920
+ return null;
921
+ }
922
+ }
1115
923
 
1116
- const updatedAtMs = Date.parse(seat.updatedAt || seat.startedAt || "");
1117
- return {
1118
- seat,
1119
- sessionName,
1120
- updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
1121
- };
1122
- })
1123
- .filter((entry) => entry !== null)
1124
- .sort((left, right) => right.updatedAtMs - left.updatedAtMs);
924
+ findLinkedTarget(targetSeatId) {
925
+ const desiredSeatId = normalizeContinueSeatId(targetSeatId);
926
+ if (!desiredSeatId) {
927
+ return null;
928
+ }
1125
929
 
1126
- if (candidates.length === 0) {
930
+ const seat = buildSeatReport(this.sessionName, desiredSeatId);
931
+ if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1127
932
  return null;
1128
933
  }
1129
934
 
1130
- const target = candidates[0];
1131
935
  return {
1132
- seatId: target.seat.seatId,
1133
- sessionName: target.sessionName,
1134
- paths: getSeatPaths(target.sessionName, target.seat.seatId),
936
+ seatId: seat.seatId,
937
+ paths: getSeatPaths(this.sessionName, seat.seatId),
1135
938
  };
1136
939
  }
1137
940
 
1138
- async pullPartnerEvents() {
1139
- const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
1140
- this.partnerOffset = nextOffset;
1141
- if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
1142
- return;
941
+ verifyInboundEntry(entry) {
942
+ const sourceSeatId = normalizeSeatId(entry?.sourceSeatId || entry?.seatId);
943
+ const targetSeatId = normalizeSeatId(entry?.targetSeatId);
944
+ const payload = sanitizeRelayText(entry?.text);
945
+ if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.sourceLinksToTarget(sourceSeatId, targetSeatId)) {
946
+ return false;
1143
947
  }
1144
948
 
1145
- const entries = parseAnswerEntries(text);
1146
- for (const entry of entries) {
1147
- if (this.stopped || this.stopRequested()) {
1148
- this.requestStop("stop_requested");
1149
- return;
1150
- }
1151
-
1152
- const inboundFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1153
- if (!shouldAcceptInboundEntry(inboundFlowMode, entry)) {
1154
- continue;
1155
- }
949
+ const publicKey = this.readSourcePublicKey(sourceSeatId);
950
+ if (!publicKey || entry.publicKey !== publicKey || typeof entry.signature !== "string") {
951
+ return false;
952
+ }
1156
953
 
1157
- const payload = sanitizeRelayText(entry.text);
1158
- const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
954
+ return verifyText(
955
+ buildAnswerSignaturePayload(this.sessionName, null, {
956
+ id: entry.id,
957
+ sourceSeatId,
958
+ targetSeatId,
1159
959
  chainId: entry.chainId || entry.id,
1160
960
  hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1161
- id: entry.id,
1162
- seatId: entry.seatId,
1163
961
  origin: entry.origin || "unknown",
1164
962
  phase: getRelayPhase(entry),
1165
- flowMode: inboundFlowMode,
1166
963
  createdAt: entry.createdAt,
1167
964
  text: payload,
1168
- });
1169
- if (
1170
- !payload ||
1171
- entry.challenge !== this.trustState.challenge ||
1172
- entry.publicKey !== this.trustState.peerPublicKey ||
1173
- typeof entry.signature !== "string" ||
1174
- !verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
1175
- ) {
1176
- continue;
1177
- }
1178
-
1179
- const delivered = await sendTextAndEnter(
1180
- this.child,
1181
- payload,
1182
- () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1183
- );
1184
- if (!delivered) {
1185
- this.requestStop("relay_aborted");
1186
- return;
1187
- }
1188
-
1189
- if (this.stopped || this.stopRequested()) {
1190
- this.requestStop("stop_requested");
1191
- return;
1192
- }
1193
-
1194
- const deliveredAtMs = Date.now();
1195
- this.pendingInboundContext = {
1196
- chainId: entry.chainId || entry.id,
1197
- deliveredAtMs,
1198
- expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
1199
- hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1200
- };
1201
- this.relayCount += 1;
1202
- this.rememberInboundRelay(payload);
1203
- this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
1204
- }
965
+ }),
966
+ entry.signature,
967
+ publicKey
968
+ );
1205
969
  }
1206
970
 
1207
971
  async pullContinuationEvents() {
@@ -1218,8 +982,7 @@ class ArmedSeat {
1218
982
  return;
1219
983
  }
1220
984
 
1221
- const continueFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1222
- if (!shouldAcceptInboundEntry(continueFlowMode, entry)) {
985
+ if (!this.verifyInboundEntry(entry)) {
1223
986
  continue;
1224
987
  }
1225
988
 
@@ -1431,13 +1194,12 @@ class ArmedSeat {
1431
1194
  }
1432
1195
 
1433
1196
  const answers = [];
1434
- const captureCommentary = this.shouldCaptureCommentary();
1435
1197
  if (detectedAgent.type === "codex") {
1436
1198
  const result = readCodexAnswers(
1437
1199
  this.liveState.sessionFile,
1438
1200
  this.liveState.offset,
1439
1201
  this.liveState.captureSinceMs,
1440
- { flowMode: captureCommentary }
1202
+ { flowMode: this.shouldCaptureCommentary() }
1441
1203
  );
1442
1204
  this.liveState.offset = result.nextOffset;
1443
1205
  answers.push(...result.answers);
@@ -1446,7 +1208,7 @@ class ArmedSeat {
1446
1208
  this.liveState.sessionFile,
1447
1209
  this.liveState.offset,
1448
1210
  this.liveState.captureSinceMs,
1449
- { flowMode: captureCommentary }
1211
+ { flowMode: this.shouldCaptureCommentary() }
1450
1212
  );
1451
1213
  this.liveState.offset = result.nextOffset;
1452
1214
  answers.push(...result.answers);
@@ -1455,7 +1217,7 @@ class ArmedSeat {
1455
1217
  this.liveState.sessionFile,
1456
1218
  this.liveState.lastMessageId,
1457
1219
  this.liveState.captureSinceMs,
1458
- { flowMode: captureCommentary }
1220
+ { flowMode: this.shouldCaptureCommentary() }
1459
1221
  );
1460
1222
  this.liveState.lastMessageId = result.lastMessageId;
1461
1223
  this.liveState.offset = result.fileSize;
@@ -1488,7 +1250,7 @@ class ArmedSeat {
1488
1250
  }
1489
1251
 
1490
1252
  const payload = sanitizeRelayText(entry.text);
1491
- if (!payload || !this.identity || !this.trustState.challenge) {
1253
+ if (!payload) {
1492
1254
  return;
1493
1255
  }
1494
1256
 
@@ -1507,65 +1269,66 @@ class ArmedSeat {
1507
1269
  const pendingInboundContext = this.getPendingInboundContext();
1508
1270
 
1509
1271
  const entryId = entry.id || createId(12);
1510
- const signedEntry = {
1272
+ const relayEntry = {
1511
1273
  id: entryId,
1512
1274
  type: "answer",
1513
1275
  seatId: this.seatId,
1276
+ sourceSeatId: this.seatId,
1514
1277
  origin: entry.origin || "unknown",
1515
1278
  phase: entry.phase || "final_answer",
1516
- flowMode: this.flowMode,
1517
1279
  text: payload,
1518
1280
  createdAt: entry.createdAt || new Date().toISOString(),
1519
1281
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
1520
1282
  hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
1521
- challenge: this.trustState.challenge,
1522
- publicKey: this.identity.publicKey,
1523
1283
  };
1524
- signedEntry.signature = signText(
1525
- buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1526
- this.identity.privateKey
1527
- );
1528
- appendJsonl(this.paths.eventsPath, signedEntry);
1529
- this.forwardContinuation(signedEntry);
1284
+
1285
+ appendJsonl(this.paths.eventsPath, relayEntry);
1286
+ this.forwardContinuation(relayEntry);
1530
1287
  this.rememberEmittedAnswer(answerKey);
1531
1288
 
1532
1289
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1533
1290
  }
1534
1291
 
1535
- forwardContinuation(signedEntry) {
1536
- if (this.continueTargets.length === 0) {
1292
+ forwardContinuation(relayEntry) {
1293
+ const targets = this.getConfiguredTargets();
1294
+ if (targets.length === 0) {
1537
1295
  return;
1538
1296
  }
1539
1297
 
1540
- for (const targetEntry of this.continueTargets) {
1541
- const target = this.findContinuationTarget(targetEntry.targetSeatId);
1298
+ for (const targetEntry of targets) {
1299
+ if (!shouldAcceptInboundEntry(targetEntry.flowMode, relayEntry)) {
1300
+ continue;
1301
+ }
1302
+
1303
+ const target = this.findLinkedTarget(targetEntry.seatId);
1542
1304
  if (!target) {
1543
- this.log(`[${this.seatId}] continue ${targetEntry.targetSeatId} unavailable`);
1305
+ this.log(`[${this.seatId}] link ${targetEntry.seatId} unavailable`);
1544
1306
  continue;
1545
1307
  }
1546
1308
 
1547
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, {
1548
- ...signedEntry,
1549
- flowMode: targetEntry.flowMode,
1550
- });
1309
+ const continuationEntry = buildContinuationEntry(
1310
+ this.sessionName,
1311
+ target.seatId,
1312
+ relayEntry,
1313
+ targetEntry.flowMode
1314
+ );
1315
+ continuationEntry.publicKey = this.identity.publicKey;
1316
+ continuationEntry.signature = signText(
1317
+ buildAnswerSignaturePayload(this.sessionName, null, continuationEntry),
1318
+ this.identity.privateKey
1319
+ );
1551
1320
  appendJsonl(target.paths.continuePath, continuationEntry);
1552
- this.log(`[${this.seatId} => ${target.seatId} ${targetEntry.flowMode}] ${previewText(continuationEntry.text)}`);
1321
+ this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1553
1322
  }
1554
1323
  }
1555
1324
 
1556
1325
  async tick() {
1557
1326
  if (this.stopRequested()) {
1558
- this.writeStatus({
1559
- state: "stopping",
1560
- partnerLive: this.partnerIsLive(),
1561
- trust: this.trustState.phase,
1562
- });
1327
+ this.writeStatus({ state: "stopping" });
1563
1328
  this.requestStop("stop_requested");
1564
1329
  return;
1565
1330
  }
1566
1331
 
1567
- this.syncTrustState();
1568
- await this.pullPartnerEvents();
1569
1332
  await this.pullContinuationEvents();
1570
1333
  if (this.stopped || this.stopRequested()) {
1571
1334
  this.requestStop("stop_requested");
@@ -1584,9 +1347,6 @@ class ArmedSeat {
1584
1347
  cwd: live.cwd,
1585
1348
  log: live.log,
1586
1349
  lastAnswerAt: live.lastAnswerAt,
1587
- partnerLive: this.partnerIsLive(),
1588
- trust: this.trustState.phase,
1589
- challengeReady: Boolean(this.trustState.challenge),
1590
1350
  });
1591
1351
  }
1592
1352
 
@@ -1598,17 +1358,17 @@ class ArmedSeat {
1598
1358
 
1599
1359
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1600
1360
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1601
- this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1602
- if (this.continueTargets.length > 0) {
1361
+ this.log(`Seat ${this.seatId} default relay mode is flow ${this.flowMode}.`);
1362
+ if (this.continueSeatId) {
1363
+ this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1364
+ }
1365
+ const configuredTargets = this.getConfiguredTargets();
1366
+ if (configuredTargets.length > 0) {
1603
1367
  this.log(
1604
- `Seat ${this.seatId} continues to ${this.continueTargets.map((target) => `seat ${target.targetSeatId} (${target.flowMode})`).join(", ")}.`
1368
+ `Seat ${this.seatId} links signed relay targets: ${configuredTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
1605
1369
  );
1606
1370
  }
1607
- if (isAnchorSeat(this.seatId)) {
1608
- this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
1609
- } else {
1610
- this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
1611
- }
1371
+ this.log("Signed relays are accepted when the sender linked to this seat.");
1612
1372
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1613
1373
 
1614
1374
  try {
@@ -1703,18 +1463,10 @@ function buildSeatReport(sessionName, seatId) {
1703
1463
 
1704
1464
  return {
1705
1465
  seatId,
1706
- partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
1707
1466
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1708
1467
  flowMode: status?.flowMode || meta?.flowMode || "off",
1709
1468
  continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1710
- continueTargets: normalizeContinueTargets(
1711
- status?.continueTargets || meta?.continueTargets || (
1712
- (status?.continueSeatId || meta?.continueSeatId)
1713
- ? [{ targetSeatId: status?.continueSeatId || meta?.continueSeatId, flowMode: status?.flowMode || meta?.flowMode || "off" }]
1714
- : []
1715
- ),
1716
- status?.flowMode || meta?.flowMode || "off"
1717
- ),
1469
+ continueTargets: normalizeContinueTargets(status?.continueTargets || meta?.continueTargets),
1718
1470
  wrapperPid,
1719
1471
  childPid,
1720
1472
  wrapperLive,
@@ -1725,10 +1477,8 @@ function buildSeatReport(sessionName, seatId) {
1725
1477
  relayCount: status?.relayCount || 0,
1726
1478
  log: status?.log || null,
1727
1479
  startedAt: meta?.startedAt || null,
1728
- trust: status?.trust || null,
1729
1480
  updatedAt: status?.updatedAt || null,
1730
1481
  lastAnswerAt: status?.lastAnswerAt || null,
1731
- partnerLive: Boolean(status?.partnerLive),
1732
1482
  };
1733
1483
  }
1734
1484