muuuuse 2.2.5 → 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 +218 -37
- 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,
|
|
@@ -55,6 +60,11 @@ function normalizeFlowMode(flowMode) {
|
|
|
55
60
|
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
function normalizeContinueSeatId(value) {
|
|
64
|
+
const seatId = normalizeSeatId(value);
|
|
65
|
+
return seatId || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
function resolveShell() {
|
|
59
69
|
const shell = String(process.env.SHELL || "").trim();
|
|
60
70
|
return shell || "/bin/bash";
|
|
@@ -132,34 +142,40 @@ function sleepSync(ms) {
|
|
|
132
142
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
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
|
+
|
|
136
152
|
const candidates = listSessionNames()
|
|
137
153
|
.map((sessionName) => {
|
|
138
154
|
const sessionPaths = getSessionPaths(sessionName);
|
|
139
155
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
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);
|
|
146
162
|
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
147
163
|
|
|
148
|
-
const cwd = controller?.cwd ||
|
|
164
|
+
const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
|
|
149
165
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
150
166
|
return null;
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
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);
|
|
159
175
|
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
160
|
-
const createdAtMs = Date.parse(controller?.createdAt ||
|
|
176
|
+
const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
|
|
161
177
|
|
|
162
|
-
if (!
|
|
178
|
+
if (!anchorLive || seatLive) {
|
|
163
179
|
return null;
|
|
164
180
|
}
|
|
165
181
|
|
|
@@ -178,10 +194,10 @@ function findJoinableSessionName(currentPath = process.cwd()) {
|
|
|
178
194
|
return candidates[0]?.sessionName || null;
|
|
179
195
|
}
|
|
180
196
|
|
|
181
|
-
function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
197
|
+
function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
182
198
|
const deadline = Date.now() + timeoutMs;
|
|
183
199
|
while (Date.now() <= deadline) {
|
|
184
|
-
const sessionName = findJoinableSessionName(currentPath);
|
|
200
|
+
const sessionName = findJoinableSessionName(currentPath, seatId);
|
|
185
201
|
if (sessionName) {
|
|
186
202
|
return sessionName;
|
|
187
203
|
}
|
|
@@ -192,15 +208,11 @@ function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEA
|
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
195
|
-
if (seatId
|
|
211
|
+
if (isAnchorSeat(seatId)) {
|
|
196
212
|
return createSessionName(currentPath);
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
|
|
200
|
-
return waitForJoinableSessionName(currentPath);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return createSessionName(currentPath);
|
|
215
|
+
return waitForJoinableSessionName(currentPath, seatId);
|
|
204
216
|
}
|
|
205
217
|
|
|
206
218
|
function parseAnswerEntries(text) {
|
|
@@ -218,6 +230,26 @@ function parseAnswerEntries(text) {
|
|
|
218
230
|
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
219
231
|
}
|
|
220
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
|
+
|
|
221
253
|
function readSessionHeaderText(filePath, maxBytes = 16384) {
|
|
222
254
|
try {
|
|
223
255
|
const fd = fs.openSync(filePath, "r");
|
|
@@ -417,6 +449,36 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
417
449
|
});
|
|
418
450
|
}
|
|
419
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
|
+
|
|
420
482
|
function readSeatChallenge(paths, sessionName) {
|
|
421
483
|
const record = readJson(paths.challengePath, null);
|
|
422
484
|
if (
|
|
@@ -511,16 +573,24 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
511
573
|
class ArmedSeat {
|
|
512
574
|
constructor(options) {
|
|
513
575
|
this.seatId = options.seatId;
|
|
514
|
-
this.partnerSeatId = options.seatId
|
|
576
|
+
this.partnerSeatId = getPartnerSeatId(options.seatId);
|
|
577
|
+
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
515
578
|
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
579
|
+
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
516
580
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
581
|
+
if (this.continueSeatId === this.seatId) {
|
|
582
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
|
|
583
|
+
}
|
|
517
584
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
518
585
|
if (!this.sessionName) {
|
|
519
|
-
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
|
+
);
|
|
520
589
|
}
|
|
521
590
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
522
591
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
523
592
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
593
|
+
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
524
594
|
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
525
595
|
|
|
526
596
|
this.child = null;
|
|
@@ -542,7 +612,7 @@ class ArmedSeat {
|
|
|
542
612
|
this.trustState = {
|
|
543
613
|
challenge: null,
|
|
544
614
|
peerPublicKey: null,
|
|
545
|
-
phase: this.seatId
|
|
615
|
+
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
546
616
|
pairedAt: null,
|
|
547
617
|
};
|
|
548
618
|
this.liveState = {
|
|
@@ -565,9 +635,11 @@ class ArmedSeat {
|
|
|
565
635
|
cwd: this.cwd,
|
|
566
636
|
createdAt: current.createdAt || this.startedAt,
|
|
567
637
|
updatedAt: new Date().toISOString(),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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,
|
|
571
643
|
...extra,
|
|
572
644
|
});
|
|
573
645
|
}
|
|
@@ -582,6 +654,7 @@ class ArmedSeat {
|
|
|
582
654
|
partnerSeatId: this.partnerSeatId,
|
|
583
655
|
sessionName: this.sessionName,
|
|
584
656
|
flowMode: this.flowMode,
|
|
657
|
+
continueSeatId: this.continueSeatId,
|
|
585
658
|
cwd: this.cwd,
|
|
586
659
|
pid: process.pid,
|
|
587
660
|
childPid: this.childPid,
|
|
@@ -597,6 +670,7 @@ class ArmedSeat {
|
|
|
597
670
|
partnerSeatId: this.partnerSeatId,
|
|
598
671
|
sessionName: this.sessionName,
|
|
599
672
|
flowMode: this.flowMode,
|
|
673
|
+
continueSeatId: this.continueSeatId,
|
|
600
674
|
cwd: this.cwd,
|
|
601
675
|
pid: process.pid,
|
|
602
676
|
childPid: this.childPid,
|
|
@@ -609,7 +683,7 @@ class ArmedSeat {
|
|
|
609
683
|
initializeTrustMaterial() {
|
|
610
684
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
611
685
|
|
|
612
|
-
if (this.seatId
|
|
686
|
+
if (!isAnchorSeat(this.seatId)) {
|
|
613
687
|
return;
|
|
614
688
|
}
|
|
615
689
|
|
|
@@ -632,7 +706,7 @@ class ArmedSeat {
|
|
|
632
706
|
this.initializeTrustMaterial();
|
|
633
707
|
}
|
|
634
708
|
|
|
635
|
-
if (this.seatId
|
|
709
|
+
if (isAnchorSeat(this.seatId)) {
|
|
636
710
|
this.syncSeatOneTrust();
|
|
637
711
|
return;
|
|
638
712
|
}
|
|
@@ -713,7 +787,7 @@ class ArmedSeat {
|
|
|
713
787
|
this.trustState = {
|
|
714
788
|
challenge: null,
|
|
715
789
|
peerPublicKey: null,
|
|
716
|
-
phase: "
|
|
790
|
+
phase: "waiting_for_anchor_key",
|
|
717
791
|
pairedAt: null,
|
|
718
792
|
};
|
|
719
793
|
return;
|
|
@@ -941,6 +1015,44 @@ class ArmedSeat {
|
|
|
941
1015
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
942
1016
|
}
|
|
943
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
|
+
|
|
944
1056
|
async pullPartnerEvents() {
|
|
945
1057
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
946
1058
|
this.partnerOffset = nextOffset;
|
|
@@ -1003,6 +1115,53 @@ class ArmedSeat {
|
|
|
1003
1115
|
}
|
|
1004
1116
|
}
|
|
1005
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
|
+
|
|
1006
1165
|
rememberInboundRelay(text) {
|
|
1007
1166
|
const payload = sanitizeRelayText(text);
|
|
1008
1167
|
if (!payload) {
|
|
@@ -1269,11 +1428,28 @@ class ArmedSeat {
|
|
|
1269
1428
|
this.identity.privateKey
|
|
1270
1429
|
);
|
|
1271
1430
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1431
|
+
this.forwardContinuation(signedEntry);
|
|
1272
1432
|
this.rememberEmittedAnswer(answerKey);
|
|
1273
1433
|
|
|
1274
1434
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1275
1435
|
}
|
|
1276
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
|
+
|
|
1277
1453
|
async tick() {
|
|
1278
1454
|
if (this.stopRequested()) {
|
|
1279
1455
|
this.writeStatus({
|
|
@@ -1287,6 +1463,7 @@ class ArmedSeat {
|
|
|
1287
1463
|
|
|
1288
1464
|
this.syncTrustState();
|
|
1289
1465
|
await this.pullPartnerEvents();
|
|
1466
|
+
await this.pullContinuationEvents();
|
|
1290
1467
|
if (this.stopped || this.stopRequested()) {
|
|
1291
1468
|
this.requestStop("stop_requested");
|
|
1292
1469
|
return;
|
|
@@ -1319,10 +1496,13 @@ class ArmedSeat {
|
|
|
1319
1496
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1320
1497
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1321
1498
|
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1322
|
-
if (this.
|
|
1323
|
-
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.`);
|
|
1324
1504
|
} else {
|
|
1325
|
-
this.log(
|
|
1505
|
+
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1326
1506
|
}
|
|
1327
1507
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1328
1508
|
|
|
@@ -1420,6 +1600,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1420
1600
|
seatId,
|
|
1421
1601
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1422
1602
|
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1603
|
+
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1423
1604
|
wrapperPid,
|
|
1424
1605
|
childPid,
|
|
1425
1606
|
wrapperLive,
|
|
@@ -1443,7 +1624,7 @@ function getStatusReport() {
|
|
|
1443
1624
|
const sessionPaths = getSessionPaths(sessionName);
|
|
1444
1625
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
1445
1626
|
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
1446
|
-
const seats =
|
|
1627
|
+
const seats = listSeatIds(sessionName)
|
|
1447
1628
|
.map((seatId) => buildSeatReport(sessionName, seatId))
|
|
1448
1629
|
.filter((entry) => entry !== null);
|
|
1449
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,
|