muuuuse 7.0.1 → 7.0.2
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 +4 -3
- package/src/agents.js +2 -4
- package/src/cli.js +77 -73
- package/src/runtime.js +469 -177
- package/src/util.js +36 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "7.0.
|
|
4
|
-
"description": "🔌Muuuuse
|
|
3
|
+
"version": "7.0.2",
|
|
4
|
+
"description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"muuuuse": "bin/muuse.js"
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "node test/cli.test.js",
|
|
43
|
-
"pack:local": "npm pack"
|
|
43
|
+
"pack:local": "npm pack",
|
|
44
|
+
"prepublishOnly": "npm test"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"node-pty": "^1.1.0"
|
package/src/agents.js
CHANGED
|
@@ -468,9 +468,7 @@ function parseCodexAssistantLine(line, options = {}) {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
const phase = String(entry.payload?.phase || "").trim().toLowerCase();
|
|
471
|
-
|
|
472
|
-
const normalizedPhase = phase === "commentary" ? "commentary" : "final_answer";
|
|
473
|
-
const relayablePhase = normalizedPhase === "final_answer" || (flowMode && normalizedPhase === "commentary");
|
|
471
|
+
const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
|
|
474
472
|
if (!relayablePhase) {
|
|
475
473
|
return null;
|
|
476
474
|
}
|
|
@@ -483,7 +481,7 @@ function parseCodexAssistantLine(line, options = {}) {
|
|
|
483
481
|
return {
|
|
484
482
|
id: entry.payload.id || hashText(line),
|
|
485
483
|
text,
|
|
486
|
-
phase:
|
|
484
|
+
phase: phase === "commentary" ? "commentary" : "final_answer",
|
|
487
485
|
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
488
486
|
};
|
|
489
487
|
} catch {
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { BRAND, normalizeSeatId, usage } = require("./util");
|
|
1
|
+
const { BRAND, getPartnerSeatId, 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,10 +56,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
56
56
|
|
|
57
57
|
const seatId = normalizeSeatId(command);
|
|
58
58
|
if (seatId) {
|
|
59
|
-
const { continueTargets } = 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
|
+
continueSeatId,
|
|
62
63
|
continueTargets,
|
|
64
|
+
flowMode,
|
|
63
65
|
seatId,
|
|
64
66
|
});
|
|
65
67
|
const code = await seat.run();
|
|
@@ -73,14 +75,20 @@ function renderSeatStatus(seat) {
|
|
|
73
75
|
const bits = [
|
|
74
76
|
`seat ${seat.seatId}: ${seat.state}`,
|
|
75
77
|
`agent ${seat.agent || "idle"}`,
|
|
78
|
+
`flow ${seat.flowMode || "off"}`,
|
|
76
79
|
`relays ${seat.relayCount}`,
|
|
77
80
|
`wrapper ${seat.wrapperPid || "-"}`,
|
|
78
81
|
`child ${seat.childPid || "-"}`,
|
|
79
82
|
];
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
if (seat.partnerLive) {
|
|
85
|
+
bits.push("peer live");
|
|
86
|
+
}
|
|
87
|
+
if (seat.continueSeatId) {
|
|
88
|
+
bits.push(`continue ${seat.continueSeatId}`);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(seat.continueTargets) && seat.continueTargets.length > 0) {
|
|
91
|
+
bits.push(`links ${renderLinkTargets(seat.continueTargets)}`);
|
|
84
92
|
}
|
|
85
93
|
if (seat.trust) {
|
|
86
94
|
bits.push(`trust ${seat.trust}`);
|
|
@@ -99,41 +107,28 @@ function renderSeatStatus(seat) {
|
|
|
99
107
|
return output;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
function renderLinkTargets(
|
|
103
|
-
const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
|
|
110
|
+
function renderLinkTargets(targets) {
|
|
104
111
|
return targets
|
|
105
|
-
.map((target) => `${target.
|
|
112
|
+
.map((target) => `${target.seatId}:${target.flowMode}`)
|
|
106
113
|
.join(", ");
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
function parseSeatOptions(command, args) {
|
|
110
117
|
const seatId = normalizeSeatId(command);
|
|
118
|
+
const partnerSeatId = getPartnerSeatId(seatId);
|
|
119
|
+
let flowMode = "off";
|
|
120
|
+
let continueSeatId = null;
|
|
111
121
|
let continueTargets = [];
|
|
112
122
|
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
|
-
}
|
|
127
123
|
|
|
128
|
-
|
|
124
|
+
for (; index < args.length;) {
|
|
129
125
|
const token = String(args[index] || "").trim().toLowerCase();
|
|
130
126
|
|
|
131
|
-
if (token === "
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
index += 1 + parsedLinks.consumed;
|
|
127
|
+
if (token === "flow") {
|
|
128
|
+
const nextFlowMode = parseFlowModeToken(args[index + 1]);
|
|
129
|
+
if (nextFlowMode) {
|
|
130
|
+
flowMode = nextFlowMode;
|
|
131
|
+
index += 2;
|
|
137
132
|
continue;
|
|
138
133
|
}
|
|
139
134
|
break;
|
|
@@ -141,15 +136,22 @@ function parseSeatOptions(command, args) {
|
|
|
141
136
|
|
|
142
137
|
if (token === "continue") {
|
|
143
138
|
const targetSeatId = normalizeSeatId(args[index + 1]);
|
|
144
|
-
if (
|
|
139
|
+
if (targetSeatId && targetSeatId !== seatId) {
|
|
140
|
+
continueSeatId = targetSeatId;
|
|
141
|
+
index += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (token === "link") {
|
|
148
|
+
const parsed = parseLinkTargets(seatId, partnerSeatId, args, index + 1);
|
|
149
|
+
if (!parsed) {
|
|
145
150
|
break;
|
|
146
151
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
hasExplicitTarget = true;
|
|
152
|
-
index += 2;
|
|
152
|
+
|
|
153
|
+
continueTargets = mergeTargets(continueTargets, parsed.continueTargets);
|
|
154
|
+
index = parsed.nextIndex;
|
|
153
155
|
continue;
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -157,77 +159,79 @@ function parseSeatOptions(command, args) {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
if (index === args.length) {
|
|
160
|
-
|
|
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 };
|
|
162
|
+
return { flowMode, continueSeatId, continueTargets };
|
|
170
163
|
}
|
|
171
164
|
|
|
172
165
|
throw new Error(
|
|
173
|
-
`\`muuuuse ${command}\` accepts
|
|
166
|
+
`\`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.`
|
|
174
167
|
);
|
|
175
168
|
}
|
|
176
169
|
|
|
177
|
-
function mergeTargets(
|
|
178
|
-
const merged = [];
|
|
179
|
-
for (const target of
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
function mergeTargets(currentTargets, nextTargets) {
|
|
171
|
+
const merged = [...currentTargets];
|
|
172
|
+
for (const target of nextTargets) {
|
|
173
|
+
const currentIndex = merged.findIndex((entry) => entry.seatId === target.seatId);
|
|
174
|
+
if (currentIndex !== -1) {
|
|
175
|
+
merged.splice(currentIndex, 1);
|
|
176
|
+
}
|
|
177
|
+
merged.push(target);
|
|
184
178
|
}
|
|
185
|
-
|
|
186
179
|
return merged;
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
function parseLinkTargets(args,
|
|
182
|
+
function parseLinkTargets(seatId, partnerSeatId, args, startIndex) {
|
|
183
|
+
let index = startIndex;
|
|
190
184
|
const continueTargets = [];
|
|
191
|
-
let consumed = 0;
|
|
192
185
|
|
|
193
|
-
while (
|
|
194
|
-
const targetSeatId = normalizeSeatId(args[
|
|
195
|
-
if (!targetSeatId) {
|
|
186
|
+
while (index < args.length) {
|
|
187
|
+
const targetSeatId = normalizeSeatId(args[index]);
|
|
188
|
+
if (!targetSeatId || targetSeatId === seatId) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (String(args[index + 1] || "").trim().toLowerCase() !== "flow") {
|
|
196
193
|
break;
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
const targetFlowMode = parseFlowModeToken(args[
|
|
196
|
+
const targetFlowMode = parseFlowModeToken(args[index + 2]);
|
|
200
197
|
if (!targetFlowMode) {
|
|
201
198
|
break;
|
|
202
199
|
}
|
|
203
200
|
|
|
204
201
|
upsertTarget(continueTargets, {
|
|
205
|
-
targetSeatId,
|
|
202
|
+
seatId: targetSeatId,
|
|
206
203
|
flowMode: targetFlowMode,
|
|
207
204
|
});
|
|
208
205
|
|
|
209
|
-
|
|
206
|
+
index += 3;
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
|
|
209
|
+
if (index === startIndex) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
continueTargets,
|
|
215
|
+
nextIndex: index,
|
|
216
|
+
};
|
|
213
217
|
}
|
|
214
218
|
|
|
215
|
-
function parseFlowModeToken(
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return normalizedModeToken;
|
|
219
|
+
function parseFlowModeToken(value) {
|
|
220
|
+
const token = String(value || "").trim().toLowerCase();
|
|
221
|
+
if (token === "on" || token === "off") {
|
|
222
|
+
return token;
|
|
220
223
|
}
|
|
221
224
|
return null;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
function upsertTarget(targets, nextTarget) {
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
targets
|
|
228
|
+
const currentIndex = targets.findIndex((target) => target.seatId === nextTarget.seatId);
|
|
229
|
+
if (currentIndex === -1) {
|
|
230
|
+
targets.push(nextTarget);
|
|
228
231
|
return;
|
|
229
232
|
}
|
|
230
|
-
|
|
233
|
+
|
|
234
|
+
targets[currentIndex] = nextTarget;
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
module.exports = {
|
package/src/runtime.js
CHANGED
|
@@ -20,10 +20,12 @@ const {
|
|
|
20
20
|
ensureDir,
|
|
21
21
|
getDefaultSessionName,
|
|
22
22
|
getFileSize,
|
|
23
|
+
getPartnerSeatId,
|
|
23
24
|
getSeatPaths,
|
|
24
25
|
getSessionPaths,
|
|
25
26
|
getStateRoot,
|
|
26
27
|
hashText,
|
|
28
|
+
isAnchorSeat,
|
|
27
29
|
isPidAlive,
|
|
28
30
|
listSeatIds,
|
|
29
31
|
loadOrCreateSeatIdentity,
|
|
@@ -38,7 +40,8 @@ const {
|
|
|
38
40
|
writeJson,
|
|
39
41
|
} = require("./util");
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
// A short settle delay keeps interactive CLIs from treating submit as another newline.
|
|
44
|
+
const TYPE_CHUNK_DELAY_MS = 45;
|
|
42
45
|
const TYPE_CHUNK_SIZE = 24;
|
|
43
46
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
44
47
|
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
@@ -54,10 +57,61 @@ const CHILD_ENV_DROP_KEYS = [
|
|
|
54
57
|
"CODEX_THREAD_ID",
|
|
55
58
|
];
|
|
56
59
|
|
|
60
|
+
function bestEffortEnableChildEcho(child) {
|
|
61
|
+
const ptsName = String(child?.ptsName || "").trim();
|
|
62
|
+
if (!ptsName || process.platform === "win32") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
execFileSync("stty", [
|
|
68
|
+
"-F",
|
|
69
|
+
ptsName,
|
|
70
|
+
"echo",
|
|
71
|
+
"icanon",
|
|
72
|
+
"isig",
|
|
73
|
+
"iexten",
|
|
74
|
+
"echoe",
|
|
75
|
+
"echok",
|
|
76
|
+
"echoke",
|
|
77
|
+
"echoctl",
|
|
78
|
+
], {
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
// Best effort only. The shell or child app may later change its own tty mode.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
function normalizeFlowMode(flowMode) {
|
|
58
87
|
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
function normalizeContinueSeatId(value) {
|
|
91
|
+
const seatId = normalizeSeatId(value);
|
|
92
|
+
return seatId || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeContinueTargets(value) {
|
|
96
|
+
if (!Array.isArray(value)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return value
|
|
101
|
+
.map((entry) => {
|
|
102
|
+
const seatId = normalizeSeatId(entry?.seatId);
|
|
103
|
+
if (!seatId) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
seatId,
|
|
109
|
+
flowMode: normalizeFlowMode(entry?.flowMode),
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
.filter((entry) => entry !== null);
|
|
113
|
+
}
|
|
114
|
+
|
|
61
115
|
function resolveShell() {
|
|
62
116
|
const shell = String(process.env.SHELL || "").trim();
|
|
63
117
|
return shell || "/bin/bash";
|
|
@@ -127,11 +181,6 @@ function createSessionName(currentPath = process.cwd()) {
|
|
|
127
181
|
return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
|
|
128
182
|
}
|
|
129
183
|
|
|
130
|
-
function getAnchorSeatId(seatId = 1) {
|
|
131
|
-
const normalizedSeatId = normalizeSeatId(seatId) || 1;
|
|
132
|
-
return normalizedSeatId % 2 === 0 ? normalizedSeatId - 1 : normalizedSeatId;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
184
|
function sleepSync(ms) {
|
|
136
185
|
if (!Number.isFinite(ms) || ms <= 0) {
|
|
137
186
|
return;
|
|
@@ -140,33 +189,45 @@ function sleepSync(ms) {
|
|
|
140
189
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
141
190
|
}
|
|
142
191
|
|
|
143
|
-
function
|
|
144
|
-
const
|
|
192
|
+
function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
|
|
193
|
+
const normalizedSeatId = normalizeSeatId(seatId);
|
|
194
|
+
const partnerSeatId = getPartnerSeatId(normalizedSeatId);
|
|
195
|
+
if (!normalizedSeatId || !partnerSeatId) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
145
199
|
const candidates = listSessionNames()
|
|
146
200
|
.map((sessionName) => {
|
|
147
201
|
const sessionPaths = getSessionPaths(sessionName);
|
|
148
202
|
const controller = readJson(sessionPaths.controllerPath, null);
|
|
149
|
-
const
|
|
203
|
+
const partnerPaths = getSeatPaths(sessionName, partnerSeatId);
|
|
204
|
+
const seatPaths = getSeatPaths(sessionName, normalizedSeatId);
|
|
205
|
+
const partnerMeta = readJson(partnerPaths.metaPath, null);
|
|
206
|
+
const partnerStatus = readJson(partnerPaths.statusPath, null);
|
|
207
|
+
const seatMeta = readJson(seatPaths.metaPath, null);
|
|
208
|
+
const seatStatus = readJson(seatPaths.statusPath, null);
|
|
209
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
210
|
+
|
|
211
|
+
const cwd = controller?.cwd || partnerStatus?.cwd || partnerMeta?.cwd || seatStatus?.cwd || seatMeta?.cwd || null;
|
|
150
212
|
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
151
213
|
return null;
|
|
152
214
|
}
|
|
153
215
|
|
|
154
|
-
const
|
|
216
|
+
const partnerWrapperPid = partnerStatus?.pid || partnerMeta?.pid || null;
|
|
217
|
+
const partnerChildPid = partnerStatus?.childPid || partnerMeta?.childPid || null;
|
|
218
|
+
const seatWrapperPid = seatStatus?.pid || seatMeta?.pid || null;
|
|
219
|
+
const seatChildPid = seatStatus?.childPid || seatMeta?.childPid || null;
|
|
220
|
+
const partnerLive = isPidAlive(partnerWrapperPid) || isPidAlive(partnerChildPid);
|
|
221
|
+
const seatLive = isPidAlive(seatWrapperPid) || isPidAlive(seatChildPid);
|
|
155
222
|
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
156
|
-
const createdAtMs = Date.parse(controller?.createdAt || "");
|
|
223
|
+
const createdAtMs = Date.parse(controller?.createdAt || partnerMeta?.startedAt || partnerStatus?.updatedAt || "");
|
|
157
224
|
|
|
158
|
-
if (
|
|
225
|
+
if (!partnerLive || seatLive) {
|
|
159
226
|
return null;
|
|
160
227
|
}
|
|
161
228
|
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
if (controllerAnchorSeatId && controllerAnchorSeatId !== targetAnchorSeatId) {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
if (!controllerAnchorSeatId && !getSeatDirIfExists(sessionName, targetAnchorSeatId)) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
229
|
+
if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
|
|
230
|
+
return null;
|
|
170
231
|
}
|
|
171
232
|
|
|
172
233
|
return {
|
|
@@ -180,10 +241,10 @@ function findExistingSessionName(currentPath = process.cwd(), anchorSeatId = nul
|
|
|
180
241
|
return candidates[0]?.sessionName || null;
|
|
181
242
|
}
|
|
182
243
|
|
|
183
|
-
function
|
|
244
|
+
function waitForJoinableSessionName(currentPath = process.cwd(), seatId = 2, timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
184
245
|
const deadline = Date.now() + timeoutMs;
|
|
185
246
|
while (Date.now() <= deadline) {
|
|
186
|
-
const sessionName =
|
|
247
|
+
const sessionName = findJoinableSessionName(currentPath, seatId);
|
|
187
248
|
if (sessionName) {
|
|
188
249
|
return sessionName;
|
|
189
250
|
}
|
|
@@ -194,17 +255,13 @@ function waitForExistingSessionName(currentPath = process.cwd(), timeoutMs = SEA
|
|
|
194
255
|
}
|
|
195
256
|
|
|
196
257
|
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return existing;
|
|
258
|
+
const joinableSessionName = findJoinableSessionName(currentPath, seatId);
|
|
259
|
+
if (joinableSessionName) {
|
|
260
|
+
return joinableSessionName;
|
|
201
261
|
}
|
|
202
262
|
|
|
203
|
-
if (seatId
|
|
204
|
-
|
|
205
|
-
if (waited) {
|
|
206
|
-
return waited;
|
|
207
|
-
}
|
|
263
|
+
if (!isAnchorSeat(seatId)) {
|
|
264
|
+
return waitForJoinableSessionName(currentPath, seatId) || createSessionName(currentPath);
|
|
208
265
|
}
|
|
209
266
|
|
|
210
267
|
return createSessionName(currentPath);
|
|
@@ -409,6 +466,26 @@ function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, pr
|
|
|
409
466
|
return null;
|
|
410
467
|
}
|
|
411
468
|
|
|
469
|
+
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
470
|
+
return JSON.stringify({
|
|
471
|
+
type: "muuuuse_pair_claim",
|
|
472
|
+
sessionName,
|
|
473
|
+
challenge,
|
|
474
|
+
seat1PublicKey,
|
|
475
|
+
seat2PublicKey,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
480
|
+
return JSON.stringify({
|
|
481
|
+
type: "muuuuse_pair_ack",
|
|
482
|
+
sessionName,
|
|
483
|
+
challenge,
|
|
484
|
+
seat1PublicKey,
|
|
485
|
+
seat2PublicKey,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
412
489
|
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
413
490
|
return JSON.stringify({
|
|
414
491
|
type: "muuuuse_answer",
|
|
@@ -421,17 +498,20 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
421
498
|
origin: entry.origin,
|
|
422
499
|
phase: entry.phase || "final_answer",
|
|
423
500
|
createdAt: entry.createdAt,
|
|
501
|
+
targetSeatId: entry.targetSeatId || null,
|
|
502
|
+
targetFlowMode: normalizeFlowMode(entry.targetFlowMode),
|
|
424
503
|
text: entry.text,
|
|
425
504
|
});
|
|
426
505
|
}
|
|
427
506
|
|
|
428
|
-
function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
|
|
507
|
+
function buildContinuationEntry(sourceSessionName, targetSeatId, entry, targetFlowMode = null) {
|
|
429
508
|
return {
|
|
430
509
|
id: createId(12),
|
|
431
510
|
type: "continue",
|
|
432
511
|
sourceSessionName,
|
|
433
512
|
sourceSeatId: entry.seatId,
|
|
434
513
|
targetSeatId,
|
|
514
|
+
targetFlowMode: normalizeFlowMode(targetFlowMode),
|
|
435
515
|
origin: entry.origin || "unknown",
|
|
436
516
|
phase: entry.phase || "final_answer",
|
|
437
517
|
text: entry.text,
|
|
@@ -453,6 +533,15 @@ function shouldAcceptInboundEntry(flowMode, entry) {
|
|
|
453
533
|
return flowMode === "on" || getRelayPhase(entry) === "final_answer";
|
|
454
534
|
}
|
|
455
535
|
|
|
536
|
+
function resolveInboundFlowMode(defaultFlowMode, seatId, entry) {
|
|
537
|
+
const targetSeatId = normalizeSeatId(entry?.targetSeatId);
|
|
538
|
+
if (targetSeatId === normalizeSeatId(seatId)) {
|
|
539
|
+
return normalizeFlowMode(entry?.targetFlowMode);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return normalizeFlowMode(defaultFlowMode);
|
|
543
|
+
}
|
|
544
|
+
|
|
456
545
|
function getSeatDirIfExists(sessionName, seatId) {
|
|
457
546
|
const dir = path.join(getStateRoot(), "sessions", sessionName, `seat-${seatId}`);
|
|
458
547
|
try {
|
|
@@ -548,7 +637,7 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
548
637
|
}
|
|
549
638
|
|
|
550
639
|
try {
|
|
551
|
-
child.write("\
|
|
640
|
+
child.write("\r");
|
|
552
641
|
} catch {
|
|
553
642
|
return false;
|
|
554
643
|
}
|
|
@@ -559,52 +648,29 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
|
559
648
|
class ArmedSeat {
|
|
560
649
|
constructor(options) {
|
|
561
650
|
this.seatId = options.seatId;
|
|
562
|
-
this.
|
|
563
|
-
this.
|
|
564
|
-
this.
|
|
651
|
+
this.partnerSeatId = getPartnerSeatId(options.seatId);
|
|
652
|
+
this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
|
|
653
|
+
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
654
|
+
this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
|
|
655
|
+
this.continueTargets = normalizeContinueTargets(options.continueTargets);
|
|
565
656
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (this.continueTargets.length === 0) {
|
|
569
|
-
if (this.partnerSeatId > 0) {
|
|
570
|
-
this.continueTargets.push({ targetSeatId: this.partnerSeatId, flowMode: "on" });
|
|
571
|
-
}
|
|
657
|
+
if (this.continueSeatId === this.seatId) {
|
|
658
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
|
|
572
659
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
throw new Error(`\`muuuuse ${this.seatId}\` cannot relay to itself.`);
|
|
660
|
+
if (this.continueTargets.some((target) => target.seatId === this.seatId)) {
|
|
661
|
+
throw new Error(`\`muuuuse ${this.seatId}\` cannot link to itself.`);
|
|
576
662
|
}
|
|
577
663
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
578
664
|
if (!this.sessionName) {
|
|
579
665
|
throw new Error(
|
|
580
|
-
`
|
|
666
|
+
`No armed \`muuuuse ${this.partnerSeatId}\` seat is waiting in this cwd. Run \`muuuuse ${this.partnerSeatId}\` first.`
|
|
581
667
|
);
|
|
582
668
|
}
|
|
583
669
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
584
670
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
671
|
+
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
585
672
|
this.continueOffset = getFileSize(this.paths.continuePath);
|
|
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
|
-
}
|
|
673
|
+
this.partnerOffset = getFileSize(this.partnerPaths.eventsPath);
|
|
608
674
|
|
|
609
675
|
this.child = null;
|
|
610
676
|
this.childPid = null;
|
|
@@ -618,11 +684,16 @@ class ArmedSeat {
|
|
|
618
684
|
this.resizeCleanup = null;
|
|
619
685
|
this.forceKillTimer = null;
|
|
620
686
|
this.identity = null;
|
|
621
|
-
this.ownChallenge = null;
|
|
622
687
|
this.lastUserInputAtMs = 0;
|
|
623
688
|
this.pendingInboundContext = null;
|
|
624
689
|
this.recentInboundRelays = [];
|
|
625
690
|
this.recentEmittedAnswers = [];
|
|
691
|
+
this.trustState = {
|
|
692
|
+
challenge: null,
|
|
693
|
+
peerPublicKey: null,
|
|
694
|
+
phase: isAnchorSeat(this.seatId) ? "waiting_for_peer_signature" : "waiting_for_anchor_key",
|
|
695
|
+
pairedAt: null,
|
|
696
|
+
};
|
|
626
697
|
this.liveState = {
|
|
627
698
|
type: null,
|
|
628
699
|
pid: null,
|
|
@@ -645,6 +716,9 @@ class ArmedSeat {
|
|
|
645
716
|
updatedAt: new Date().toISOString(),
|
|
646
717
|
anchorSeatId: this.anchorSeatId,
|
|
647
718
|
partnerSeatId: this.partnerSeatId,
|
|
719
|
+
anchorSeatPid: this.seatId === this.anchorSeatId ? process.pid : current.anchorSeatPid || null,
|
|
720
|
+
partnerSeatPid: this.seatId === this.partnerSeatId ? process.pid : current.partnerSeatPid || null,
|
|
721
|
+
pid: this.seatId === this.anchorSeatId ? process.pid : current.pid || null,
|
|
648
722
|
...extra,
|
|
649
723
|
});
|
|
650
724
|
}
|
|
@@ -656,7 +730,10 @@ class ArmedSeat {
|
|
|
656
730
|
writeMeta(extra = {}) {
|
|
657
731
|
writeJson(this.paths.metaPath, {
|
|
658
732
|
seatId: this.seatId,
|
|
733
|
+
partnerSeatId: this.partnerSeatId,
|
|
659
734
|
sessionName: this.sessionName,
|
|
735
|
+
flowMode: this.flowMode,
|
|
736
|
+
continueSeatId: this.continueSeatId,
|
|
660
737
|
continueTargets: this.continueTargets,
|
|
661
738
|
cwd: this.cwd,
|
|
662
739
|
pid: process.pid,
|
|
@@ -670,7 +747,10 @@ class ArmedSeat {
|
|
|
670
747
|
writeStatus(extra = {}) {
|
|
671
748
|
writeJson(this.paths.statusPath, {
|
|
672
749
|
seatId: this.seatId,
|
|
750
|
+
partnerSeatId: this.partnerSeatId,
|
|
673
751
|
sessionName: this.sessionName,
|
|
752
|
+
flowMode: this.flowMode,
|
|
753
|
+
continueSeatId: this.continueSeatId,
|
|
674
754
|
continueTargets: this.continueTargets,
|
|
675
755
|
cwd: this.cwd,
|
|
676
756
|
pid: process.pid,
|
|
@@ -683,84 +763,168 @@ class ArmedSeat {
|
|
|
683
763
|
|
|
684
764
|
initializeTrustMaterial() {
|
|
685
765
|
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
686
|
-
|
|
766
|
+
|
|
767
|
+
if (!isAnchorSeat(this.seatId)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
687
771
|
writeJson(this.paths.challengePath, {
|
|
688
772
|
sessionName: this.sessionName,
|
|
689
|
-
challenge:
|
|
773
|
+
challenge: createId(48),
|
|
690
774
|
publicKey: this.identity.publicKey,
|
|
691
775
|
createdAt: new Date().toISOString(),
|
|
692
776
|
});
|
|
693
|
-
this.
|
|
777
|
+
this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
|
|
778
|
+
this.trustState.peerPublicKey = null;
|
|
779
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
780
|
+
this.trustState.pairedAt = null;
|
|
781
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
782
|
+
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
694
783
|
}
|
|
695
784
|
|
|
696
|
-
|
|
785
|
+
syncTrustState() {
|
|
697
786
|
if (!this.identity) {
|
|
698
787
|
this.initializeTrustMaterial();
|
|
699
788
|
}
|
|
700
789
|
|
|
701
|
-
|
|
702
|
-
this.
|
|
790
|
+
if (isAnchorSeat(this.seatId)) {
|
|
791
|
+
this.syncSeatOneTrust();
|
|
792
|
+
return;
|
|
703
793
|
}
|
|
794
|
+
|
|
795
|
+
this.syncSeatTwoTrust();
|
|
704
796
|
}
|
|
705
797
|
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
if (!
|
|
798
|
+
syncSeatOneTrust() {
|
|
799
|
+
const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
|
|
800
|
+
if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
|
|
801
|
+
this.trustState = {
|
|
802
|
+
challenge: null,
|
|
803
|
+
peerPublicKey: null,
|
|
804
|
+
phase: "waiting_for_peer_signature",
|
|
805
|
+
pairedAt: null,
|
|
806
|
+
};
|
|
709
807
|
return;
|
|
710
808
|
}
|
|
711
809
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
810
|
+
this.trustState.challenge = challengeRecord.challenge;
|
|
811
|
+
const claim = readJson(this.partnerPaths.claimPath, null);
|
|
812
|
+
if (
|
|
813
|
+
!claim ||
|
|
814
|
+
claim.sessionName !== this.sessionName ||
|
|
815
|
+
claim.challenge !== challengeRecord.challenge ||
|
|
816
|
+
typeof claim.publicKey !== "string" ||
|
|
817
|
+
typeof claim.signature !== "string" ||
|
|
818
|
+
!verifyText(
|
|
819
|
+
buildClaimMessage(
|
|
820
|
+
this.sessionName,
|
|
821
|
+
challengeRecord.challenge,
|
|
822
|
+
this.identity.publicKey,
|
|
823
|
+
claim.publicKey.trim()
|
|
824
|
+
),
|
|
825
|
+
claim.signature,
|
|
826
|
+
claim.publicKey
|
|
827
|
+
)
|
|
828
|
+
) {
|
|
829
|
+
this.trustState.peerPublicKey = null;
|
|
830
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
831
|
+
this.trustState.pairedAt = null;
|
|
832
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
716
833
|
return;
|
|
717
834
|
}
|
|
718
835
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
836
|
+
const peerPublicKey = claim.publicKey.trim();
|
|
837
|
+
const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
|
|
838
|
+
const currentAck = readJson(this.paths.ackPath, null);
|
|
839
|
+
const ackIsValid = Boolean(
|
|
840
|
+
currentAck &&
|
|
841
|
+
currentAck.sessionName === this.sessionName &&
|
|
842
|
+
currentAck.challenge === challengeRecord.challenge &&
|
|
843
|
+
currentAck.publicKey === this.identity.publicKey &&
|
|
844
|
+
currentAck.peerPublicKey === peerPublicKey &&
|
|
845
|
+
typeof currentAck.signature === "string" &&
|
|
846
|
+
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
847
|
+
);
|
|
848
|
+
if (!ackIsValid) {
|
|
849
|
+
writeJson(this.paths.ackPath, {
|
|
850
|
+
sessionName: this.sessionName,
|
|
851
|
+
challenge: challengeRecord.challenge,
|
|
852
|
+
publicKey: this.identity.publicKey,
|
|
853
|
+
peerPublicKey,
|
|
854
|
+
signature: signText(ackMessage, this.identity.privateKey),
|
|
855
|
+
signedAt: new Date().toISOString(),
|
|
856
|
+
});
|
|
724
857
|
}
|
|
725
858
|
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
859
|
+
const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
|
|
860
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
861
|
+
this.trustState.phase = "paired";
|
|
862
|
+
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
syncSeatTwoTrust() {
|
|
866
|
+
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
867
|
+
if (!challengeRecord) {
|
|
868
|
+
this.trustState = {
|
|
869
|
+
challenge: null,
|
|
870
|
+
peerPublicKey: null,
|
|
871
|
+
phase: "waiting_for_anchor_key",
|
|
872
|
+
pairedAt: null,
|
|
873
|
+
};
|
|
729
874
|
return;
|
|
730
875
|
}
|
|
731
876
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
877
|
+
const challenge = challengeRecord.challenge;
|
|
878
|
+
const peerPublicKey = challengeRecord.publicKey;
|
|
879
|
+
const claimPayload = {
|
|
880
|
+
sessionName: this.sessionName,
|
|
881
|
+
challenge,
|
|
882
|
+
publicKey: this.identity.publicKey,
|
|
883
|
+
};
|
|
884
|
+
const claimSignature = signText(
|
|
885
|
+
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
886
|
+
this.identity.privateKey
|
|
887
|
+
);
|
|
888
|
+
const currentClaim = readJson(this.paths.claimPath, null);
|
|
889
|
+
if (
|
|
890
|
+
!currentClaim ||
|
|
891
|
+
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
892
|
+
currentClaim.challenge !== claimPayload.challenge ||
|
|
893
|
+
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
894
|
+
currentClaim.signature !== claimSignature
|
|
895
|
+
) {
|
|
896
|
+
writeJson(this.paths.claimPath, {
|
|
897
|
+
...claimPayload,
|
|
898
|
+
signature: claimSignature,
|
|
899
|
+
signedAt: new Date().toISOString(),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
736
902
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
903
|
+
const ack = readJson(this.partnerPaths.ackPath, null);
|
|
904
|
+
const paired = Boolean(
|
|
905
|
+
ack &&
|
|
906
|
+
ack.sessionName === this.sessionName &&
|
|
907
|
+
ack.challenge === challenge &&
|
|
908
|
+
ack.peerPublicKey === this.identity.publicKey &&
|
|
909
|
+
ack.publicKey === peerPublicKey &&
|
|
910
|
+
typeof ack.signature === "string" &&
|
|
911
|
+
verifyText(
|
|
912
|
+
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
913
|
+
ack.signature,
|
|
914
|
+
peerPublicKey
|
|
915
|
+
)
|
|
916
|
+
);
|
|
740
917
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
918
|
+
this.trustState.challenge = challenge;
|
|
919
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
920
|
+
this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
|
|
921
|
+
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
744
922
|
}
|
|
745
923
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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));
|
|
924
|
+
isPaired() {
|
|
925
|
+
return this.trustState.phase === "paired" &&
|
|
926
|
+
typeof this.trustState.challenge === "string" &&
|
|
927
|
+
typeof this.trustState.peerPublicKey === "string";
|
|
764
928
|
}
|
|
765
929
|
|
|
766
930
|
launchShell() {
|
|
@@ -768,7 +932,6 @@ class ArmedSeat {
|
|
|
768
932
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
769
933
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
770
934
|
this.initializeTrustMaterial();
|
|
771
|
-
this.syncTargetTrust();
|
|
772
935
|
this.writeController();
|
|
773
936
|
|
|
774
937
|
const shell = resolveShell();
|
|
@@ -781,10 +944,11 @@ class ArmedSeat {
|
|
|
781
944
|
env: childEnv,
|
|
782
945
|
name: childEnv.TERM,
|
|
783
946
|
});
|
|
947
|
+
bestEffortEnableChildEcho(this.child);
|
|
784
948
|
|
|
785
949
|
this.childPid = this.child.pid;
|
|
786
950
|
this.writeMeta();
|
|
787
|
-
this.writeStatus({ state: "running", trust: this.
|
|
951
|
+
this.writeStatus({ state: "running", trust: this.trustState.phase });
|
|
788
952
|
|
|
789
953
|
this.child.onData((data) => {
|
|
790
954
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
@@ -918,6 +1082,32 @@ class ArmedSeat {
|
|
|
918
1082
|
}
|
|
919
1083
|
}
|
|
920
1084
|
|
|
1085
|
+
partnerIsLive() {
|
|
1086
|
+
const partner = readJson(this.partnerPaths.statusPath, null);
|
|
1087
|
+
return Boolean(partner?.pid && isPidAlive(partner.pid));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
getLinkTarget(seatId) {
|
|
1091
|
+
const desiredSeatId = normalizeContinueSeatId(seatId);
|
|
1092
|
+
if (!desiredSeatId) {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return this.continueTargets.find((target) => target.seatId === desiredSeatId) || null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
getPartnerLinkTarget() {
|
|
1100
|
+
return this.getLinkTarget(this.partnerSeatId);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
shouldCaptureCommentary() {
|
|
1104
|
+
if (this.flowMode === "on") {
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return this.continueTargets.some((target) => target.flowMode === "on");
|
|
1109
|
+
}
|
|
1110
|
+
|
|
921
1111
|
stopRequested() {
|
|
922
1112
|
const request = readJson(this.sessionPaths.stopPath, null);
|
|
923
1113
|
if (!request?.requestedAt) {
|
|
@@ -929,18 +1119,18 @@ class ArmedSeat {
|
|
|
929
1119
|
}
|
|
930
1120
|
|
|
931
1121
|
findContinuationTarget(targetSeatId = null) {
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
1122
|
+
const desiredSeatId = normalizeContinueSeatId(targetSeatId);
|
|
1123
|
+
if (!desiredSeatId) {
|
|
934
1124
|
return null;
|
|
935
1125
|
}
|
|
936
1126
|
|
|
937
1127
|
const candidates = listSessionNames()
|
|
938
1128
|
.map((sessionName) => {
|
|
939
|
-
if (!getSeatDirIfExists(sessionName,
|
|
1129
|
+
if (!getSeatDirIfExists(sessionName, desiredSeatId)) {
|
|
940
1130
|
return null;
|
|
941
1131
|
}
|
|
942
1132
|
|
|
943
|
-
const seat = buildSeatReport(sessionName,
|
|
1133
|
+
const seat = buildSeatReport(sessionName, desiredSeatId);
|
|
944
1134
|
if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
|
|
945
1135
|
return null;
|
|
946
1136
|
}
|
|
@@ -967,6 +1157,73 @@ class ArmedSeat {
|
|
|
967
1157
|
};
|
|
968
1158
|
}
|
|
969
1159
|
|
|
1160
|
+
async pullPartnerEvents() {
|
|
1161
|
+
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
1162
|
+
this.partnerOffset = nextOffset;
|
|
1163
|
+
if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const entries = parseAnswerEntries(text);
|
|
1168
|
+
for (const entry of entries) {
|
|
1169
|
+
if (this.stopped || this.stopRequested()) {
|
|
1170
|
+
this.requestStop("stop_requested");
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!shouldAcceptInboundEntry(resolveInboundFlowMode(this.flowMode, this.seatId, entry), entry)) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const payload = sanitizeRelayText(entry.text);
|
|
1179
|
+
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
1180
|
+
chainId: entry.chainId || entry.id,
|
|
1181
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1182
|
+
id: entry.id,
|
|
1183
|
+
seatId: entry.seatId,
|
|
1184
|
+
origin: entry.origin || "unknown",
|
|
1185
|
+
phase: getRelayPhase(entry),
|
|
1186
|
+
createdAt: entry.createdAt,
|
|
1187
|
+
text: payload,
|
|
1188
|
+
});
|
|
1189
|
+
if (
|
|
1190
|
+
!payload ||
|
|
1191
|
+
entry.challenge !== this.trustState.challenge ||
|
|
1192
|
+
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
1193
|
+
typeof entry.signature !== "string" ||
|
|
1194
|
+
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
1195
|
+
) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const delivered = await sendTextAndEnter(
|
|
1200
|
+
this.child,
|
|
1201
|
+
payload,
|
|
1202
|
+
() => this.stopped || this.stopRequested() || !this.child || Boolean(this.childExit)
|
|
1203
|
+
);
|
|
1204
|
+
if (!delivered) {
|
|
1205
|
+
this.requestStop("relay_aborted");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (this.stopped || this.stopRequested()) {
|
|
1210
|
+
this.requestStop("stop_requested");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const deliveredAtMs = Date.now();
|
|
1215
|
+
this.pendingInboundContext = {
|
|
1216
|
+
chainId: entry.chainId || entry.id,
|
|
1217
|
+
deliveredAtMs,
|
|
1218
|
+
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
1219
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
1220
|
+
};
|
|
1221
|
+
this.relayCount += 1;
|
|
1222
|
+
this.rememberInboundRelay(payload);
|
|
1223
|
+
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
970
1227
|
async pullContinuationEvents() {
|
|
971
1228
|
const { nextOffset, text } = readAppendedText(this.paths.continuePath, this.continueOffset);
|
|
972
1229
|
this.continueOffset = nextOffset;
|
|
@@ -981,6 +1238,10 @@ class ArmedSeat {
|
|
|
981
1238
|
return;
|
|
982
1239
|
}
|
|
983
1240
|
|
|
1241
|
+
if (!shouldAcceptInboundEntry(resolveInboundFlowMode(this.flowMode, this.seatId, entry), entry)) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
984
1245
|
const payload = sanitizeRelayText(entry.text);
|
|
985
1246
|
if (!payload) {
|
|
986
1247
|
continue;
|
|
@@ -1194,7 +1455,7 @@ class ArmedSeat {
|
|
|
1194
1455
|
this.liveState.sessionFile,
|
|
1195
1456
|
this.liveState.offset,
|
|
1196
1457
|
this.liveState.captureSinceMs,
|
|
1197
|
-
{ flowMode:
|
|
1458
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1198
1459
|
);
|
|
1199
1460
|
this.liveState.offset = result.nextOffset;
|
|
1200
1461
|
answers.push(...result.answers);
|
|
@@ -1203,7 +1464,7 @@ class ArmedSeat {
|
|
|
1203
1464
|
this.liveState.sessionFile,
|
|
1204
1465
|
this.liveState.offset,
|
|
1205
1466
|
this.liveState.captureSinceMs,
|
|
1206
|
-
{ flowMode:
|
|
1467
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1207
1468
|
);
|
|
1208
1469
|
this.liveState.offset = result.nextOffset;
|
|
1209
1470
|
answers.push(...result.answers);
|
|
@@ -1212,7 +1473,7 @@ class ArmedSeat {
|
|
|
1212
1473
|
this.liveState.sessionFile,
|
|
1213
1474
|
this.liveState.lastMessageId,
|
|
1214
1475
|
this.liveState.captureSinceMs,
|
|
1215
|
-
{ flowMode:
|
|
1476
|
+
{ flowMode: this.shouldCaptureCommentary() }
|
|
1216
1477
|
);
|
|
1217
1478
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
1218
1479
|
this.liveState.offset = result.fileSize;
|
|
@@ -1245,11 +1506,7 @@ class ArmedSeat {
|
|
|
1245
1506
|
}
|
|
1246
1507
|
|
|
1247
1508
|
const payload = sanitizeRelayText(entry.text);
|
|
1248
|
-
if (!payload
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
if (!this.hasAnyPairedTarget()) {
|
|
1509
|
+
if (!payload) {
|
|
1253
1510
|
return;
|
|
1254
1511
|
}
|
|
1255
1512
|
|
|
@@ -1266,11 +1523,9 @@ class ArmedSeat {
|
|
|
1266
1523
|
}
|
|
1267
1524
|
|
|
1268
1525
|
const pendingInboundContext = this.getPendingInboundContext();
|
|
1269
|
-
const entryId = entry.id || createId(12);
|
|
1270
1526
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const signedEntry = {
|
|
1527
|
+
const entryId = entry.id || createId(12);
|
|
1528
|
+
const relayEntry = {
|
|
1274
1529
|
id: entryId,
|
|
1275
1530
|
type: "answer",
|
|
1276
1531
|
seatId: this.seatId,
|
|
@@ -1280,67 +1535,85 @@ class ArmedSeat {
|
|
|
1280
1535
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1281
1536
|
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
1282
1537
|
hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
|
|
1283
|
-
challenge: this.ownChallenge,
|
|
1284
|
-
publicKey: this.identity.publicKey,
|
|
1285
1538
|
};
|
|
1286
|
-
signedEntry.signature = signText(
|
|
1287
|
-
buildAnswerSignaturePayload(this.sessionName, this.ownChallenge, signedEntry),
|
|
1288
|
-
this.identity.privateKey
|
|
1289
|
-
);
|
|
1290
|
-
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1291
1539
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1540
|
+
const partnerLinkTarget = this.getPartnerLinkTarget();
|
|
1541
|
+
let outboundEntry = relayEntry;
|
|
1542
|
+
if (this.isPaired() && this.identity) {
|
|
1543
|
+
const signedEntry = {
|
|
1544
|
+
...relayEntry,
|
|
1545
|
+
challenge: this.trustState.challenge,
|
|
1546
|
+
publicKey: this.identity.publicKey,
|
|
1547
|
+
targetSeatId: partnerLinkTarget?.seatId || null,
|
|
1548
|
+
targetFlowMode: partnerLinkTarget?.flowMode || null,
|
|
1549
|
+
};
|
|
1550
|
+
signedEntry.signature = signText(
|
|
1551
|
+
buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
|
|
1552
|
+
this.identity.privateKey
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
if (!partnerLinkTarget) {
|
|
1556
|
+
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1557
|
+
}
|
|
1558
|
+
outboundEntry = signedEntry;
|
|
1295
1559
|
}
|
|
1296
1560
|
|
|
1561
|
+
if (this.continueSeatId || this.continueTargets.length > 0) {
|
|
1562
|
+
this.forwardContinuation(outboundEntry);
|
|
1563
|
+
}
|
|
1297
1564
|
this.rememberEmittedAnswer(answerKey);
|
|
1565
|
+
|
|
1298
1566
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1299
1567
|
}
|
|
1300
1568
|
|
|
1301
|
-
forwardContinuation(signedEntry
|
|
1302
|
-
|
|
1303
|
-
|
|
1569
|
+
forwardContinuation(signedEntry) {
|
|
1570
|
+
const targets = [...this.continueTargets];
|
|
1571
|
+
if (this.continueSeatId && !targets.some((target) => target.seatId === this.continueSeatId)) {
|
|
1572
|
+
targets.push({
|
|
1573
|
+
seatId: this.continueSeatId,
|
|
1574
|
+
flowMode: null,
|
|
1575
|
+
});
|
|
1304
1576
|
}
|
|
1305
1577
|
|
|
1306
|
-
|
|
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)}`);
|
|
1314
|
-
}
|
|
1578
|
+
if (targets.length === 0) {
|
|
1315
1579
|
return;
|
|
1316
1580
|
}
|
|
1317
1581
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1582
|
+
for (const targetEntry of targets) {
|
|
1583
|
+
if (targetEntry.flowMode && !shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const target = this.findContinuationTarget(targetEntry.seatId);
|
|
1588
|
+
if (!target) {
|
|
1589
|
+
this.log(`[${this.seatId}] continue ${targetEntry.seatId} unavailable`);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1323
1592
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1593
|
+
const continuationEntry = buildContinuationEntry(
|
|
1594
|
+
this.sessionName,
|
|
1595
|
+
target.seatId,
|
|
1596
|
+
signedEntry,
|
|
1597
|
+
targetEntry.flowMode
|
|
1598
|
+
);
|
|
1599
|
+
appendJsonl(target.paths.continuePath, continuationEntry);
|
|
1600
|
+
this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
|
|
1601
|
+
}
|
|
1327
1602
|
}
|
|
1328
1603
|
|
|
1329
1604
|
async tick() {
|
|
1330
1605
|
if (this.stopRequested()) {
|
|
1331
1606
|
this.writeStatus({
|
|
1332
1607
|
state: "stopping",
|
|
1333
|
-
|
|
1608
|
+
partnerLive: this.partnerIsLive(),
|
|
1609
|
+
trust: this.trustState.phase,
|
|
1334
1610
|
});
|
|
1335
1611
|
this.requestStop("stop_requested");
|
|
1336
1612
|
return;
|
|
1337
1613
|
}
|
|
1338
1614
|
|
|
1339
|
-
this.
|
|
1340
|
-
|
|
1341
|
-
this.requestStop("stop_requested");
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1615
|
+
this.syncTrustState();
|
|
1616
|
+
await this.pullPartnerEvents();
|
|
1344
1617
|
await this.pullContinuationEvents();
|
|
1345
1618
|
if (this.stopped || this.stopRequested()) {
|
|
1346
1619
|
this.requestStop("stop_requested");
|
|
@@ -1355,11 +1628,13 @@ class ArmedSeat {
|
|
|
1355
1628
|
this.writeStatus({
|
|
1356
1629
|
state: live.state,
|
|
1357
1630
|
agent: live.agent,
|
|
1631
|
+
flowMode: this.flowMode,
|
|
1358
1632
|
cwd: live.cwd,
|
|
1359
1633
|
log: live.log,
|
|
1360
1634
|
lastAnswerAt: live.lastAnswerAt,
|
|
1361
|
-
|
|
1362
|
-
|
|
1635
|
+
partnerLive: this.partnerIsLive(),
|
|
1636
|
+
trust: this.trustState.phase,
|
|
1637
|
+
challengeReady: Boolean(this.trustState.challenge),
|
|
1363
1638
|
});
|
|
1364
1639
|
}
|
|
1365
1640
|
|
|
@@ -1371,9 +1646,23 @@ class ArmedSeat {
|
|
|
1371
1646
|
|
|
1372
1647
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1373
1648
|
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1649
|
+
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1650
|
+
if (this.continueSeatId) {
|
|
1651
|
+
this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
|
|
1652
|
+
}
|
|
1374
1653
|
if (this.continueTargets.length > 0) {
|
|
1375
|
-
|
|
1376
|
-
|
|
1654
|
+
this.log(
|
|
1655
|
+
`Seat ${this.seatId} links additional targets: ${this.continueTargets.map((target) => `${target.seatId}:${target.flowMode}`).join(", ")}.`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (this.partnerIsLive()) {
|
|
1659
|
+
if (isAnchorSeat(this.seatId)) {
|
|
1660
|
+
this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
|
|
1661
|
+
} else {
|
|
1662
|
+
this.log(`Seat ${this.seatId} will sign the session key from seat ${this.partnerSeatId}, then relay goes live.`);
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
this.log(`Seat ${this.seatId} is armed without a live pair partner. Link targets can relay immediately; direct pair relay activates after seat ${this.partnerSeatId} joins.`);
|
|
1377
1666
|
}
|
|
1378
1667
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
1379
1668
|
|
|
@@ -1470,7 +1759,9 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1470
1759
|
return {
|
|
1471
1760
|
seatId,
|
|
1472
1761
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1473
|
-
|
|
1762
|
+
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1763
|
+
continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
|
|
1764
|
+
continueTargets: normalizeContinueTargets(status?.continueTargets || meta?.continueTargets),
|
|
1474
1765
|
wrapperPid,
|
|
1475
1766
|
childPid,
|
|
1476
1767
|
wrapperLive,
|
|
@@ -1484,6 +1775,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1484
1775
|
trust: status?.trust || null,
|
|
1485
1776
|
updatedAt: status?.updatedAt || null,
|
|
1486
1777
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
1778
|
+
partnerLive: Boolean(status?.partnerLive),
|
|
1487
1779
|
};
|
|
1488
1780
|
}
|
|
1489
1781
|
|
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";
|
|
13
13
|
const POLL_MS = 220;
|
|
14
14
|
const MAX_RELAY_CHARS = 4000;
|
|
15
15
|
const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -175,6 +175,17 @@ 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
|
+
}
|
|
178
189
|
|
|
179
190
|
function listSeatIds(sessionName) {
|
|
180
191
|
const sessionDir = getSessionDir(sessionName);
|
|
@@ -271,28 +282,39 @@ function listSessionNames() {
|
|
|
271
282
|
|
|
272
283
|
function usage() {
|
|
273
284
|
return [
|
|
274
|
-
`${BRAND}
|
|
285
|
+
`${BRAND} arms regular terminals and relays assistant output across odd/even partners plus explicit links.`,
|
|
275
286
|
"",
|
|
276
287
|
"Usage:",
|
|
277
288
|
" muuuuse 1",
|
|
278
|
-
" muuuuse 1
|
|
279
|
-
" muuuuse 1
|
|
289
|
+
" muuuuse 1 flow on",
|
|
290
|
+
" muuuuse 1 flow off",
|
|
291
|
+
" muuuuse 1 flow on continue 3",
|
|
292
|
+
" muuuuse 1 link 2 flow on 3 flow off 5 flow off",
|
|
280
293
|
" muuuuse 2",
|
|
281
|
-
" muuuuse 2
|
|
294
|
+
" muuuuse 2 flow on",
|
|
295
|
+
" muuuuse 2 flow off",
|
|
296
|
+
" muuuuse 2 flow on continue 3",
|
|
297
|
+
" muuuuse 2 link 3 flow off",
|
|
282
298
|
" muuuuse 3",
|
|
299
|
+
" muuuuse 4",
|
|
300
|
+
" muuuuse 4 flow on continue 1",
|
|
283
301
|
" muuuuse stop",
|
|
284
302
|
" muuuuse status",
|
|
285
303
|
"",
|
|
286
304
|
"Flow:",
|
|
287
|
-
" 1. Run `muuuuse <seat>` in
|
|
288
|
-
" 2.
|
|
289
|
-
" 3.
|
|
290
|
-
" 4.
|
|
291
|
-
" 5.
|
|
305
|
+
" 1. Run `muuuuse <seat>` in the terminal you want to arm.",
|
|
306
|
+
" 2. Matching odd/even partners still auto-pair when both seats are armed, in either order.",
|
|
307
|
+
" 3. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
|
|
308
|
+
" 4. Optional: arm each seat with `flow on` or `flow off`.",
|
|
309
|
+
" 5. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
|
|
310
|
+
" 6. Or use `link <seat> flow on|off ...` to fan out to multiple armed seats.",
|
|
311
|
+
" 7. Standalone seats can still route through links even before a pair partner joins.",
|
|
312
|
+
" 8. Use those armed shells normally.",
|
|
313
|
+
" 9. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
|
|
314
|
+
" 10. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
292
315
|
"",
|
|
293
316
|
"Notes:",
|
|
294
|
-
" -
|
|
295
|
-
" - `muuuuse stop` and `muuuuse status` work from any terminal.",
|
|
317
|
+
" - `muuuuse stop` and `muuuuse status` work from another terminal or the same one.",
|
|
296
318
|
" - State lives under `~/.muuuuse`.",
|
|
297
319
|
].join("\n");
|
|
298
320
|
}
|
|
@@ -306,7 +328,9 @@ module.exports = {
|
|
|
306
328
|
ensureDir,
|
|
307
329
|
getDefaultSessionName,
|
|
308
330
|
getFileSize,
|
|
331
|
+
getPartnerSeatId,
|
|
309
332
|
loadOrCreateSeatIdentity,
|
|
333
|
+
isAnchorSeat,
|
|
310
334
|
getSeatPaths,
|
|
311
335
|
getSessionPaths,
|
|
312
336
|
getStateRoot,
|