muuuuse 7.0.1 → 7.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "7.0.1",
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.",
3
+ "version": "7.0.2",
4
+ "description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "muuuuse": "bin/muuse.js"
@@ -40,7 +40,8 @@
40
40
  ],
41
41
  "scripts": {
42
42
  "test": "node test/cli.test.js",
43
- "pack:local": "npm pack"
43
+ "pack:local": "npm pack",
44
+ "prepublishOnly": "npm test"
44
45
  },
45
46
  "dependencies": {
46
47
  "node-pty": "^1.1.0"
package/src/agents.js CHANGED
@@ -468,9 +468,7 @@ function parseCodexAssistantLine(line, options = {}) {
468
468
  }
469
469
 
470
470
  const phase = String(entry.payload?.phase || "").trim().toLowerCase();
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");
471
+ const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
474
472
  if (!relayablePhase) {
475
473
  return null;
476
474
  }
@@ -483,7 +481,7 @@ function parseCodexAssistantLine(line, options = {}) {
483
481
  return {
484
482
  id: entry.payload.id || hashText(line),
485
483
  text,
486
- phase: normalizedPhase,
484
+ phase: phase === "commentary" ? "commentary" : "final_answer",
487
485
  timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
488
486
  };
489
487
  } catch {
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- const { BRAND, normalizeSeatId, usage } = require("./util");
1
+ const { BRAND, getPartnerSeatId, 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,10 +56,12 @@ async function main(argv = process.argv.slice(2)) {
56
56
 
57
57
  const seatId = normalizeSeatId(command);
58
58
  if (seatId) {
59
- const { continueTargets } = parseSeatOptions(command, argv.slice(1));
59
+ const { flowMode, continueSeatId, continueTargets } = parseSeatOptions(command, argv.slice(1));
60
60
  const seat = new ArmedSeat({
61
61
  cwd: process.cwd(),
62
+ continueSeatId,
62
63
  continueTargets,
64
+ flowMode,
63
65
  seatId,
64
66
  });
65
67
  const code = await seat.run();
@@ -73,14 +75,20 @@ function renderSeatStatus(seat) {
73
75
  const bits = [
74
76
  `seat ${seat.seatId}: ${seat.state}`,
75
77
  `agent ${seat.agent || "idle"}`,
78
+ `flow ${seat.flowMode || "off"}`,
76
79
  `relays ${seat.relayCount}`,
77
80
  `wrapper ${seat.wrapperPid || "-"}`,
78
81
  `child ${seat.childPid || "-"}`,
79
82
  ];
80
83
 
81
- const renderedLinks = renderLinkTargets(seat);
82
- if (renderedLinks) {
83
- bits.push(`link ${renderedLinks}`);
84
+ if (seat.partnerLive) {
85
+ bits.push("peer live");
86
+ }
87
+ if (seat.continueSeatId) {
88
+ bits.push(`continue ${seat.continueSeatId}`);
89
+ }
90
+ if (Array.isArray(seat.continueTargets) && seat.continueTargets.length > 0) {
91
+ bits.push(`links ${renderLinkTargets(seat.continueTargets)}`);
84
92
  }
85
93
  if (seat.trust) {
86
94
  bits.push(`trust ${seat.trust}`);
@@ -99,41 +107,28 @@ function renderSeatStatus(seat) {
99
107
  return output;
100
108
  }
101
109
 
102
- function renderLinkTargets(seat) {
103
- const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
110
+ function renderLinkTargets(targets) {
104
111
  return targets
105
- .map((target) => `${target.targetSeatId} (flow ${target.flowMode})`)
112
+ .map((target) => `${target.seatId}:${target.flowMode}`)
106
113
  .join(", ");
107
114
  }
108
115
 
109
116
  function parseSeatOptions(command, args) {
110
117
  const seatId = normalizeSeatId(command);
118
+ const partnerSeatId = getPartnerSeatId(seatId);
119
+ let flowMode = "off";
120
+ let continueSeatId = null;
111
121
  let continueTargets = [];
112
122
  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
- }
127
123
 
128
- while (index < args.length) {
124
+ for (; index < args.length;) {
129
125
  const token = String(args[index] || "").trim().toLowerCase();
130
126
 
131
- if (token === "link") {
132
- const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
133
- if (parsedLinks.consumed > 0) {
134
- continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
135
- hasExplicitTarget = true;
136
- index += 1 + parsedLinks.consumed;
127
+ if (token === "flow") {
128
+ const nextFlowMode = parseFlowModeToken(args[index + 1]);
129
+ if (nextFlowMode) {
130
+ flowMode = nextFlowMode;
131
+ index += 2;
137
132
  continue;
138
133
  }
139
134
  break;
@@ -141,15 +136,22 @@ function parseSeatOptions(command, args) {
141
136
 
142
137
  if (token === "continue") {
143
138
  const targetSeatId = normalizeSeatId(args[index + 1]);
144
- if (!targetSeatId) {
139
+ if (targetSeatId && targetSeatId !== seatId) {
140
+ continueSeatId = targetSeatId;
141
+ index += 2;
142
+ continue;
143
+ }
144
+ break;
145
+ }
146
+
147
+ if (token === "link") {
148
+ const parsed = parseLinkTargets(seatId, partnerSeatId, args, index + 1);
149
+ if (!parsed) {
145
150
  break;
146
151
  }
147
- upsertTarget(continueTargets, {
148
- targetSeatId,
149
- flowMode: seatFlowMode || "on",
150
- });
151
- hasExplicitTarget = true;
152
- index += 2;
152
+
153
+ continueTargets = mergeTargets(continueTargets, parsed.continueTargets);
154
+ index = parsed.nextIndex;
153
155
  continue;
154
156
  }
155
157
 
@@ -157,77 +159,79 @@ function parseSeatOptions(command, args) {
157
159
  }
158
160
 
159
161
  if (index === args.length) {
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 };
162
+ return { flowMode, continueSeatId, continueTargets };
170
163
  }
171
164
 
172
165
  throw new Error(
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.`
166
+ `\`muuuuse ${command}\` accepts \`flow on\` / \`flow off\`, optional \`continue <seat>\`, and optional \`link <seat> flow on|off ...\` groups. Run it directly in the terminal you want to arm.`
174
167
  );
175
168
  }
176
169
 
177
- function mergeTargets(existingTargets, nextTargets) {
178
- const merged = [];
179
- for (const target of Array.isArray(existingTargets) ? existingTargets : []) {
180
- upsertTarget(merged, target);
181
- }
182
- for (const target of Array.isArray(nextTargets) ? nextTargets : []) {
183
- upsertTarget(merged, target);
170
+ function mergeTargets(currentTargets, nextTargets) {
171
+ const merged = [...currentTargets];
172
+ for (const target of nextTargets) {
173
+ const currentIndex = merged.findIndex((entry) => entry.seatId === target.seatId);
174
+ if (currentIndex !== -1) {
175
+ merged.splice(currentIndex, 1);
176
+ }
177
+ merged.push(target);
184
178
  }
185
-
186
179
  return merged;
187
180
  }
188
181
 
189
- function parseLinkTargets(args, seatId) {
182
+ function parseLinkTargets(seatId, partnerSeatId, args, startIndex) {
183
+ let index = startIndex;
190
184
  const continueTargets = [];
191
- let consumed = 0;
192
185
 
193
- while (consumed < args.length) {
194
- const targetSeatId = normalizeSeatId(args[consumed]);
195
- if (!targetSeatId) {
186
+ while (index < args.length) {
187
+ const targetSeatId = normalizeSeatId(args[index]);
188
+ if (!targetSeatId || targetSeatId === seatId) {
189
+ break;
190
+ }
191
+
192
+ if (String(args[index + 1] || "").trim().toLowerCase() !== "flow") {
196
193
  break;
197
194
  }
198
195
 
199
- const targetFlowMode = parseFlowModeToken(args[consumed + 1], args[consumed + 2]);
196
+ const targetFlowMode = parseFlowModeToken(args[index + 2]);
200
197
  if (!targetFlowMode) {
201
198
  break;
202
199
  }
203
200
 
204
201
  upsertTarget(continueTargets, {
205
- targetSeatId,
202
+ seatId: targetSeatId,
206
203
  flowMode: targetFlowMode,
207
204
  });
208
205
 
209
- consumed += 3;
206
+ index += 3;
210
207
  }
211
208
 
212
- return { consumed, continueTargets };
209
+ if (index === startIndex) {
210
+ return null;
211
+ }
212
+
213
+ return {
214
+ continueTargets,
215
+ nextIndex: index,
216
+ };
213
217
  }
214
218
 
215
- function parseFlowModeToken(flowToken, modeToken) {
216
- const normalizedFlowToken = String(flowToken || "").trim().toLowerCase();
217
- const normalizedModeToken = String(modeToken || "").trim().toLowerCase();
218
- if (normalizedFlowToken === "flow" && (normalizedModeToken === "on" || normalizedModeToken === "off")) {
219
- return normalizedModeToken;
219
+ function parseFlowModeToken(value) {
220
+ const token = String(value || "").trim().toLowerCase();
221
+ if (token === "on" || token === "off") {
222
+ return token;
220
223
  }
221
224
  return null;
222
225
  }
223
226
 
224
227
  function upsertTarget(targets, nextTarget) {
225
- const existingIndex = targets.findIndex((entry) => entry.targetSeatId === nextTarget.targetSeatId);
226
- if (existingIndex >= 0) {
227
- targets[existingIndex] = nextTarget;
228
+ const currentIndex = targets.findIndex((target) => target.seatId === nextTarget.seatId);
229
+ if (currentIndex === -1) {
230
+ targets.push(nextTarget);
228
231
  return;
229
232
  }
230
- targets.push(nextTarget);
233
+
234
+ targets[currentIndex] = nextTarget;
231
235
  }
232
236
 
233
237
  module.exports = {
package/src/runtime.js CHANGED
@@ -20,10 +20,12 @@ const {
20
20
  ensureDir,
21
21
  getDefaultSessionName,
22
22
  getFileSize,
23
+ getPartnerSeatId,
23
24
  getSeatPaths,
24
25
  getSessionPaths,
25
26
  getStateRoot,
26
27
  hashText,
28
+ isAnchorSeat,
27
29
  isPidAlive,
28
30
  listSeatIds,
29
31
  loadOrCreateSeatIdentity,
@@ -38,7 +40,8 @@ const {
38
40
  writeJson,
39
41
  } = require("./util");
40
42
 
41
- const TYPE_CHUNK_DELAY_MS = 18;
43
+ // A short settle delay keeps interactive CLIs from treating submit as another newline.
44
+ const TYPE_CHUNK_DELAY_MS = 45;
42
45
  const TYPE_CHUNK_SIZE = 24;
43
46
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
44
47
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
@@ -54,10 +57,61 @@ const CHILD_ENV_DROP_KEYS = [
54
57
  "CODEX_THREAD_ID",
55
58
  ];
56
59
 
60
+ function bestEffortEnableChildEcho(child) {
61
+ const ptsName = String(child?.ptsName || "").trim();
62
+ if (!ptsName || process.platform === "win32") {
63
+ return;
64
+ }
65
+
66
+ try {
67
+ execFileSync("stty", [
68
+ "-F",
69
+ ptsName,
70
+ "echo",
71
+ "icanon",
72
+ "isig",
73
+ "iexten",
74
+ "echoe",
75
+ "echok",
76
+ "echoke",
77
+ "echoctl",
78
+ ], {
79
+ stdio: "ignore",
80
+ });
81
+ } catch {
82
+ // Best effort only. The shell or child app may later change its own tty mode.
83
+ }
84
+ }
85
+
57
86
  function normalizeFlowMode(flowMode) {
58
87
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
59
88
  }
60
89
 
90
+ function normalizeContinueSeatId(value) {
91
+ const seatId = normalizeSeatId(value);
92
+ return seatId || null;
93
+ }
94
+
95
+ function normalizeContinueTargets(value) {
96
+ if (!Array.isArray(value)) {
97
+ return [];
98
+ }
99
+
100
+ return value
101
+ .map((entry) => {
102
+ const seatId = normalizeSeatId(entry?.seatId);
103
+ if (!seatId) {
104
+ return null;
105
+ }
106
+
107
+ return {
108
+ seatId,
109
+ flowMode: normalizeFlowMode(entry?.flowMode),
110
+ };
111
+ })
112
+ .filter((entry) => entry !== null);
113
+ }
114
+
61
115
  function resolveShell() {
62
116
  const shell = String(process.env.SHELL || "").trim();
63
117
  return shell || "/bin/bash";
@@ -127,11 +181,6 @@ function createSessionName(currentPath = process.cwd()) {
127
181
  return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
128
182
  }
129
183
 
130
- function getAnchorSeatId(seatId = 1) {
131
- const normalizedSeatId = normalizeSeatId(seatId) || 1;
132
- return normalizedSeatId % 2 === 0 ? normalizedSeatId - 1 : normalizedSeatId;
133
- }
134
-
135
184
  function sleepSync(ms) {
136
185
  if (!Number.isFinite(ms) || ms <= 0) {
137
186
  return;
@@ -140,33 +189,45 @@ function sleepSync(ms) {
140
189
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
141
190
  }
142
191
 
143
- function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = null) {
144
- const targetAnchorSeatId = normalizeSeatId(anchorSeatId) || null;
192
+ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
193
+ const normalizedSeatId = normalizeSeatId(seatId);
194
+ const partnerSeatId = getPartnerSeatId(normalizedSeatId);
195
+ if (!normalizedSeatId || !partnerSeatId) {
196
+ return null;
197
+ }
198
+
145
199
  const candidates = listSessionNames()
146
200
  .map((sessionName) => {
147
201
  const sessionPaths = getSessionPaths(sessionName);
148
202
  const controller = readJson(sessionPaths.controllerPath, null);
149
- const cwd = controller?.cwd || null;
203
+ const partnerPaths = getSeatPaths(sessionName, partnerSeatId);
204
+ const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
205
+ const partnerMeta = readJson(partnerPaths.metaPath, null);
206
+ const partnerStatus = readJson(partnerPaths.statusPath, null);
207
+ const seatMeta = readJson(seatPaths.metaPath, null);
208
+ const seatStatus = readJson(seatPaths.statusPath, null);
209
+ const stopRequest = readJson(sessionPaths.stopPath, null);
210
+
211
+ const cwd = controller?.cwd || partnerStatus?.cwd || partnerMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
150
212
  if (!matchesWorkingPath(cwd, currentPath)) {
151
213
  return null;
152
214
  }
153
215
 
154
- const stopRequest = readJson(sessionPaths.stopPath, null);
216
+ const partnerWrapperPid = partnerStatus?.pid || partnerMeta?.pid || null;
217
+ const partnerChildPid = partnerStatus?.childPid || partnerMeta?.childPid || null;
218
+ const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
219
+ const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
220
+ const partnerLive = isPidAlive(partnerWrapperPid) || isPidAlive(partnerChildPid);
221
+ const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
155
222
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
156
- const createdAtMs = Date.parse(controller?.createdAt || "");
223
+ const createdAtMs = Date.parse(controller?.createdAt || partnerMeta?.startedAt || partnerStatus?.updatedAt || "");
157
224
 
158
- if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
225
+ if (!partnerLive || seatLive) {
159
226
  return null;
160
227
  }
161
228
 
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
- }
229
+ if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
230
+ return null;
170
231
  }
171
232
 
172
233
  return {
@@ -180,10 +241,10 @@ function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = nul
180
241
  return candidates[0]?.sessionName || null;
181
242
  }
182
243
 
183
- function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS, anchorSeatId = null) {
244
+ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
184
245
  const deadline = Date.now() + timeoutMs;
185
246
  while (Date.now() <= deadline) {
186
- const sessionName = findExistingSessionName(currentPath, anchorSeatId);
247
+ const sessionName = findJoinableSessionName(currentPath, seatId);
187
248
  if (sessionName) {
188
249
  return sessionName;
189
250
  }
@@ -194,17 +255,13 @@ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEA
194
255
  }
195
256
 
196
257
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
197
- const anchorSeatId = getAnchorSeatId(seatId);
198
- const existing = findExistingSessionName(currentPath, anchorSeatId);
199
- if (existing) {
200
- return existing;
258
+ const joinableSessionName = findJoinableSessionName(currentPath, seatId);
259
+ if (joinableSessionName) {
260
+ return joinableSessionName;
201
261
  }
202
262
 
203
- if (seatId % 2 === 0) {
204
- const waited = waitForExistingSessionName(currentPath, SEAT_JOIN_WAIT_MS, anchorSeatId);
205
- if (waited) {
206
- return waited;
207
- }
263
+ if (!isAnchorSeat(seatId)) {
264
+ return waitForJoinableSessionName(currentPath, seatId) || createSessionName(currentPath);
208
265
  }
209
266
 
210
267
  return createSessionName(currentPath);
@@ -409,6 +466,26 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
409
466
  return null;
410
467
  }
411
468
 
469
+ function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
470
+ return JSON.stringify({
471
+ type: "muuuuse_pair_claim",
472
+ sessionName,
473
+ challenge,
474
+ seat1PublicKey,
475
+ seat2PublicKey,
476
+ });
477
+ }
478
+
479
+ function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
480
+ return JSON.stringify({
481
+ type: "muuuuse_pair_ack",
482
+ sessionName,
483
+ challenge,
484
+ seat1PublicKey,
485
+ seat2PublicKey,
486
+ });
487
+ }
488
+
412
489
  function buildAnswerSignaturePayload(sessionName, challenge, entry) {
413
490
  return JSON.stringify({
414
491
  type: "muuuuse_answer",
@@ -421,17 +498,20 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
421
498
  origin: entry.origin,
422
499
  phase: entry.phase || "final_answer",
423
500
  createdAt: entry.createdAt,
501
+ targetSeatId: entry.targetSeatId || null,
502
+ targetFlowMode: normalizeFlowMode(entry.targetFlowMode),
424
503
  text: entry.text,
425
504
  });
426
505
  }
427
506
 
428
- function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
507
+ function buildContinuationEntry(sourceSessionName, targetSeatId, entry, targetFlowMode = null) {
429
508
  return {
430
509
  id: createId(12),
431
510
  type: "continue",
432
511
  sourceSessionName,
433
512
  sourceSeatId: entry.seatId,
434
513
  targetSeatId,
514
+ targetFlowMode: normalizeFlowMode(targetFlowMode),
435
515
  origin: entry.origin || "unknown",
436
516
  phase: entry.phase || "final_answer",
437
517
  text: entry.text,
@@ -453,6 +533,15 @@ function shouldAcceptInboundEntry(flowMode, entry) {
453
533
  return flowMode === "on" || getRelayPhase(entry) === "final_answer";
454
534
  }
455
535
 
536
+ function resolveInboundFlowMode(defaultFlowMode, seatId, entry) {
537
+ const targetSeatId = normalizeSeatId(entry?.targetSeatId);
538
+ if (targetSeatId === normalizeSeatId(seatId)) {
539
+ return normalizeFlowMode(entry?.targetFlowMode);
540
+ }
541
+
542
+ return normalizeFlowMode(defaultFlowMode);
543
+ }
544
+
456
545
  function getSeatDirIfExists(sessionName, seatId) {
457
546
  const dir = path.join(getStateRoot(), "sessions", sessionName, `seat-${seatId}`);
458
547
  try {
@@ -548,7 +637,7 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
548
637
  }
549
638
 
550
639
  try {
551
- child.write("\n");
640
+ child.write("\r");
552
641
  } catch {
553
642
  return false;
554
643
  }
@@ -559,52 +648,29 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
559
648
  class ArmedSeat {
560
649
  constructor(options) {
561
650
  this.seatId = options.seatId;
562
- this.anchorSeatId = getAnchorSeatId(this.seatId);
563
- this.partnerSeatId = this.seatId % 2 === 0 ? this.seatId - 1 : this.seatId + 1;
564
- this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
651
+ this.partnerSeatId = getPartnerSeatId(options.seatId);
652
+ this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
653
+ this.flowMode = normalizeFlowMode(options.flowMode);
654
+ this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
655
+ this.continueTargets = normalizeContinueTargets(options.continueTargets);
565
656
  this.cwd = normalizeWorkingPath(options.cwd);
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
- }
657
+ if (this.continueSeatId === this.seatId) {
658
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
572
659
  }
573
-
574
- if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
575
- throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
660
+ if (this.continueTargets.some((target) => target.seatId === this.seatId)) {
661
+ throw new Error(`\`muuuuse ${this.seatId}\` cannot link to itself.`);
576
662
  }
577
663
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
578
664
  if (!this.sessionName) {
579
665
  throw new Error(
580
- `Failed to create or find session in ${this.cwd}.`
666
+ `No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
581
667
  );
582
668
  }
583
669
  this.sessionPaths = getSessionPaths(this.sessionName);
584
670
  this.paths = getSeatPaths(this.sessionName, this.seatId);
671
+ this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
585
672
  this.continueOffset = getFileSize(this.paths.continuePath);
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
- }
673
+ this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
608
674
 
609
675
  this.child = null;
610
676
  this.childPid = null;
@@ -618,11 +684,16 @@ class ArmedSeat {
618
684
  this.resizeCleanup = null;
619
685
  this.forceKillTimer = null;
620
686
  this.identity = null;
621
- this.ownChallenge = null;
622
687
  this.lastUserInputAtMs = 0;
623
688
  this.pendingInboundContext = null;
624
689
  this.recentInboundRelays = [];
625
690
  this.recentEmittedAnswers = [];
691
+ this.trustState = {
692
+ challenge: null,
693
+ peerPublicKey: null,
694
+ phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
695
+ pairedAt: null,
696
+ };
626
697
  this.liveState = {
627
698
  type: null,
628
699
  pid: null,
@@ -645,6 +716,9 @@ class ArmedSeat {
645
716
  updatedAt: new Date().toISOString(),
646
717
  anchorSeatId: this.anchorSeatId,
647
718
  partnerSeatId: this.partnerSeatId,
719
+ anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
720
+ partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
721
+ pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
648
722
  ...extra,
649
723
  });
650
724
  }
@@ -656,7 +730,10 @@ class ArmedSeat {
656
730
  writeMeta(extra = {}) {
657
731
  writeJson(this.paths.metaPath, {
658
732
  seatId: this.seatId,
733
+ partnerSeatId: this.partnerSeatId,
659
734
  sessionName: this.sessionName,
735
+ flowMode: this.flowMode,
736
+ continueSeatId: this.continueSeatId,
660
737
  continueTargets: this.continueTargets,
661
738
  cwd: this.cwd,
662
739
  pid: process.pid,
@@ -670,7 +747,10 @@ class ArmedSeat {
670
747
  writeStatus(extra = {}) {
671
748
  writeJson(this.paths.statusPath, {
672
749
  seatId: this.seatId,
750
+ partnerSeatId: this.partnerSeatId,
673
751
  sessionName: this.sessionName,
752
+ flowMode: this.flowMode,
753
+ continueSeatId: this.continueSeatId,
674
754
  continueTargets: this.continueTargets,
675
755
  cwd: this.cwd,
676
756
  pid: process.pid,
@@ -683,84 +763,168 @@ class ArmedSeat {
683
763
 
684
764
  initializeTrustMaterial() {
685
765
  this.identity = loadOrCreateSeatIdentity(this.paths);
686
- const ownChallenge = createId(48);
766
+
767
+ if (!isAnchorSeat(this.seatId)) {
768
+ return;
769
+ }
770
+
687
771
  writeJson(this.paths.challengePath, {
688
772
  sessionName: this.sessionName,
689
- challenge: ownChallenge,
773
+ challenge: createId(48),
690
774
  publicKey: this.identity.publicKey,
691
775
  createdAt: new Date().toISOString(),
692
776
  });
693
- this.ownChallenge = ownChallenge;
777
+ this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
778
+ this.trustState.peerPublicKey = null;
779
+ this.trustState.phase = "waiting_for_peer_signature";
780
+ this.trustState.pairedAt = null;
781
+ fs.rmSync(this.paths.ackPath, { force: true });
782
+ fs.rmSync(this.partnerPaths.claimPath, { force: true });
694
783
  }
695
784
 
696
- syncTargetTrust() {
785
+ syncTrustState() {
697
786
  if (!this.identity) {
698
787
  this.initializeTrustMaterial();
699
788
  }
700
789
 
701
- for (const target of this.continueTargets) {
702
- this.syncOneTargetTrust(target.targetSeatId);
790
+ if (isAnchorSeat(this.seatId)) {
791
+ this.syncSeatOneTrust();
792
+ return;
703
793
  }
794
+
795
+ this.syncSeatTwoTrust();
704
796
  }
705
797
 
706
- syncOneTargetTrust(targetSeatId) {
707
- const trust = this.targetTrust[targetSeatId];
708
- if (!trust || trust.phase === "paired") {
798
+ syncSeatOneTrust() {
799
+ const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
800
+ if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
801
+ this.trustState = {
802
+ challenge: null,
803
+ peerPublicKey: null,
804
+ phase: "waiting_for_peer_signature",
805
+ pairedAt: null,
806
+ };
709
807
  return;
710
808
  }
711
809
 
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();
810
+ this.trustState.challenge = challengeRecord.challenge;
811
+ const claim = readJson(this.partnerPaths.claimPath, null);
812
+ if (
813
+ !claim ||
814
+ claim.sessionName !== this.sessionName ||
815
+ claim.challenge !== challengeRecord.challenge ||
816
+ typeof claim.publicKey !== "string" ||
817
+ typeof claim.signature !== "string" ||
818
+ !verifyText(
819
+ buildClaimMessage(
820
+ this.sessionName,
821
+ challengeRecord.challenge,
822
+ this.identity.publicKey,
823
+ claim.publicKey.trim()
824
+ ),
825
+ claim.signature,
826
+ claim.publicKey
827
+ )
828
+ ) {
829
+ this.trustState.peerPublicKey = null;
830
+ this.trustState.phase = "waiting_for_peer_signature";
831
+ this.trustState.pairedAt = null;
832
+ fs.rmSync(this.paths.ackPath, { force: true });
716
833
  return;
717
834
  }
718
835
 
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;
836
+ const peerPublicKey = claim.publicKey.trim();
837
+ const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
838
+ const currentAck = readJson(this.paths.ackPath, null);
839
+ const ackIsValid = Boolean(
840
+ currentAck &&
841
+ currentAck.sessionName === this.sessionName &&
842
+ currentAck.challenge === challengeRecord.challenge &&
843
+ currentAck.publicKey === this.identity.publicKey &&
844
+ currentAck.peerPublicKey === peerPublicKey &&
845
+ typeof currentAck.signature === "string" &&
846
+ verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
847
+ );
848
+ if (!ackIsValid) {
849
+ writeJson(this.paths.ackPath, {
850
+ sessionName: this.sessionName,
851
+ challenge: challengeRecord.challenge,
852
+ publicKey: this.identity.publicKey,
853
+ peerPublicKey,
854
+ signature: signText(ackMessage, this.identity.privateKey),
855
+ signedAt: new Date().toISOString(),
856
+ });
724
857
  }
725
858
 
726
- const targetChallenge = readSeatChallenge(targetPaths, this.sessionName);
727
- if (!targetChallenge) {
728
- trust.phase = "waiting_for_target";
859
+ const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
860
+ this.trustState.peerPublicKey = peerPublicKey;
861
+ this.trustState.phase = "paired";
862
+ this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
863
+ }
864
+
865
+ syncSeatTwoTrust() {
866
+ const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
867
+ if (!challengeRecord) {
868
+ this.trustState = {
869
+ challenge: null,
870
+ peerPublicKey: null,
871
+ phase: "waiting_for_anchor_key",
872
+ pairedAt: null,
873
+ };
729
874
  return;
730
875
  }
731
876
 
732
- trust.challenge = targetChallenge.challenge;
733
- trust.peerPublicKey = targetChallenge.publicKey;
734
- trust.phase = "paired";
735
- trust.pairedAt = new Date().toISOString();
877
+ const challenge = challengeRecord.challenge;
878
+ const peerPublicKey = challengeRecord.publicKey;
879
+ const claimPayload = {
880
+ sessionName: this.sessionName,
881
+ challenge,
882
+ publicKey: this.identity.publicKey,
883
+ };
884
+ const claimSignature = signText(
885
+ buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
886
+ this.identity.privateKey
887
+ );
888
+ const currentClaim = readJson(this.paths.claimPath, null);
889
+ if (
890
+ !currentClaim ||
891
+ currentClaim.sessionName !== claimPayload.sessionName ||
892
+ currentClaim.challenge !== claimPayload.challenge ||
893
+ currentClaim.publicKey !== claimPayload.publicKey ||
894
+ currentClaim.signature !== claimSignature
895
+ ) {
896
+ writeJson(this.paths.claimPath, {
897
+ ...claimPayload,
898
+ signature: claimSignature,
899
+ signedAt: new Date().toISOString(),
900
+ });
901
+ }
736
902
 
737
- // Initialize offset to current file size so we only read new events.
738
- this.targetOffsets[targetSeatId] = getFileSize(targetPaths.eventsPath);
739
- }
903
+ const ack = readJson(this.partnerPaths.ackPath, null);
904
+ const paired = Boolean(
905
+ ack &&
906
+ ack.sessionName === this.sessionName &&
907
+ ack.challenge === challenge &&
908
+ ack.peerPublicKey === this.identity.publicKey &&
909
+ ack.publicKey === peerPublicKey &&
910
+ typeof ack.signature === "string" &&
911
+ verifyText(
912
+ buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
913
+ ack.signature,
914
+ peerPublicKey
915
+ )
916
+ );
740
917
 
741
- isTargetPaired(targetSeatId) {
742
- const trust = this.targetTrust[targetSeatId];
743
- return Boolean(trust && trust.phase === "paired");
918
+ this.trustState.challenge = challenge;
919
+ this.trustState.peerPublicKey = peerPublicKey;
920
+ this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
921
+ this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
744
922
  }
745
923
 
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));
924
+ isPaired() {
925
+ return this.trustState.phase === "paired" &&
926
+ typeof this.trustState.challenge === "string" &&
927
+ typeof this.trustState.peerPublicKey === "string";
764
928
  }
765
929
 
766
930
  launchShell() {
@@ -768,7 +932,6 @@ class ArmedSeat {
768
932
  fs.rmSync(this.paths.pipePath, { force: true });
769
933
  clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
770
934
  this.initializeTrustMaterial();
771
- this.syncTargetTrust();
772
935
  this.writeController();
773
936
 
774
937
  const shell = resolveShell();
@@ -781,10 +944,11 @@ class ArmedSeat {
781
944
  env: childEnv,
782
945
  name: childEnv.TERM,
783
946
  });
947
+ bestEffortEnableChildEcho(this.child);
784
948
 
785
949
  this.childPid = this.child.pid;
786
950
  this.writeMeta();
787
- this.writeStatus({ state: "running", trust: this.getOverallTrustPhase() });
951
+ this.writeStatus({ state: "running", trust: this.trustState.phase });
788
952
 
789
953
  this.child.onData((data) => {
790
954
  fs.appendFileSync(this.paths.pipePath, data);
@@ -918,6 +1082,32 @@ class ArmedSeat {
918
1082
  }
919
1083
  }
920
1084
 
1085
+ partnerIsLive() {
1086
+ const partner = readJson(this.partnerPaths.statusPath, null);
1087
+ return Boolean(partner?.pid && isPidAlive(partner.pid));
1088
+ }
1089
+
1090
+ getLinkTarget(seatId) {
1091
+ const desiredSeatId = normalizeContinueSeatId(seatId);
1092
+ if (!desiredSeatId) {
1093
+ return null;
1094
+ }
1095
+
1096
+ return this.continueTargets.find((target) => target.seatId === desiredSeatId) || null;
1097
+ }
1098
+
1099
+ getPartnerLinkTarget() {
1100
+ return this.getLinkTarget(this.partnerSeatId);
1101
+ }
1102
+
1103
+ shouldCaptureCommentary() {
1104
+ if (this.flowMode === "on") {
1105
+ return true;
1106
+ }
1107
+
1108
+ return this.continueTargets.some((target) => target.flowMode === "on");
1109
+ }
1110
+
921
1111
  stopRequested() {
922
1112
  const request = readJson(this.sessionPaths.stopPath, null);
923
1113
  if (!request?.requestedAt) {
@@ -929,18 +1119,18 @@ class ArmedSeat {
929
1119
  }
930
1120
 
931
1121
  findContinuationTarget(targetSeatId = null) {
932
- const seatIdToFind = targetSeatId || this.continueSeatId;
933
- if (!seatIdToFind) {
1122
+ const desiredSeatId = normalizeContinueSeatId(targetSeatId);
1123
+ if (!desiredSeatId) {
934
1124
  return null;
935
1125
  }
936
1126
 
937
1127
  const candidates = listSessionNames()
938
1128
  .map((sessionName) => {
939
- if (!getSeatDirIfExists(sessionName, seatIdToFind)) {
1129
+ if (!getSeatDirIfExists(sessionName, desiredSeatId)) {
940
1130
  return null;
941
1131
  }
942
1132
 
943
- const seat = buildSeatReport(sessionName, seatIdToFind);
1133
+ const seat = buildSeatReport(sessionName, desiredSeatId);
944
1134
  if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
945
1135
  return null;
946
1136
  }
@@ -967,6 +1157,73 @@ class ArmedSeat {
967
1157
  };
968
1158
  }
969
1159
 
1160
+ async pullPartnerEvents() {
1161
+ const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
1162
+ this.partnerOffset = nextOffset;
1163
+ if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
1164
+ return;
1165
+ }
1166
+
1167
+ const entries = parseAnswerEntries(text);
1168
+ for (const entry of entries) {
1169
+ if (this.stopped || this.stopRequested()) {
1170
+ this.requestStop("stop_requested");
1171
+ return;
1172
+ }
1173
+
1174
+ if (!shouldAcceptInboundEntry(resolveInboundFlowMode(this.flowMode, this.seatId, entry), entry)) {
1175
+ continue;
1176
+ }
1177
+
1178
+ const payload = sanitizeRelayText(entry.text);
1179
+ const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
1180
+ chainId: entry.chainId || entry.id,
1181
+ hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1182
+ id: entry.id,
1183
+ seatId: entry.seatId,
1184
+ origin: entry.origin || "unknown",
1185
+ phase: getRelayPhase(entry),
1186
+ createdAt: entry.createdAt,
1187
+ text: payload,
1188
+ });
1189
+ if (
1190
+ !payload ||
1191
+ entry.challenge !== this.trustState.challenge ||
1192
+ entry.publicKey !== this.trustState.peerPublicKey ||
1193
+ typeof entry.signature !== "string" ||
1194
+ !verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
1195
+ ) {
1196
+ continue;
1197
+ }
1198
+
1199
+ const delivered = await sendTextAndEnter(
1200
+ this.child,
1201
+ payload,
1202
+ () => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
1203
+ );
1204
+ if (!delivered) {
1205
+ this.requestStop("relay_aborted");
1206
+ return;
1207
+ }
1208
+
1209
+ if (this.stopped || this.stopRequested()) {
1210
+ this.requestStop("stop_requested");
1211
+ return;
1212
+ }
1213
+
1214
+ const deliveredAtMs = Date.now();
1215
+ this.pendingInboundContext = {
1216
+ chainId: entry.chainId || entry.id,
1217
+ deliveredAtMs,
1218
+ expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
1219
+ hop: Number.isInteger(entry.hop) ? entry.hop : 0,
1220
+ };
1221
+ this.relayCount += 1;
1222
+ this.rememberInboundRelay(payload);
1223
+ this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
1224
+ }
1225
+ }
1226
+
970
1227
  async pullContinuationEvents() {
971
1228
  const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
972
1229
  this.continueOffset = nextOffset;
@@ -981,6 +1238,10 @@ class ArmedSeat {
981
1238
  return;
982
1239
  }
983
1240
 
1241
+ if (!shouldAcceptInboundEntry(resolveInboundFlowMode(this.flowMode, this.seatId, entry), entry)) {
1242
+ continue;
1243
+ }
1244
+
984
1245
  const payload = sanitizeRelayText(entry.text);
985
1246
  if (!payload) {
986
1247
  continue;
@@ -1194,7 +1455,7 @@ class ArmedSeat {
1194
1455
  this.liveState.sessionFile,
1195
1456
  this.liveState.offset,
1196
1457
  this.liveState.captureSinceMs,
1197
- { flowMode: true }
1458
+ { flowMode: this.shouldCaptureCommentary() }
1198
1459
  );
1199
1460
  this.liveState.offset = result.nextOffset;
1200
1461
  answers.push(...result.answers);
@@ -1203,7 +1464,7 @@ class ArmedSeat {
1203
1464
  this.liveState.sessionFile,
1204
1465
  this.liveState.offset,
1205
1466
  this.liveState.captureSinceMs,
1206
- { flowMode: true }
1467
+ { flowMode: this.shouldCaptureCommentary() }
1207
1468
  );
1208
1469
  this.liveState.offset = result.nextOffset;
1209
1470
  answers.push(...result.answers);
@@ -1212,7 +1473,7 @@ class ArmedSeat {
1212
1473
  this.liveState.sessionFile,
1213
1474
  this.liveState.lastMessageId,
1214
1475
  this.liveState.captureSinceMs,
1215
- { flowMode: true }
1476
+ { flowMode: this.shouldCaptureCommentary() }
1216
1477
  );
1217
1478
  this.liveState.lastMessageId = result.lastMessageId;
1218
1479
  this.liveState.offset = result.fileSize;
@@ -1245,11 +1506,7 @@ class ArmedSeat {
1245
1506
  }
1246
1507
 
1247
1508
  const payload = sanitizeRelayText(entry.text);
1248
- if (!payload || !this.identity || !this.ownChallenge) {
1249
- return;
1250
- }
1251
-
1252
- if (!this.hasAnyPairedTarget()) {
1509
+ if (!payload) {
1253
1510
  return;
1254
1511
  }
1255
1512
 
@@ -1266,11 +1523,9 @@ class ArmedSeat {
1266
1523
  }
1267
1524
 
1268
1525
  const pendingInboundContext = this.getPendingInboundContext();
1269
- const entryId = entry.id || createId(12);
1270
1526
 
1271
- // Sign with OUR OWN challenge. Each reader verifies using our challenge
1272
- // (which they obtained during the trust handshake as peerChallenge).
1273
- const signedEntry = {
1527
+ const entryId = entry.id || createId(12);
1528
+ const relayEntry = {
1274
1529
  id: entryId,
1275
1530
  type: "answer",
1276
1531
  seatId: this.seatId,
@@ -1280,67 +1535,85 @@ class ArmedSeat {
1280
1535
  createdAt: entry.createdAt || new Date().toISOString(),
1281
1536
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
1282
1537
  hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
1283
- challenge: this.ownChallenge,
1284
- publicKey: this.identity.publicKey,
1285
1538
  };
1286
- signedEntry.signature = signText(
1287
- buildAnswerSignaturePayload(this.sessionName, this.ownChallenge, signedEntry),
1288
- this.identity.privateKey
1289
- );
1290
- appendJsonl(this.paths.eventsPath, signedEntry);
1291
1539
 
1292
- // Forward via continuation channel for cross-session targets.
1293
- for (const target of this.continueTargets) {
1294
- this.forwardContinuation(signedEntry, target);
1540
+ const partnerLinkTarget = this.getPartnerLinkTarget();
1541
+ let outboundEntry = relayEntry;
1542
+ if (this.isPaired() && this.identity) {
1543
+ const signedEntry = {
1544
+ ...relayEntry,
1545
+ challenge: this.trustState.challenge,
1546
+ publicKey: this.identity.publicKey,
1547
+ targetSeatId: partnerLinkTarget?.seatId || null,
1548
+ targetFlowMode: partnerLinkTarget?.flowMode || null,
1549
+ };
1550
+ signedEntry.signature = signText(
1551
+ buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1552
+ this.identity.privateKey
1553
+ );
1554
+
1555
+ if (!partnerLinkTarget) {
1556
+ appendJsonl(this.paths.eventsPath, signedEntry);
1557
+ }
1558
+ outboundEntry = signedEntry;
1295
1559
  }
1296
1560
 
1561
+ if (this.continueSeatId || this.continueTargets.length > 0) {
1562
+ this.forwardContinuation(outboundEntry);
1563
+ }
1297
1564
  this.rememberEmittedAnswer(answerKey);
1565
+
1298
1566
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1299
1567
  }
1300
1568
 
1301
- forwardContinuation(signedEntry, targetEntry) {
1302
- if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1303
- return;
1569
+ forwardContinuation(signedEntry) {
1570
+ const targets = [...this.continueTargets];
1571
+ if (this.continueSeatId && !targets.some((target) => target.seatId === this.continueSeatId)) {
1572
+ targets.push({
1573
+ seatId: this.continueSeatId,
1574
+ flowMode: null,
1575
+ });
1304
1576
  }
1305
1577
 
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)}`);
1314
- }
1578
+ if (targets.length === 0) {
1315
1579
  return;
1316
1580
  }
1317
1581
 
1318
- // Cross-session target: find the target across sessions.
1319
- const target = this.findContinuationTarget(targetEntry.targetSeatId);
1320
- if (!target) {
1321
- return;
1322
- }
1582
+ for (const targetEntry of targets) {
1583
+ if (targetEntry.flowMode && !shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1584
+ continue;
1585
+ }
1586
+
1587
+ const target = this.findContinuationTarget(targetEntry.seatId);
1588
+ if (!target) {
1589
+ this.log(`[${this.seatId}] continue ${targetEntry.seatId} unavailable`);
1590
+ continue;
1591
+ }
1323
1592
 
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)}`);
1593
+ const continuationEntry = buildContinuationEntry(
1594
+ this.sessionName,
1595
+ target.seatId,
1596
+ signedEntry,
1597
+ targetEntry.flowMode
1598
+ );
1599
+ appendJsonl(target.paths.continuePath, continuationEntry);
1600
+ this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1601
+ }
1327
1602
  }
1328
1603
 
1329
1604
  async tick() {
1330
1605
  if (this.stopRequested()) {
1331
1606
  this.writeStatus({
1332
1607
  state: "stopping",
1333
- trust: this.getOverallTrustPhase(),
1608
+ partnerLive: this.partnerIsLive(),
1609
+ trust: this.trustState.phase,
1334
1610
  });
1335
1611
  this.requestStop("stop_requested");
1336
1612
  return;
1337
1613
  }
1338
1614
 
1339
- this.syncTargetTrust();
1340
- if (this.stopped || this.stopRequested()) {
1341
- this.requestStop("stop_requested");
1342
- return;
1343
- }
1615
+ this.syncTrustState();
1616
+ await this.pullPartnerEvents();
1344
1617
  await this.pullContinuationEvents();
1345
1618
  if (this.stopped || this.stopRequested()) {
1346
1619
  this.requestStop("stop_requested");
@@ -1355,11 +1628,13 @@ class ArmedSeat {
1355
1628
  this.writeStatus({
1356
1629
  state: live.state,
1357
1630
  agent: live.agent,
1631
+ flowMode: this.flowMode,
1358
1632
  cwd: live.cwd,
1359
1633
  log: live.log,
1360
1634
  lastAnswerAt: live.lastAnswerAt,
1361
- trust: this.getOverallTrustPhase(),
1362
- challengeReady: this.hasAnyPairedTarget(),
1635
+ partnerLive: this.partnerIsLive(),
1636
+ trust: this.trustState.phase,
1637
+ challengeReady: Boolean(this.trustState.challenge),
1363
1638
  });
1364
1639
  }
1365
1640
 
@@ -1371,9 +1646,23 @@ class ArmedSeat {
1371
1646
 
1372
1647
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1373
1648
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1649
+ this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1650
+ if (this.continueSeatId) {
1651
+ this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1652
+ }
1374
1653
  if (this.continueTargets.length > 0) {
1375
- const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
1376
- this.log(`Seat ${this.seatId} relays to ${targets}. Establishing trust.`);
1654
+ this.log(
1655
+ `Seat ${this.seatId} links additional targets: ${this.continueTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
1656
+ );
1657
+ }
1658
+ if (this.partnerIsLive()) {
1659
+ if (isAnchorSeat(this.seatId)) {
1660
+ this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
1661
+ } else {
1662
+ this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
1663
+ }
1664
+ } else {
1665
+ this.log(`Seat ${this.seatId} is armed without a live pair partner. Link targets can relay immediately; direct pair relay activates after seat ${this.partnerSeatId} joins.`);
1377
1666
  }
1378
1667
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1379
1668
 
@@ -1470,7 +1759,9 @@ function buildSeatReport(sessionName, seatId) {
1470
1759
  return {
1471
1760
  seatId,
1472
1761
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1473
- continueTargets: status?.continueTargets || meta?.continueTargets || [],
1762
+ flowMode: status?.flowMode || meta?.flowMode || "off",
1763
+ continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1764
+ continueTargets: normalizeContinueTargets(status?.continueTargets || meta?.continueTargets),
1474
1765
  wrapperPid,
1475
1766
  childPid,
1476
1767
  wrapperLive,
@@ -1484,6 +1775,7 @@ function buildSeatReport(sessionName, seatId) {
1484
1775
  trust: status?.trust || null,
1485
1776
  updatedAt: status?.updatedAt || null,
1486
1777
  lastAnswerAt: status?.lastAnswerAt || null,
1778
+ partnerLive: Boolean(status?.partnerLive),
1487
1779
  };
1488
1780
  }
1489
1781
 
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 v7.0.1";
12
+ const BRAND = "🔌Muuuuse";
13
13
  const POLL_MS = 220;
14
14
  const MAX_RELAY_CHARS = 4000;
15
15
  const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
@@ -175,6 +175,17 @@ 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
+ }
178
189
 
179
190
  function listSeatIds(sessionName) {
180
191
  const sessionDir = getSessionDir(sessionName);
@@ -271,28 +282,39 @@ function listSessionNames() {
271
282
 
272
283
  function usage() {
273
284
  return [
274
- `${BRAND} relay protocol for long-horizon zero-drift agentic code loops. agents relay output between terminals, converging to lucid conclusions.`,
285
+ `${BRAND} arms regular terminals and relays assistant output across odd/even partners plus explicit links.`,
275
286
  "",
276
287
  "Usage:",
277
288
  " muuuuse 1",
278
- " muuuuse 1 link 2 flow on",
279
- " muuuuse 1 link 2 flow on 3 flow off",
289
+ " muuuuse 1 flow on",
290
+ " muuuuse 1 flow off",
291
+ " muuuuse 1 flow on continue 3",
292
+ " muuuuse 1 link 2 flow on 3 flow off 5 flow off",
280
293
  " muuuuse 2",
281
- " muuuuse 2 link 3 flow on",
294
+ " muuuuse 2 flow on",
295
+ " muuuuse 2 flow off",
296
+ " muuuuse 2 flow on continue 3",
297
+ " muuuuse 2 link 3 flow off",
282
298
  " muuuuse 3",
299
+ " muuuuse 4",
300
+ " muuuuse 4 flow on continue 1",
283
301
  " muuuuse stop",
284
302
  " muuuuse status",
285
303
  "",
286
304
  "Flow:",
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.",
305
+ " 1. Run `muuuuse <seat>` in the terminal you want to arm.",
306
+ " 2. Matching odd/even partners still auto-pair when both seats are armed, in either order.",
307
+ " 3. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
308
+ " 4. Optional: arm each seat with `flow on` or `flow off`.",
309
+ " 5. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
310
+ " 6. Or use `link <seat> flow on|off ...` to fan out to multiple armed seats.",
311
+ " 7. Standalone seats can still route through links even before a pair partner joins.",
312
+ " 8. Use those armed shells normally.",
313
+ " 9. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
314
+ " 10. Run `muuuuse status` or `muuuuse stop` from any shell.",
292
315
  "",
293
316
  "Notes:",
294
- " - Any seat can relay to any other seat independently.",
295
- " - `muuuuse stop` and `muuuuse status` work from any terminal.",
317
+ " - `muuuuse stop` and `muuuuse status` work from another terminal or the same one.",
296
318
  " - State lives under `~/.muuuuse`.",
297
319
  ].join("\n");
298
320
  }
@@ -306,7 +328,9 @@ module.exports = {
306
328
  ensureDir,
307
329
  getDefaultSessionName,
308
330
  getFileSize,
331
+ getPartnerSeatId,
309
332
  loadOrCreateSeatIdentity,
333
+ isAnchorSeat,
310
334
  getSeatPaths,
311
335
  getSessionPaths,
312
336
  getStateRoot,