muuuuse 2.2.5 → 2.3.1
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 +13 -2
- package/package.json +2 -2
- package/src/agents.js +3 -0
- package/src/cli.js +52 -13
- package/src/runtime.js +240 -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,9 +44,11 @@ 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
|
-
`flow on` means that seat
|
|
49
|
+
`flow on` means that seat relays commentary and final answers. `flow off` means that seat relays and accepts final answers only. Mixed calibration is allowed per seat.
|
|
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.
|
|
42
52
|
|
|
43
53
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
44
54
|
|
|
@@ -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.1",
|
|
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/agents.js
CHANGED
|
@@ -481,6 +481,7 @@ function parseCodexAssistantLine(line, options = {}) {
|
|
|
481
481
|
return {
|
|
482
482
|
id: entry.payload.id || hashText(line),
|
|
483
483
|
text,
|
|
484
|
+
phase: phase === "commentary" ? "commentary" : "final_answer",
|
|
484
485
|
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
485
486
|
};
|
|
486
487
|
} catch {
|
|
@@ -597,6 +598,7 @@ function parseClaudeAssistantLine(line, options = {}) {
|
|
|
597
598
|
return {
|
|
598
599
|
id: entry.uuid || entry.message.id || hashText(line),
|
|
599
600
|
text,
|
|
601
|
+
phase: flowMode && entry.message?.stop_reason !== "end_turn" ? "commentary" : "final_answer",
|
|
600
602
|
timestamp: entry.timestamp || new Date().toISOString(),
|
|
601
603
|
};
|
|
602
604
|
} catch {
|
|
@@ -672,6 +674,7 @@ function readGeminiAnswers(filePath, lastMessageId = null, sinceMs = null) {
|
|
|
672
674
|
const answers = finalMessages.slice(startIndex).map((message) => ({
|
|
673
675
|
id: message.id || hashText(JSON.stringify(message)),
|
|
674
676
|
text: sanitizeRelayText(message.content),
|
|
677
|
+
phase: "final_answer",
|
|
675
678
|
timestamp: message.timestamp || entry.lastUpdated || new Date().toISOString(),
|
|
676
679
|
}));
|
|
677
680
|
|
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");
|
|
@@ -412,11 +444,52 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
412
444
|
id: entry.id,
|
|
413
445
|
seatId: entry.seatId,
|
|
414
446
|
origin: entry.origin,
|
|
447
|
+
phase: entry.phase || "final_answer",
|
|
415
448
|
createdAt: entry.createdAt,
|
|
416
449
|
text: entry.text,
|
|
417
450
|
});
|
|
418
451
|
}
|
|
419
452
|
|
|
453
|
+
function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
|
|
454
|
+
return {
|
|
455
|
+
id: createId(12),
|
|
456
|
+
type: "continue",
|
|
457
|
+
sourceSessionName,
|
|
458
|
+
sourceSeatId: entry.seatId,
|
|
459
|
+
targetSeatId,
|
|
460
|
+
origin: entry.origin || "unknown",
|
|
461
|
+
phase: entry.phase || "final_answer",
|
|
462
|
+
text: entry.text,
|
|
463
|
+
createdAt: entry.createdAt || new Date().toISOString(),
|
|
464
|
+
chainId: entry.chainId,
|
|
465
|
+
hop: entry.hop,
|
|
466
|
+
sourceAnswerId: entry.id,
|
|
467
|
+
publicKey: entry.publicKey || null,
|
|
468
|
+
signature: entry.signature || null,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getRelayPhase(entry) {
|
|
473
|
+
const phase = String(entry?.phase || "").trim().toLowerCase();
|
|
474
|
+
return phase === "commentary" ? "commentary" : "final_answer";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function shouldAcceptInboundEntry(flowMode, entry) {
|
|
478
|
+
return flowMode === "on" || getRelayPhase(entry) === "final_answer";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getSeatDirIfExists(sessionName, seatId) {
|
|
482
|
+
const dir = path.join(getStateRoot(), "sessions", sessionName, `seat-${seatId}`);
|
|
483
|
+
try {
|
|
484
|
+
if (fs.statSync(dir).isDirectory()) {
|
|
485
|
+
return dir;
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
420
493
|
function readSeatChallenge(paths, sessionName) {
|
|
421
494
|
const record = readJson(paths.challengePath, null);
|
|
422
495
|
if (
|
|
@@ -511,16 +584,24 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
511
584
|
class ArmedSeat {
|
|
512
585
|
constructor(options) {
|
|
513
586
|
this.seatId = options.seatId;
|
|
514
|
-
this.partnerSeatId = options.seatId
|
|
587
|
+
this.partnerSeatId = getPartnerSeatId(options.seatId);
|
|
588
|
+
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
515
589
|
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
590
|
+
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
516
591
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
592
|
+
if (this.continueSeatId === this.seatId) {
|
|
593
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
|
|
594
|
+
}
|
|
517
595
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
518
596
|
if (!this.sessionName) {
|
|
519
|
-
throw new Error(
|
|
597
|
+
throw new Error(
|
|
598
|
+
`No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
|
|
599
|
+
);
|
|
520
600
|
}
|
|
521
601
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
522
602
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
523
603
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
604
|
+
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
524
605
|
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
525
606
|
|
|
526
607
|
this.child = null;
|
|
@@ -542,7 +623,7 @@ class ArmedSeat {
|
|
|
542
623
|
this.trustState = {
|
|
543
624
|
challenge: null,
|
|
544
625
|
peerPublicKey: null,
|
|
545
|
-
phase: this.seatId
|
|
626
|
+
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
546
627
|
pairedAt: null,
|
|
547
628
|
};
|
|
548
629
|
this.liveState = {
|
|
@@ -565,9 +646,11 @@ class ArmedSeat {
|
|
|
565
646
|
cwd: this.cwd,
|
|
566
647
|
createdAt: current.createdAt || this.startedAt,
|
|
567
648
|
updatedAt: new Date().toISOString(),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
649
|
+
anchorSeatId: this.anchorSeatId,
|
|
650
|
+
partnerSeatId: this.partnerSeatId,
|
|
651
|
+
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
652
|
+
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
653
|
+
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
571
654
|
...extra,
|
|
572
655
|
});
|
|
573
656
|
}
|
|
@@ -582,6 +665,7 @@ class ArmedSeat {
|
|
|
582
665
|
partnerSeatId: this.partnerSeatId,
|
|
583
666
|
sessionName: this.sessionName,
|
|
584
667
|
flowMode: this.flowMode,
|
|
668
|
+
continueSeatId: this.continueSeatId,
|
|
585
669
|
cwd: this.cwd,
|
|
586
670
|
pid: process.pid,
|
|
587
671
|
childPid: this.childPid,
|
|
@@ -597,6 +681,7 @@ class ArmedSeat {
|
|
|
597
681
|
partnerSeatId: this.partnerSeatId,
|
|
598
682
|
sessionName: this.sessionName,
|
|
599
683
|
flowMode: this.flowMode,
|
|
684
|
+
continueSeatId: this.continueSeatId,
|
|
600
685
|
cwd: this.cwd,
|
|
601
686
|
pid: process.pid,
|
|
602
687
|
childPid: this.childPid,
|
|
@@ -609,7 +694,7 @@ class ArmedSeat {
|
|
|
609
694
|
initializeTrustMaterial() {
|
|
610
695
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
611
696
|
|
|
612
|
-
if (this.seatId
|
|
697
|
+
if (!isAnchorSeat(this.seatId)) {
|
|
613
698
|
return;
|
|
614
699
|
}
|
|
615
700
|
|
|
@@ -632,7 +717,7 @@ class ArmedSeat {
|
|
|
632
717
|
this.initializeTrustMaterial();
|
|
633
718
|
}
|
|
634
719
|
|
|
635
|
-
if (this.seatId
|
|
720
|
+
if (isAnchorSeat(this.seatId)) {
|
|
636
721
|
this.syncSeatOneTrust();
|
|
637
722
|
return;
|
|
638
723
|
}
|
|
@@ -713,7 +798,7 @@ class ArmedSeat {
|
|
|
713
798
|
this.trustState = {
|
|
714
799
|
challenge: null,
|
|
715
800
|
peerPublicKey: null,
|
|
716
|
-
phase: "
|
|
801
|
+
phase: "waiting_for_anchor_key",
|
|
717
802
|
pairedAt: null,
|
|
718
803
|
};
|
|
719
804
|
return;
|
|
@@ -941,6 +1026,44 @@ class ArmedSeat {
|
|
|
941
1026
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
942
1027
|
}
|
|
943
1028
|
|
|
1029
|
+
findContinuationTarget() {
|
|
1030
|
+
if (!this.continueSeatId) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const candidates = listSessionNames()
|
|
1035
|
+
.map((sessionName) => {
|
|
1036
|
+
if (!getSeatDirIfExists(sessionName, this.continueSeatId)) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const seat = buildSeatReport(sessionName, this.continueSeatId);
|
|
1041
|
+
if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const updatedAtMs = Date.parse(seat.updatedAt || seat.startedAt || "");
|
|
1046
|
+
return {
|
|
1047
|
+
seat,
|
|
1048
|
+
sessionName,
|
|
1049
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
|
|
1050
|
+
};
|
|
1051
|
+
})
|
|
1052
|
+
.filter((entry) => entry !== null)
|
|
1053
|
+
.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
1054
|
+
|
|
1055
|
+
if (candidates.length === 0) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const target = candidates[0];
|
|
1060
|
+
return {
|
|
1061
|
+
seatId: target.seat.seatId,
|
|
1062
|
+
sessionName: target.sessionName,
|
|
1063
|
+
paths: getSeatPaths(target.sessionName, target.seat.seatId),
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
944
1067
|
async pullPartnerEvents() {
|
|
945
1068
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
946
1069
|
this.partnerOffset = nextOffset;
|
|
@@ -955,6 +1078,10 @@ class ArmedSeat {
|
|
|
955
1078
|
return;
|
|
956
1079
|
}
|
|
957
1080
|
|
|
1081
|
+
if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
958
1085
|
const payload = sanitizeRelayText(entry.text);
|
|
959
1086
|
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
960
1087
|
chainId: entry.chainId || entry.id,
|
|
@@ -962,6 +1089,7 @@ class ArmedSeat {
|
|
|
962
1089
|
id: entry.id,
|
|
963
1090
|
seatId: entry.seatId,
|
|
964
1091
|
origin: entry.origin || "unknown",
|
|
1092
|
+
phase: getRelayPhase(entry),
|
|
965
1093
|
createdAt: entry.createdAt,
|
|
966
1094
|
text: payload,
|
|
967
1095
|
});
|
|
@@ -1003,6 +1131,57 @@ class ArmedSeat {
|
|
|
1003
1131
|
}
|
|
1004
1132
|
}
|
|
1005
1133
|
|
|
1134
|
+
async pullContinuationEvents() {
|
|
1135
|
+
const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
|
|
1136
|
+
this.continueOffset = nextOffset;
|
|
1137
|
+
if (!text.trim() || !this.child || this.stopped) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const entries = parseContinueEntries(text, this.seatId);
|
|
1142
|
+
for (const entry of entries) {
|
|
1143
|
+
if (this.stopped || this.stopRequested()) {
|
|
1144
|
+
this.requestStop("stop_requested");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const payload = sanitizeRelayText(entry.text);
|
|
1153
|
+
if (!payload) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const delivered = await sendTextAndEnter(
|
|
1158
|
+
this.child,
|
|
1159
|
+
payload,
|
|
1160
|
+
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1161
|
+
);
|
|
1162
|
+
if (!delivered) {
|
|
1163
|
+
this.requestStop("relay_aborted");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (this.stopped || this.stopRequested()) {
|
|
1168
|
+
this.requestStop("stop_requested");
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const deliveredAtMs = Date.now();
|
|
1173
|
+
this.pendingInboundContext = {
|
|
1174
|
+
chainId: entry.chainId || entry.sourceAnswerId || entry.id,
|
|
1175
|
+
deliveredAtMs,
|
|
1176
|
+
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1177
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1178
|
+
};
|
|
1179
|
+
this.relayCount += 1;
|
|
1180
|
+
this.rememberInboundRelay(payload);
|
|
1181
|
+
this.log(`[${entry.sourceSeatId} => ${this.seatId}] ${previewText(payload)}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1006
1185
|
rememberInboundRelay(text) {
|
|
1007
1186
|
const payload = sanitizeRelayText(text);
|
|
1008
1187
|
if (!payload) {
|
|
@@ -1212,6 +1391,7 @@ class ArmedSeat {
|
|
|
1212
1391
|
this.emitAnswer({
|
|
1213
1392
|
id: answer.id || createId(12),
|
|
1214
1393
|
origin: detectedAgent.type,
|
|
1394
|
+
phase: answer.phase || "final_answer",
|
|
1215
1395
|
text: answer.text,
|
|
1216
1396
|
createdAt: answer.timestamp || new Date().toISOString(),
|
|
1217
1397
|
});
|
|
@@ -1257,6 +1437,7 @@ class ArmedSeat {
|
|
|
1257
1437
|
type: "answer",
|
|
1258
1438
|
seatId: this.seatId,
|
|
1259
1439
|
origin: entry.origin || "unknown",
|
|
1440
|
+
phase: entry.phase || "final_answer",
|
|
1260
1441
|
text: payload,
|
|
1261
1442
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1262
1443
|
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
@@ -1269,11 +1450,28 @@ class ArmedSeat {
|
|
|
1269
1450
|
this.identity.privateKey
|
|
1270
1451
|
);
|
|
1271
1452
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1453
|
+
this.forwardContinuation(signedEntry);
|
|
1272
1454
|
this.rememberEmittedAnswer(answerKey);
|
|
1273
1455
|
|
|
1274
1456
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1275
1457
|
}
|
|
1276
1458
|
|
|
1459
|
+
forwardContinuation(signedEntry) {
|
|
1460
|
+
if (!this.continueSeatId) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const target = this.findContinuationTarget();
|
|
1465
|
+
if (!target) {
|
|
1466
|
+
this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
|
|
1471
|
+
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1472
|
+
this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1277
1475
|
async tick() {
|
|
1278
1476
|
if (this.stopRequested()) {
|
|
1279
1477
|
this.writeStatus({
|
|
@@ -1287,6 +1485,7 @@ class ArmedSeat {
|
|
|
1287
1485
|
|
|
1288
1486
|
this.syncTrustState();
|
|
1289
1487
|
await this.pullPartnerEvents();
|
|
1488
|
+
await this.pullContinuationEvents();
|
|
1290
1489
|
if (this.stopped || this.stopRequested()) {
|
|
1291
1490
|
this.requestStop("stop_requested");
|
|
1292
1491
|
return;
|
|
@@ -1319,10 +1518,13 @@ class ArmedSeat {
|
|
|
1319
1518
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1320
1519
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1321
1520
|
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1322
|
-
if (this.
|
|
1323
|
-
this.log(
|
|
1521
|
+
if (this.continueSeatId) {
|
|
1522
|
+
this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
|
|
1523
|
+
}
|
|
1524
|
+
if (isAnchorSeat(this.seatId)) {
|
|
1525
|
+
this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
|
|
1324
1526
|
} else {
|
|
1325
|
-
this.log(
|
|
1527
|
+
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1326
1528
|
}
|
|
1327
1529
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1328
1530
|
|
|
@@ -1420,6 +1622,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1420
1622
|
seatId,
|
|
1421
1623
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1422
1624
|
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1625
|
+
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1423
1626
|
wrapperPid,
|
|
1424
1627
|
childPid,
|
|
1425
1628
|
wrapperLive,
|
|
@@ -1443,7 +1646,7 @@ function getStatusReport() {
|
|
|
1443
1646
|
const sessionPaths = getSessionPaths(sessionName);
|
|
1444
1647
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
1445
1648
|
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
1446
|
-
const seats =
|
|
1649
|
+
const seats = listSeatIds(sessionName)
|
|
1447
1650
|
.map((seatId) => buildSeatReport(sessionName, seatId))
|
|
1448
1651
|
.filter((entry) => entry !== null);
|
|
1449
1652
|
|
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,
|