muuuuse 5.0.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -18
- package/package.json +3 -4
- package/src/agents.js +16 -89
- package/src/cli.js +37 -45
- package/src/runtime.js +52 -431
- package/src/util.js +10 -38
package/README.md
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
`🔌Muuuuse` is a tiny terminal relay
|
|
3
|
+
`🔌Muuuuse` is a tiny terminal relay.
|
|
4
4
|
|
|
5
5
|
It does one job:
|
|
6
|
-
- arm
|
|
7
|
-
-
|
|
6
|
+
- arm terminal one with `muuuuse 1`
|
|
7
|
+
- arm terminal two with `muuuuse 2`
|
|
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`, ...
|
|
10
|
+
- choose per-seat relay mode with `flow on` or `flow off`
|
|
8
11
|
- watch Codex, Claude, or Gemini for local assistant output
|
|
9
|
-
-
|
|
12
|
+
- inject that output into the other armed terminal
|
|
10
13
|
- keep looping until you stop it
|
|
11
14
|
|
|
12
15
|
The whole surface is:
|
|
13
16
|
|
|
14
17
|
```bash
|
|
15
18
|
muuuuse 1
|
|
19
|
+
muuuuse 1 flow on
|
|
20
|
+
muuuuse 1 flow off
|
|
21
|
+
muuuuse 1 flow off continue 5
|
|
16
22
|
muuuuse 2
|
|
17
|
-
muuuuse
|
|
18
|
-
muuuuse
|
|
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
|
|
19
29
|
muuuuse status
|
|
20
30
|
muuuuse stop
|
|
21
31
|
```
|
|
22
32
|
|
|
23
|
-
##
|
|
33
|
+
## Flow
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
Terminal 1:
|
|
26
36
|
|
|
27
37
|
```bash
|
|
28
|
-
muuuuse
|
|
38
|
+
muuuuse 1 flow on
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
Terminal 2:
|
|
32
42
|
|
|
33
43
|
```bash
|
|
34
|
-
muuuuse
|
|
44
|
+
muuuuse 2 flow off
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
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.
|
|
38
48
|
|
|
39
|
-
`flow on` means
|
|
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.
|
|
40
50
|
|
|
41
|
-
|
|
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
|
+
|
|
53
|
+
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
42
54
|
|
|
43
55
|
```bash
|
|
44
56
|
codex
|
|
@@ -48,7 +60,7 @@ codex
|
|
|
48
60
|
gemini
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
`🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each
|
|
63
|
+
`🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each seat's flow mode, types that output into the other seat, and then sends Enter as a separate keystroke.
|
|
52
64
|
|
|
53
65
|
Check the live state from any terminal:
|
|
54
66
|
|
|
@@ -65,9 +77,8 @@ muuuuse stop
|
|
|
65
77
|
## Notes
|
|
66
78
|
|
|
67
79
|
- state lives under `~/.muuuuse`
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- flow modes are per-target (not per-seat)
|
|
80
|
+
- only the signed armed pair can exchange relay events
|
|
81
|
+
- `continue <seat>` is a separate local forwarding lane and can target any armed seat number
|
|
71
82
|
- supported relay detection is built for Codex, Claude, and Gemini
|
|
72
83
|
|
|
73
84
|
## Install
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "🔌Muuuuse arms terminals
|
|
3
|
+
"version": "6.0.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"
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "node test/cli.test.js",
|
|
43
|
-
"pack:local": "npm pack"
|
|
44
|
-
"prepublishOnly": "npm test"
|
|
43
|
+
"pack:local": "npm pack"
|
|
45
44
|
},
|
|
46
45
|
"dependencies": {
|
|
47
46
|
"node-pty": "^1.1.0"
|
package/src/agents.js
CHANGED
|
@@ -69,44 +69,6 @@ function detectAgent(processes) {
|
|
|
69
69
|
return buildDetectedAgent("gemini", process);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
// Fallback: if no agent process found, check for Claude Code running in a separate terminal
|
|
74
|
-
// (Claude Code's session updates even if it's not a child process of this shell)
|
|
75
|
-
if (processes.length > 0) {
|
|
76
|
-
// Use the bash/shell process as reference for timing
|
|
77
|
-
const shellProcess = processes[0];
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
const recentThreshold = now - 2 * 60 * 1000; // Last 2 minutes
|
|
80
|
-
|
|
81
|
-
const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
82
|
-
if (fs.existsSync(CLAUDE_ROOT)) {
|
|
83
|
-
try {
|
|
84
|
-
const sessionFiles = walkFiles(CLAUDE_ROOT, (f) => f.endsWith(".jsonl"))
|
|
85
|
-
.map(filePath => {
|
|
86
|
-
try {
|
|
87
|
-
const stat = fs.statSync(filePath);
|
|
88
|
-
return { filePath, mtimeMs: stat.mtimeMs };
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.filter(e => e && e.mtimeMs > recentThreshold)
|
|
94
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
95
|
-
|
|
96
|
-
if (sessionFiles) {
|
|
97
|
-
return {
|
|
98
|
-
type: "claude",
|
|
99
|
-
pid: process.pid || shellProcess.pid,
|
|
100
|
-
args: "claude-code-terminal-session",
|
|
101
|
-
cwd: shellProcess.cwd || null,
|
|
102
|
-
elapsedSeconds: Math.round((Date.now() - shellProcess.startedAtMs) / 1000),
|
|
103
|
-
processStartedAtMs: Date.now() - (shellProcess.elapsedSeconds ?? 0) * 1000,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
} catch {}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
72
|
return null;
|
|
111
73
|
}
|
|
112
74
|
|
|
@@ -593,7 +555,8 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
|
|
|
593
555
|
return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
|
|
594
556
|
}
|
|
595
557
|
|
|
596
|
-
function extractClaudeAssistantText(content) {
|
|
558
|
+
function extractClaudeAssistantText(content, options = {}) {
|
|
559
|
+
const flowMode = options.flowMode === true;
|
|
597
560
|
if (!Array.isArray(content)) {
|
|
598
561
|
return "";
|
|
599
562
|
}
|
|
@@ -606,23 +569,7 @@ function extractClaudeAssistantText(content) {
|
|
|
606
569
|
if (item.type === "text" && typeof item.text === "string") {
|
|
607
570
|
return [item.text.trim()];
|
|
608
571
|
}
|
|
609
|
-
|
|
610
|
-
})
|
|
611
|
-
.filter((text) => text.length > 0)
|
|
612
|
-
.join("\n");
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function extractClaudeThinkingText(content) {
|
|
616
|
-
if (!Array.isArray(content)) {
|
|
617
|
-
return "";
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return content
|
|
621
|
-
.flatMap((item) => {
|
|
622
|
-
if (!item || typeof item !== "object") {
|
|
623
|
-
return [];
|
|
624
|
-
}
|
|
625
|
-
if (item.type === "thinking" && typeof item.thinking === "string") {
|
|
572
|
+
if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
|
|
626
573
|
return [item.thinking.trim()];
|
|
627
574
|
}
|
|
628
575
|
return [];
|
|
@@ -639,46 +586,28 @@ function parseClaudeAssistantLine(line, options = {}) {
|
|
|
639
586
|
return null;
|
|
640
587
|
}
|
|
641
588
|
|
|
642
|
-
|
|
643
|
-
if (!flowMode && !isFinal) {
|
|
589
|
+
if (!flowMode && entry.message?.stop_reason !== "end_turn") {
|
|
644
590
|
return null;
|
|
645
591
|
}
|
|
646
592
|
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (flowMode) {
|
|
652
|
-
const thinking = sanitizeRelayText(extractClaudeThinkingText(entry.message.content));
|
|
653
|
-
if (thinking) {
|
|
654
|
-
results.push({
|
|
655
|
-
id: `${id}-thinking`,
|
|
656
|
-
text: thinking,
|
|
657
|
-
phase: "commentary",
|
|
658
|
-
timestamp,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
|
|
664
|
-
if (text) {
|
|
665
|
-
results.push({
|
|
666
|
-
id,
|
|
667
|
-
text,
|
|
668
|
-
phase: isFinal ? "final_answer" : "commentary",
|
|
669
|
-
timestamp,
|
|
670
|
-
});
|
|
593
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
|
|
594
|
+
if (!text) {
|
|
595
|
+
return null;
|
|
671
596
|
}
|
|
672
597
|
|
|
673
|
-
return
|
|
598
|
+
return {
|
|
599
|
+
id: entry.uuid || entry.message.id || hashText(line),
|
|
600
|
+
text,
|
|
601
|
+
phase: flowMode && entry.message?.stop_reason !== "end_turn" ? "commentary" : "final_answer",
|
|
602
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
603
|
+
};
|
|
674
604
|
} catch {
|
|
675
605
|
return null;
|
|
676
606
|
}
|
|
677
607
|
}
|
|
678
608
|
|
|
679
609
|
function parseClaudeFinalLine(line) {
|
|
680
|
-
|
|
681
|
-
return Array.isArray(results) ? results[0] || null : results;
|
|
610
|
+
return parseClaudeAssistantLine(line, { flowMode: false });
|
|
682
611
|
}
|
|
683
612
|
|
|
684
613
|
function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
@@ -687,10 +616,8 @@ function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
|
687
616
|
.split("\n")
|
|
688
617
|
.map((line) => line.trim())
|
|
689
618
|
.filter((line) => line.length > 0)
|
|
690
|
-
.
|
|
691
|
-
|
|
692
|
-
return Array.isArray(result) ? result : result ? [result] : [];
|
|
693
|
-
})
|
|
619
|
+
.map((line) => parseClaudeAssistantLine(line, options))
|
|
620
|
+
.filter((entry) => entry !== null)
|
|
694
621
|
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
695
622
|
|
|
696
623
|
return { nextOffset, answers };
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { BRAND,
|
|
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)) {
|
|
@@ -56,11 +56,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
56
56
|
|
|
57
57
|
const seatId = normalizeSeatId(command);
|
|
58
58
|
if (seatId) {
|
|
59
|
-
const {
|
|
59
|
+
const { continueTargets } = parseSeatOptions(command, argv.slice(1));
|
|
60
60
|
const seat = new ArmedSeat({
|
|
61
61
|
cwd: process.cwd(),
|
|
62
62
|
continueTargets,
|
|
63
|
-
flowMode,
|
|
64
63
|
seatId,
|
|
65
64
|
});
|
|
66
65
|
const code = await seat.run();
|
|
@@ -74,15 +73,11 @@ function renderSeatStatus(seat) {
|
|
|
74
73
|
const bits = [
|
|
75
74
|
`seat ${seat.seatId}: ${seat.state}`,
|
|
76
75
|
`agent ${seat.agent || "idle"}`,
|
|
77
|
-
`flow ${seat.flowMode || "off"}`,
|
|
78
76
|
`relays ${seat.relayCount}`,
|
|
79
77
|
`wrapper ${seat.wrapperPid || "-"}`,
|
|
80
78
|
`child ${seat.childPid || "-"}`,
|
|
81
79
|
];
|
|
82
80
|
|
|
83
|
-
if (seat.partnerLive) {
|
|
84
|
-
bits.push("peer live");
|
|
85
|
-
}
|
|
86
81
|
const renderedLinks = renderLinkTargets(seat);
|
|
87
82
|
if (renderedLinks) {
|
|
88
83
|
bits.push(`link ${renderedLinks}`);
|
|
@@ -106,48 +101,34 @@ function renderSeatStatus(seat) {
|
|
|
106
101
|
|
|
107
102
|
function renderLinkTargets(seat) {
|
|
108
103
|
const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return targets.map((target) => `${target.targetSeatId}:${target.flowMode}`).join(", ");
|
|
104
|
+
return targets
|
|
105
|
+
.map((target) => `${target.targetSeatId}:${target.flowMode}`)
|
|
106
|
+
.join(", ");
|
|
113
107
|
}
|
|
114
108
|
|
|
115
109
|
function parseSeatOptions(command, args) {
|
|
116
110
|
const seatId = normalizeSeatId(command);
|
|
117
|
-
let flowMode = "off";
|
|
118
111
|
let continueTargets = [];
|
|
119
|
-
|
|
120
|
-
return { flowMode, continueTargets };
|
|
121
|
-
}
|
|
112
|
+
let index = 0;
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (parsedLinks.consumed === args.length - 1 && parsedLinks.consumed > 0) {
|
|
126
|
-
return {
|
|
127
|
-
flowMode: parsedLinks.flowMode,
|
|
128
|
-
continueTargets: parsedLinks.continueTargets,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
let index = 0;
|
|
133
|
-
|
|
134
|
-
const flowToken = String(args[index] || "").trim().toLowerCase();
|
|
135
|
-
const flowModeToken = String(args[index + 1] || "").trim().toLowerCase();
|
|
136
|
-
if (flowToken === "flow" && (flowModeToken === "on" || flowModeToken === "off")) {
|
|
137
|
-
flowMode = flowModeToken;
|
|
138
|
-
index += 2;
|
|
139
|
-
}
|
|
114
|
+
while (index < args.length) {
|
|
115
|
+
const token = String(args[index] || "").trim().toLowerCase();
|
|
140
116
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
117
|
+
if (token === "link") {
|
|
118
|
+
const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
|
|
119
|
+
if (parsedLinks.consumed > 0) {
|
|
120
|
+
continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
|
|
121
|
+
index += 1 + parsedLinks.consumed;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
146
125
|
}
|
|
147
126
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (index === args.length) {
|
|
131
|
+
return { continueTargets };
|
|
151
132
|
}
|
|
152
133
|
|
|
153
134
|
throw new Error(
|
|
@@ -155,8 +136,20 @@ function parseSeatOptions(command, args) {
|
|
|
155
136
|
);
|
|
156
137
|
}
|
|
157
138
|
|
|
158
|
-
function
|
|
159
|
-
const
|
|
139
|
+
function mergeTargets(existingTargets, nextTargets) {
|
|
140
|
+
const merged = [];
|
|
141
|
+
for (const target of Array.isArray(existingTargets) ? existingTargets : []) {
|
|
142
|
+
upsertTarget(merged, target);
|
|
143
|
+
}
|
|
144
|
+
for (const target of Array.isArray(nextTargets) ? nextTargets : []) {
|
|
145
|
+
upsertTarget(merged, target);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return merged;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseLinkTargets(args, seatId) {
|
|
152
|
+
const continueTargets = [];
|
|
160
153
|
let consumed = 0;
|
|
161
154
|
|
|
162
155
|
while (consumed < args.length) {
|
|
@@ -170,7 +163,7 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
|
|
|
170
163
|
break;
|
|
171
164
|
}
|
|
172
165
|
|
|
173
|
-
upsertTarget(
|
|
166
|
+
upsertTarget(continueTargets, {
|
|
174
167
|
targetSeatId,
|
|
175
168
|
flowMode: targetFlowMode,
|
|
176
169
|
});
|
|
@@ -178,7 +171,7 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
|
|
|
178
171
|
consumed += 3;
|
|
179
172
|
}
|
|
180
173
|
|
|
181
|
-
return { consumed, continueTargets
|
|
174
|
+
return { consumed, continueTargets };
|
|
182
175
|
}
|
|
183
176
|
|
|
184
177
|
function parseFlowModeToken(flowToken, modeToken) {
|
|
@@ -201,5 +194,4 @@ function upsertTarget(targets, nextTarget) {
|
|
|
201
194
|
|
|
202
195
|
module.exports = {
|
|
203
196
|
main,
|
|
204
|
-
parseSeatOptions,
|
|
205
197
|
};
|
package/src/runtime.js
CHANGED
|
@@ -20,12 +20,10 @@ const {
|
|
|
20
20
|
ensureDir,
|
|
21
21
|
getDefaultSessionName,
|
|
22
22
|
getFileSize,
|
|
23
|
-
getPartnerSeatId,
|
|
24
23
|
getSeatPaths,
|
|
25
24
|
getSessionPaths,
|
|
26
25
|
getStateRoot,
|
|
27
26
|
hashText,
|
|
28
|
-
isAnchorSeat,
|
|
29
27
|
isPidAlive,
|
|
30
28
|
listSeatIds,
|
|
31
29
|
loadOrCreateSeatIdentity,
|
|
@@ -42,7 +40,6 @@ const {
|
|
|
42
40
|
|
|
43
41
|
const TYPE_CHUNK_DELAY_MS = 18;
|
|
44
42
|
const TYPE_CHUNK_SIZE = 24;
|
|
45
|
-
const TYPE_SUBMIT_DELAY_MS = 60;
|
|
46
43
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
47
44
|
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
48
45
|
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
@@ -57,32 +54,6 @@ const CHILD_ENV_DROP_KEYS = [
|
|
|
57
54
|
"CODEX_THREAD_ID",
|
|
58
55
|
];
|
|
59
56
|
|
|
60
|
-
function bestEffortEnableChildEcho(child) {
|
|
61
|
-
const ptsName = String(child?.ptsName || "").trim();
|
|
62
|
-
if (!ptsName || process.platform === "win32") {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
execFileSync("stty", [
|
|
68
|
-
"-F",
|
|
69
|
-
ptsName,
|
|
70
|
-
"echo",
|
|
71
|
-
"icanon",
|
|
72
|
-
"isig",
|
|
73
|
-
"iexten",
|
|
74
|
-
"echoe",
|
|
75
|
-
"echok",
|
|
76
|
-
"echoke",
|
|
77
|
-
"echoctl",
|
|
78
|
-
], {
|
|
79
|
-
stdio: "ignore",
|
|
80
|
-
});
|
|
81
|
-
} catch {
|
|
82
|
-
// Best effort only. The shell or child app may later change its own tty mode.
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
57
|
function normalizeFlowMode(flowMode) {
|
|
87
58
|
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
88
59
|
}
|
|
@@ -92,26 +63,6 @@ function normalizeContinueSeatId(value) {
|
|
|
92
63
|
return seatId || null;
|
|
93
64
|
}
|
|
94
65
|
|
|
95
|
-
function normalizeContinueTargets(targets, defaultFlowMode = "off") {
|
|
96
|
-
const normalized = [];
|
|
97
|
-
const seen = new Set();
|
|
98
|
-
|
|
99
|
-
for (const entry of Array.isArray(targets) ? targets : []) {
|
|
100
|
-
const targetSeatId = normalizeSeatId(entry?.targetSeatId ?? entry?.seatId ?? entry);
|
|
101
|
-
if (!targetSeatId || seen.has(targetSeatId)) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
seen.add(targetSeatId);
|
|
106
|
-
normalized.push({
|
|
107
|
-
targetSeatId,
|
|
108
|
-
flowMode: normalizeFlowMode(entry?.flowMode ?? entry?.flow ?? defaultFlowMode),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return normalized;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
66
|
function resolveShell() {
|
|
116
67
|
const shell = String(process.env.SHELL || "").trim();
|
|
117
68
|
return shell || "/bin/bash";
|
|
@@ -189,47 +140,19 @@ function sleepSync(ms) {
|
|
|
189
140
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
190
141
|
}
|
|
191
142
|
|
|
192
|
-
function
|
|
193
|
-
const normalizedSeatId = normalizeSeatId(seatId);
|
|
194
|
-
const anchorSeatId = getPartnerSeatId(normalizedSeatId);
|
|
195
|
-
if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
143
|
+
function findExistingSessionName(currentPath = process.cwd()) {
|
|
199
144
|
const candidates = listSessionNames()
|
|
200
145
|
.map((sessionName) => {
|
|
201
146
|
const sessionPaths = getSessionPaths(sessionName);
|
|
202
147
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
203
|
-
const
|
|
204
|
-
const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
|
|
205
|
-
const anchorMeta = readJson(anchorPaths.metaPath, null);
|
|
206
|
-
const anchorStatus = readJson(anchorPaths.statusPath, null);
|
|
207
|
-
const seatMeta = readJson(seatPaths.metaPath, null);
|
|
208
|
-
const seatStatus = readJson(seatPaths.statusPath, null);
|
|
209
|
-
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
210
|
-
|
|
211
|
-
const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
|
|
148
|
+
const cwd = controller?.cwd || null;
|
|
212
149
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
213
150
|
return null;
|
|
214
151
|
}
|
|
215
152
|
|
|
216
|
-
const
|
|
217
|
-
const anchorChildPid = anchorStatus?.childPid || anchorMeta?.childPid || null;
|
|
218
|
-
const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
|
|
219
|
-
const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
|
|
220
|
-
const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
|
|
221
|
-
const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
|
|
153
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
222
154
|
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
223
|
-
const createdAtMs = Date.parse(controller?.createdAt ||
|
|
224
|
-
|
|
225
|
-
// Accept session if: controller exists AND (anchor is live OR anchor has written meta/status files).
|
|
226
|
-
// This handles the startup race: anchor might not have a live PID yet, but if it wrote files, it's initialized.
|
|
227
|
-
const controllerExists = controller !== null;
|
|
228
|
-
const anchorInitialized = anchorMeta !== null || anchorStatus !== null;
|
|
229
|
-
const anchorReady = anchorLive || anchorInitialized;
|
|
230
|
-
if (!controllerExists || !anchorReady || seatLive) {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
155
|
+
const createdAtMs = Date.parse(controller?.createdAt || "");
|
|
233
156
|
|
|
234
157
|
if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
|
|
235
158
|
return null;
|
|
@@ -246,10 +169,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
|
|
|
246
169
|
return candidates[0]?.sessionName || null;
|
|
247
170
|
}
|
|
248
171
|
|
|
249
|
-
function
|
|
172
|
+
function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
250
173
|
const deadline = Date.now() + timeoutMs;
|
|
251
174
|
while (Date.now() <= deadline) {
|
|
252
|
-
const sessionName =
|
|
175
|
+
const sessionName = findExistingSessionName(currentPath);
|
|
253
176
|
if (sessionName) {
|
|
254
177
|
return sessionName;
|
|
255
178
|
}
|
|
@@ -260,11 +183,12 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
|
|
|
260
183
|
}
|
|
261
184
|
|
|
262
185
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
263
|
-
|
|
264
|
-
|
|
186
|
+
const existing = findExistingSessionName(currentPath);
|
|
187
|
+
if (existing) {
|
|
188
|
+
return existing;
|
|
265
189
|
}
|
|
266
190
|
|
|
267
|
-
return
|
|
191
|
+
return createSessionName(currentPath);
|
|
268
192
|
}
|
|
269
193
|
|
|
270
194
|
function parseAnswerEntries(text) {
|
|
@@ -466,26 +390,6 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
|
|
|
466
390
|
return null;
|
|
467
391
|
}
|
|
468
392
|
|
|
469
|
-
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
470
|
-
return JSON.stringify({
|
|
471
|
-
type: "muuuuse_pair_claim",
|
|
472
|
-
sessionName,
|
|
473
|
-
challenge,
|
|
474
|
-
seat1PublicKey,
|
|
475
|
-
seat2PublicKey,
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
480
|
-
return JSON.stringify({
|
|
481
|
-
type: "muuuuse_pair_ack",
|
|
482
|
-
sessionName,
|
|
483
|
-
challenge,
|
|
484
|
-
seat1PublicKey,
|
|
485
|
-
seat2PublicKey,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
393
|
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
490
394
|
return JSON.stringify({
|
|
491
395
|
type: "muuuuse_answer",
|
|
@@ -497,7 +401,6 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
497
401
|
seatId: entry.seatId,
|
|
498
402
|
origin: entry.origin,
|
|
499
403
|
phase: entry.phase || "final_answer",
|
|
500
|
-
flowMode: entry.flowMode || "off",
|
|
501
404
|
createdAt: entry.createdAt,
|
|
502
405
|
text: entry.text,
|
|
503
406
|
});
|
|
@@ -517,7 +420,6 @@ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
|
|
|
517
420
|
chainId: entry.chainId,
|
|
518
421
|
hop: entry.hop,
|
|
519
422
|
sourceAnswerId: entry.id,
|
|
520
|
-
flowMode: entry.flowMode || null,
|
|
521
423
|
publicKey: entry.publicKey || null,
|
|
522
424
|
signature: entry.signature || null,
|
|
523
425
|
};
|
|
@@ -626,14 +528,6 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
626
528
|
return false;
|
|
627
529
|
}
|
|
628
530
|
|
|
629
|
-
// Some TUIs treat fast, chunked relay typing as paste input and suppress an
|
|
630
|
-
// immediate Enter. A short settle delay keeps submit behavior reliable.
|
|
631
|
-
await sleep(TYPE_SUBMIT_DELAY_MS);
|
|
632
|
-
|
|
633
|
-
if (shouldAbort() || !child) {
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
531
|
try {
|
|
638
532
|
child.write("\r");
|
|
639
533
|
} catch {
|
|
@@ -646,30 +540,35 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
646
540
|
class ArmedSeat {
|
|
647
541
|
constructor(options) {
|
|
648
542
|
this.seatId = options.seatId;
|
|
649
|
-
this.
|
|
650
|
-
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
651
|
-
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
652
|
-
this.continueTargets = normalizeContinueTargets(
|
|
653
|
-
options.continueTargets || (
|
|
654
|
-
options.continueSeatId ? [{ targetSeatId: options.continueSeatId, flowMode: options.flowMode }] : []
|
|
655
|
-
),
|
|
656
|
-
this.flowMode
|
|
657
|
-
);
|
|
543
|
+
this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
|
|
658
544
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
659
|
-
|
|
660
|
-
|
|
545
|
+
|
|
546
|
+
// Auto-link partner seat for backwards compatibility (seat 1→2, seat 2→1)
|
|
547
|
+
// This preserves v5 behavior where odd seats (anchor) initiate relay to even (partner)
|
|
548
|
+
if (this.continueTargets.length === 0) {
|
|
549
|
+
if (this.seatId === 1) {
|
|
550
|
+
// Seat 1 (odd/anchor) relays to seat 2
|
|
551
|
+
this.continueTargets.push({ targetSeatId: 2, flowMode: "on" });
|
|
552
|
+
} else if (this.seatId === 3) {
|
|
553
|
+
// Seat 3 relays to seat 4
|
|
554
|
+
this.continueTargets.push({ targetSeatId: 4, flowMode: "on" });
|
|
555
|
+
}
|
|
556
|
+
// Even seats don't auto-link (they receive from odd partners)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
|
|
560
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
|
|
661
561
|
}
|
|
662
|
-
this.sessionName = resolveSessionName(this.cwd
|
|
562
|
+
this.sessionName = resolveSessionName(this.cwd);
|
|
663
563
|
if (!this.sessionName) {
|
|
664
564
|
throw new Error(
|
|
665
|
-
`
|
|
565
|
+
`Failed to create or find session in ${this.cwd}.`
|
|
666
566
|
);
|
|
667
567
|
}
|
|
668
568
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
669
569
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
670
|
-
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
671
570
|
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
672
|
-
this.
|
|
571
|
+
this.relayTargets = {};
|
|
673
572
|
|
|
674
573
|
this.child = null;
|
|
675
574
|
this.childPid = null;
|
|
@@ -689,9 +588,8 @@ class ArmedSeat {
|
|
|
689
588
|
this.recentEmittedAnswers = [];
|
|
690
589
|
this.trustState = {
|
|
691
590
|
challenge: null,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
pairedAt: null,
|
|
591
|
+
phase: "initialized",
|
|
592
|
+
createdAt: null,
|
|
695
593
|
};
|
|
696
594
|
this.liveState = {
|
|
697
595
|
type: null,
|
|
@@ -713,11 +611,6 @@ class ArmedSeat {
|
|
|
713
611
|
cwd: this.cwd,
|
|
714
612
|
createdAt: current.createdAt || this.startedAt,
|
|
715
613
|
updatedAt: new Date().toISOString(),
|
|
716
|
-
anchorSeatId: this.anchorSeatId,
|
|
717
|
-
partnerSeatId: this.partnerSeatId,
|
|
718
|
-
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
719
|
-
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
720
|
-
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
721
614
|
...extra,
|
|
722
615
|
});
|
|
723
616
|
}
|
|
@@ -729,9 +622,7 @@ class ArmedSeat {
|
|
|
729
622
|
writeMeta(extra = {}) {
|
|
730
623
|
writeJson(this.paths.metaPath, {
|
|
731
624
|
seatId: this.seatId,
|
|
732
|
-
partnerSeatId: this.partnerSeatId,
|
|
733
625
|
sessionName: this.sessionName,
|
|
734
|
-
flowMode: this.flowMode,
|
|
735
626
|
continueTargets: this.continueTargets,
|
|
736
627
|
cwd: this.cwd,
|
|
737
628
|
pid: process.pid,
|
|
@@ -745,9 +636,7 @@ class ArmedSeat {
|
|
|
745
636
|
writeStatus(extra = {}) {
|
|
746
637
|
writeJson(this.paths.statusPath, {
|
|
747
638
|
seatId: this.seatId,
|
|
748
|
-
partnerSeatId: this.partnerSeatId,
|
|
749
639
|
sessionName: this.sessionName,
|
|
750
|
-
flowMode: this.flowMode,
|
|
751
640
|
continueTargets: this.continueTargets,
|
|
752
641
|
cwd: this.cwd,
|
|
753
642
|
pid: process.pid,
|
|
@@ -760,168 +649,19 @@ class ArmedSeat {
|
|
|
760
649
|
|
|
761
650
|
initializeTrustMaterial() {
|
|
762
651
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
763
|
-
|
|
764
|
-
if (!isAnchorSeat(this.seatId)) {
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
652
|
writeJson(this.paths.challengePath, {
|
|
769
653
|
sessionName: this.sessionName,
|
|
770
|
-
challenge: createId(48),
|
|
771
654
|
publicKey: this.identity.publicKey,
|
|
772
655
|
createdAt: new Date().toISOString(),
|
|
773
656
|
});
|
|
774
|
-
this.trustState.
|
|
775
|
-
this.trustState.
|
|
776
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
777
|
-
this.trustState.pairedAt = null;
|
|
778
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
779
|
-
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
657
|
+
this.trustState.phase = "initialized";
|
|
658
|
+
this.trustState.createdAt = new Date().toISOString();
|
|
780
659
|
}
|
|
781
660
|
|
|
782
661
|
syncTrustState() {
|
|
783
662
|
if (!this.identity) {
|
|
784
663
|
this.initializeTrustMaterial();
|
|
785
664
|
}
|
|
786
|
-
|
|
787
|
-
if (isAnchorSeat(this.seatId)) {
|
|
788
|
-
this.syncSeatOneTrust();
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
this.syncSeatTwoTrust();
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
syncSeatOneTrust() {
|
|
796
|
-
const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
|
|
797
|
-
if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
|
|
798
|
-
this.trustState = {
|
|
799
|
-
challenge: null,
|
|
800
|
-
peerPublicKey: null,
|
|
801
|
-
phase: "waiting_for_peer_signature",
|
|
802
|
-
pairedAt: null,
|
|
803
|
-
};
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
this.trustState.challenge = challengeRecord.challenge;
|
|
808
|
-
const claim = readJson(this.partnerPaths.claimPath, null);
|
|
809
|
-
if (
|
|
810
|
-
!claim ||
|
|
811
|
-
claim.sessionName !== this.sessionName ||
|
|
812
|
-
claim.challenge !== challengeRecord.challenge ||
|
|
813
|
-
typeof claim.publicKey !== "string" ||
|
|
814
|
-
typeof claim.signature !== "string" ||
|
|
815
|
-
!verifyText(
|
|
816
|
-
buildClaimMessage(
|
|
817
|
-
this.sessionName,
|
|
818
|
-
challengeRecord.challenge,
|
|
819
|
-
this.identity.publicKey,
|
|
820
|
-
claim.publicKey.trim()
|
|
821
|
-
),
|
|
822
|
-
claim.signature,
|
|
823
|
-
claim.publicKey
|
|
824
|
-
)
|
|
825
|
-
) {
|
|
826
|
-
this.trustState.peerPublicKey = null;
|
|
827
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
828
|
-
this.trustState.pairedAt = null;
|
|
829
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const peerPublicKey = claim.publicKey.trim();
|
|
834
|
-
const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
|
|
835
|
-
const currentAck = readJson(this.paths.ackPath, null);
|
|
836
|
-
const ackIsValid = Boolean(
|
|
837
|
-
currentAck &&
|
|
838
|
-
currentAck.sessionName === this.sessionName &&
|
|
839
|
-
currentAck.challenge === challengeRecord.challenge &&
|
|
840
|
-
currentAck.publicKey === this.identity.publicKey &&
|
|
841
|
-
currentAck.peerPublicKey === peerPublicKey &&
|
|
842
|
-
typeof currentAck.signature === "string" &&
|
|
843
|
-
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
844
|
-
);
|
|
845
|
-
if (!ackIsValid) {
|
|
846
|
-
writeJson(this.paths.ackPath, {
|
|
847
|
-
sessionName: this.sessionName,
|
|
848
|
-
challenge: challengeRecord.challenge,
|
|
849
|
-
publicKey: this.identity.publicKey,
|
|
850
|
-
peerPublicKey,
|
|
851
|
-
signature: signText(ackMessage, this.identity.privateKey),
|
|
852
|
-
signedAt: new Date().toISOString(),
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
|
|
857
|
-
this.trustState.peerPublicKey = peerPublicKey;
|
|
858
|
-
this.trustState.phase = "paired";
|
|
859
|
-
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
syncSeatTwoTrust() {
|
|
863
|
-
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
864
|
-
if (!challengeRecord) {
|
|
865
|
-
this.trustState = {
|
|
866
|
-
challenge: null,
|
|
867
|
-
peerPublicKey: null,
|
|
868
|
-
phase: "waiting_for_anchor_key",
|
|
869
|
-
pairedAt: null,
|
|
870
|
-
};
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const challenge = challengeRecord.challenge;
|
|
875
|
-
const peerPublicKey = challengeRecord.publicKey;
|
|
876
|
-
const claimPayload = {
|
|
877
|
-
sessionName: this.sessionName,
|
|
878
|
-
challenge,
|
|
879
|
-
publicKey: this.identity.publicKey,
|
|
880
|
-
};
|
|
881
|
-
const claimSignature = signText(
|
|
882
|
-
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
883
|
-
this.identity.privateKey
|
|
884
|
-
);
|
|
885
|
-
const currentClaim = readJson(this.paths.claimPath, null);
|
|
886
|
-
if (
|
|
887
|
-
!currentClaim ||
|
|
888
|
-
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
889
|
-
currentClaim.challenge !== claimPayload.challenge ||
|
|
890
|
-
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
891
|
-
currentClaim.signature !== claimSignature
|
|
892
|
-
) {
|
|
893
|
-
writeJson(this.paths.claimPath, {
|
|
894
|
-
...claimPayload,
|
|
895
|
-
signature: claimSignature,
|
|
896
|
-
signedAt: new Date().toISOString(),
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const ack = readJson(this.partnerPaths.ackPath, null);
|
|
901
|
-
const paired = Boolean(
|
|
902
|
-
ack &&
|
|
903
|
-
ack.sessionName === this.sessionName &&
|
|
904
|
-
ack.challenge === challenge &&
|
|
905
|
-
ack.peerPublicKey === this.identity.publicKey &&
|
|
906
|
-
ack.publicKey === peerPublicKey &&
|
|
907
|
-
typeof ack.signature === "string" &&
|
|
908
|
-
verifyText(
|
|
909
|
-
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
910
|
-
ack.signature,
|
|
911
|
-
peerPublicKey
|
|
912
|
-
)
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
this.trustState.challenge = challenge;
|
|
916
|
-
this.trustState.peerPublicKey = peerPublicKey;
|
|
917
|
-
this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
|
|
918
|
-
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
isPaired() {
|
|
922
|
-
return this.trustState.phase === "paired" &&
|
|
923
|
-
typeof this.trustState.challenge === "string" &&
|
|
924
|
-
typeof this.trustState.peerPublicKey === "string";
|
|
925
665
|
}
|
|
926
666
|
|
|
927
667
|
launchShell() {
|
|
@@ -941,7 +681,6 @@ class ArmedSeat {
|
|
|
941
681
|
env: childEnv,
|
|
942
682
|
name: childEnv.TERM,
|
|
943
683
|
});
|
|
944
|
-
bestEffortEnableChildEcho(this.child);
|
|
945
684
|
|
|
946
685
|
this.childPid = this.child.pid;
|
|
947
686
|
this.writeMeta();
|
|
@@ -1079,11 +818,6 @@ class ArmedSeat {
|
|
|
1079
818
|
}
|
|
1080
819
|
}
|
|
1081
820
|
|
|
1082
|
-
partnerIsLive() {
|
|
1083
|
-
const partner = readJson(this.partnerPaths.statusPath, null);
|
|
1084
|
-
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
821
|
stopRequested() {
|
|
1088
822
|
const request = readJson(this.sessionPaths.stopPath, null);
|
|
1089
823
|
if (!request?.requestedAt) {
|
|
@@ -1094,23 +828,19 @@ class ArmedSeat {
|
|
|
1094
828
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
1095
829
|
}
|
|
1096
830
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
findContinuationTarget(targetSeatId) {
|
|
1102
|
-
const normalizedTargetSeatId = normalizeSeatId(targetSeatId);
|
|
1103
|
-
if (!normalizedTargetSeatId) {
|
|
831
|
+
findContinuationTarget(targetSeatId = null) {
|
|
832
|
+
const seatIdToFind = targetSeatId || this.continueSeatId;
|
|
833
|
+
if (!seatIdToFind) {
|
|
1104
834
|
return null;
|
|
1105
835
|
}
|
|
1106
836
|
|
|
1107
837
|
const candidates = listSessionNames()
|
|
1108
838
|
.map((sessionName) => {
|
|
1109
|
-
if (!getSeatDirIfExists(sessionName,
|
|
839
|
+
if (!getSeatDirIfExists(sessionName, seatIdToFind)) {
|
|
1110
840
|
return null;
|
|
1111
841
|
}
|
|
1112
842
|
|
|
1113
|
-
const seat = buildSeatReport(sessionName,
|
|
843
|
+
const seat = buildSeatReport(sessionName, seatIdToFind);
|
|
1114
844
|
if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
|
|
1115
845
|
return null;
|
|
1116
846
|
}
|
|
@@ -1137,75 +867,6 @@ class ArmedSeat {
|
|
|
1137
867
|
};
|
|
1138
868
|
}
|
|
1139
869
|
|
|
1140
|
-
async pullPartnerEvents() {
|
|
1141
|
-
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
1142
|
-
this.partnerOffset = nextOffset;
|
|
1143
|
-
if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const entries = parseAnswerEntries(text);
|
|
1148
|
-
for (const entry of entries) {
|
|
1149
|
-
if (this.stopped || this.stopRequested()) {
|
|
1150
|
-
this.requestStop("stop_requested");
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const inboundFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
|
|
1155
|
-
if (!shouldAcceptInboundEntry(inboundFlowMode, entry)) {
|
|
1156
|
-
continue;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const payload = sanitizeRelayText(entry.text);
|
|
1160
|
-
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
1161
|
-
chainId: entry.chainId || entry.id,
|
|
1162
|
-
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1163
|
-
id: entry.id,
|
|
1164
|
-
seatId: entry.seatId,
|
|
1165
|
-
origin: entry.origin || "unknown",
|
|
1166
|
-
phase: getRelayPhase(entry),
|
|
1167
|
-
flowMode: inboundFlowMode,
|
|
1168
|
-
createdAt: entry.createdAt,
|
|
1169
|
-
text: payload,
|
|
1170
|
-
});
|
|
1171
|
-
if (
|
|
1172
|
-
!payload ||
|
|
1173
|
-
entry.challenge !== this.trustState.challenge ||
|
|
1174
|
-
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
1175
|
-
typeof entry.signature !== "string" ||
|
|
1176
|
-
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
1177
|
-
) {
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const delivered = await sendTextAndEnter(
|
|
1182
|
-
this.child,
|
|
1183
|
-
payload,
|
|
1184
|
-
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1185
|
-
);
|
|
1186
|
-
if (!delivered) {
|
|
1187
|
-
this.requestStop("relay_aborted");
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
if (this.stopped || this.stopRequested()) {
|
|
1192
|
-
this.requestStop("stop_requested");
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const deliveredAtMs = Date.now();
|
|
1197
|
-
this.pendingInboundContext = {
|
|
1198
|
-
chainId: entry.chainId || entry.id,
|
|
1199
|
-
deliveredAtMs,
|
|
1200
|
-
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1201
|
-
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1202
|
-
};
|
|
1203
|
-
this.relayCount += 1;
|
|
1204
|
-
this.rememberInboundRelay(payload);
|
|
1205
|
-
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
870
|
async pullContinuationEvents() {
|
|
1210
871
|
const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
|
|
1211
872
|
this.continueOffset = nextOffset;
|
|
@@ -1220,11 +881,6 @@ class ArmedSeat {
|
|
|
1220
881
|
return;
|
|
1221
882
|
}
|
|
1222
883
|
|
|
1223
|
-
const continueFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
|
|
1224
|
-
if (!shouldAcceptInboundEntry(continueFlowMode, entry)) {
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
884
|
const payload = sanitizeRelayText(entry.text);
|
|
1229
885
|
if (!payload) {
|
|
1230
886
|
continue;
|
|
@@ -1433,13 +1089,12 @@ class ArmedSeat {
|
|
|
1433
1089
|
}
|
|
1434
1090
|
|
|
1435
1091
|
const answers = [];
|
|
1436
|
-
const captureCommentary = this.shouldCaptureCommentary();
|
|
1437
1092
|
if (detectedAgent.type === "codex") {
|
|
1438
1093
|
const result = readCodexAnswers(
|
|
1439
1094
|
this.liveState.sessionFile,
|
|
1440
1095
|
this.liveState.offset,
|
|
1441
1096
|
this.liveState.captureSinceMs,
|
|
1442
|
-
{ flowMode:
|
|
1097
|
+
{ flowMode: true }
|
|
1443
1098
|
);
|
|
1444
1099
|
this.liveState.offset = result.nextOffset;
|
|
1445
1100
|
answers.push(...result.answers);
|
|
@@ -1448,7 +1103,7 @@ class ArmedSeat {
|
|
|
1448
1103
|
this.liveState.sessionFile,
|
|
1449
1104
|
this.liveState.offset,
|
|
1450
1105
|
this.liveState.captureSinceMs,
|
|
1451
|
-
{ flowMode:
|
|
1106
|
+
{ flowMode: true }
|
|
1452
1107
|
);
|
|
1453
1108
|
this.liveState.offset = result.nextOffset;
|
|
1454
1109
|
answers.push(...result.answers);
|
|
@@ -1457,7 +1112,7 @@ class ArmedSeat {
|
|
|
1457
1112
|
this.liveState.sessionFile,
|
|
1458
1113
|
this.liveState.lastMessageId,
|
|
1459
1114
|
this.liveState.captureSinceMs,
|
|
1460
|
-
{ flowMode:
|
|
1115
|
+
{ flowMode: true }
|
|
1461
1116
|
);
|
|
1462
1117
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
1463
1118
|
this.liveState.offset = result.fileSize;
|
|
@@ -1515,7 +1170,6 @@ class ArmedSeat {
|
|
|
1515
1170
|
seatId: this.seatId,
|
|
1516
1171
|
origin: entry.origin || "unknown",
|
|
1517
1172
|
phase: entry.phase || "final_answer",
|
|
1518
|
-
flowMode: this.flowMode,
|
|
1519
1173
|
text: payload,
|
|
1520
1174
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1521
1175
|
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
@@ -1527,19 +1181,17 @@ class ArmedSeat {
|
|
|
1527
1181
|
buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
|
|
1528
1182
|
this.identity.privateKey
|
|
1529
1183
|
);
|
|
1530
|
-
|
|
1531
1184
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1532
|
-
this.
|
|
1185
|
+
this.forwardContinuation(signedEntry);
|
|
1533
1186
|
this.rememberEmittedAnswer(answerKey);
|
|
1187
|
+
|
|
1534
1188
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1535
1189
|
}
|
|
1536
1190
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1191
|
+
forwardContinuation(signedEntry) {
|
|
1192
|
+
// Route to all continueTargets with per-target flow modes
|
|
1542
1193
|
for (const targetEntry of this.continueTargets) {
|
|
1194
|
+
// Skip entries that don't match the target's flowMode
|
|
1543
1195
|
if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
|
|
1544
1196
|
continue;
|
|
1545
1197
|
}
|
|
@@ -1550,12 +1202,9 @@ class ArmedSeat {
|
|
|
1550
1202
|
continue;
|
|
1551
1203
|
}
|
|
1552
1204
|
|
|
1553
|
-
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId,
|
|
1554
|
-
...signedEntry,
|
|
1555
|
-
flowMode: targetEntry.flowMode,
|
|
1556
|
-
});
|
|
1205
|
+
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
|
|
1557
1206
|
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1558
|
-
this.log(`[${this.seatId} => ${target.seatId} ${targetEntry.flowMode}] ${previewText(continuationEntry.text)}`);
|
|
1207
|
+
this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
|
|
1559
1208
|
}
|
|
1560
1209
|
}
|
|
1561
1210
|
|
|
@@ -1563,7 +1212,6 @@ class ArmedSeat {
|
|
|
1563
1212
|
if (this.stopRequested()) {
|
|
1564
1213
|
this.writeStatus({
|
|
1565
1214
|
state: "stopping",
|
|
1566
|
-
partnerLive: this.partnerIsLive(),
|
|
1567
1215
|
trust: this.trustState.phase,
|
|
1568
1216
|
});
|
|
1569
1217
|
this.requestStop("stop_requested");
|
|
@@ -1571,7 +1219,6 @@ class ArmedSeat {
|
|
|
1571
1219
|
}
|
|
1572
1220
|
|
|
1573
1221
|
this.syncTrustState();
|
|
1574
|
-
await this.pullPartnerEvents();
|
|
1575
1222
|
await this.pullContinuationEvents();
|
|
1576
1223
|
if (this.stopped || this.stopRequested()) {
|
|
1577
1224
|
this.requestStop("stop_requested");
|
|
@@ -1586,11 +1233,9 @@ class ArmedSeat {
|
|
|
1586
1233
|
this.writeStatus({
|
|
1587
1234
|
state: live.state,
|
|
1588
1235
|
agent: live.agent,
|
|
1589
|
-
flowMode: this.flowMode,
|
|
1590
1236
|
cwd: live.cwd,
|
|
1591
1237
|
log: live.log,
|
|
1592
1238
|
lastAnswerAt: live.lastAnswerAt,
|
|
1593
|
-
partnerLive: this.partnerIsLive(),
|
|
1594
1239
|
trust: this.trustState.phase,
|
|
1595
1240
|
challengeReady: Boolean(this.trustState.challenge),
|
|
1596
1241
|
});
|
|
@@ -1604,16 +1249,9 @@ class ArmedSeat {
|
|
|
1604
1249
|
|
|
1605
1250
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1606
1251
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1607
|
-
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1608
1252
|
if (this.continueTargets.length > 0) {
|
|
1609
|
-
this.
|
|
1610
|
-
|
|
1611
|
-
);
|
|
1612
|
-
}
|
|
1613
|
-
if (isAnchorSeat(this.seatId)) {
|
|
1614
|
-
this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
|
|
1615
|
-
} else {
|
|
1616
|
-
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1253
|
+
const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
|
|
1254
|
+
this.log(`Seat ${this.seatId} relays to ${targets}.`);
|
|
1617
1255
|
}
|
|
1618
1256
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1619
1257
|
|
|
@@ -1709,17 +1347,8 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1709
1347
|
|
|
1710
1348
|
return {
|
|
1711
1349
|
seatId,
|
|
1712
|
-
partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
|
|
1713
1350
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1714
|
-
|
|
1715
|
-
continueTargets: normalizeContinueTargets(
|
|
1716
|
-
status?.continueTargets || meta?.continueTargets || (
|
|
1717
|
-
(status?.continueSeatId || meta?.continueSeatId)
|
|
1718
|
-
? [{ targetSeatId: status?.continueSeatId || meta?.continueSeatId, flowMode: status?.flowMode || meta?.flowMode || "off" }]
|
|
1719
|
-
: []
|
|
1720
|
-
),
|
|
1721
|
-
status?.flowMode || meta?.flowMode || "off"
|
|
1722
|
-
),
|
|
1351
|
+
continueTargets: status?.continueTargets || meta?.continueTargets || [],
|
|
1723
1352
|
wrapperPid,
|
|
1724
1353
|
childPid,
|
|
1725
1354
|
wrapperLive,
|
|
@@ -1733,7 +1362,6 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1733
1362
|
trust: status?.trust || null,
|
|
1734
1363
|
updatedAt: status?.updatedAt || null,
|
|
1735
1364
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
1736
|
-
partnerLive: Boolean(status?.partnerLive),
|
|
1737
1365
|
};
|
|
1738
1366
|
}
|
|
1739
1367
|
|
|
@@ -1795,13 +1423,6 @@ function stopAllSessions() {
|
|
|
1795
1423
|
signalPid(seat.wrapperPid, "SIGTERM");
|
|
1796
1424
|
}
|
|
1797
1425
|
}
|
|
1798
|
-
|
|
1799
|
-
// Clean up session directory so stale data doesn't bleed into next session.
|
|
1800
|
-
try {
|
|
1801
|
-
fs.rmSync(sessionPaths.dir, { recursive: true, force: true });
|
|
1802
|
-
} catch {
|
|
1803
|
-
// Best-effort cleanup.
|
|
1804
|
-
}
|
|
1805
1426
|
}
|
|
1806
1427
|
|
|
1807
1428
|
return {
|
package/src/util.js
CHANGED
|
@@ -9,16 +9,7 @@ const fs = require("node:fs");
|
|
|
9
9
|
const os = require("node:os");
|
|
10
10
|
const path = require("node:path");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
|
|
15
|
-
return `🔌Muuuuse v${pkg.version}`;
|
|
16
|
-
} catch {
|
|
17
|
-
return "🔌Muuuuse";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const BRAND = getBrand();
|
|
12
|
+
const BRAND = "🔌Muuuuse v6.0.0";
|
|
22
13
|
const POLL_MS = 220;
|
|
23
14
|
const MAX_RELAY_CHARS = 4000;
|
|
24
15
|
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -184,17 +175,6 @@ function normalizeSeatId(value) {
|
|
|
184
175
|
return seatId;
|
|
185
176
|
}
|
|
186
177
|
|
|
187
|
-
function isAnchorSeat(seatId) {
|
|
188
|
-
return normalizeSeatId(seatId) % 2 === 1;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function getPartnerSeatId(seatId) {
|
|
192
|
-
const normalized = normalizeSeatId(seatId);
|
|
193
|
-
if (!normalized) {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
return isAnchorSeat(normalized) ? normalized + 1 : normalized - 1;
|
|
197
|
-
}
|
|
198
178
|
|
|
199
179
|
function listSeatIds(sessionName) {
|
|
200
180
|
const sessionDir = getSessionDir(sessionName);
|
|
@@ -291,34 +271,28 @@ function listSessionNames() {
|
|
|
291
271
|
|
|
292
272
|
function usage() {
|
|
293
273
|
return [
|
|
294
|
-
`${BRAND}
|
|
274
|
+
`${BRAND} relay protocol for long-horizon zero-drift agentic code loops. agents relay output between terminals, converging to lucid conclusions.`,
|
|
295
275
|
"",
|
|
296
276
|
"Usage:",
|
|
297
|
-
" muuuuse",
|
|
298
277
|
" muuuuse 1",
|
|
299
|
-
" muuuuse 1 link 2 flow off",
|
|
300
278
|
" muuuuse 1 link 2 flow on",
|
|
301
279
|
" muuuuse 1 link 2 flow on 3 flow off",
|
|
302
280
|
" muuuuse 2",
|
|
281
|
+
" muuuuse 2 link 3 flow on",
|
|
303
282
|
" muuuuse 3",
|
|
304
|
-
" muuuuse 4",
|
|
305
|
-
" muuuuse 4 link 3 flow off 1 flow on",
|
|
306
283
|
" muuuuse stop",
|
|
307
284
|
" muuuuse status",
|
|
308
285
|
"",
|
|
309
286
|
"Flow:",
|
|
310
|
-
" 1. Run `muuuuse
|
|
311
|
-
" 2.
|
|
312
|
-
" 3.
|
|
313
|
-
" 4.
|
|
314
|
-
" 5.
|
|
315
|
-
" 6. Include the odd/even partner in `link` to set normal pair flow.",
|
|
316
|
-
" 7. Any extra linked seats receive routed copies with their own flow mode.",
|
|
317
|
-
" 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
|
|
318
|
-
" 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
287
|
+
" 1. Run `muuuuse <seat>` in each terminal to arm it (any seat number, any count).",
|
|
288
|
+
" 2. Optionally add `link <target> flow on/off [<target> flow on/off ...]` to relay output to other seats.",
|
|
289
|
+
" 3. Use those armed shells normally. Codex, Claude, and Gemini relay automatically from their local session logs.",
|
|
290
|
+
" 4. `flow off` sends final answers only. `flow on` sends both commentary and final answers.",
|
|
291
|
+
" 5. Run `muuuuse status` or `muuuuse stop` from any terminal.",
|
|
319
292
|
"",
|
|
320
293
|
"Notes:",
|
|
321
|
-
" -
|
|
294
|
+
" - Any seat can relay to any other seat independently.",
|
|
295
|
+
" - `muuuuse stop` and `muuuuse status` work from any terminal.",
|
|
322
296
|
" - State lives under `~/.muuuuse`.",
|
|
323
297
|
].join("\n");
|
|
324
298
|
}
|
|
@@ -332,9 +306,7 @@ module.exports = {
|
|
|
332
306
|
ensureDir,
|
|
333
307
|
getDefaultSessionName,
|
|
334
308
|
getFileSize,
|
|
335
|
-
getPartnerSeatId,
|
|
336
309
|
loadOrCreateSeatIdentity,
|
|
337
|
-
isAnchorSeat,
|
|
338
310
|
getSeatPaths,
|
|
339
311
|
getSessionPaths,
|
|
340
312
|
getStateRoot,
|