muuuuse 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "6.0.0",
4
- "description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
3
+ "version": "7.0.0",
4
+ "description": "🔌Muuuuse relay protocol for long-horizon zero-drift agentic code loops. Any seat relays to any other with per-target signed trust and flow control.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "muuuuse": "bin/muuse.js"
package/src/agents.js CHANGED
@@ -468,7 +468,9 @@ function parseCodexAssistantLine(line, options = {}) {
468
468
  }
469
469
 
470
470
  const phase = String(entry.payload?.phase || "").trim().toLowerCase();
471
- const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
471
+ // Newer Codex sessions can omit `payload.phase` for final answers.
472
+ const normalizedPhase = phase === "commentary" ? "commentary" : "final_answer";
473
+ const relayablePhase = normalizedPhase === "final_answer" || (flowMode && normalizedPhase === "commentary");
472
474
  if (!relayablePhase) {
473
475
  return null;
474
476
  }
@@ -481,7 +483,7 @@ function parseCodexAssistantLine(line, options = {}) {
481
483
  return {
482
484
  id: entry.payload.id || hashText(line),
483
485
  text,
484
- phase: phase === "commentary" ? "commentary" : "final_answer",
486
+ phase: normalizedPhase,
485
487
  timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
486
488
  };
487
489
  } catch {
package/src/cli.js CHANGED
@@ -102,7 +102,7 @@ function renderSeatStatus(seat) {
102
102
  function renderLinkTargets(seat) {
103
103
  const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
104
104
  return targets
105
- .map((target) => `${target.targetSeatId}:${target.flowMode}`)
105
+ .map((target) => `${target.targetSeatId} (flow ${target.flowMode})`)
106
106
  .join(", ");
107
107
  }
108
108
 
@@ -110,6 +110,20 @@ function parseSeatOptions(command, args) {
110
110
  const seatId = normalizeSeatId(command);
111
111
  let continueTargets = [];
112
112
  let index = 0;
113
+ let seatFlowMode = null;
114
+ let hasExplicitTarget = false;
115
+
116
+ const flowToken = String(args[index] || "").trim().toLowerCase();
117
+ if (flowToken === "flow") {
118
+ const parsedSeatFlow = parseFlowModeToken("flow", args[index + 1]);
119
+ if (!parsedSeatFlow) {
120
+ throw new Error(
121
+ `\`muuuuse ${command} flow\` requires \`on\` or \`off\`.`
122
+ );
123
+ }
124
+ seatFlowMode = parsedSeatFlow;
125
+ index += 2;
126
+ }
113
127
 
114
128
  while (index < args.length) {
115
129
  const token = String(args[index] || "").trim().toLowerCase();
@@ -118,21 +132,45 @@ function parseSeatOptions(command, args) {
118
132
  const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
119
133
  if (parsedLinks.consumed > 0) {
120
134
  continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
135
+ hasExplicitTarget = true;
121
136
  index += 1 + parsedLinks.consumed;
122
137
  continue;
123
138
  }
124
139
  break;
125
140
  }
126
141
 
142
+ if (token === "continue") {
143
+ const targetSeatId = normalizeSeatId(args[index + 1]);
144
+ if (!targetSeatId) {
145
+ break;
146
+ }
147
+ upsertTarget(continueTargets, {
148
+ targetSeatId,
149
+ flowMode: seatFlowMode || "on",
150
+ });
151
+ hasExplicitTarget = true;
152
+ index += 2;
153
+ continue;
154
+ }
155
+
127
156
  break;
128
157
  }
129
158
 
130
159
  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
+ }
131
169
  return { continueTargets };
132
170
  }
133
171
 
134
172
  throw new Error(
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.`
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.`
136
174
  );
137
175
  }
138
176
 
package/src/runtime.js CHANGED
@@ -58,11 +58,6 @@ function normalizeFlowMode(flowMode) {
58
58
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
59
59
  }
60
60
 
61
- function normalizeContinueSeatId(value) {
62
- const seatId = normalizeSeatId(value);
63
- return seatId || null;
64
- }
65
-
66
61
  function resolveShell() {
67
62
  const shell = String(process.env.SHELL || "").trim();
68
63
  return shell || "/bin/bash";
@@ -132,6 +127,11 @@ function createSessionName(currentPath = process.cwd()) {
132
127
  return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
133
128
  }
134
129
 
130
+ function getAnchorSeatId(seatId = 1) {
131
+ const normalizedSeatId = normalizeSeatId(seatId) || 1;
132
+ return normalizedSeatId % 2 === 0 ? normalizedSeatId - 1 : normalizedSeatId;
133
+ }
134
+
135
135
  function sleepSync(ms) {
136
136
  if (!Number.isFinite(ms) || ms <= 0) {
137
137
  return;
@@ -140,7 +140,8 @@ function sleepSync(ms) {
140
140
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
141
141
  }
142
142
 
143
- function findExistingSessionName(currentPath = process.cwd()) {
143
+ function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = null) {
144
+ const targetAnchorSeatId = normalizeSeatId(anchorSeatId) || null;
144
145
  const candidates = listSessionNames()
145
146
  .map((sessionName) => {
146
147
  const sessionPaths = getSessionPaths(sessionName);
@@ -158,6 +159,16 @@ function findExistingSessionName(currentPath = process.cwd()) {
158
159
  return null;
159
160
  }
160
161
 
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
+ }
170
+ }
171
+
161
172
  return {
162
173
  sessionName,
163
174
  createdAtMs,
@@ -169,10 +180,10 @@ function findExistingSessionName(currentPath = process.cwd()) {
169
180
  return candidates[0]?.sessionName || null;
170
181
  }
171
182
 
172
- function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
183
+ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS, anchorSeatId = null) {
173
184
  const deadline = Date.now() + timeoutMs;
174
185
  while (Date.now() <= deadline) {
175
- const sessionName = findExistingSessionName(currentPath);
186
+ const sessionName = findExistingSessionName(currentPath, anchorSeatId);
176
187
  if (sessionName) {
177
188
  return sessionName;
178
189
  }
@@ -183,11 +194,19 @@ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEA
183
194
  }
184
195
 
185
196
  function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
186
- const existing = findExistingSessionName(currentPath);
197
+ const anchorSeatId = getAnchorSeatId(seatId);
198
+ const existing = findExistingSessionName(currentPath, anchorSeatId);
187
199
  if (existing) {
188
200
  return existing;
189
201
  }
190
202
 
203
+ if (seatId % 2 === 0) {
204
+ const waited = waitForExistingSessionName(currentPath, SEAT_JOIN_WAIT_MS, anchorSeatId);
205
+ if (waited) {
206
+ return waited;
207
+ }
208
+ }
209
+
191
210
  return createSessionName(currentPath);
192
211
  }
193
212
 
@@ -540,26 +559,22 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
540
559
  class ArmedSeat {
541
560
  constructor(options) {
542
561
  this.seatId = options.seatId;
562
+ this.anchorSeatId = getAnchorSeatId(this.seatId);
563
+ this.partnerSeatId = this.seatId % 2 === 0 ? this.seatId - 1 : this.seatId + 1;
543
564
  this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
544
565
  this.cwd = normalizeWorkingPath(options.cwd);
545
566
 
546
- // Auto-link partner seat for backwards compatibility (seat 12, seat 2→1)
547
- // This preserves v5 behavior where odd seats (anchor) initiate relay to even (partner)
567
+ // Auto-link adjacent partner seat for backwards compatibility (12, 3↔4, ...).
548
568
  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" });
569
+ if (this.partnerSeatId > 0) {
570
+ this.continueTargets.push({ targetSeatId: this.partnerSeatId, flowMode: "on" });
555
571
  }
556
- // Even seats don't auto-link (they receive from odd partners)
557
572
  }
558
573
 
559
574
  if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
560
575
  throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
561
576
  }
562
- this.sessionName = resolveSessionName(this.cwd);
577
+ this.sessionName = resolveSessionName(this.cwd, this.seatId);
563
578
  if (!this.sessionName) {
564
579
  throw new Error(
565
580
  `Failed to create or find session in ${this.cwd}.`
@@ -568,7 +583,28 @@ class ArmedSeat {
568
583
  this.sessionPaths = getSessionPaths(this.sessionName);
569
584
  this.paths = getSeatPaths(this.sessionName, this.seatId);
570
585
  this.continueOffset = getFileSize(this.paths.continuePath);
571
- this.relayTargets = {};
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
+ }
572
608
 
573
609
  this.child = null;
574
610
  this.childPid = null;
@@ -582,15 +618,11 @@ class ArmedSeat {
582
618
  this.resizeCleanup = null;
583
619
  this.forceKillTimer = null;
584
620
  this.identity = null;
621
+ this.ownChallenge = null;
585
622
  this.lastUserInputAtMs = 0;
586
623
  this.pendingInboundContext = null;
587
624
  this.recentInboundRelays = [];
588
625
  this.recentEmittedAnswers = [];
589
- this.trustState = {
590
- challenge: null,
591
- phase: "initialized",
592
- createdAt: null,
593
- };
594
626
  this.liveState = {
595
627
  type: null,
596
628
  pid: null,
@@ -611,6 +643,8 @@ class ArmedSeat {
611
643
  cwd: this.cwd,
612
644
  createdAt: current.createdAt || this.startedAt,
613
645
  updatedAt: new Date().toISOString(),
646
+ anchorSeatId: this.anchorSeatId,
647
+ partnerSeatId: this.partnerSeatId,
614
648
  ...extra,
615
649
  });
616
650
  }
@@ -649,19 +683,84 @@ class ArmedSeat {
649
683
 
650
684
  initializeTrustMaterial() {
651
685
  this.identity = loadOrCreateSeatIdentity(this.paths);
686
+ const ownChallenge = createId(48);
652
687
  writeJson(this.paths.challengePath, {
653
688
  sessionName: this.sessionName,
689
+ challenge: ownChallenge,
654
690
  publicKey: this.identity.publicKey,
655
691
  createdAt: new Date().toISOString(),
656
692
  });
657
- this.trustState.phase = "initialized";
658
- this.trustState.createdAt = new Date().toISOString();
693
+ this.ownChallenge = ownChallenge;
659
694
  }
660
695
 
661
- syncTrustState() {
696
+ syncTargetTrust() {
662
697
  if (!this.identity) {
663
698
  this.initializeTrustMaterial();
664
699
  }
700
+
701
+ for (const target of this.continueTargets) {
702
+ this.syncOneTargetTrust(target.targetSeatId);
703
+ }
704
+ }
705
+
706
+ syncOneTargetTrust(targetSeatId) {
707
+ const trust = this.targetTrust[targetSeatId];
708
+ if (!trust || trust.phase === "paired") {
709
+ return;
710
+ }
711
+
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();
716
+ return;
717
+ }
718
+
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;
724
+ }
725
+
726
+ const targetChallenge = readSeatChallenge(targetPaths, this.sessionName);
727
+ if (!targetChallenge) {
728
+ trust.phase = "waiting_for_target";
729
+ return;
730
+ }
731
+
732
+ trust.challenge = targetChallenge.challenge;
733
+ trust.peerPublicKey = targetChallenge.publicKey;
734
+ trust.phase = "paired";
735
+ trust.pairedAt = new Date().toISOString();
736
+
737
+ // Initialize offset to current file size so we only read new events.
738
+ this.targetOffsets[targetSeatId] = getFileSize(targetPaths.eventsPath);
739
+ }
740
+
741
+ isTargetPaired(targetSeatId) {
742
+ const trust = this.targetTrust[targetSeatId];
743
+ return Boolean(trust && trust.phase === "paired");
744
+ }
745
+
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));
665
764
  }
666
765
 
667
766
  launchShell() {
@@ -669,6 +768,7 @@ class ArmedSeat {
669
768
  fs.rmSync(this.paths.pipePath, { force: true });
670
769
  clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
671
770
  this.initializeTrustMaterial();
771
+ this.syncTargetTrust();
672
772
  this.writeController();
673
773
 
674
774
  const shell = resolveShell();
@@ -684,7 +784,7 @@ class ArmedSeat {
684
784
 
685
785
  this.childPid = this.child.pid;
686
786
  this.writeMeta();
687
- this.writeStatus({ state: "running", trust: this.trustState.phase });
787
+ this.writeStatus({ state: "running", trust: this.getOverallTrustPhase() });
688
788
 
689
789
  this.child.onData((data) => {
690
790
  fs.appendFileSync(this.paths.pipePath, data);
@@ -1145,7 +1245,11 @@ class ArmedSeat {
1145
1245
  }
1146
1246
 
1147
1247
  const payload = sanitizeRelayText(entry.text);
1148
- if (!payload || !this.identity || !this.trustState.challenge) {
1248
+ if (!payload || !this.identity || !this.ownChallenge) {
1249
+ return;
1250
+ }
1251
+
1252
+ if (!this.hasAnyPairedTarget()) {
1149
1253
  return;
1150
1254
  }
1151
1255
 
@@ -1162,8 +1266,10 @@ class ArmedSeat {
1162
1266
  }
1163
1267
 
1164
1268
  const pendingInboundContext = this.getPendingInboundContext();
1165
-
1166
1269
  const entryId = entry.id || createId(12);
1270
+
1271
+ // Sign with OUR OWN challenge. Each reader verifies using our challenge
1272
+ // (which they obtained during the trust handshake as peerChallenge).
1167
1273
  const signedEntry = {
1168
1274
  id: entryId,
1169
1275
  type: "answer",
@@ -1174,51 +1280,67 @@ class ArmedSeat {
1174
1280
  createdAt: entry.createdAt || new Date().toISOString(),
1175
1281
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
1176
1282
  hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
1177
- challenge: this.trustState.challenge,
1283
+ challenge: this.ownChallenge,
1178
1284
  publicKey: this.identity.publicKey,
1179
1285
  };
1180
1286
  signedEntry.signature = signText(
1181
- buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1287
+ buildAnswerSignaturePayload(this.sessionName, this.ownChallenge, signedEntry),
1182
1288
  this.identity.privateKey
1183
1289
  );
1184
1290
  appendJsonl(this.paths.eventsPath, signedEntry);
1185
- this.forwardContinuation(signedEntry);
1186
- this.rememberEmittedAnswer(answerKey);
1187
1291
 
1292
+ // Forward via continuation channel for cross-session targets.
1293
+ for (const target of this.continueTargets) {
1294
+ this.forwardContinuation(signedEntry, target);
1295
+ }
1296
+
1297
+ this.rememberEmittedAnswer(answerKey);
1188
1298
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1189
1299
  }
1190
1300
 
1191
- forwardContinuation(signedEntry) {
1192
- // Route to all continueTargets with per-target flow modes
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
- }
1301
+ forwardContinuation(signedEntry, targetEntry) {
1302
+ if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1303
+ return;
1304
+ }
1198
1305
 
1199
- const target = this.findContinuationTarget(targetEntry.targetSeatId);
1200
- if (!target) {
1201
- this.log(`[${this.seatId}] target ${targetEntry.targetSeatId} unavailable`);
1202
- continue;
1306
+ // Same-session target: write directly to their continuation channel.
1307
+ const trust = this.targetTrust[targetEntry.targetSeatId];
1308
+ if (trust && trust.sameSession) {
1309
+ const targetPaths = this.targetPaths[targetEntry.targetSeatId];
1310
+ if (targetPaths) {
1311
+ const continuationEntry = buildContinuationEntry(this.sessionName, targetEntry.targetSeatId, signedEntry);
1312
+ appendJsonl(targetPaths.continuePath, continuationEntry);
1313
+ this.log(`[${this.seatId} => ${targetEntry.targetSeatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1203
1314
  }
1315
+ return;
1316
+ }
1204
1317
 
1205
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1206
- appendJsonl(target.paths.continuePath, continuationEntry);
1207
- this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1318
+ // Cross-session target: find the target across sessions.
1319
+ const target = this.findContinuationTarget(targetEntry.targetSeatId);
1320
+ if (!target) {
1321
+ return;
1208
1322
  }
1323
+
1324
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1325
+ appendJsonl(target.paths.continuePath, continuationEntry);
1326
+ this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1209
1327
  }
1210
1328
 
1211
1329
  async tick() {
1212
1330
  if (this.stopRequested()) {
1213
1331
  this.writeStatus({
1214
1332
  state: "stopping",
1215
- trust: this.trustState.phase,
1333
+ trust: this.getOverallTrustPhase(),
1216
1334
  });
1217
1335
  this.requestStop("stop_requested");
1218
1336
  return;
1219
1337
  }
1220
1338
 
1221
- this.syncTrustState();
1339
+ this.syncTargetTrust();
1340
+ if (this.stopped || this.stopRequested()) {
1341
+ this.requestStop("stop_requested");
1342
+ return;
1343
+ }
1222
1344
  await this.pullContinuationEvents();
1223
1345
  if (this.stopped || this.stopRequested()) {
1224
1346
  this.requestStop("stop_requested");
@@ -1236,8 +1358,8 @@ class ArmedSeat {
1236
1358
  cwd: live.cwd,
1237
1359
  log: live.log,
1238
1360
  lastAnswerAt: live.lastAnswerAt,
1239
- trust: this.trustState.phase,
1240
- challengeReady: Boolean(this.trustState.challenge),
1361
+ trust: this.getOverallTrustPhase(),
1362
+ challengeReady: this.hasAnyPairedTarget(),
1241
1363
  });
1242
1364
  }
1243
1365
 
@@ -1251,7 +1373,7 @@ class ArmedSeat {
1251
1373
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1252
1374
  if (this.continueTargets.length > 0) {
1253
1375
  const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
1254
- this.log(`Seat ${this.seatId} relays to ${targets}.`);
1376
+ this.log(`Seat ${this.seatId} relays to ${targets}. Establishing trust.`);
1255
1377
  }
1256
1378
  this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
1257
1379
 
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 v6.0.0";
12
+ const BRAND = "🔌Muuuuse v7.0.0";
13
13
  const POLL_MS = 220;
14
14
  const MAX_RELAY_CHARS = 4000;
15
15
  const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;