muuuuse 5.5.4 → 7.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/package.json +3 -4
- package/src/agents.js +4 -2
- package/src/cli.js +51 -82
- package/src/runtime.js +170 -344
- package/src/util.js +12 -33
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "🔌Muuuuse
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "🔌Muuuuse relay protocol for long-horizon zero-drift agentic code loops. Any seat relays to any other with per-target signed trust and flow control.",
|
|
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
|
@@ -468,7 +468,9 @@ function parseCodexAssistantLine(line, options = {}) {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
const phase = String(entry.payload?.phase || "").trim().toLowerCase();
|
|
471
|
-
|
|
471
|
+
// Newer Codex sessions can omit `payload.phase` for final answers.
|
|
472
|
+
const normalizedPhase = phase === "commentary" ? "commentary" : "final_answer";
|
|
473
|
+
const relayablePhase = normalizedPhase === "final_answer" || (flowMode && normalizedPhase === "commentary");
|
|
472
474
|
if (!relayablePhase) {
|
|
473
475
|
return null;
|
|
474
476
|
}
|
|
@@ -481,7 +483,7 @@ function parseCodexAssistantLine(line, options = {}) {
|
|
|
481
483
|
return {
|
|
482
484
|
id: entry.payload.id || hashText(line),
|
|
483
485
|
text,
|
|
484
|
-
phase:
|
|
486
|
+
phase: normalizedPhase,
|
|
485
487
|
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
486
488
|
};
|
|
487
489
|
} catch {
|
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,12 +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
|
-
continueSeatId,
|
|
64
|
-
flowMode,
|
|
65
63
|
seatId,
|
|
66
64
|
});
|
|
67
65
|
const code = await seat.run();
|
|
@@ -75,15 +73,11 @@ function renderSeatStatus(seat) {
|
|
|
75
73
|
const bits = [
|
|
76
74
|
`seat ${seat.seatId}: ${seat.state}`,
|
|
77
75
|
`agent ${seat.agent || "idle"}`,
|
|
78
|
-
`flow ${seat.flowMode || "off"}`,
|
|
79
76
|
`relays ${seat.relayCount}`,
|
|
80
77
|
`wrapper ${seat.wrapperPid || "-"}`,
|
|
81
78
|
`child ${seat.childPid || "-"}`,
|
|
82
79
|
];
|
|
83
80
|
|
|
84
|
-
if (seat.partnerLive) {
|
|
85
|
-
bits.push("peer live");
|
|
86
|
-
}
|
|
87
81
|
const renderedLinks = renderLinkTargets(seat);
|
|
88
82
|
if (renderedLinks) {
|
|
89
83
|
bits.push(`link ${renderedLinks}`);
|
|
@@ -106,74 +100,77 @@ function renderSeatStatus(seat) {
|
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
function renderLinkTargets(seat) {
|
|
109
|
-
const targets = [];
|
|
110
|
-
if (seat.partnerSeatId) {
|
|
111
|
-
targets.push({
|
|
112
|
-
targetSeatId: seat.partnerSeatId,
|
|
113
|
-
flowMode: seat.flowMode || "off",
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
for (const target of Array.isArray(seat.continueTargets) ? seat.continueTargets : []) {
|
|
117
|
-
targets.push(target);
|
|
118
|
-
}
|
|
119
|
-
|
|
103
|
+
const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
|
|
120
104
|
return targets
|
|
121
|
-
.map((target) => `${target.targetSeatId}
|
|
105
|
+
.map((target) => `${target.targetSeatId} (flow ${target.flowMode})`)
|
|
122
106
|
.join(", ");
|
|
123
107
|
}
|
|
124
108
|
|
|
125
109
|
function parseSeatOptions(command, args) {
|
|
126
110
|
const seatId = normalizeSeatId(command);
|
|
127
|
-
let flowMode = "off";
|
|
128
|
-
let continueSeatId = null;
|
|
129
111
|
let continueTargets = [];
|
|
130
112
|
let index = 0;
|
|
113
|
+
let seatFlowMode = null;
|
|
114
|
+
let hasExplicitTarget = false;
|
|
115
|
+
|
|
116
|
+
const flowToken = String(args[index] || "").trim().toLowerCase();
|
|
117
|
+
if (flowToken === "flow") {
|
|
118
|
+
const parsedSeatFlow = parseFlowModeToken("flow", args[index + 1]);
|
|
119
|
+
if (!parsedSeatFlow) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`\`muuuuse ${command} flow\` requires \`on\` or \`off\`.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
seatFlowMode = parsedSeatFlow;
|
|
125
|
+
index += 2;
|
|
126
|
+
}
|
|
131
127
|
|
|
132
128
|
while (index < args.length) {
|
|
133
129
|
const token = String(args[index] || "").trim().toLowerCase();
|
|
134
130
|
|
|
135
|
-
if (token === "flow") {
|
|
136
|
-
const flowToken = String(args[index + 1] || "").trim().toLowerCase();
|
|
137
|
-
if (flowToken === "on" || flowToken === "off") {
|
|
138
|
-
flowMode = flowToken;
|
|
139
|
-
index += 2;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (token === "continue") {
|
|
146
|
-
const parsedTargets = parseContinueTargets(args.slice(index + 1), flowMode);
|
|
147
|
-
if (parsedTargets.targets.length > 0) {
|
|
148
|
-
continueTargets = mergeTargets(continueTargets, parsedTargets.targets);
|
|
149
|
-
continueSeatId = continueTargets[0].targetSeatId;
|
|
150
|
-
index += 1 + parsedTargets.consumed;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
131
|
if (token === "link") {
|
|
157
|
-
const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId
|
|
132
|
+
const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId);
|
|
158
133
|
if (parsedLinks.consumed > 0) {
|
|
159
|
-
flowMode = parsedLinks.flowMode;
|
|
160
134
|
continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
|
|
161
|
-
|
|
135
|
+
hasExplicitTarget = true;
|
|
162
136
|
index += 1 + parsedLinks.consumed;
|
|
163
137
|
continue;
|
|
164
138
|
}
|
|
165
139
|
break;
|
|
166
140
|
}
|
|
167
141
|
|
|
142
|
+
if (token === "continue") {
|
|
143
|
+
const targetSeatId = normalizeSeatId(args[index + 1]);
|
|
144
|
+
if (!targetSeatId) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
upsertTarget(continueTargets, {
|
|
148
|
+
targetSeatId,
|
|
149
|
+
flowMode: seatFlowMode || "on",
|
|
150
|
+
});
|
|
151
|
+
hasExplicitTarget = true;
|
|
152
|
+
index += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
168
156
|
break;
|
|
169
157
|
}
|
|
170
158
|
|
|
171
159
|
if (index === args.length) {
|
|
172
|
-
|
|
160
|
+
if (seatFlowMode && !hasExplicitTarget) {
|
|
161
|
+
const partnerSeatId = seatId % 2 === 0 ? seatId - 1 : seatId + 1;
|
|
162
|
+
if (partnerSeatId > 0) {
|
|
163
|
+
upsertTarget(continueTargets, {
|
|
164
|
+
targetSeatId: partnerSeatId,
|
|
165
|
+
flowMode: seatFlowMode,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { continueTargets };
|
|
173
170
|
}
|
|
174
171
|
|
|
175
172
|
throw new Error(
|
|
176
|
-
`\`muuuuse ${command}\` accepts no extra arguments, \`flow on
|
|
173
|
+
`\`muuuuse ${command}\` accepts no extra arguments, optional \`flow on/off\`, \`continue <seat>\`, or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
|
|
177
174
|
);
|
|
178
175
|
}
|
|
179
176
|
|
|
@@ -189,32 +186,8 @@ function mergeTargets(existingTargets, nextTargets) {
|
|
|
189
186
|
return merged;
|
|
190
187
|
}
|
|
191
188
|
|
|
192
|
-
function
|
|
193
|
-
const targets = [];
|
|
194
|
-
let consumed = 0;
|
|
195
|
-
|
|
196
|
-
while (consumed < args.length) {
|
|
197
|
-
const targetSeatId = normalizeSeatId(args[consumed]);
|
|
198
|
-
if (!targetSeatId) {
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const nextFlowMode = parseFlowModeToken(args[consumed + 1], args[consumed + 2]);
|
|
203
|
-
const target = {
|
|
204
|
-
targetSeatId,
|
|
205
|
-
flowMode: nextFlowMode || defaultFlowMode,
|
|
206
|
-
};
|
|
207
|
-
upsertTarget(targets, target);
|
|
208
|
-
consumed += nextFlowMode ? 3 : 1;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return { consumed, targets };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function parseLinkTargets(args, seatId, defaultFlowMode) {
|
|
215
|
-
const partnerSeatId = seatId ? getPartnerSeatId(seatId) : null;
|
|
189
|
+
function parseLinkTargets(args, seatId) {
|
|
216
190
|
const continueTargets = [];
|
|
217
|
-
let flowMode = defaultFlowMode;
|
|
218
191
|
let consumed = 0;
|
|
219
192
|
|
|
220
193
|
while (consumed < args.length) {
|
|
@@ -228,19 +201,15 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
|
|
|
228
201
|
break;
|
|
229
202
|
}
|
|
230
203
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
targetSeatId,
|
|
236
|
-
flowMode: targetFlowMode,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
204
|
+
upsertTarget(continueTargets, {
|
|
205
|
+
targetSeatId,
|
|
206
|
+
flowMode: targetFlowMode,
|
|
207
|
+
});
|
|
239
208
|
|
|
240
209
|
consumed += 3;
|
|
241
210
|
}
|
|
242
211
|
|
|
243
|
-
return { consumed, continueTargets
|
|
212
|
+
return { consumed, continueTargets };
|
|
244
213
|
}
|
|
245
214
|
|
|
246
215
|
function parseFlowModeToken(flowToken, modeToken) {
|
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,
|
|
@@ -60,11 +58,6 @@ function normalizeFlowMode(flowMode) {
|
|
|
60
58
|
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
function normalizeContinueSeatId(value) {
|
|
64
|
-
const seatId = normalizeSeatId(value);
|
|
65
|
-
return seatId || null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
61
|
function resolveShell() {
|
|
69
62
|
const shell = String(process.env.SHELL || "").trim();
|
|
70
63
|
return shell || "/bin/bash";
|
|
@@ -134,6 +127,11 @@ function createSessionName(currentPath = process.cwd()) {
|
|
|
134
127
|
return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
|
|
135
128
|
}
|
|
136
129
|
|
|
130
|
+
function getAnchorSeatId(seatId = 1) {
|
|
131
|
+
const normalizedSeatId = normalizeSeatId(seatId) || 1;
|
|
132
|
+
return normalizedSeatId % 2 === 0 ? normalizedSeatId - 1 : normalizedSeatId;
|
|
133
|
+
}
|
|
134
|
+
|
|
137
135
|
function sleepSync(ms) {
|
|
138
136
|
if (!Number.isFinite(ms) || ms <= 0) {
|
|
139
137
|
return;
|
|
@@ -142,45 +140,33 @@ function sleepSync(ms) {
|
|
|
142
140
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
143
141
|
}
|
|
144
142
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
const anchorSeatId = getPartnerSeatId(normalizedSeatId);
|
|
148
|
-
if (!normalizedSeatId || !anchorSeatId || isAnchorSeat(normalizedSeatId)) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
143
|
+
function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = null) {
|
|
144
|
+
const targetAnchorSeatId = normalizeSeatId(anchorSeatId) || null;
|
|
152
145
|
const candidates = listSessionNames()
|
|
153
146
|
.map((sessionName) => {
|
|
154
147
|
const sessionPaths = getSessionPaths(sessionName);
|
|
155
148
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
156
|
-
const
|
|
157
|
-
const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
|
|
158
|
-
const anchorMeta = readJson(anchorPaths.metaPath, null);
|
|
159
|
-
const anchorStatus = readJson(anchorPaths.statusPath, null);
|
|
160
|
-
const seatMeta = readJson(seatPaths.metaPath, null);
|
|
161
|
-
const seatStatus = readJson(seatPaths.statusPath, null);
|
|
162
|
-
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
163
|
-
|
|
164
|
-
const cwd = controller?.cwd || anchorStatus?.cwd || anchorMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
|
|
149
|
+
const cwd = controller?.cwd || null;
|
|
165
150
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
166
151
|
return null;
|
|
167
152
|
}
|
|
168
153
|
|
|
169
|
-
const
|
|
170
|
-
const anchorChildPid = anchorStatus?.childPid || anchorMeta?.childPid || null;
|
|
171
|
-
const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
|
|
172
|
-
const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
|
|
173
|
-
const anchorLive = isPidAlive(anchorWrapperPid) || isPidAlive(anchorChildPid);
|
|
174
|
-
const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
|
|
154
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
175
155
|
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
176
|
-
const createdAtMs = Date.parse(controller?.createdAt ||
|
|
156
|
+
const createdAtMs = Date.parse(controller?.createdAt || "");
|
|
177
157
|
|
|
178
|
-
if (
|
|
158
|
+
if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
|
|
179
159
|
return null;
|
|
180
160
|
}
|
|
181
161
|
|
|
182
|
-
if (
|
|
183
|
-
|
|
162
|
+
if (targetAnchorSeatId) {
|
|
163
|
+
const controllerAnchorSeatId = normalizeSeatId(controller?.anchorSeatId);
|
|
164
|
+
if (controllerAnchorSeatId && controllerAnchorSeatId !== targetAnchorSeatId) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (!controllerAnchorSeatId && !getSeatDirIfExists(sessionName, targetAnchorSeatId)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
184
170
|
}
|
|
185
171
|
|
|
186
172
|
return {
|
|
@@ -194,10 +180,10 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
|
|
|
194
180
|
return candidates[0]?.sessionName || null;
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
function
|
|
183
|
+
function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS, anchorSeatId = null) {
|
|
198
184
|
const deadline = Date.now() + timeoutMs;
|
|
199
185
|
while (Date.now() <= deadline) {
|
|
200
|
-
const sessionName =
|
|
186
|
+
const sessionName = findExistingSessionName(currentPath, anchorSeatId);
|
|
201
187
|
if (sessionName) {
|
|
202
188
|
return sessionName;
|
|
203
189
|
}
|
|
@@ -208,11 +194,20 @@ function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, tim
|
|
|
208
194
|
}
|
|
209
195
|
|
|
210
196
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
211
|
-
|
|
212
|
-
|
|
197
|
+
const anchorSeatId = getAnchorSeatId(seatId);
|
|
198
|
+
const existing = findExistingSessionName(currentPath, anchorSeatId);
|
|
199
|
+
if (existing) {
|
|
200
|
+
return existing;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (seatId % 2 === 0) {
|
|
204
|
+
const waited = waitForExistingSessionName(currentPath, SEAT_JOIN_WAIT_MS, anchorSeatId);
|
|
205
|
+
if (waited) {
|
|
206
|
+
return waited;
|
|
207
|
+
}
|
|
213
208
|
}
|
|
214
209
|
|
|
215
|
-
return
|
|
210
|
+
return createSessionName(currentPath);
|
|
216
211
|
}
|
|
217
212
|
|
|
218
213
|
function parseAnswerEntries(text) {
|
|
@@ -414,26 +409,6 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
|
|
|
414
409
|
return null;
|
|
415
410
|
}
|
|
416
411
|
|
|
417
|
-
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
418
|
-
return JSON.stringify({
|
|
419
|
-
type: "muuuuse_pair_claim",
|
|
420
|
-
sessionName,
|
|
421
|
-
challenge,
|
|
422
|
-
seat1PublicKey,
|
|
423
|
-
seat2PublicKey,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
428
|
-
return JSON.stringify({
|
|
429
|
-
type: "muuuuse_pair_ack",
|
|
430
|
-
sessionName,
|
|
431
|
-
challenge,
|
|
432
|
-
seat1PublicKey,
|
|
433
|
-
seat2PublicKey,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
412
|
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
438
413
|
return JSON.stringify({
|
|
439
414
|
type: "muuuuse_answer",
|
|
@@ -584,26 +559,52 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
584
559
|
class ArmedSeat {
|
|
585
560
|
constructor(options) {
|
|
586
561
|
this.seatId = options.seatId;
|
|
587
|
-
this.
|
|
588
|
-
this.
|
|
589
|
-
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
590
|
-
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
562
|
+
this.anchorSeatId = getAnchorSeatId(this.seatId);
|
|
563
|
+
this.partnerSeatId = this.seatId % 2 === 0 ? this.seatId - 1 : this.seatId + 1;
|
|
591
564
|
this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
|
|
592
565
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
593
|
-
|
|
594
|
-
|
|
566
|
+
|
|
567
|
+
// Auto-link adjacent partner seat for backwards compatibility (1↔2, 3↔4, ...).
|
|
568
|
+
if (this.continueTargets.length === 0) {
|
|
569
|
+
if (this.partnerSeatId > 0) {
|
|
570
|
+
this.continueTargets.push({ targetSeatId: this.partnerSeatId, flowMode: "on" });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
|
|
575
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
|
|
595
576
|
}
|
|
596
577
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
597
578
|
if (!this.sessionName) {
|
|
598
579
|
throw new Error(
|
|
599
|
-
`
|
|
580
|
+
`Failed to create or find session in ${this.cwd}.`
|
|
600
581
|
);
|
|
601
582
|
}
|
|
602
583
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
603
584
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
604
|
-
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
605
585
|
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
606
|
-
|
|
586
|
+
|
|
587
|
+
// Per-target trust state, paths, and event offsets for the signed relay channel.
|
|
588
|
+
// Only create directories for same-session targets (same anchor pair).
|
|
589
|
+
// Cross-session targets use the continuation channel and don't need local seat dirs.
|
|
590
|
+
const ownAnchor = getAnchorSeatId(this.seatId);
|
|
591
|
+
this.targetTrust = {};
|
|
592
|
+
this.targetPaths = {};
|
|
593
|
+
this.targetOffsets = {};
|
|
594
|
+
for (const t of this.continueTargets) {
|
|
595
|
+
const sameSession = getAnchorSeatId(t.targetSeatId) === ownAnchor;
|
|
596
|
+
this.targetTrust[t.targetSeatId] = {
|
|
597
|
+
challenge: null,
|
|
598
|
+
peerPublicKey: null,
|
|
599
|
+
phase: "initializing",
|
|
600
|
+
pairedAt: null,
|
|
601
|
+
sameSession,
|
|
602
|
+
};
|
|
603
|
+
this.targetPaths[t.targetSeatId] = sameSession
|
|
604
|
+
? getSeatPaths(this.sessionName, t.targetSeatId)
|
|
605
|
+
: null;
|
|
606
|
+
this.targetOffsets[t.targetSeatId] = 0;
|
|
607
|
+
}
|
|
607
608
|
|
|
608
609
|
this.child = null;
|
|
609
610
|
this.childPid = null;
|
|
@@ -617,16 +618,11 @@ class ArmedSeat {
|
|
|
617
618
|
this.resizeCleanup = null;
|
|
618
619
|
this.forceKillTimer = null;
|
|
619
620
|
this.identity = null;
|
|
621
|
+
this.ownChallenge = null;
|
|
620
622
|
this.lastUserInputAtMs = 0;
|
|
621
623
|
this.pendingInboundContext = null;
|
|
622
624
|
this.recentInboundRelays = [];
|
|
623
625
|
this.recentEmittedAnswers = [];
|
|
624
|
-
this.trustState = {
|
|
625
|
-
challenge: null,
|
|
626
|
-
peerPublicKey: null,
|
|
627
|
-
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
628
|
-
pairedAt: null,
|
|
629
|
-
};
|
|
630
626
|
this.liveState = {
|
|
631
627
|
type: null,
|
|
632
628
|
pid: null,
|
|
@@ -649,9 +645,6 @@ class ArmedSeat {
|
|
|
649
645
|
updatedAt: new Date().toISOString(),
|
|
650
646
|
anchorSeatId: this.anchorSeatId,
|
|
651
647
|
partnerSeatId: this.partnerSeatId,
|
|
652
|
-
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
653
|
-
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
654
|
-
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
655
648
|
...extra,
|
|
656
649
|
});
|
|
657
650
|
}
|
|
@@ -663,10 +656,7 @@ class ArmedSeat {
|
|
|
663
656
|
writeMeta(extra = {}) {
|
|
664
657
|
writeJson(this.paths.metaPath, {
|
|
665
658
|
seatId: this.seatId,
|
|
666
|
-
partnerSeatId: this.partnerSeatId,
|
|
667
659
|
sessionName: this.sessionName,
|
|
668
|
-
flowMode: this.flowMode,
|
|
669
|
-
continueSeatId: this.continueSeatId,
|
|
670
660
|
continueTargets: this.continueTargets,
|
|
671
661
|
cwd: this.cwd,
|
|
672
662
|
pid: process.pid,
|
|
@@ -680,10 +670,7 @@ class ArmedSeat {
|
|
|
680
670
|
writeStatus(extra = {}) {
|
|
681
671
|
writeJson(this.paths.statusPath, {
|
|
682
672
|
seatId: this.seatId,
|
|
683
|
-
partnerSeatId: this.partnerSeatId,
|
|
684
673
|
sessionName: this.sessionName,
|
|
685
|
-
flowMode: this.flowMode,
|
|
686
|
-
continueSeatId: this.continueSeatId,
|
|
687
674
|
continueTargets: this.continueTargets,
|
|
688
675
|
cwd: this.cwd,
|
|
689
676
|
pid: process.pid,
|
|
@@ -696,168 +683,84 @@ class ArmedSeat {
|
|
|
696
683
|
|
|
697
684
|
initializeTrustMaterial() {
|
|
698
685
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
699
|
-
|
|
700
|
-
if (!isAnchorSeat(this.seatId)) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
686
|
+
const ownChallenge = createId(48);
|
|
704
687
|
writeJson(this.paths.challengePath, {
|
|
705
688
|
sessionName: this.sessionName,
|
|
706
|
-
challenge:
|
|
689
|
+
challenge: ownChallenge,
|
|
707
690
|
publicKey: this.identity.publicKey,
|
|
708
691
|
createdAt: new Date().toISOString(),
|
|
709
692
|
});
|
|
710
|
-
this.
|
|
711
|
-
this.trustState.peerPublicKey = null;
|
|
712
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
713
|
-
this.trustState.pairedAt = null;
|
|
714
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
715
|
-
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
693
|
+
this.ownChallenge = ownChallenge;
|
|
716
694
|
}
|
|
717
695
|
|
|
718
|
-
|
|
696
|
+
syncTargetTrust() {
|
|
719
697
|
if (!this.identity) {
|
|
720
698
|
this.initializeTrustMaterial();
|
|
721
699
|
}
|
|
722
700
|
|
|
723
|
-
|
|
724
|
-
this.
|
|
725
|
-
return;
|
|
701
|
+
for (const target of this.continueTargets) {
|
|
702
|
+
this.syncOneTargetTrust(target.targetSeatId);
|
|
726
703
|
}
|
|
727
|
-
|
|
728
|
-
this.syncSeatTwoTrust();
|
|
729
704
|
}
|
|
730
705
|
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
if (!
|
|
734
|
-
this.trustState = {
|
|
735
|
-
challenge: null,
|
|
736
|
-
peerPublicKey: null,
|
|
737
|
-
phase: "waiting_for_peer_signature",
|
|
738
|
-
pairedAt: null,
|
|
739
|
-
};
|
|
706
|
+
syncOneTargetTrust(targetSeatId) {
|
|
707
|
+
const trust = this.targetTrust[targetSeatId];
|
|
708
|
+
if (!trust || trust.phase === "paired") {
|
|
740
709
|
return;
|
|
741
710
|
}
|
|
742
711
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
claim.sessionName !== this.sessionName ||
|
|
748
|
-
claim.challenge !== challengeRecord.challenge ||
|
|
749
|
-
typeof claim.publicKey !== "string" ||
|
|
750
|
-
typeof claim.signature !== "string" ||
|
|
751
|
-
!verifyText(
|
|
752
|
-
buildClaimMessage(
|
|
753
|
-
this.sessionName,
|
|
754
|
-
challengeRecord.challenge,
|
|
755
|
-
this.identity.publicKey,
|
|
756
|
-
claim.publicKey.trim()
|
|
757
|
-
),
|
|
758
|
-
claim.signature,
|
|
759
|
-
claim.publicKey
|
|
760
|
-
)
|
|
761
|
-
) {
|
|
762
|
-
this.trustState.peerPublicKey = null;
|
|
763
|
-
this.trustState.phase = "waiting_for_peer_signature";
|
|
764
|
-
this.trustState.pairedAt = null;
|
|
765
|
-
fs.rmSync(this.paths.ackPath, { force: true });
|
|
712
|
+
// Cross-session target: relay goes through the continuation channel only.
|
|
713
|
+
if (!trust.sameSession) {
|
|
714
|
+
trust.phase = "paired";
|
|
715
|
+
trust.pairedAt = new Date().toISOString();
|
|
766
716
|
return;
|
|
767
717
|
}
|
|
768
718
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
currentAck.sessionName === this.sessionName &&
|
|
775
|
-
currentAck.challenge === challengeRecord.challenge &&
|
|
776
|
-
currentAck.publicKey === this.identity.publicKey &&
|
|
777
|
-
currentAck.peerPublicKey === peerPublicKey &&
|
|
778
|
-
typeof currentAck.signature === "string" &&
|
|
779
|
-
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
780
|
-
);
|
|
781
|
-
if (!ackIsValid) {
|
|
782
|
-
writeJson(this.paths.ackPath, {
|
|
783
|
-
sessionName: this.sessionName,
|
|
784
|
-
challenge: challengeRecord.challenge,
|
|
785
|
-
publicKey: this.identity.publicKey,
|
|
786
|
-
peerPublicKey,
|
|
787
|
-
signature: signText(ackMessage, this.identity.privateKey),
|
|
788
|
-
signedAt: new Date().toISOString(),
|
|
789
|
-
});
|
|
719
|
+
// Same-session target: read their challenge.json to get their public key
|
|
720
|
+
// and challenge. One-way trust — we just need to verify their events.
|
|
721
|
+
const targetPaths = this.targetPaths[targetSeatId];
|
|
722
|
+
if (!targetPaths) {
|
|
723
|
+
return;
|
|
790
724
|
}
|
|
791
725
|
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
syncSeatTwoTrust() {
|
|
799
|
-
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
800
|
-
if (!challengeRecord) {
|
|
801
|
-
this.trustState = {
|
|
802
|
-
challenge: null,
|
|
803
|
-
peerPublicKey: null,
|
|
804
|
-
phase: "waiting_for_anchor_key",
|
|
805
|
-
pairedAt: null,
|
|
806
|
-
};
|
|
726
|
+
const targetChallenge = readSeatChallenge(targetPaths, this.sessionName);
|
|
727
|
+
if (!targetChallenge) {
|
|
728
|
+
trust.phase = "waiting_for_target";
|
|
807
729
|
return;
|
|
808
730
|
}
|
|
809
731
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
challenge,
|
|
815
|
-
publicKey: this.identity.publicKey,
|
|
816
|
-
};
|
|
817
|
-
const claimSignature = signText(
|
|
818
|
-
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
819
|
-
this.identity.privateKey
|
|
820
|
-
);
|
|
821
|
-
const currentClaim = readJson(this.paths.claimPath, null);
|
|
822
|
-
if (
|
|
823
|
-
!currentClaim ||
|
|
824
|
-
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
825
|
-
currentClaim.challenge !== claimPayload.challenge ||
|
|
826
|
-
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
827
|
-
currentClaim.signature !== claimSignature
|
|
828
|
-
) {
|
|
829
|
-
writeJson(this.paths.claimPath, {
|
|
830
|
-
...claimPayload,
|
|
831
|
-
signature: claimSignature,
|
|
832
|
-
signedAt: new Date().toISOString(),
|
|
833
|
-
});
|
|
834
|
-
}
|
|
732
|
+
trust.challenge = targetChallenge.challenge;
|
|
733
|
+
trust.peerPublicKey = targetChallenge.publicKey;
|
|
734
|
+
trust.phase = "paired";
|
|
735
|
+
trust.pairedAt = new Date().toISOString();
|
|
835
736
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
ack.sessionName === this.sessionName &&
|
|
840
|
-
ack.challenge === challenge &&
|
|
841
|
-
ack.peerPublicKey === this.identity.publicKey &&
|
|
842
|
-
ack.publicKey === peerPublicKey &&
|
|
843
|
-
typeof ack.signature === "string" &&
|
|
844
|
-
verifyText(
|
|
845
|
-
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
846
|
-
ack.signature,
|
|
847
|
-
peerPublicKey
|
|
848
|
-
)
|
|
849
|
-
);
|
|
737
|
+
// Initialize offset to current file size so we only read new events.
|
|
738
|
+
this.targetOffsets[targetSeatId] = getFileSize(targetPaths.eventsPath);
|
|
739
|
+
}
|
|
850
740
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
741
|
+
isTargetPaired(targetSeatId) {
|
|
742
|
+
const trust = this.targetTrust[targetSeatId];
|
|
743
|
+
return Boolean(trust && trust.phase === "paired");
|
|
855
744
|
}
|
|
856
745
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
746
|
+
getOverallTrustPhase() {
|
|
747
|
+
const targets = this.continueTargets;
|
|
748
|
+
if (targets.length === 0) {
|
|
749
|
+
return "paired";
|
|
750
|
+
}
|
|
751
|
+
const allPaired = targets.every((t) => this.isTargetPaired(t.targetSeatId));
|
|
752
|
+
if (allPaired) {
|
|
753
|
+
return "paired";
|
|
754
|
+
}
|
|
755
|
+
const anyPaired = targets.some((t) => this.isTargetPaired(t.targetSeatId));
|
|
756
|
+
if (anyPaired) {
|
|
757
|
+
return "partial";
|
|
758
|
+
}
|
|
759
|
+
return "initializing";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
hasAnyPairedTarget() {
|
|
763
|
+
return this.continueTargets.some((t) => this.isTargetPaired(t.targetSeatId));
|
|
861
764
|
}
|
|
862
765
|
|
|
863
766
|
launchShell() {
|
|
@@ -865,6 +768,7 @@ class ArmedSeat {
|
|
|
865
768
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
866
769
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
867
770
|
this.initializeTrustMaterial();
|
|
771
|
+
this.syncTargetTrust();
|
|
868
772
|
this.writeController();
|
|
869
773
|
|
|
870
774
|
const shell = resolveShell();
|
|
@@ -880,7 +784,7 @@ class ArmedSeat {
|
|
|
880
784
|
|
|
881
785
|
this.childPid = this.child.pid;
|
|
882
786
|
this.writeMeta();
|
|
883
|
-
this.writeStatus({ state: "running", trust: this.
|
|
787
|
+
this.writeStatus({ state: "running", trust: this.getOverallTrustPhase() });
|
|
884
788
|
|
|
885
789
|
this.child.onData((data) => {
|
|
886
790
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
@@ -1014,11 +918,6 @@ class ArmedSeat {
|
|
|
1014
918
|
}
|
|
1015
919
|
}
|
|
1016
920
|
|
|
1017
|
-
partnerIsLive() {
|
|
1018
|
-
const partner = readJson(this.partnerPaths.statusPath, null);
|
|
1019
|
-
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
921
|
stopRequested() {
|
|
1023
922
|
const request = readJson(this.sessionPaths.stopPath, null);
|
|
1024
923
|
if (!request?.requestedAt) {
|
|
@@ -1068,73 +967,6 @@ class ArmedSeat {
|
|
|
1068
967
|
};
|
|
1069
968
|
}
|
|
1070
969
|
|
|
1071
|
-
async pullPartnerEvents() {
|
|
1072
|
-
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
1073
|
-
this.partnerOffset = nextOffset;
|
|
1074
|
-
if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
const entries = parseAnswerEntries(text);
|
|
1079
|
-
for (const entry of entries) {
|
|
1080
|
-
if (this.stopped || this.stopRequested()) {
|
|
1081
|
-
this.requestStop("stop_requested");
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const payload = sanitizeRelayText(entry.text);
|
|
1090
|
-
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
1091
|
-
chainId: entry.chainId || entry.id,
|
|
1092
|
-
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1093
|
-
id: entry.id,
|
|
1094
|
-
seatId: entry.seatId,
|
|
1095
|
-
origin: entry.origin || "unknown",
|
|
1096
|
-
phase: getRelayPhase(entry),
|
|
1097
|
-
createdAt: entry.createdAt,
|
|
1098
|
-
text: payload,
|
|
1099
|
-
});
|
|
1100
|
-
if (
|
|
1101
|
-
!payload ||
|
|
1102
|
-
entry.challenge !== this.trustState.challenge ||
|
|
1103
|
-
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
1104
|
-
typeof entry.signature !== "string" ||
|
|
1105
|
-
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
1106
|
-
) {
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
const delivered = await sendTextAndEnter(
|
|
1111
|
-
this.child,
|
|
1112
|
-
payload,
|
|
1113
|
-
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1114
|
-
);
|
|
1115
|
-
if (!delivered) {
|
|
1116
|
-
this.requestStop("relay_aborted");
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (this.stopped || this.stopRequested()) {
|
|
1121
|
-
this.requestStop("stop_requested");
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
const deliveredAtMs = Date.now();
|
|
1126
|
-
this.pendingInboundContext = {
|
|
1127
|
-
chainId: entry.chainId || entry.id,
|
|
1128
|
-
deliveredAtMs,
|
|
1129
|
-
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1130
|
-
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1131
|
-
};
|
|
1132
|
-
this.relayCount += 1;
|
|
1133
|
-
this.rememberInboundRelay(payload);
|
|
1134
|
-
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
970
|
async pullContinuationEvents() {
|
|
1139
971
|
const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
|
|
1140
972
|
this.continueOffset = nextOffset;
|
|
@@ -1149,10 +981,6 @@ class ArmedSeat {
|
|
|
1149
981
|
return;
|
|
1150
982
|
}
|
|
1151
983
|
|
|
1152
|
-
if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
984
|
const payload = sanitizeRelayText(entry.text);
|
|
1157
985
|
if (!payload) {
|
|
1158
986
|
continue;
|
|
@@ -1366,7 +1194,7 @@ class ArmedSeat {
|
|
|
1366
1194
|
this.liveState.sessionFile,
|
|
1367
1195
|
this.liveState.offset,
|
|
1368
1196
|
this.liveState.captureSinceMs,
|
|
1369
|
-
{ flowMode:
|
|
1197
|
+
{ flowMode: true }
|
|
1370
1198
|
);
|
|
1371
1199
|
this.liveState.offset = result.nextOffset;
|
|
1372
1200
|
answers.push(...result.answers);
|
|
@@ -1375,7 +1203,7 @@ class ArmedSeat {
|
|
|
1375
1203
|
this.liveState.sessionFile,
|
|
1376
1204
|
this.liveState.offset,
|
|
1377
1205
|
this.liveState.captureSinceMs,
|
|
1378
|
-
{ flowMode:
|
|
1206
|
+
{ flowMode: true }
|
|
1379
1207
|
);
|
|
1380
1208
|
this.liveState.offset = result.nextOffset;
|
|
1381
1209
|
answers.push(...result.answers);
|
|
@@ -1384,7 +1212,7 @@ class ArmedSeat {
|
|
|
1384
1212
|
this.liveState.sessionFile,
|
|
1385
1213
|
this.liveState.lastMessageId,
|
|
1386
1214
|
this.liveState.captureSinceMs,
|
|
1387
|
-
{ flowMode:
|
|
1215
|
+
{ flowMode: true }
|
|
1388
1216
|
);
|
|
1389
1217
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
1390
1218
|
this.liveState.offset = result.fileSize;
|
|
@@ -1417,7 +1245,11 @@ class ArmedSeat {
|
|
|
1417
1245
|
}
|
|
1418
1246
|
|
|
1419
1247
|
const payload = sanitizeRelayText(entry.text);
|
|
1420
|
-
if (!payload || !this.identity || !this.
|
|
1248
|
+
if (!payload || !this.identity || !this.ownChallenge) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (!this.hasAnyPairedTarget()) {
|
|
1421
1253
|
return;
|
|
1422
1254
|
}
|
|
1423
1255
|
|
|
@@ -1434,8 +1266,10 @@ class ArmedSeat {
|
|
|
1434
1266
|
}
|
|
1435
1267
|
|
|
1436
1268
|
const pendingInboundContext = this.getPendingInboundContext();
|
|
1437
|
-
|
|
1438
1269
|
const entryId = entry.id || createId(12);
|
|
1270
|
+
|
|
1271
|
+
// Sign with OUR OWN challenge. Each reader verifies using our challenge
|
|
1272
|
+
// (which they obtained during the trust handshake as peerChallenge).
|
|
1439
1273
|
const signedEntry = {
|
|
1440
1274
|
id: entryId,
|
|
1441
1275
|
type: "answer",
|
|
@@ -1446,61 +1280,67 @@ class ArmedSeat {
|
|
|
1446
1280
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1447
1281
|
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
1448
1282
|
hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
|
|
1449
|
-
challenge: this.
|
|
1283
|
+
challenge: this.ownChallenge,
|
|
1450
1284
|
publicKey: this.identity.publicKey,
|
|
1451
1285
|
};
|
|
1452
1286
|
signedEntry.signature = signText(
|
|
1453
|
-
buildAnswerSignaturePayload(this.sessionName, this.
|
|
1287
|
+
buildAnswerSignaturePayload(this.sessionName, this.ownChallenge, signedEntry),
|
|
1454
1288
|
this.identity.privateKey
|
|
1455
1289
|
);
|
|
1456
1290
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1457
|
-
this.forwardContinuation(signedEntry);
|
|
1458
|
-
this.rememberEmittedAnswer(answerKey);
|
|
1459
1291
|
|
|
1292
|
+
// Forward via continuation channel for cross-session targets.
|
|
1293
|
+
for (const target of this.continueTargets) {
|
|
1294
|
+
this.forwardContinuation(signedEntry, target);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
this.rememberEmittedAnswer(answerKey);
|
|
1460
1298
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1461
1299
|
}
|
|
1462
1300
|
|
|
1463
|
-
forwardContinuation(signedEntry) {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const target = this.findContinuationTarget();
|
|
1467
|
-
if (!target) {
|
|
1468
|
-
this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
|
|
1473
|
-
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1474
|
-
this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
|
|
1301
|
+
forwardContinuation(signedEntry, targetEntry) {
|
|
1302
|
+
if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
|
|
1303
|
+
return;
|
|
1475
1304
|
}
|
|
1476
1305
|
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1306
|
+
// Same-session target: write directly to their continuation channel.
|
|
1307
|
+
const trust = this.targetTrust[targetEntry.targetSeatId];
|
|
1308
|
+
if (trust && trust.sameSession) {
|
|
1309
|
+
const targetPaths = this.targetPaths[targetEntry.targetSeatId];
|
|
1310
|
+
if (targetPaths) {
|
|
1311
|
+
const continuationEntry = buildContinuationEntry(this.sessionName, targetEntry.targetSeatId, signedEntry);
|
|
1312
|
+
appendJsonl(targetPaths.continuePath, continuationEntry);
|
|
1313
|
+
this.log(`[${this.seatId} => ${targetEntry.targetSeatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
|
|
1483
1314
|
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1484
1317
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1318
|
+
// Cross-session target: find the target across sessions.
|
|
1319
|
+
const target = this.findContinuationTarget(targetEntry.targetSeatId);
|
|
1320
|
+
if (!target) {
|
|
1321
|
+
return;
|
|
1488
1322
|
}
|
|
1323
|
+
|
|
1324
|
+
const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
|
|
1325
|
+
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1326
|
+
this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
|
|
1489
1327
|
}
|
|
1490
1328
|
|
|
1491
1329
|
async tick() {
|
|
1492
1330
|
if (this.stopRequested()) {
|
|
1493
1331
|
this.writeStatus({
|
|
1494
1332
|
state: "stopping",
|
|
1495
|
-
|
|
1496
|
-
trust: this.trustState.phase,
|
|
1333
|
+
trust: this.getOverallTrustPhase(),
|
|
1497
1334
|
});
|
|
1498
1335
|
this.requestStop("stop_requested");
|
|
1499
1336
|
return;
|
|
1500
1337
|
}
|
|
1501
1338
|
|
|
1502
|
-
this.
|
|
1503
|
-
|
|
1339
|
+
this.syncTargetTrust();
|
|
1340
|
+
if (this.stopped || this.stopRequested()) {
|
|
1341
|
+
this.requestStop("stop_requested");
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1504
1344
|
await this.pullContinuationEvents();
|
|
1505
1345
|
if (this.stopped || this.stopRequested()) {
|
|
1506
1346
|
this.requestStop("stop_requested");
|
|
@@ -1515,13 +1355,11 @@ class ArmedSeat {
|
|
|
1515
1355
|
this.writeStatus({
|
|
1516
1356
|
state: live.state,
|
|
1517
1357
|
agent: live.agent,
|
|
1518
|
-
flowMode: this.flowMode,
|
|
1519
1358
|
cwd: live.cwd,
|
|
1520
1359
|
log: live.log,
|
|
1521
1360
|
lastAnswerAt: live.lastAnswerAt,
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
challengeReady: Boolean(this.trustState.challenge),
|
|
1361
|
+
trust: this.getOverallTrustPhase(),
|
|
1362
|
+
challengeReady: this.hasAnyPairedTarget(),
|
|
1525
1363
|
});
|
|
1526
1364
|
}
|
|
1527
1365
|
|
|
@@ -1533,18 +1371,9 @@ class ArmedSeat {
|
|
|
1533
1371
|
|
|
1534
1372
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1535
1373
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1536
|
-
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1537
|
-
if (this.continueSeatId) {
|
|
1538
|
-
this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
|
|
1539
|
-
}
|
|
1540
1374
|
if (this.continueTargets.length > 0) {
|
|
1541
|
-
const targets = this.continueTargets.map((t) => `${t.targetSeatId} (${t.flowMode})`).join(", ");
|
|
1542
|
-
this.log(`Seat ${this.seatId}
|
|
1543
|
-
}
|
|
1544
|
-
if (isAnchorSeat(this.seatId)) {
|
|
1545
|
-
this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
|
|
1546
|
-
} else {
|
|
1547
|
-
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1375
|
+
const targets = this.continueTargets.map((t) => `${t.targetSeatId} (flow ${t.flowMode})`).join(", ");
|
|
1376
|
+
this.log(`Seat ${this.seatId} relays to ${targets}. Establishing trust.`);
|
|
1548
1377
|
}
|
|
1549
1378
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1550
1379
|
|
|
@@ -1641,8 +1470,6 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1641
1470
|
return {
|
|
1642
1471
|
seatId,
|
|
1643
1472
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1644
|
-
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1645
|
-
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1646
1473
|
continueTargets: status?.continueTargets || meta?.continueTargets || [],
|
|
1647
1474
|
wrapperPid,
|
|
1648
1475
|
childPid,
|
|
@@ -1657,7 +1484,6 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1657
1484
|
trust: status?.trust || null,
|
|
1658
1485
|
updatedAt: status?.updatedAt || null,
|
|
1659
1486
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
1660
|
-
partnerLive: Boolean(status?.partnerLive),
|
|
1661
1487
|
};
|
|
1662
1488
|
}
|
|
1663
1489
|
|
package/src/util.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs = require("node:fs");
|
|
|
9
9
|
const os = require("node:os");
|
|
10
10
|
const path = require("node:path");
|
|
11
11
|
|
|
12
|
-
const BRAND = "🔌Muuuuse";
|
|
12
|
+
const BRAND = "🔌Muuuuse v7.0.0";
|
|
13
13
|
const POLL_MS = 220;
|
|
14
14
|
const MAX_RELAY_CHARS = 4000;
|
|
15
15
|
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -175,17 +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
178
|
|
|
190
179
|
function listSeatIds(sessionName) {
|
|
191
180
|
const sessionDir = getSessionDir(sessionName);
|
|
@@ -282,36 +271,28 @@ function listSessionNames() {
|
|
|
282
271
|
|
|
283
272
|
function usage() {
|
|
284
273
|
return [
|
|
285
|
-
`${BRAND}
|
|
274
|
+
`${BRAND} relay protocol for long-horizon zero-drift agentic code loops. agents relay output between terminals, converging to lucid conclusions.`,
|
|
286
275
|
"",
|
|
287
276
|
"Usage:",
|
|
288
277
|
" muuuuse 1",
|
|
289
|
-
" muuuuse 1 flow on",
|
|
290
|
-
" muuuuse 1 flow off",
|
|
291
|
-
" muuuuse 1 flow on continue 3",
|
|
278
|
+
" muuuuse 1 link 2 flow on",
|
|
279
|
+
" muuuuse 1 link 2 flow on 3 flow off",
|
|
292
280
|
" muuuuse 2",
|
|
293
|
-
" muuuuse 2 flow on",
|
|
294
|
-
" muuuuse 2 flow off",
|
|
295
|
-
" muuuuse 2 flow on continue 3",
|
|
281
|
+
" muuuuse 2 link 3 flow on",
|
|
296
282
|
" muuuuse 3",
|
|
297
|
-
" muuuuse 4",
|
|
298
|
-
" muuuuse 4 flow on continue 1",
|
|
299
283
|
" muuuuse stop",
|
|
300
284
|
" muuuuse status",
|
|
301
285
|
"",
|
|
302
286
|
"Flow:",
|
|
303
|
-
" 1. Run `muuuuse
|
|
304
|
-
" 2.
|
|
305
|
-
" 3.
|
|
306
|
-
" 4.
|
|
307
|
-
" 5.
|
|
308
|
-
" 6. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
|
|
309
|
-
" 7. Use those armed shells normally.",
|
|
310
|
-
" 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
|
|
311
|
-
" 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
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.",
|
|
312
292
|
"",
|
|
313
293
|
"Notes:",
|
|
314
|
-
" -
|
|
294
|
+
" - Any seat can relay to any other seat independently.",
|
|
295
|
+
" - `muuuuse stop` and `muuuuse status` work from any terminal.",
|
|
315
296
|
" - State lives under `~/.muuuuse`.",
|
|
316
297
|
].join("\n");
|
|
317
298
|
}
|
|
@@ -325,9 +306,7 @@ module.exports = {
|
|
|
325
306
|
ensureDir,
|
|
326
307
|
getDefaultSessionName,
|
|
327
308
|
getFileSize,
|
|
328
|
-
getPartnerSeatId,
|
|
329
309
|
loadOrCreateSeatIdentity,
|
|
330
|
-
isAnchorSeat,
|
|
331
310
|
getSeatPaths,
|
|
332
311
|
getSessionPaths,
|
|
333
312
|
getStateRoot,
|