muuuuse 2.3.6 → 3.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 -11
- package/package.json +2 -2
- package/src/cli.js +95 -22
- package/src/runtime.js +192 -397
- package/src/util.js +11 -23
package/README.md
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
It does one job:
|
|
6
6
|
- arm terminal one with `muuuuse 1`
|
|
7
7
|
- arm terminal two with `muuuuse 2`
|
|
8
|
-
-
|
|
9
|
-
- additional isolated pairs work the same way: `3/4`, `5/6`, `7/8`, ...
|
|
8
|
+
- let each seat define signed relay links to any other armed seat
|
|
10
9
|
- choose per-seat relay mode with `flow on` or `flow off`
|
|
11
10
|
- watch Codex, Claude, or Gemini for local assistant output
|
|
12
|
-
- inject that output into
|
|
11
|
+
- inject that output into linked armed terminals
|
|
13
12
|
- keep looping until you stop it
|
|
14
13
|
|
|
15
14
|
The whole surface is:
|
|
@@ -19,9 +18,11 @@ muuuuse 1
|
|
|
19
18
|
muuuuse 1 flow on
|
|
20
19
|
muuuuse 1 flow off
|
|
21
20
|
muuuuse 1 flow off continue 5
|
|
21
|
+
muuuuse 1 link 2 flow on 3 flow off 5 flow off
|
|
22
22
|
muuuuse 2
|
|
23
23
|
muuuuse 2 flow off
|
|
24
24
|
muuuuse 2 flow on continue 3
|
|
25
|
+
muuuuse 2 link 1 flow off 3 flow on 4 flow on
|
|
25
26
|
muuuuse 3
|
|
26
27
|
muuuuse 3 flow on
|
|
27
28
|
muuuuse 4
|
|
@@ -35,20 +36,20 @@ muuuuse stop
|
|
|
35
36
|
Terminal 1:
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
|
-
muuuuse 1 flow on
|
|
39
|
+
muuuuse 1 link 2 flow on
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
Terminal 2:
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
|
-
muuuuse 2 flow off
|
|
45
|
+
muuuuse 2 link 1 flow off link 3 flow on link 4 flow on
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
Now both shells are armed
|
|
48
|
+
Now both shells are armed in the same cwd and join the same relay graph. Every seat has its own Ed25519 keypair. Each forwarded relay is signed by the sending seat. A target seat only accepts inbound relays from seats it links back to, so the graph can be open-ended without becoming an all-to-all broadcast.
|
|
48
49
|
|
|
49
|
-
`flow on` means that
|
|
50
|
+
`link <seat> flow on` means that outbound edge sends commentary and final answers. `link <seat> flow off` means that outbound edge sends final answers only. This is sender-side routing, not receiver-side filtering.
|
|
50
51
|
|
|
51
|
-
`continue <seat>`
|
|
52
|
+
`continue <seat>` is shorthand for one outbound link that uses the seat's default `flow on|off`. Explicit `link ... flow ...` edges are the full model and can be arranged into loops such as `1 -> 2 -> 3 -> 4 -> 1`.
|
|
52
53
|
|
|
53
54
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
54
55
|
|
|
@@ -60,7 +61,7 @@ codex
|
|
|
60
61
|
gemini
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
`🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each
|
|
64
|
+
`🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each outbound link's flow mode, types that output into the target seat, and then sends Enter as a separate keystroke.
|
|
64
65
|
|
|
65
66
|
Check the live state from any terminal:
|
|
66
67
|
|
|
@@ -77,8 +78,9 @@ muuuuse stop
|
|
|
77
78
|
## Notes
|
|
78
79
|
|
|
79
80
|
- state lives under `~/.muuuuse`
|
|
80
|
-
-
|
|
81
|
-
-
|
|
81
|
+
- all armed seats in the same cwd share one relay session graph
|
|
82
|
+
- only signed relays from reciprocally linked seats are accepted
|
|
83
|
+
- `continue <seat>` is a convenience alias for a single signed outbound link
|
|
82
84
|
- supported relay detection is built for Codex, Claude, and Gemini
|
|
83
85
|
|
|
84
86
|
## Install
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "🔌Muuuuse arms regular terminals
|
|
3
|
+
"version": "3.3.1",
|
|
4
|
+
"description": "🔌Muuuuse arms regular terminals and relays assistant output across signed terminal links.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"muuuuse": "bin/muuse.js"
|
package/src/cli.js
CHANGED
|
@@ -56,10 +56,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
56
56
|
|
|
57
57
|
const seatId = normalizeSeatId(command);
|
|
58
58
|
if (seatId) {
|
|
59
|
-
const { flowMode, continueSeatId } = parseSeatOptions(command, argv.slice(1));
|
|
59
|
+
const { flowMode, continueSeatId, continueTargets } = parseSeatOptions(command, argv.slice(1));
|
|
60
60
|
const seat = new ArmedSeat({
|
|
61
61
|
cwd: process.cwd(),
|
|
62
62
|
continueSeatId,
|
|
63
|
+
continueTargets,
|
|
63
64
|
flowMode,
|
|
64
65
|
seatId,
|
|
65
66
|
});
|
|
@@ -80,14 +81,11 @@ function renderSeatStatus(seat) {
|
|
|
80
81
|
`child ${seat.childPid || "-"}`,
|
|
81
82
|
];
|
|
82
83
|
|
|
83
|
-
if (seat.partnerLive) {
|
|
84
|
-
bits.push("peer live");
|
|
85
|
-
}
|
|
86
84
|
if (seat.continueSeatId) {
|
|
87
85
|
bits.push(`continue ${seat.continueSeatId}`);
|
|
88
86
|
}
|
|
89
|
-
if (seat.
|
|
90
|
-
bits.push(`
|
|
87
|
+
if (Array.isArray(seat.continueTargets) && seat.continueTargets.length > 0) {
|
|
88
|
+
bits.push(`links ${renderLinkTargets(seat.continueTargets)}`);
|
|
91
89
|
}
|
|
92
90
|
if (seat.lastAnswerAt) {
|
|
93
91
|
bits.push(`last answer ${seat.lastAnswerAt}`);
|
|
@@ -103,17 +101,26 @@ function renderSeatStatus(seat) {
|
|
|
103
101
|
return output;
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
function renderLinkTargets(targets) {
|
|
105
|
+
return targets
|
|
106
|
+
.map((target) => `${target.seatId}:${target.flowMode}`)
|
|
107
|
+
.join(", ");
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
function parseSeatOptions(command, args) {
|
|
111
|
+
const seatId = normalizeSeatId(command);
|
|
107
112
|
let flowMode = "off";
|
|
108
113
|
let continueSeatId = null;
|
|
114
|
+
let continueTargets = [];
|
|
115
|
+
let index = 0;
|
|
109
116
|
|
|
110
|
-
for (
|
|
117
|
+
for (; index < args.length;) {
|
|
111
118
|
const token = String(args[index] || "").trim().toLowerCase();
|
|
112
119
|
|
|
113
120
|
if (token === "flow") {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
flowMode =
|
|
121
|
+
const nextFlowMode = parseFlowModeToken(args[index + 1]);
|
|
122
|
+
if (nextFlowMode) {
|
|
123
|
+
flowMode = nextFlowMode;
|
|
117
124
|
index += 2;
|
|
118
125
|
continue;
|
|
119
126
|
}
|
|
@@ -122,7 +129,7 @@ function parseSeatOptions(command, args) {
|
|
|
122
129
|
|
|
123
130
|
if (token === "continue") {
|
|
124
131
|
const targetSeatId = normalizeSeatId(args[index + 1]);
|
|
125
|
-
if (targetSeatId) {
|
|
132
|
+
if (targetSeatId && targetSeatId !== seatId) {
|
|
126
133
|
continueSeatId = targetSeatId;
|
|
127
134
|
index += 2;
|
|
128
135
|
continue;
|
|
@@ -130,28 +137,94 @@ function parseSeatOptions(command, args) {
|
|
|
130
137
|
break;
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
if (token === "link") {
|
|
141
|
+
const parsed = parseLinkTargets(seatId, args, index + 1);
|
|
142
|
+
if (!parsed) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
continueTargets = mergeTargets(continueTargets, parsed.continueTargets);
|
|
147
|
+
index = parsed.nextIndex;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
break;
|
|
134
152
|
}
|
|
135
153
|
|
|
136
|
-
if (
|
|
137
|
-
return { flowMode, continueSeatId };
|
|
154
|
+
if (index === args.length) {
|
|
155
|
+
return { flowMode, continueSeatId, continueTargets };
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
throw new Error(
|
|
141
|
-
`\`muuuuse ${command}\` accepts
|
|
159
|
+
`\`muuuuse ${command}\` accepts \`flow on\` / \`flow off\`, optional \`continue <seat>\`, and optional \`link <seat> flow on|off ...\` groups. Run it directly in the terminal you want to arm.`
|
|
142
160
|
);
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
function mergeTargets(currentTargets, nextTargets) {
|
|
164
|
+
const merged = [...currentTargets];
|
|
165
|
+
for (const target of nextTargets) {
|
|
166
|
+
const currentIndex = merged.findIndex((entry) => entry.seatId === target.seatId);
|
|
167
|
+
if (currentIndex !== -1) {
|
|
168
|
+
merged.splice(currentIndex, 1);
|
|
169
|
+
}
|
|
170
|
+
merged.push(target);
|
|
171
|
+
}
|
|
172
|
+
return merged;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseLinkTargets(seatId, args, startIndex) {
|
|
176
|
+
let index = startIndex;
|
|
177
|
+
const continueTargets = [];
|
|
178
|
+
|
|
179
|
+
while (index < args.length) {
|
|
180
|
+
const targetSeatId = normalizeSeatId(args[index]);
|
|
181
|
+
if (!targetSeatId || targetSeatId === seatId) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (String(args[index + 1] || "").trim().toLowerCase() !== "flow") {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetFlowMode = parseFlowModeToken(args[index + 2]);
|
|
190
|
+
if (!targetFlowMode) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
upsertTarget(continueTargets, {
|
|
195
|
+
seatId: targetSeatId,
|
|
196
|
+
flowMode: targetFlowMode,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
index += 3;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (index === startIndex) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
continueTargets,
|
|
208
|
+
nextIndex: index,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseFlowModeToken(value) {
|
|
213
|
+
const token = String(value || "").trim().toLowerCase();
|
|
214
|
+
if (token === "on" || token === "off") {
|
|
215
|
+
return token;
|
|
149
216
|
}
|
|
150
|
-
|
|
151
|
-
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function upsertTarget(targets, nextTarget) {
|
|
221
|
+
const currentIndex = targets.findIndex((target) => target.seatId === nextTarget.seatId);
|
|
222
|
+
if (currentIndex === -1) {
|
|
223
|
+
targets.push(nextTarget);
|
|
224
|
+
return;
|
|
152
225
|
}
|
|
153
|
-
|
|
154
|
-
|
|
226
|
+
|
|
227
|
+
targets[currentIndex] = nextTarget;
|
|
155
228
|
}
|
|
156
229
|
|
|
157
230
|
module.exports = {
|
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,
|
|
@@ -40,7 +38,8 @@ const {
|
|
|
40
38
|
writeJson,
|
|
41
39
|
} = require("./util");
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
// A short settle delay keeps interactive CLIs from treating submit as another newline.
|
|
42
|
+
const TYPE_CHUNK_DELAY_MS = 45;
|
|
44
43
|
const TYPE_CHUNK_SIZE = 24;
|
|
45
44
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
46
45
|
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
@@ -91,6 +90,26 @@ function normalizeContinueSeatId(value) {
|
|
|
91
90
|
return seatId || null;
|
|
92
91
|
}
|
|
93
92
|
|
|
93
|
+
function normalizeContinueTargets(value) {
|
|
94
|
+
if (!Array.isArray(value)) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return value
|
|
99
|
+
.map((entry) => {
|
|
100
|
+
const seatId = normalizeSeatId(entry?.seatId);
|
|
101
|
+
if (!seatId) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
seatId,
|
|
107
|
+
flowMode: normalizeFlowMode(entry?.flowMode),
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.filter((entry) => entry !== null);
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
function resolveShell() {
|
|
95
114
|
const shell = String(process.env.SHELL || "").trim();
|
|
96
115
|
return shell || "/bin/bash";
|
|
@@ -168,50 +187,37 @@ function sleepSync(ms) {
|
|
|
168
187
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
function
|
|
172
|
-
const normalizedSeatId = normalizeSeatId(seatId);
|
|
173
|
-
const anchorSeatId = getPartnerSeatId(normalizedSeatId);
|
|
174
|
-
if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
190
|
+
function findExistingSessionName(currentPath = process.cwd()) {
|
|
178
191
|
const candidates = listSessionNames()
|
|
179
192
|
.map((sessionName) => {
|
|
180
193
|
const sessionPaths = getSessionPaths(sessionName);
|
|
181
194
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const anchorStatus = readJson(anchorPaths.statusPath, null);
|
|
186
|
-
const seatMeta = readJson(seatPaths.metaPath, null);
|
|
187
|
-
const seatStatus = readJson(seatPaths.statusPath, null);
|
|
188
|
-
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
195
|
+
const seats = listSeatIds(sessionName)
|
|
196
|
+
.map((seatId) => buildSeatReport(sessionName, seatId))
|
|
197
|
+
.filter((entry) => entry !== null);
|
|
189
198
|
|
|
190
|
-
const cwd = controller?.cwd ||
|
|
199
|
+
const cwd = controller?.cwd || seats[0]?.cwd || null;
|
|
191
200
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
192
201
|
return null;
|
|
193
202
|
}
|
|
194
203
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
|
|
199
|
-
const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
|
|
200
|
-
const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
|
|
201
|
-
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
202
|
-
const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
|
|
203
|
-
|
|
204
|
-
if (!anchorLive || seatLive) {
|
|
204
|
+
const controllerPid = controller?.pid || null;
|
|
205
|
+
const controllerLive = isPidAlive(controllerPid);
|
|
206
|
+
if (seats.length === 0 && !controllerLive) {
|
|
205
207
|
return null;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const createdAtMs = Date.parse(
|
|
211
|
+
controller?.createdAt ||
|
|
212
|
+
seats
|
|
213
|
+
.map((seat) => seat.startedAt || seat.updatedAt || "")
|
|
214
|
+
.find((value) => value) ||
|
|
215
|
+
""
|
|
216
|
+
);
|
|
211
217
|
|
|
212
218
|
return {
|
|
213
219
|
sessionName,
|
|
214
|
-
createdAtMs,
|
|
220
|
+
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
|
|
215
221
|
};
|
|
216
222
|
})
|
|
217
223
|
.filter((entry) => entry !== null)
|
|
@@ -220,10 +226,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
|
|
|
220
226
|
return candidates[0]?.sessionName || null;
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
function
|
|
229
|
+
function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
224
230
|
const deadline = Date.now() + timeoutMs;
|
|
225
231
|
while (Date.now() <= deadline) {
|
|
226
|
-
const sessionName =
|
|
232
|
+
const sessionName = findExistingSessionName(currentPath);
|
|
227
233
|
if (sessionName) {
|
|
228
234
|
return sessionName;
|
|
229
235
|
}
|
|
@@ -234,11 +240,31 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
|
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
const existingSessionName = findExistingSessionName(currentPath);
|
|
244
|
+
if (!existingSessionName) {
|
|
245
|
+
const normalizedSeatId = normalizeSeatId(seatId) || 1;
|
|
246
|
+
const joinWaitMs = Math.min(1000, Math.max(0, normalizedSeatId - 1) * 250);
|
|
247
|
+
const waitedSessionName = waitForExistingSessionName(currentPath, joinWaitMs);
|
|
248
|
+
if (!waitedSessionName) {
|
|
249
|
+
return createSessionName(currentPath);
|
|
250
|
+
}
|
|
251
|
+
const conflictingWaitedSeat = buildSeatReport(waitedSessionName, seatId);
|
|
252
|
+
if (conflictingWaitedSeat) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Seat ${seatId} is already armed in this cwd. Stop it first or choose another seat number.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return waitedSessionName;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const conflictingSeat = buildSeatReport(existingSessionName, seatId);
|
|
261
|
+
if (conflictingSeat) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Seat ${seatId} is already armed in this cwd. Stop it first or choose another seat number.`
|
|
264
|
+
);
|
|
239
265
|
}
|
|
240
266
|
|
|
241
|
-
return
|
|
267
|
+
return existingSessionName;
|
|
242
268
|
}
|
|
243
269
|
|
|
244
270
|
function parseAnswerEntries(text) {
|
|
@@ -440,49 +466,30 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
|
|
|
440
466
|
return null;
|
|
441
467
|
}
|
|
442
468
|
|
|
443
|
-
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
444
|
-
return JSON.stringify({
|
|
445
|
-
type: "muuuuse_pair_claim",
|
|
446
|
-
sessionName,
|
|
447
|
-
challenge,
|
|
448
|
-
seat1PublicKey,
|
|
449
|
-
seat2PublicKey,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
454
|
-
return JSON.stringify({
|
|
455
|
-
type: "muuuuse_pair_ack",
|
|
456
|
-
sessionName,
|
|
457
|
-
challenge,
|
|
458
|
-
seat1PublicKey,
|
|
459
|
-
seat2PublicKey,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
|
|
463
469
|
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
464
470
|
return JSON.stringify({
|
|
465
|
-
type: "
|
|
471
|
+
type: "muuuuse_relay",
|
|
466
472
|
sessionName,
|
|
467
|
-
challenge,
|
|
468
473
|
chainId: entry.chainId,
|
|
469
474
|
hop: entry.hop,
|
|
470
475
|
id: entry.id,
|
|
471
|
-
|
|
476
|
+
sourceSeatId: normalizeSeatId(entry.sourceSeatId || entry.seatId),
|
|
477
|
+
targetSeatId: normalizeSeatId(entry.targetSeatId),
|
|
472
478
|
origin: entry.origin,
|
|
473
|
-
phase: entry
|
|
479
|
+
phase: getRelayPhase(entry),
|
|
474
480
|
createdAt: entry.createdAt,
|
|
475
481
|
text: entry.text,
|
|
476
482
|
});
|
|
477
483
|
}
|
|
478
484
|
|
|
479
|
-
function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
|
|
485
|
+
function buildContinuationEntry(sourceSessionName, targetSeatId, entry, targetFlowMode = null) {
|
|
480
486
|
return {
|
|
481
487
|
id: createId(12),
|
|
482
488
|
type: "continue",
|
|
483
489
|
sourceSessionName,
|
|
484
490
|
sourceSeatId: entry.seatId,
|
|
485
491
|
targetSeatId,
|
|
492
|
+
targetFlowMode: normalizeFlowMode(targetFlowMode),
|
|
486
493
|
origin: entry.origin || "unknown",
|
|
487
494
|
phase: entry.phase || "final_answer",
|
|
488
495
|
text: entry.text,
|
|
@@ -516,24 +523,6 @@ function getSeatDirIfExists(sessionName, seatId) {
|
|
|
516
523
|
return null;
|
|
517
524
|
}
|
|
518
525
|
|
|
519
|
-
function readSeatChallenge(paths, sessionName) {
|
|
520
|
-
const record = readJson(paths.challengePath, null);
|
|
521
|
-
if (
|
|
522
|
-
!record ||
|
|
523
|
-
record.sessionName !== sessionName ||
|
|
524
|
-
typeof record.challenge !== "string" ||
|
|
525
|
-
typeof record.publicKey !== "string"
|
|
526
|
-
) {
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
challenge: record.challenge,
|
|
532
|
-
publicKey: record.publicKey.trim(),
|
|
533
|
-
createdAt: record.createdAt || null,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
526
|
function normalizeRelayPayloadForTyping(text) {
|
|
538
527
|
return String(text || "")
|
|
539
528
|
.replace(/\r/g, "")
|
|
@@ -599,7 +588,7 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
599
588
|
}
|
|
600
589
|
|
|
601
590
|
try {
|
|
602
|
-
child.write("\
|
|
591
|
+
child.write("\r");
|
|
603
592
|
} catch {
|
|
604
593
|
return false;
|
|
605
594
|
}
|
|
@@ -610,25 +599,20 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
610
599
|
class ArmedSeat {
|
|
611
600
|
constructor(options) {
|
|
612
601
|
this.seatId = options.seatId;
|
|
613
|
-
this.partnerSeatId = getPartnerSeatId(options.seatId);
|
|
614
|
-
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
615
602
|
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
616
603
|
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
604
|
+
this.continueTargets = normalizeContinueTargets(options.continueTargets);
|
|
617
605
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
618
606
|
if (this.continueSeatId === this.seatId) {
|
|
619
607
|
throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
|
|
620
608
|
}
|
|
621
|
-
this.
|
|
622
|
-
|
|
623
|
-
throw new Error(
|
|
624
|
-
`No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
|
|
625
|
-
);
|
|
609
|
+
if (this.continueTargets.some((target) => target.seatId === this.seatId)) {
|
|
610
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot link to itself.`);
|
|
626
611
|
}
|
|
612
|
+
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
627
613
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
628
614
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
629
|
-
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
630
615
|
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
631
|
-
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
632
616
|
|
|
633
617
|
this.child = null;
|
|
634
618
|
this.childPid = null;
|
|
@@ -641,17 +625,11 @@ class ArmedSeat {
|
|
|
641
625
|
this.stdinCleanup = null;
|
|
642
626
|
this.resizeCleanup = null;
|
|
643
627
|
this.forceKillTimer = null;
|
|
644
|
-
this.identity =
|
|
628
|
+
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
645
629
|
this.lastUserInputAtMs = 0;
|
|
646
630
|
this.pendingInboundContext = null;
|
|
647
631
|
this.recentInboundRelays = [];
|
|
648
632
|
this.recentEmittedAnswers = [];
|
|
649
|
-
this.trustState = {
|
|
650
|
-
challenge: null,
|
|
651
|
-
peerPublicKey: null,
|
|
652
|
-
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
653
|
-
pairedAt: null,
|
|
654
|
-
};
|
|
655
633
|
this.liveState = {
|
|
656
634
|
type: null,
|
|
657
635
|
pid: null,
|
|
@@ -672,11 +650,7 @@ class ArmedSeat {
|
|
|
672
650
|
cwd: this.cwd,
|
|
673
651
|
createdAt: current.createdAt || this.startedAt,
|
|
674
652
|
updatedAt: new Date().toISOString(),
|
|
675
|
-
|
|
676
|
-
partnerSeatId: this.partnerSeatId,
|
|
677
|
-
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
678
|
-
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
679
|
-
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
653
|
+
pid: process.pid,
|
|
680
654
|
...extra,
|
|
681
655
|
});
|
|
682
656
|
}
|
|
@@ -688,13 +662,14 @@ class ArmedSeat {
|
|
|
688
662
|
writeMeta(extra = {}) {
|
|
689
663
|
writeJson(this.paths.metaPath, {
|
|
690
664
|
seatId: this.seatId,
|
|
691
|
-
partnerSeatId: this.partnerSeatId,
|
|
692
665
|
sessionName: this.sessionName,
|
|
693
666
|
flowMode: this.flowMode,
|
|
694
667
|
continueSeatId: this.continueSeatId,
|
|
668
|
+
continueTargets: this.continueTargets,
|
|
695
669
|
cwd: this.cwd,
|
|
696
670
|
pid: process.pid,
|
|
697
671
|
childPid: this.childPid,
|
|
672
|
+
publicKey: this.identity?.publicKey || null,
|
|
698
673
|
command: [resolveShell(), ...resolveShellArgs(resolveShell())],
|
|
699
674
|
startedAt: this.startedAt,
|
|
700
675
|
...extra,
|
|
@@ -704,190 +679,24 @@ class ArmedSeat {
|
|
|
704
679
|
writeStatus(extra = {}) {
|
|
705
680
|
writeJson(this.paths.statusPath, {
|
|
706
681
|
seatId: this.seatId,
|
|
707
|
-
partnerSeatId: this.partnerSeatId,
|
|
708
682
|
sessionName: this.sessionName,
|
|
709
683
|
flowMode: this.flowMode,
|
|
710
684
|
continueSeatId: this.continueSeatId,
|
|
685
|
+
continueTargets: this.continueTargets,
|
|
711
686
|
cwd: this.cwd,
|
|
712
687
|
pid: process.pid,
|
|
713
688
|
childPid: this.childPid,
|
|
689
|
+
publicKey: this.identity?.publicKey || null,
|
|
714
690
|
relayCount: this.relayCount,
|
|
715
691
|
updatedAt: new Date().toISOString(),
|
|
716
692
|
...extra,
|
|
717
693
|
});
|
|
718
694
|
}
|
|
719
695
|
|
|
720
|
-
initializeTrustMaterial() {
|
|
721
|
-
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
722
|
-
|
|
723
|
-
if (!isAnchorSeat(this.seatId)) {
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
writeJson(this.paths.challengePath, {
|
|
728
|
-
sessionName: this.sessionName,
|
|
729
|
-
challenge: createId(48),
|
|
730
|
-
publicKey: this.identity.publicKey,
|
|
731
|
-
createdAt: new Date().toISOString(),
|
|
732
|
-
});
|
|
733
|
-
this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
|
|
734
|
-
this.trustState.peerPublicKey = null;
|
|
735
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
736
|
-
this.trustState.pairedAt = null;
|
|
737
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
738
|
-
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
syncTrustState() {
|
|
742
|
-
if (!this.identity) {
|
|
743
|
-
this.initializeTrustMaterial();
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (isAnchorSeat(this.seatId)) {
|
|
747
|
-
this.syncSeatOneTrust();
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
this.syncSeatTwoTrust();
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
syncSeatOneTrust() {
|
|
755
|
-
const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
|
|
756
|
-
if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
|
|
757
|
-
this.trustState = {
|
|
758
|
-
challenge: null,
|
|
759
|
-
peerPublicKey: null,
|
|
760
|
-
phase: "waiting_for_peer_signature",
|
|
761
|
-
pairedAt: null,
|
|
762
|
-
};
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
this.trustState.challenge = challengeRecord.challenge;
|
|
767
|
-
const claim = readJson(this.partnerPaths.claimPath, null);
|
|
768
|
-
if (
|
|
769
|
-
!claim ||
|
|
770
|
-
claim.sessionName !== this.sessionName ||
|
|
771
|
-
claim.challenge !== challengeRecord.challenge ||
|
|
772
|
-
typeof claim.publicKey !== "string" ||
|
|
773
|
-
typeof claim.signature !== "string" ||
|
|
774
|
-
!verifyText(
|
|
775
|
-
buildClaimMessage(
|
|
776
|
-
this.sessionName,
|
|
777
|
-
challengeRecord.challenge,
|
|
778
|
-
this.identity.publicKey,
|
|
779
|
-
claim.publicKey.trim()
|
|
780
|
-
),
|
|
781
|
-
claim.signature,
|
|
782
|
-
claim.publicKey
|
|
783
|
-
)
|
|
784
|
-
) {
|
|
785
|
-
this.trustState.peerPublicKey = null;
|
|
786
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
787
|
-
this.trustState.pairedAt = null;
|
|
788
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const peerPublicKey = claim.publicKey.trim();
|
|
793
|
-
const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
|
|
794
|
-
const currentAck = readJson(this.paths.ackPath, null);
|
|
795
|
-
const ackIsValid = Boolean(
|
|
796
|
-
currentAck &&
|
|
797
|
-
currentAck.sessionName === this.sessionName &&
|
|
798
|
-
currentAck.challenge === challengeRecord.challenge &&
|
|
799
|
-
currentAck.publicKey === this.identity.publicKey &&
|
|
800
|
-
currentAck.peerPublicKey === peerPublicKey &&
|
|
801
|
-
typeof currentAck.signature === "string" &&
|
|
802
|
-
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
803
|
-
);
|
|
804
|
-
if (!ackIsValid) {
|
|
805
|
-
writeJson(this.paths.ackPath, {
|
|
806
|
-
sessionName: this.sessionName,
|
|
807
|
-
challenge: challengeRecord.challenge,
|
|
808
|
-
publicKey: this.identity.publicKey,
|
|
809
|
-
peerPublicKey,
|
|
810
|
-
signature: signText(ackMessage, this.identity.privateKey),
|
|
811
|
-
signedAt: new Date().toISOString(),
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
|
|
816
|
-
this.trustState.peerPublicKey = peerPublicKey;
|
|
817
|
-
this.trustState.phase = "paired";
|
|
818
|
-
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
syncSeatTwoTrust() {
|
|
822
|
-
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
823
|
-
if (!challengeRecord) {
|
|
824
|
-
this.trustState = {
|
|
825
|
-
challenge: null,
|
|
826
|
-
peerPublicKey: null,
|
|
827
|
-
phase: "waiting_for_anchor_key",
|
|
828
|
-
pairedAt: null,
|
|
829
|
-
};
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const challenge = challengeRecord.challenge;
|
|
834
|
-
const peerPublicKey = challengeRecord.publicKey;
|
|
835
|
-
const claimPayload = {
|
|
836
|
-
sessionName: this.sessionName,
|
|
837
|
-
challenge,
|
|
838
|
-
publicKey: this.identity.publicKey,
|
|
839
|
-
};
|
|
840
|
-
const claimSignature = signText(
|
|
841
|
-
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
842
|
-
this.identity.privateKey
|
|
843
|
-
);
|
|
844
|
-
const currentClaim = readJson(this.paths.claimPath, null);
|
|
845
|
-
if (
|
|
846
|
-
!currentClaim ||
|
|
847
|
-
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
848
|
-
currentClaim.challenge !== claimPayload.challenge ||
|
|
849
|
-
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
850
|
-
currentClaim.signature !== claimSignature
|
|
851
|
-
) {
|
|
852
|
-
writeJson(this.paths.claimPath, {
|
|
853
|
-
...claimPayload,
|
|
854
|
-
signature: claimSignature,
|
|
855
|
-
signedAt: new Date().toISOString(),
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const ack = readJson(this.partnerPaths.ackPath, null);
|
|
860
|
-
const paired = Boolean(
|
|
861
|
-
ack &&
|
|
862
|
-
ack.sessionName === this.sessionName &&
|
|
863
|
-
ack.challenge === challenge &&
|
|
864
|
-
ack.peerPublicKey === this.identity.publicKey &&
|
|
865
|
-
ack.publicKey === peerPublicKey &&
|
|
866
|
-
typeof ack.signature === "string" &&
|
|
867
|
-
verifyText(
|
|
868
|
-
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
869
|
-
ack.signature,
|
|
870
|
-
peerPublicKey
|
|
871
|
-
)
|
|
872
|
-
);
|
|
873
|
-
|
|
874
|
-
this.trustState.challenge = challenge;
|
|
875
|
-
this.trustState.peerPublicKey = peerPublicKey;
|
|
876
|
-
this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
|
|
877
|
-
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
isPaired() {
|
|
881
|
-
return this.trustState.phase === "paired" &&
|
|
882
|
-
typeof this.trustState.challenge === "string" &&
|
|
883
|
-
typeof this.trustState.peerPublicKey === "string";
|
|
884
|
-
}
|
|
885
|
-
|
|
886
696
|
launchShell() {
|
|
887
697
|
ensureDir(this.paths.dir);
|
|
888
698
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
889
699
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
890
|
-
this.initializeTrustMaterial();
|
|
891
700
|
this.writeController();
|
|
892
701
|
|
|
893
702
|
const shell = resolveShell();
|
|
@@ -904,7 +713,7 @@ class ArmedSeat {
|
|
|
904
713
|
|
|
905
714
|
this.childPid = this.child.pid;
|
|
906
715
|
this.writeMeta();
|
|
907
|
-
this.writeStatus({ state: "running"
|
|
716
|
+
this.writeStatus({ state: "running" });
|
|
908
717
|
|
|
909
718
|
this.child.onData((data) => {
|
|
910
719
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
@@ -1038,9 +847,19 @@ class ArmedSeat {
|
|
|
1038
847
|
}
|
|
1039
848
|
}
|
|
1040
849
|
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1043
|
-
|
|
850
|
+
getConfiguredTargets() {
|
|
851
|
+
const targets = [...this.continueTargets];
|
|
852
|
+
if (this.continueSeatId && !targets.some((target) => target.seatId === this.continueSeatId)) {
|
|
853
|
+
targets.push({
|
|
854
|
+
seatId: this.continueSeatId,
|
|
855
|
+
flowMode: this.flowMode,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
return targets;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
shouldCaptureCommentary() {
|
|
862
|
+
return this.getConfiguredTargets().some((target) => target.flowMode === "on");
|
|
1044
863
|
}
|
|
1045
864
|
|
|
1046
865
|
stopRequested() {
|
|
@@ -1053,109 +872,80 @@ class ArmedSeat {
|
|
|
1053
872
|
return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
|
|
1054
873
|
}
|
|
1055
874
|
|
|
1056
|
-
|
|
1057
|
-
|
|
875
|
+
hasAuthorizedSource(sourceSeatId) {
|
|
876
|
+
const desiredSeatId = normalizeSeatId(sourceSeatId);
|
|
877
|
+
if (!desiredSeatId) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return this.getConfiguredTargets().some((target) => target.seatId === desiredSeatId);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
readSourcePublicKey(sourceSeatId) {
|
|
885
|
+
const desiredSeatId = normalizeSeatId(sourceSeatId);
|
|
886
|
+
if (!desiredSeatId) {
|
|
1058
887
|
return null;
|
|
1059
888
|
}
|
|
1060
889
|
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
890
|
+
const sourcePaths = getSeatPaths(this.sessionName, desiredSeatId);
|
|
891
|
+
const sourceMeta = readJson(sourcePaths.metaPath, null);
|
|
892
|
+
if (typeof sourceMeta?.publicKey === "string" && sourceMeta.publicKey.trim()) {
|
|
893
|
+
return sourceMeta.publicKey.trim();
|
|
894
|
+
}
|
|
1066
895
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
896
|
+
try {
|
|
897
|
+
const key = fs.readFileSync(sourcePaths.publicKeyPath, "utf8").trim();
|
|
898
|
+
return key || null;
|
|
899
|
+
} catch {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
1071
903
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
};
|
|
1078
|
-
})
|
|
1079
|
-
.filter((entry) => entry !== null)
|
|
1080
|
-
.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
904
|
+
findLinkedTarget(targetSeatId) {
|
|
905
|
+
const desiredSeatId = normalizeContinueSeatId(targetSeatId);
|
|
906
|
+
if (!desiredSeatId) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
1081
909
|
|
|
1082
|
-
|
|
910
|
+
const seat = buildSeatReport(this.sessionName, desiredSeatId);
|
|
911
|
+
if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
|
|
1083
912
|
return null;
|
|
1084
913
|
}
|
|
1085
914
|
|
|
1086
|
-
const target = candidates[0];
|
|
1087
915
|
return {
|
|
1088
|
-
seatId:
|
|
1089
|
-
|
|
1090
|
-
paths: getSeatPaths(target.sessionName, target.seat.seatId),
|
|
916
|
+
seatId: seat.seatId,
|
|
917
|
+
paths: getSeatPaths(this.sessionName, seat.seatId),
|
|
1091
918
|
};
|
|
1092
919
|
}
|
|
1093
920
|
|
|
1094
|
-
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
921
|
+
verifyInboundEntry(entry) {
|
|
922
|
+
const sourceSeatId = normalizeSeatId(entry?.sourceSeatId || entry?.seatId);
|
|
923
|
+
const targetSeatId = normalizeSeatId(entry?.targetSeatId);
|
|
924
|
+
const payload = sanitizeRelayText(entry?.text);
|
|
925
|
+
if (!sourceSeatId || targetSeatId !== this.seatId || !payload || !this.hasAuthorizedSource(sourceSeatId)) {
|
|
926
|
+
return false;
|
|
1099
927
|
}
|
|
1100
928
|
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
|
|
1109
|
-
continue;
|
|
1110
|
-
}
|
|
929
|
+
const publicKey = this.readSourcePublicKey(sourceSeatId);
|
|
930
|
+
if (!publicKey || entry.publicKey !== publicKey || typeof entry.signature !== "string") {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
1111
933
|
|
|
1112
|
-
|
|
1113
|
-
|
|
934
|
+
return verifyText(
|
|
935
|
+
buildAnswerSignaturePayload(this.sessionName, null, {
|
|
936
|
+
id: entry.id,
|
|
937
|
+
sourceSeatId,
|
|
938
|
+
targetSeatId,
|
|
1114
939
|
chainId: entry.chainId || entry.id,
|
|
1115
940
|
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1116
|
-
id: entry.id,
|
|
1117
|
-
seatId: entry.seatId,
|
|
1118
941
|
origin: entry.origin || "unknown",
|
|
1119
942
|
phase: getRelayPhase(entry),
|
|
1120
943
|
createdAt: entry.createdAt,
|
|
1121
944
|
text: payload,
|
|
1122
|
-
})
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
1127
|
-
typeof entry.signature !== "string" ||
|
|
1128
|
-
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
1129
|
-
) {
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const delivered = await sendTextAndEnter(
|
|
1134
|
-
this.child,
|
|
1135
|
-
payload,
|
|
1136
|
-
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1137
|
-
);
|
|
1138
|
-
if (!delivered) {
|
|
1139
|
-
this.requestStop("relay_aborted");
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (this.stopped || this.stopRequested()) {
|
|
1144
|
-
this.requestStop("stop_requested");
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const deliveredAtMs = Date.now();
|
|
1149
|
-
this.pendingInboundContext = {
|
|
1150
|
-
chainId: entry.chainId || entry.id,
|
|
1151
|
-
deliveredAtMs,
|
|
1152
|
-
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1153
|
-
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1154
|
-
};
|
|
1155
|
-
this.relayCount += 1;
|
|
1156
|
-
this.rememberInboundRelay(payload);
|
|
1157
|
-
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
1158
|
-
}
|
|
945
|
+
}),
|
|
946
|
+
entry.signature,
|
|
947
|
+
publicKey
|
|
948
|
+
);
|
|
1159
949
|
}
|
|
1160
950
|
|
|
1161
951
|
async pullContinuationEvents() {
|
|
@@ -1172,7 +962,7 @@ class ArmedSeat {
|
|
|
1172
962
|
return;
|
|
1173
963
|
}
|
|
1174
964
|
|
|
1175
|
-
if (!
|
|
965
|
+
if (!this.verifyInboundEntry(entry)) {
|
|
1176
966
|
continue;
|
|
1177
967
|
}
|
|
1178
968
|
|
|
@@ -1389,7 +1179,7 @@ class ArmedSeat {
|
|
|
1389
1179
|
this.liveState.sessionFile,
|
|
1390
1180
|
this.liveState.offset,
|
|
1391
1181
|
this.liveState.captureSinceMs,
|
|
1392
|
-
{ flowMode: this.
|
|
1182
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1393
1183
|
);
|
|
1394
1184
|
this.liveState.offset = result.nextOffset;
|
|
1395
1185
|
answers.push(...result.answers);
|
|
@@ -1398,7 +1188,7 @@ class ArmedSeat {
|
|
|
1398
1188
|
this.liveState.sessionFile,
|
|
1399
1189
|
this.liveState.offset,
|
|
1400
1190
|
this.liveState.captureSinceMs,
|
|
1401
|
-
{ flowMode: this.
|
|
1191
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1402
1192
|
);
|
|
1403
1193
|
this.liveState.offset = result.nextOffset;
|
|
1404
1194
|
answers.push(...result.answers);
|
|
@@ -1407,7 +1197,7 @@ class ArmedSeat {
|
|
|
1407
1197
|
this.liveState.sessionFile,
|
|
1408
1198
|
this.liveState.lastMessageId,
|
|
1409
1199
|
this.liveState.captureSinceMs,
|
|
1410
|
-
{ flowMode: this.
|
|
1200
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1411
1201
|
);
|
|
1412
1202
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
1413
1203
|
this.liveState.offset = result.fileSize;
|
|
@@ -1440,7 +1230,7 @@ class ArmedSeat {
|
|
|
1440
1230
|
}
|
|
1441
1231
|
|
|
1442
1232
|
const payload = sanitizeRelayText(entry.text);
|
|
1443
|
-
if (!payload
|
|
1233
|
+
if (!payload) {
|
|
1444
1234
|
return;
|
|
1445
1235
|
}
|
|
1446
1236
|
|
|
@@ -1459,59 +1249,66 @@ class ArmedSeat {
|
|
|
1459
1249
|
const pendingInboundContext = this.getPendingInboundContext();
|
|
1460
1250
|
|
|
1461
1251
|
const entryId = entry.id || createId(12);
|
|
1462
|
-
const
|
|
1252
|
+
const relayEntry = {
|
|
1463
1253
|
id: entryId,
|
|
1464
1254
|
type: "answer",
|
|
1465
1255
|
seatId: this.seatId,
|
|
1256
|
+
sourceSeatId: this.seatId,
|
|
1466
1257
|
origin: entry.origin || "unknown",
|
|
1467
1258
|
phase: entry.phase || "final_answer",
|
|
1468
1259
|
text: payload,
|
|
1469
1260
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1470
1261
|
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
1471
1262
|
hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
|
|
1472
|
-
challenge: this.trustState.challenge,
|
|
1473
|
-
publicKey: this.identity.publicKey,
|
|
1474
1263
|
};
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
);
|
|
1479
|
-
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1480
|
-
this.forwardContinuation(signedEntry);
|
|
1264
|
+
|
|
1265
|
+
appendJsonl(this.paths.eventsPath, relayEntry);
|
|
1266
|
+
this.forwardContinuation(relayEntry);
|
|
1481
1267
|
this.rememberEmittedAnswer(answerKey);
|
|
1482
1268
|
|
|
1483
1269
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1484
1270
|
}
|
|
1485
1271
|
|
|
1486
|
-
forwardContinuation(
|
|
1487
|
-
|
|
1272
|
+
forwardContinuation(relayEntry) {
|
|
1273
|
+
const targets = this.getConfiguredTargets();
|
|
1274
|
+
if (targets.length === 0) {
|
|
1488
1275
|
return;
|
|
1489
1276
|
}
|
|
1490
1277
|
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1278
|
+
for (const targetEntry of targets) {
|
|
1279
|
+
if (!shouldAcceptInboundEntry(targetEntry.flowMode, relayEntry)) {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const target = this.findLinkedTarget(targetEntry.seatId);
|
|
1284
|
+
if (!target) {
|
|
1285
|
+
this.log(`[${this.seatId}] link ${targetEntry.seatId} unavailable`);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1496
1288
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1289
|
+
const continuationEntry = buildContinuationEntry(
|
|
1290
|
+
this.sessionName,
|
|
1291
|
+
target.seatId,
|
|
1292
|
+
relayEntry,
|
|
1293
|
+
targetEntry.flowMode
|
|
1294
|
+
);
|
|
1295
|
+
continuationEntry.publicKey = this.identity.publicKey;
|
|
1296
|
+
continuationEntry.signature = signText(
|
|
1297
|
+
buildAnswerSignaturePayload(this.sessionName, null, continuationEntry),
|
|
1298
|
+
this.identity.privateKey
|
|
1299
|
+
);
|
|
1300
|
+
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1301
|
+
this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
|
|
1302
|
+
}
|
|
1500
1303
|
}
|
|
1501
1304
|
|
|
1502
1305
|
async tick() {
|
|
1503
1306
|
if (this.stopRequested()) {
|
|
1504
|
-
this.writeStatus({
|
|
1505
|
-
state: "stopping",
|
|
1506
|
-
partnerLive: this.partnerIsLive(),
|
|
1507
|
-
trust: this.trustState.phase,
|
|
1508
|
-
});
|
|
1307
|
+
this.writeStatus({ state: "stopping" });
|
|
1509
1308
|
this.requestStop("stop_requested");
|
|
1510
1309
|
return;
|
|
1511
1310
|
}
|
|
1512
1311
|
|
|
1513
|
-
this.syncTrustState();
|
|
1514
|
-
await this.pullPartnerEvents();
|
|
1515
1312
|
await this.pullContinuationEvents();
|
|
1516
1313
|
if (this.stopped || this.stopRequested()) {
|
|
1517
1314
|
this.requestStop("stop_requested");
|
|
@@ -1530,9 +1327,6 @@ class ArmedSeat {
|
|
|
1530
1327
|
cwd: live.cwd,
|
|
1531
1328
|
log: live.log,
|
|
1532
1329
|
lastAnswerAt: live.lastAnswerAt,
|
|
1533
|
-
partnerLive: this.partnerIsLive(),
|
|
1534
|
-
trust: this.trustState.phase,
|
|
1535
|
-
challengeReady: Boolean(this.trustState.challenge),
|
|
1536
1330
|
});
|
|
1537
1331
|
}
|
|
1538
1332
|
|
|
@@ -1544,15 +1338,17 @@ class ArmedSeat {
|
|
|
1544
1338
|
|
|
1545
1339
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1546
1340
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1547
|
-
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1341
|
+
this.log(`Seat ${this.seatId} default relay mode is flow ${this.flowMode}.`);
|
|
1548
1342
|
if (this.continueSeatId) {
|
|
1549
1343
|
this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
|
|
1550
1344
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1345
|
+
const configuredTargets = this.getConfiguredTargets();
|
|
1346
|
+
if (configuredTargets.length > 0) {
|
|
1347
|
+
this.log(
|
|
1348
|
+
`Seat ${this.seatId} links signed relay targets: ${configuredTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
|
|
1349
|
+
);
|
|
1555
1350
|
}
|
|
1351
|
+
this.log("Signed relays are accepted only from seats that this seat links back to.");
|
|
1556
1352
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1557
1353
|
|
|
1558
1354
|
try {
|
|
@@ -1650,6 +1446,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1650
1446
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1651
1447
|
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1652
1448
|
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1449
|
+
continueTargets: normalizeContinueTargets(status?.continueTargets || meta?.continueTargets),
|
|
1653
1450
|
wrapperPid,
|
|
1654
1451
|
childPid,
|
|
1655
1452
|
wrapperLive,
|
|
@@ -1660,10 +1457,8 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1660
1457
|
relayCount: status?.relayCount || 0,
|
|
1661
1458
|
log: status?.log || null,
|
|
1662
1459
|
startedAt: meta?.startedAt || null,
|
|
1663
|
-
trust: status?.trust || null,
|
|
1664
1460
|
updatedAt: status?.updatedAt || null,
|
|
1665
1461
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
1666
|
-
partnerLive: Boolean(status?.partnerLive),
|
|
1667
1462
|
};
|
|
1668
1463
|
}
|
|
1669
1464
|
|
package/src/util.js
CHANGED
|
@@ -175,18 +175,6 @@ function normalizeSeatId(value) {
|
|
|
175
175
|
return seatId;
|
|
176
176
|
}
|
|
177
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
178
|
function listSeatIds(sessionName) {
|
|
191
179
|
const sessionDir = getSessionDir(sessionName);
|
|
192
180
|
try {
|
|
@@ -282,17 +270,19 @@ function listSessionNames() {
|
|
|
282
270
|
|
|
283
271
|
function usage() {
|
|
284
272
|
return [
|
|
285
|
-
`${BRAND} arms regular terminals
|
|
273
|
+
`${BRAND} arms regular terminals and relays assistant output across signed terminal links.`,
|
|
286
274
|
"",
|
|
287
275
|
"Usage:",
|
|
288
276
|
" muuuuse 1",
|
|
289
277
|
" muuuuse 1 flow on",
|
|
290
278
|
" muuuuse 1 flow off",
|
|
291
279
|
" muuuuse 1 flow on continue 3",
|
|
280
|
+
" muuuuse 1 link 2 flow on 3 flow off 5 flow off",
|
|
292
281
|
" muuuuse 2",
|
|
293
282
|
" muuuuse 2 flow on",
|
|
294
283
|
" muuuuse 2 flow off",
|
|
295
284
|
" muuuuse 2 flow on continue 3",
|
|
285
|
+
" muuuuse 2 link 3 flow off",
|
|
296
286
|
" muuuuse 3",
|
|
297
287
|
" muuuuse 4",
|
|
298
288
|
" muuuuse 4 flow on continue 1",
|
|
@@ -300,14 +290,14 @@ function usage() {
|
|
|
300
290
|
" muuuuse status",
|
|
301
291
|
"",
|
|
302
292
|
"Flow:",
|
|
303
|
-
" 1. Run `muuuuse
|
|
304
|
-
" 2.
|
|
305
|
-
" 3.
|
|
306
|
-
" 4.
|
|
307
|
-
" 5.
|
|
308
|
-
" 6.
|
|
309
|
-
" 7.
|
|
310
|
-
" 8.
|
|
293
|
+
" 1. Run `muuuuse <seat>` in the terminal you want to arm.",
|
|
294
|
+
" 2. All armed seats in the same cwd join one relay graph.",
|
|
295
|
+
" 3. Use `link <seat> flow on|off ...` to define each outbound relay edge.",
|
|
296
|
+
" 4. `flow on` sends commentary and final answers on that edge. `flow off` sends final answers only.",
|
|
297
|
+
" 5. `continue <seat>` is shorthand for one outbound link that uses the seat's default `flow on|off`.",
|
|
298
|
+
" 6. Every forwarded relay is signed with the sender seat's key.",
|
|
299
|
+
" 7. A seat only accepts signed inbound relays from seats it links back to.",
|
|
300
|
+
" 8. Use those armed shells normally.",
|
|
311
301
|
" 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
312
302
|
"",
|
|
313
303
|
"Notes:",
|
|
@@ -325,9 +315,7 @@ module.exports = {
|
|
|
325
315
|
ensureDir,
|
|
326
316
|
getDefaultSessionName,
|
|
327
317
|
getFileSize,
|
|
328
|
-
getPartnerSeatId,
|
|
329
318
|
loadOrCreateSeatIdentity,
|
|
330
|
-
isAnchorSeat,
|
|
331
319
|
getSeatPaths,
|
|
332
320
|
getSessionPaths,
|
|
333
321
|
getStateRoot,
|