muuuuse 2.2.4 → 2.3.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/README.md +12 -1
- package/package.json +2 -2
- package/src/cli.js +52 -13
- package/src/runtime.js +221 -65
- package/src/util.js +54 -6
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ It does one job:
|
|
|
6
6
|
- arm terminal one with `muuuuse 1`
|
|
7
7
|
- arm terminal two with `muuuuse 2`
|
|
8
8
|
- have seat 1 generate a session key and seat 2 sign it
|
|
9
|
+
- additional isolated pairs work the same way: `3/4`, `5/6`, `7/8`, ...
|
|
9
10
|
- choose per-seat relay mode with `flow on` or `flow off`
|
|
10
11
|
- watch Codex, Claude, or Gemini for local assistant output
|
|
11
12
|
- inject that output into the other armed terminal
|
|
@@ -16,8 +17,15 @@ The whole surface is:
|
|
|
16
17
|
```bash
|
|
17
18
|
muuuuse 1
|
|
18
19
|
muuuuse 1 flow on
|
|
20
|
+
muuuuse 1 flow off
|
|
21
|
+
muuuuse 1 flow off continue 5
|
|
19
22
|
muuuuse 2
|
|
20
23
|
muuuuse 2 flow off
|
|
24
|
+
muuuuse 2 flow on continue 3
|
|
25
|
+
muuuuse 3
|
|
26
|
+
muuuuse 3 flow on
|
|
27
|
+
muuuuse 4
|
|
28
|
+
muuuuse 4 flow off continue 1
|
|
21
29
|
muuuuse status
|
|
22
30
|
muuuuse stop
|
|
23
31
|
```
|
|
@@ -36,10 +44,12 @@ Terminal 2:
|
|
|
36
44
|
muuuuse 2 flow off
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Use those shells normally.
|
|
47
|
+
Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Every odd/even adjacent pair works the same way in parallel: `3/4`, `5/6`, `7/8`, and so on. Use those shells normally.
|
|
40
48
|
|
|
41
49
|
`flow on` means that seat sends commentary and final answers. `flow off` means that seat waits for final answers only. Each seat decides what it sends out, so mixed calibration is allowed.
|
|
42
50
|
|
|
51
|
+
`continue <seat>` forwards that seat's relayed output into another armed seat without changing the signed odd/even pair law. This lets you build local loops like `1 -> 2 -> 3 -> 4 -> 1` while every adjacent pair still keeps its own session keypair.
|
|
52
|
+
|
|
43
53
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
44
54
|
|
|
45
55
|
```bash
|
|
@@ -69,6 +79,7 @@ muuuuse stop
|
|
|
69
79
|
- no tmux
|
|
70
80
|
- state lives under `~/.muuuuse`
|
|
71
81
|
- only the signed armed pair can exchange relay events
|
|
82
|
+
- `continue <seat>` is a separate local forwarding lane and can target any armed seat number
|
|
72
83
|
- supported relay detection is built for Codex, Claude, and Gemini
|
|
73
84
|
- `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
|
|
74
85
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "🔌Muuuuse arms
|
|
3
|
+
"version": "2.3.0",
|
|
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"
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { BRAND, usage } = require("./util");
|
|
1
|
+
const { BRAND, normalizeSeatId, usage } = require("./util");
|
|
2
2
|
const { ArmedSeat, getStatusReport, stopAllSessions } = require("./runtime");
|
|
3
3
|
|
|
4
4
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -54,12 +54,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const seatId = normalizeSeatId(command);
|
|
58
|
+
if (seatId) {
|
|
59
|
+
const { flowMode, continueSeatId } = parseSeatOptions(command, argv.slice(1));
|
|
59
60
|
const seat = new ArmedSeat({
|
|
60
61
|
cwd: process.cwd(),
|
|
62
|
+
continueSeatId,
|
|
61
63
|
flowMode,
|
|
62
|
-
seatId
|
|
64
|
+
seatId,
|
|
63
65
|
});
|
|
64
66
|
const code = await seat.run();
|
|
65
67
|
process.exit(code);
|
|
@@ -81,6 +83,9 @@ function renderSeatStatus(seat) {
|
|
|
81
83
|
if (seat.partnerLive) {
|
|
82
84
|
bits.push("peer live");
|
|
83
85
|
}
|
|
86
|
+
if (seat.continueSeatId) {
|
|
87
|
+
bits.push(`continue ${seat.continueSeatId}`);
|
|
88
|
+
}
|
|
84
89
|
if (seat.trust) {
|
|
85
90
|
bits.push(`trust ${seat.trust}`);
|
|
86
91
|
}
|
|
@@ -98,23 +103,57 @@ function renderSeatStatus(seat) {
|
|
|
98
103
|
return output;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
106
|
+
function parseSeatOptions(command, args) {
|
|
107
|
+
let flowMode = "off";
|
|
108
|
+
let continueSeatId = null;
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
for (let index = 0; index < args.length;) {
|
|
111
|
+
const token = String(args[index] || "").trim().toLowerCase();
|
|
112
|
+
|
|
113
|
+
if (token === "flow") {
|
|
114
|
+
const flowToken = String(args[index + 1] || "").trim().toLowerCase();
|
|
115
|
+
if (flowToken === "on" || flowToken === "off") {
|
|
116
|
+
flowMode = flowToken;
|
|
117
|
+
index += 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (token === "continue") {
|
|
124
|
+
const targetSeatId = normalizeSeatId(args[index + 1]);
|
|
125
|
+
if (targetSeatId) {
|
|
126
|
+
continueSeatId = targetSeatId;
|
|
127
|
+
index += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
110
131
|
}
|
|
132
|
+
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (args.length === 0 || (flowMode || continueSeatId !== null) && consumedAllArgs(args, flowMode, continueSeatId)) {
|
|
137
|
+
return { flowMode, continueSeatId };
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
throw new Error(
|
|
114
|
-
`\`muuuuse ${command}\` accepts
|
|
141
|
+
`\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat>\`, or both in sequence. Run it directly in the terminal you want to arm.`
|
|
115
142
|
);
|
|
116
143
|
}
|
|
117
144
|
|
|
145
|
+
function consumedAllArgs(args, flowMode, continueSeatId) {
|
|
146
|
+
const expected = [];
|
|
147
|
+
if (flowMode !== "off" || args.includes("flow")) {
|
|
148
|
+
expected.push("flow", flowMode);
|
|
149
|
+
}
|
|
150
|
+
if (continueSeatId !== null || args.includes("continue")) {
|
|
151
|
+
expected.push("continue", String(continueSeatId));
|
|
152
|
+
}
|
|
153
|
+
return expected.length === args.length &&
|
|
154
|
+
expected.every((value, index) => String(args[index]).trim().toLowerCase() === String(value).trim().toLowerCase());
|
|
155
|
+
}
|
|
156
|
+
|
|
118
157
|
module.exports = {
|
|
119
158
|
main,
|
|
120
159
|
};
|
package/src/runtime.js
CHANGED
|
@@ -20,12 +20,17 @@ const {
|
|
|
20
20
|
ensureDir,
|
|
21
21
|
getDefaultSessionName,
|
|
22
22
|
getFileSize,
|
|
23
|
+
getPartnerSeatId,
|
|
23
24
|
getSeatPaths,
|
|
24
25
|
getSessionPaths,
|
|
26
|
+
getStateRoot,
|
|
25
27
|
hashText,
|
|
28
|
+
isAnchorSeat,
|
|
26
29
|
isPidAlive,
|
|
30
|
+
listSeatIds,
|
|
27
31
|
loadOrCreateSeatIdentity,
|
|
28
32
|
listSessionNames,
|
|
33
|
+
normalizeSeatId,
|
|
29
34
|
readAppendedText,
|
|
30
35
|
readJson,
|
|
31
36
|
sanitizeRelayText,
|
|
@@ -42,7 +47,6 @@ const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
|
42
47
|
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
43
48
|
const MAX_RECENT_INBOUND_RELAYS = 12;
|
|
44
49
|
const MAX_RECENT_EMITTED_ANSWERS = 48;
|
|
45
|
-
const MAX_RELAY_CHAIN_HOP = 1;
|
|
46
50
|
const STOP_FORCE_KILL_MS = 1200;
|
|
47
51
|
const SEAT_JOIN_WAIT_MS = 3000;
|
|
48
52
|
const SEAT_JOIN_POLL_MS = 60;
|
|
@@ -56,6 +60,11 @@ function normalizeFlowMode(flowMode) {
|
|
|
56
60
|
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function normalizeContinueSeatId(value) {
|
|
64
|
+
const seatId = normalizeSeatId(value);
|
|
65
|
+
return seatId || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
function resolveShell() {
|
|
60
69
|
const shell = String(process.env.SHELL || "").trim();
|
|
61
70
|
return shell || "/bin/bash";
|
|
@@ -133,34 +142,40 @@ function sleepSync(ms) {
|
|
|
133
142
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
function findJoinableSessionName(currentPath = process.cwd()) {
|
|
145
|
+
function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
|
|
146
|
+
const normalizedSeatId = normalizeSeatId(seatId);
|
|
147
|
+
const anchorSeatId = getPartnerSeatId(normalizedSeatId);
|
|
148
|
+
if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
const candidates = listSessionNames()
|
|
138
153
|
.map((sessionName) => {
|
|
139
154
|
const sessionPaths = getSessionPaths(sessionName);
|
|
140
155
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
156
|
+
const anchorPaths = getSeatPaths(sessionName, anchorSeatId);
|
|
157
|
+
const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
|
|
158
|
+
const anchorMeta = readJson(anchorPaths.metaPath, null);
|
|
159
|
+
const anchorStatus = readJson(anchorPaths.statusPath, null);
|
|
160
|
+
const seatMeta = readJson(seatPaths.metaPath, null);
|
|
161
|
+
const seatStatus = readJson(seatPaths.statusPath, null);
|
|
147
162
|
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
148
163
|
|
|
149
|
-
const cwd = controller?.cwd ||
|
|
164
|
+
const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
|
|
150
165
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
151
166
|
return null;
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
169
|
+
const anchorWrapperPid = anchorStatus?.pid || anchorMeta?.pid || null;
|
|
170
|
+
const anchorChildPid = anchorStatus?.childPid || anchorMeta?.childPid || null;
|
|
171
|
+
const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
|
|
172
|
+
const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
|
|
173
|
+
const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
|
|
174
|
+
const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
|
|
160
175
|
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
161
|
-
const createdAtMs = Date.parse(controller?.createdAt ||
|
|
176
|
+
const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
|
|
162
177
|
|
|
163
|
-
if (!
|
|
178
|
+
if (!anchorLive || seatLive) {
|
|
164
179
|
return null;
|
|
165
180
|
}
|
|
166
181
|
|
|
@@ -179,10 +194,10 @@ function findJoinableSessionName(currentPath = process.cwd()) {
|
|
|
179
194
|
return candidates[0]?.sessionName || null;
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
197
|
+
function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
183
198
|
const deadline = Date.now() + timeoutMs;
|
|
184
199
|
while (Date.now() <= deadline) {
|
|
185
|
-
const sessionName = findJoinableSessionName(currentPath);
|
|
200
|
+
const sessionName = findJoinableSessionName(currentPath, seatId);
|
|
186
201
|
if (sessionName) {
|
|
187
202
|
return sessionName;
|
|
188
203
|
}
|
|
@@ -193,15 +208,11 @@ function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEA
|
|
|
193
208
|
}
|
|
194
209
|
|
|
195
210
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
196
|
-
if (seatId
|
|
211
|
+
if (isAnchorSeat(seatId)) {
|
|
197
212
|
return createSessionName(currentPath);
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
return waitForJoinableSessionName(currentPath);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return createSessionName(currentPath);
|
|
215
|
+
return waitForJoinableSessionName(currentPath, seatId);
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
function parseAnswerEntries(text) {
|
|
@@ -219,6 +230,26 @@ function parseAnswerEntries(text) {
|
|
|
219
230
|
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
220
231
|
}
|
|
221
232
|
|
|
233
|
+
function parseContinueEntries(text, targetSeatId) {
|
|
234
|
+
return String(text || "")
|
|
235
|
+
.split("\n")
|
|
236
|
+
.map((line) => line.trim())
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.map((line) => {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(line);
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
.filter((entry) => (
|
|
246
|
+
entry &&
|
|
247
|
+
entry.type === "continue" &&
|
|
248
|
+
typeof entry.text === "string" &&
|
|
249
|
+
normalizeSeatId(entry.targetSeatId) === targetSeatId
|
|
250
|
+
));
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
function readSessionHeaderText(filePath, maxBytes = 16384) {
|
|
223
254
|
try {
|
|
224
255
|
const fd = fs.openSync(filePath, "r");
|
|
@@ -418,6 +449,36 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
418
449
|
});
|
|
419
450
|
}
|
|
420
451
|
|
|
452
|
+
function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
|
|
453
|
+
return {
|
|
454
|
+
id: createId(12),
|
|
455
|
+
type: "continue",
|
|
456
|
+
sourceSessionName,
|
|
457
|
+
sourceSeatId: entry.seatId,
|
|
458
|
+
targetSeatId,
|
|
459
|
+
origin: entry.origin || "unknown",
|
|
460
|
+
text: entry.text,
|
|
461
|
+
createdAt: entry.createdAt || new Date().toISOString(),
|
|
462
|
+
chainId: entry.chainId,
|
|
463
|
+
hop: entry.hop,
|
|
464
|
+
sourceAnswerId: entry.id,
|
|
465
|
+
publicKey: entry.publicKey || null,
|
|
466
|
+
signature: entry.signature || null,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getSeatDirIfExists(sessionName, seatId) {
|
|
471
|
+
const dir = path.join(getStateRoot(), "sessions", sessionName, `seat-${seatId}`);
|
|
472
|
+
try {
|
|
473
|
+
if (fs.statSync(dir).isDirectory()) {
|
|
474
|
+
return dir;
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
421
482
|
function readSeatChallenge(paths, sessionName) {
|
|
422
483
|
const record = readJson(paths.challengePath, null);
|
|
423
484
|
if (
|
|
@@ -512,16 +573,24 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
512
573
|
class ArmedSeat {
|
|
513
574
|
constructor(options) {
|
|
514
575
|
this.seatId = options.seatId;
|
|
515
|
-
this.partnerSeatId = options.seatId
|
|
576
|
+
this.partnerSeatId = getPartnerSeatId(options.seatId);
|
|
577
|
+
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
516
578
|
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
579
|
+
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
517
580
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
581
|
+
if (this.continueSeatId === this.seatId) {
|
|
582
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
|
|
583
|
+
}
|
|
518
584
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
519
585
|
if (!this.sessionName) {
|
|
520
|
-
throw new Error(
|
|
586
|
+
throw new Error(
|
|
587
|
+
`No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
|
|
588
|
+
);
|
|
521
589
|
}
|
|
522
590
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
523
591
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
524
592
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
593
|
+
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
525
594
|
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
526
595
|
|
|
527
596
|
this.child = null;
|
|
@@ -543,7 +612,7 @@ class ArmedSeat {
|
|
|
543
612
|
this.trustState = {
|
|
544
613
|
challenge: null,
|
|
545
614
|
peerPublicKey: null,
|
|
546
|
-
phase: this.seatId
|
|
615
|
+
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
547
616
|
pairedAt: null,
|
|
548
617
|
};
|
|
549
618
|
this.liveState = {
|
|
@@ -566,9 +635,11 @@ class ArmedSeat {
|
|
|
566
635
|
cwd: this.cwd,
|
|
567
636
|
createdAt: current.createdAt || this.startedAt,
|
|
568
637
|
updatedAt: new Date().toISOString(),
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
638
|
+
anchorSeatId: this.anchorSeatId,
|
|
639
|
+
partnerSeatId: this.partnerSeatId,
|
|
640
|
+
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
641
|
+
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
642
|
+
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
572
643
|
...extra,
|
|
573
644
|
});
|
|
574
645
|
}
|
|
@@ -583,6 +654,7 @@ class ArmedSeat {
|
|
|
583
654
|
partnerSeatId: this.partnerSeatId,
|
|
584
655
|
sessionName: this.sessionName,
|
|
585
656
|
flowMode: this.flowMode,
|
|
657
|
+
continueSeatId: this.continueSeatId,
|
|
586
658
|
cwd: this.cwd,
|
|
587
659
|
pid: process.pid,
|
|
588
660
|
childPid: this.childPid,
|
|
@@ -598,6 +670,7 @@ class ArmedSeat {
|
|
|
598
670
|
partnerSeatId: this.partnerSeatId,
|
|
599
671
|
sessionName: this.sessionName,
|
|
600
672
|
flowMode: this.flowMode,
|
|
673
|
+
continueSeatId: this.continueSeatId,
|
|
601
674
|
cwd: this.cwd,
|
|
602
675
|
pid: process.pid,
|
|
603
676
|
childPid: this.childPid,
|
|
@@ -610,7 +683,7 @@ class ArmedSeat {
|
|
|
610
683
|
initializeTrustMaterial() {
|
|
611
684
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
612
685
|
|
|
613
|
-
if (this.seatId
|
|
686
|
+
if (!isAnchorSeat(this.seatId)) {
|
|
614
687
|
return;
|
|
615
688
|
}
|
|
616
689
|
|
|
@@ -633,7 +706,7 @@ class ArmedSeat {
|
|
|
633
706
|
this.initializeTrustMaterial();
|
|
634
707
|
}
|
|
635
708
|
|
|
636
|
-
if (this.seatId
|
|
709
|
+
if (isAnchorSeat(this.seatId)) {
|
|
637
710
|
this.syncSeatOneTrust();
|
|
638
711
|
return;
|
|
639
712
|
}
|
|
@@ -714,7 +787,7 @@ class ArmedSeat {
|
|
|
714
787
|
this.trustState = {
|
|
715
788
|
challenge: null,
|
|
716
789
|
peerPublicKey: null,
|
|
717
|
-
phase: "
|
|
790
|
+
phase: "waiting_for_anchor_key",
|
|
718
791
|
pairedAt: null,
|
|
719
792
|
};
|
|
720
793
|
return;
|
|
@@ -932,12 +1005,6 @@ class ArmedSeat {
|
|
|
932
1005
|
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
933
1006
|
}
|
|
934
1007
|
|
|
935
|
-
getPartnerFlowMode() {
|
|
936
|
-
const partnerStatus = readJson(this.partnerPaths.statusPath, null);
|
|
937
|
-
const partnerMeta = readJson(this.partnerPaths.metaPath, null);
|
|
938
|
-
return normalizeFlowMode(partnerStatus?.flowMode || partnerMeta?.flowMode || "off");
|
|
939
|
-
}
|
|
940
|
-
|
|
941
1008
|
stopRequested() {
|
|
942
1009
|
const request = readJson(this.sessionPaths.stopPath, null);
|
|
943
1010
|
if (!request?.requestedAt) {
|
|
@@ -948,6 +1015,44 @@ class ArmedSeat {
|
|
|
948
1015
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
949
1016
|
}
|
|
950
1017
|
|
|
1018
|
+
findContinuationTarget() {
|
|
1019
|
+
if (!this.continueSeatId) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const candidates = listSessionNames()
|
|
1024
|
+
.map((sessionName) => {
|
|
1025
|
+
if (!getSeatDirIfExists(sessionName, this.continueSeatId)) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const seat = buildSeatReport(sessionName, this.continueSeatId);
|
|
1030
|
+
if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const updatedAtMs = Date.parse(seat.updatedAt || seat.startedAt || "");
|
|
1035
|
+
return {
|
|
1036
|
+
seat,
|
|
1037
|
+
sessionName,
|
|
1038
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
|
|
1039
|
+
};
|
|
1040
|
+
})
|
|
1041
|
+
.filter((entry) => entry !== null)
|
|
1042
|
+
.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
1043
|
+
|
|
1044
|
+
if (candidates.length === 0) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const target = candidates[0];
|
|
1049
|
+
return {
|
|
1050
|
+
seatId: target.seat.seatId,
|
|
1051
|
+
sessionName: target.sessionName,
|
|
1052
|
+
paths: getSeatPaths(target.sessionName, target.seat.seatId),
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
951
1056
|
async pullPartnerEvents() {
|
|
952
1057
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
953
1058
|
this.partnerOffset = nextOffset;
|
|
@@ -1003,7 +1108,6 @@ class ArmedSeat {
|
|
|
1003
1108
|
deliveredAtMs,
|
|
1004
1109
|
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1005
1110
|
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1006
|
-
relayUsed: false,
|
|
1007
1111
|
};
|
|
1008
1112
|
this.relayCount += 1;
|
|
1009
1113
|
this.rememberInboundRelay(payload);
|
|
@@ -1011,6 +1115,53 @@ class ArmedSeat {
|
|
|
1011
1115
|
}
|
|
1012
1116
|
}
|
|
1013
1117
|
|
|
1118
|
+
async pullContinuationEvents() {
|
|
1119
|
+
const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
|
|
1120
|
+
this.continueOffset = nextOffset;
|
|
1121
|
+
if (!text.trim() || !this.child || this.stopped) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const entries = parseContinueEntries(text, this.seatId);
|
|
1126
|
+
for (const entry of entries) {
|
|
1127
|
+
if (this.stopped || this.stopRequested()) {
|
|
1128
|
+
this.requestStop("stop_requested");
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const payload = sanitizeRelayText(entry.text);
|
|
1133
|
+
if (!payload) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const delivered = await sendTextAndEnter(
|
|
1138
|
+
this.child,
|
|
1139
|
+
payload,
|
|
1140
|
+
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1141
|
+
);
|
|
1142
|
+
if (!delivered) {
|
|
1143
|
+
this.requestStop("relay_aborted");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (this.stopped || this.stopRequested()) {
|
|
1148
|
+
this.requestStop("stop_requested");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const deliveredAtMs = Date.now();
|
|
1153
|
+
this.pendingInboundContext = {
|
|
1154
|
+
chainId: entry.chainId || entry.sourceAnswerId || entry.id,
|
|
1155
|
+
deliveredAtMs,
|
|
1156
|
+
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1157
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1158
|
+
};
|
|
1159
|
+
this.relayCount += 1;
|
|
1160
|
+
this.rememberInboundRelay(payload);
|
|
1161
|
+
this.log(`[${entry.sourceSeatId} => ${this.seatId}] ${previewText(payload)}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1014
1165
|
rememberInboundRelay(text) {
|
|
1015
1166
|
const payload = sanitizeRelayText(text);
|
|
1016
1167
|
if (!payload) {
|
|
@@ -1258,21 +1409,6 @@ class ArmedSeat {
|
|
|
1258
1409
|
}
|
|
1259
1410
|
|
|
1260
1411
|
const pendingInboundContext = this.getPendingInboundContext();
|
|
1261
|
-
const partnerFlowMode = this.getPartnerFlowMode();
|
|
1262
|
-
if (
|
|
1263
|
-
this.flowMode !== "on" &&
|
|
1264
|
-
partnerFlowMode !== "on" &&
|
|
1265
|
-
pendingInboundContext &&
|
|
1266
|
-
pendingInboundContext.hop >= MAX_RELAY_CHAIN_HOP
|
|
1267
|
-
) {
|
|
1268
|
-
this.log(`[${this.seatId}] suppressed relay loop: ${previewText(payload)}`);
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (pendingInboundContext?.relayUsed) {
|
|
1273
|
-
this.log(`[${this.seatId}] suppressed extra queued relay output: ${previewText(payload)}`);
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
1412
|
|
|
1277
1413
|
const entryId = entry.id || createId(12);
|
|
1278
1414
|
const signedEntry = {
|
|
@@ -1292,14 +1428,28 @@ class ArmedSeat {
|
|
|
1292
1428
|
this.identity.privateKey
|
|
1293
1429
|
);
|
|
1294
1430
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1431
|
+
this.forwardContinuation(signedEntry);
|
|
1295
1432
|
this.rememberEmittedAnswer(answerKey);
|
|
1296
|
-
if (pendingInboundContext) {
|
|
1297
|
-
pendingInboundContext.relayUsed = true;
|
|
1298
|
-
}
|
|
1299
1433
|
|
|
1300
1434
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1301
1435
|
}
|
|
1302
1436
|
|
|
1437
|
+
forwardContinuation(signedEntry) {
|
|
1438
|
+
if (!this.continueSeatId) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const target = this.findContinuationTarget();
|
|
1443
|
+
if (!target) {
|
|
1444
|
+
this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
|
|
1449
|
+
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1450
|
+
this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1303
1453
|
async tick() {
|
|
1304
1454
|
if (this.stopRequested()) {
|
|
1305
1455
|
this.writeStatus({
|
|
@@ -1313,6 +1463,7 @@ class ArmedSeat {
|
|
|
1313
1463
|
|
|
1314
1464
|
this.syncTrustState();
|
|
1315
1465
|
await this.pullPartnerEvents();
|
|
1466
|
+
await this.pullContinuationEvents();
|
|
1316
1467
|
if (this.stopped || this.stopRequested()) {
|
|
1317
1468
|
this.requestStop("stop_requested");
|
|
1318
1469
|
return;
|
|
@@ -1345,10 +1496,13 @@ class ArmedSeat {
|
|
|
1345
1496
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1346
1497
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1347
1498
|
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1348
|
-
if (this.
|
|
1349
|
-
this.log(
|
|
1499
|
+
if (this.continueSeatId) {
|
|
1500
|
+
this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
|
|
1501
|
+
}
|
|
1502
|
+
if (isAnchorSeat(this.seatId)) {
|
|
1503
|
+
this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
|
|
1350
1504
|
} else {
|
|
1351
|
-
this.log(
|
|
1505
|
+
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1352
1506
|
}
|
|
1353
1507
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1354
1508
|
|
|
@@ -1413,12 +1567,13 @@ function previewText(text, maxLength = 88) {
|
|
|
1413
1567
|
function buildAnswerKey(entry, payload) {
|
|
1414
1568
|
const origin = String(entry.origin || "unknown").trim() || "unknown";
|
|
1415
1569
|
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
1570
|
+
const payloadHash = hashText(payload);
|
|
1416
1571
|
if (id) {
|
|
1417
|
-
return `${origin}:${id}`;
|
|
1572
|
+
return `${origin}:${id}:${payloadHash}`;
|
|
1418
1573
|
}
|
|
1419
1574
|
|
|
1420
1575
|
const createdAt = typeof entry.createdAt === "string" ? entry.createdAt : "";
|
|
1421
|
-
return `${origin}:${createdAt}:${
|
|
1576
|
+
return `${origin}:${createdAt}:${payloadHash}`;
|
|
1422
1577
|
}
|
|
1423
1578
|
|
|
1424
1579
|
function buildSeatReport(sessionName, seatId) {
|
|
@@ -1445,6 +1600,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1445
1600
|
seatId,
|
|
1446
1601
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1447
1602
|
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1603
|
+
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1448
1604
|
wrapperPid,
|
|
1449
1605
|
childPid,
|
|
1450
1606
|
wrapperLive,
|
|
@@ -1468,7 +1624,7 @@ function getStatusReport() {
|
|
|
1468
1624
|
const sessionPaths = getSessionPaths(sessionName);
|
|
1469
1625
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
1470
1626
|
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
1471
|
-
const seats =
|
|
1627
|
+
const seats = listSeatIds(sessionName)
|
|
1472
1628
|
.map((seatId) => buildSeatReport(sessionName, seatId))
|
|
1473
1629
|
.filter((entry) => entry !== null);
|
|
1474
1630
|
|
package/src/util.js
CHANGED
|
@@ -167,6 +167,42 @@ function getSessionPaths(sessionName) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
function normalizeSeatId(value) {
|
|
171
|
+
const seatId = Number.parseInt(String(value || "").trim(), 10);
|
|
172
|
+
if (!Number.isInteger(seatId) || seatId <= 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return seatId;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isAnchorSeat(seatId) {
|
|
179
|
+
return normalizeSeatId(seatId) % 2 === 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getPartnerSeatId(seatId) {
|
|
183
|
+
const normalized = normalizeSeatId(seatId);
|
|
184
|
+
if (!normalized) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return isAnchorSeat(normalized) ? normalized + 1 : normalized - 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function listSeatIds(sessionName) {
|
|
191
|
+
const sessionDir = getSessionDir(sessionName);
|
|
192
|
+
try {
|
|
193
|
+
return fs.readdirSync(sessionDir, { withFileTypes: true })
|
|
194
|
+
.filter((entry) => entry.isDirectory())
|
|
195
|
+
.map((entry) => {
|
|
196
|
+
const match = entry.name.match(/^seat-(\d+)$/);
|
|
197
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
198
|
+
})
|
|
199
|
+
.filter((seatId) => Number.isInteger(seatId))
|
|
200
|
+
.sort((left, right) => left - right);
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
170
206
|
function getSeatDir(sessionName, seatId) {
|
|
171
207
|
return ensureDir(path.join(getSessionDir(sessionName), `seat-${seatId}`));
|
|
172
208
|
}
|
|
@@ -177,6 +213,7 @@ function getSeatPaths(sessionName, seatId) {
|
|
|
177
213
|
ackPath: path.join(dir, "ack.json"),
|
|
178
214
|
challengePath: path.join(dir, "challenge.json"),
|
|
179
215
|
claimPath: path.join(dir, "claim.json"),
|
|
216
|
+
continuePath: path.join(dir, "continue.jsonl"),
|
|
180
217
|
dir,
|
|
181
218
|
daemonPath: path.join(dir, "daemon.json"),
|
|
182
219
|
eventsPath: path.join(dir, "events.jsonl"),
|
|
@@ -245,26 +282,33 @@ function listSessionNames() {
|
|
|
245
282
|
|
|
246
283
|
function usage() {
|
|
247
284
|
return [
|
|
248
|
-
`${BRAND} arms
|
|
285
|
+
`${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
|
|
249
286
|
"",
|
|
250
287
|
"Usage:",
|
|
251
288
|
" muuuuse 1",
|
|
252
289
|
" muuuuse 1 flow on",
|
|
253
290
|
" muuuuse 1 flow off",
|
|
291
|
+
" muuuuse 1 flow on continue 3",
|
|
254
292
|
" muuuuse 2",
|
|
255
293
|
" muuuuse 2 flow on",
|
|
256
294
|
" muuuuse 2 flow off",
|
|
295
|
+
" muuuuse 2 flow on continue 3",
|
|
296
|
+
" muuuuse 3",
|
|
297
|
+
" muuuuse 4",
|
|
298
|
+
" muuuuse 4 flow on continue 1",
|
|
257
299
|
" muuuuse stop",
|
|
258
300
|
" muuuuse status",
|
|
259
301
|
"",
|
|
260
302
|
"Flow:",
|
|
261
303
|
" 1. Run `muuuuse 1` in terminal one.",
|
|
262
304
|
" 2. Run `muuuuse 2` in terminal two.",
|
|
263
|
-
" 3.
|
|
264
|
-
" 4.
|
|
265
|
-
" 5.
|
|
266
|
-
" 6.
|
|
267
|
-
" 7.
|
|
305
|
+
" 3. The odd seat generates the session key and the matching even seat signs it automatically.",
|
|
306
|
+
" 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
|
|
307
|
+
" 5. Optional: arm each seat with `flow on` or `flow off`.",
|
|
308
|
+
" 6. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
|
|
309
|
+
" 7. Use those armed shells normally.",
|
|
310
|
+
" 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
|
|
311
|
+
" 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
268
312
|
"",
|
|
269
313
|
"Notes:",
|
|
270
314
|
" - No tmux.",
|
|
@@ -282,13 +326,17 @@ module.exports = {
|
|
|
282
326
|
ensureDir,
|
|
283
327
|
getDefaultSessionName,
|
|
284
328
|
getFileSize,
|
|
329
|
+
getPartnerSeatId,
|
|
285
330
|
loadOrCreateSeatIdentity,
|
|
331
|
+
isAnchorSeat,
|
|
286
332
|
getSeatPaths,
|
|
287
333
|
getSessionPaths,
|
|
288
334
|
getStateRoot,
|
|
289
335
|
hashText,
|
|
290
336
|
isPidAlive,
|
|
337
|
+
listSeatIds,
|
|
291
338
|
listSessionNames,
|
|
339
|
+
normalizeSeatId,
|
|
292
340
|
readAppendedText,
|
|
293
341
|
readJson,
|
|
294
342
|
sanitizeRelayText,
|