muuuuse 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,7 +69,7 @@ muuuuse 2 bash -lc 'while read line; do printf "right: %s\n\n" "$line"; done'
69
69
 
70
70
  Type into one seat and the other seat will receive the relayed block.
71
71
 
72
- For Codex, Claude, and Gemini, `🔌Muuuuse` prefers their structured session logs. For anything else, it falls back to the last stable output block after a turn goes idle.
72
+ For Codex, Claude, and Gemini, `🔌Muuuuse` waits for their structured final-answer logs instead of relaying transient screen chatter. For anything else, it first looks for an explicit `(answer)` block and otherwise falls back to the last stable output block after a turn goes idle.
73
73
 
74
74
  ## Sessions
75
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "🔌Muuuuse wraps two local programs and bounces final blocks between them from the terminal you launched.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -76,6 +76,7 @@ const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
76
76
  const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
77
77
  const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
78
78
  const codexSnapshotPaneCache = new Map();
79
+ const codexSnapshotExportsCache = new Map();
79
80
 
80
81
  function walkFiles(rootPath, predicate, results = []) {
81
82
  try {
@@ -157,6 +158,10 @@ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
157
158
  return null;
158
159
  }
159
160
 
161
+ if (cwdMatches.length === 1) {
162
+ return cwdMatches[0].path;
163
+ }
164
+
160
165
  if (processStartedAtMs !== null) {
161
166
  const preciseMatches = cwdMatches
162
167
  .map((candidate) => ({
@@ -166,13 +171,12 @@ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
166
171
  .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
167
172
  .sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
168
173
 
169
- if (preciseMatches.length > 0) {
174
+ if (preciseMatches.length === 1) {
170
175
  return preciseMatches[0].path;
171
176
  }
172
177
  }
173
178
 
174
- const fallback = cwdMatches.sort((left, right) => right.mtimeMs - left.mtimeMs)[0];
175
- return fallback ? fallback.path : null;
179
+ return null;
176
180
  }
177
181
 
178
182
  function extractThreadId(filePath) {
@@ -202,6 +206,49 @@ function readCodexSnapshotPane(threadId) {
202
206
  }
203
207
  }
204
208
 
209
+ function readCodexSnapshotExports(threadId) {
210
+ if (!threadId) {
211
+ return {};
212
+ }
213
+
214
+ if (codexSnapshotExportsCache.has(threadId)) {
215
+ return codexSnapshotExportsCache.get(threadId);
216
+ }
217
+
218
+ const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
219
+ try {
220
+ const contents = fs.readFileSync(snapshotPath, "utf8");
221
+ const exportsMap = {};
222
+ const pattern = /^declare -x ([A-Z0-9_]+)="((?:[^"\\]|\\.)*)"$/gm;
223
+
224
+ for (const match of contents.matchAll(pattern)) {
225
+ const [, key = "", rawValue = ""] = match;
226
+ exportsMap[key] = rawValue
227
+ .replace(/\\"/g, "\"")
228
+ .replace(/\\\\/g, "\\");
229
+ }
230
+
231
+ codexSnapshotExportsCache.set(threadId, exportsMap);
232
+ return exportsMap;
233
+ } catch (error) {
234
+ codexSnapshotExportsCache.set(threadId, {});
235
+ return {};
236
+ }
237
+ }
238
+
239
+ function snapshotEnvMatches(exportsMap, expectedEnv = null) {
240
+ if (!expectedEnv || typeof expectedEnv !== "object") {
241
+ return true;
242
+ }
243
+
244
+ return Object.entries(expectedEnv).every(([key, value]) => {
245
+ if (value === undefined || value === null) {
246
+ return true;
247
+ }
248
+ return exportsMap[key] === String(value);
249
+ });
250
+ }
251
+
205
252
  function readCodexCandidate(filePath) {
206
253
  try {
207
254
  const [firstLine] = readFirstLines(filePath, 1);
@@ -216,6 +263,7 @@ function readCodexCandidate(filePath) {
216
263
  return {
217
264
  path: filePath,
218
265
  threadId: extractThreadId(filePath),
266
+ snapshotExports: readCodexSnapshotExports(extractThreadId(filePath)),
219
267
  snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
220
268
  cwd: entry.payload.cwd,
221
269
  startedAtMs: Date.parse(entry.payload.timestamp),
@@ -226,12 +274,24 @@ function readCodexCandidate(filePath) {
226
274
  }
227
275
  }
228
276
 
229
- function selectCodexSessionFile(currentPath, processStartedAtMs, paneId = null) {
277
+ function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
278
+ const paneId = options.paneId || null;
279
+ const snapshotEnv = options.snapshotEnv || null;
230
280
  const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
231
281
  .map((filePath) => readCodexCandidate(filePath))
232
282
  .filter((candidate) => candidate !== null);
233
283
 
234
284
  let scopedCandidates = candidates;
285
+ if (snapshotEnv) {
286
+ const exactEnvMatches = scopedCandidates.filter((candidate) => snapshotEnvMatches(candidate.snapshotExports, snapshotEnv));
287
+ if (exactEnvMatches.length === 1) {
288
+ return exactEnvMatches[0].path;
289
+ }
290
+ if (exactEnvMatches.length > 1) {
291
+ scopedCandidates = exactEnvMatches;
292
+ }
293
+ }
294
+
235
295
  if (paneId) {
236
296
  const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
237
297
  if (exactPaneMatches.length > 0) {
@@ -415,12 +475,16 @@ function selectGeminiSessionFile(currentPath, processStartedAtMs) {
415
475
  .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
416
476
  .sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
417
477
 
418
- if (preciseMatches.length > 0) {
478
+ if (preciseMatches.length === 1) {
419
479
  return preciseMatches[0].path;
420
480
  }
421
481
  }
422
482
 
423
- return candidates.sort((left, right) => right.lastUpdatedMs - left.lastUpdatedMs || right.mtimeMs - left.mtimeMs)[0].path;
483
+ if (candidates.length === 1) {
484
+ return candidates[0].path;
485
+ }
486
+
487
+ return null;
424
488
  }
425
489
 
426
490
  function readGeminiAnswers(filePath, lastMessageId = null) {
@@ -460,6 +524,7 @@ function readGeminiAnswers(filePath, lastMessageId = null) {
460
524
 
461
525
  module.exports = {
462
526
  PRESETS,
527
+ chooseCandidate,
463
528
  detectAgent,
464
529
  detectAgentTypeFromCommand,
465
530
  expandPresetCommand,
package/src/runtime.js CHANGED
@@ -31,7 +31,6 @@ const {
31
31
  } = require("./util");
32
32
 
33
33
  const GENERIC_IDLE_MS = 900;
34
- const GENERIC_FALLBACK_DELAY_MS = 4000;
35
34
 
36
35
  function resolveSessionName(sessionOverride, currentPath = process.cwd()) {
37
36
  return sessionOverride || getDefaultSessionName(currentPath);
@@ -79,9 +78,9 @@ function parseAnswerEntries(text) {
79
78
  .filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
80
79
  }
81
80
 
82
- function resolveSessionFile(agentType, currentPath, processStartedAtMs) {
81
+ function resolveSessionFile(agentType, currentPath, processStartedAtMs, options = {}) {
83
82
  if (agentType === "codex") {
84
- return selectCodexSessionFile(currentPath, processStartedAtMs);
83
+ return selectCodexSessionFile(currentPath, processStartedAtMs, options);
85
84
  }
86
85
  if (agentType === "claude") {
87
86
  return selectClaudeSessionFile(currentPath, processStartedAtMs);
@@ -161,6 +160,11 @@ function extractGenericAnswer(rawText, lastInputText) {
161
160
  }
162
161
  }
163
162
 
163
+ const markerAnswer = extractMarkedAnswer(candidate);
164
+ if (markerAnswer) {
165
+ return markerAnswer;
166
+ }
167
+
164
168
  const blocks = candidate
165
169
  .split(/\n{2,}/)
166
170
  .map((block) => block.trim())
@@ -173,6 +177,18 @@ function extractGenericAnswer(rawText, lastInputText) {
173
177
  return sanitizeRelayText(blocks[blocks.length - 1]);
174
178
  }
175
179
 
180
+ function extractMarkedAnswer(content) {
181
+ const lines = String(content || "").split("\n");
182
+ const answerIndex = lines.findIndex((line) => line.trim().startsWith("(answer)"));
183
+ if (answerIndex === -1) {
184
+ return null;
185
+ }
186
+
187
+ const answerLines = lines.slice(answerIndex);
188
+ answerLines[0] = answerLines[0].trim().replace(/^\(answer\)\s*/, "");
189
+ return sanitizeRelayText(answerLines.join("\n"));
190
+ }
191
+
176
192
  class SeatProcess {
177
193
  constructor(options) {
178
194
  this.seatId = options.seatId;
@@ -196,6 +212,8 @@ class SeatProcess {
196
212
  this.stopped = false;
197
213
  this.stdinCleanup = null;
198
214
  this.resizeCleanup = null;
215
+ this.childToken = createId(16);
216
+ this.processStartedAtMs = null;
199
217
 
200
218
  this.sessionState = {
201
219
  file: null,
@@ -217,6 +235,7 @@ class SeatProcess {
217
235
  cwd: this.cwd,
218
236
  pid: process.pid,
219
237
  childPid: this.childPid,
238
+ childToken: this.childToken,
220
239
  agentType: this.agentType,
221
240
  command: this.commandTokens,
222
241
  commandLine: formatCommand(this.commandTokens),
@@ -232,6 +251,7 @@ class SeatProcess {
232
251
  cwd: this.cwd,
233
252
  pid: process.pid,
234
253
  childPid: this.childPid,
254
+ childToken: this.childToken,
235
255
  agentType: this.agentType,
236
256
  command: this.commandTokens,
237
257
  relayCount: this.relayCount,
@@ -307,6 +327,7 @@ class SeatProcess {
307
327
  cwd: this.cwd,
308
328
  env: {
309
329
  ...process.env,
330
+ MUUUUSE_CHILD_TOKEN: this.childToken,
310
331
  MUUUUSE_SEAT: String(this.seatId),
311
332
  MUUUUSE_SESSION: this.sessionName,
312
333
  },
@@ -315,6 +336,7 @@ class SeatProcess {
315
336
  });
316
337
 
317
338
  this.childPid = this.child.pid;
339
+ this.processStartedAtMs = Date.now();
318
340
  this.writeMeta();
319
341
  this.writeStatus({
320
342
  partnerSeatId: this.partnerSeatId,
@@ -352,15 +374,7 @@ class SeatProcess {
352
374
  }
353
375
 
354
376
  shouldUseGenericCapture() {
355
- if (!this.agentType) {
356
- return true;
357
- }
358
-
359
- if (this.sessionState.file) {
360
- return false;
361
- }
362
-
363
- return Date.now() - this.startedAtMs >= GENERIC_FALLBACK_DELAY_MS;
377
+ return !this.agentType;
364
378
  }
365
379
 
366
380
  pullPartnerEvents() {
@@ -401,7 +415,15 @@ class SeatProcess {
401
415
  return;
402
416
  }
403
417
 
404
- const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.startedAtMs);
418
+ const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.processStartedAtMs, {
419
+ snapshotEnv: this.agentType === "codex"
420
+ ? {
421
+ MUUUUSE_CHILD_TOKEN: this.childToken,
422
+ MUUUUSE_SEAT: String(this.seatId),
423
+ MUUUUSE_SESSION: this.sessionName,
424
+ }
425
+ : null,
426
+ });
405
427
  if (!sessionFile) {
406
428
  return;
407
429
  }