botmux 2.48.2 → 2.48.3

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.
@@ -0,0 +1,14 @@
1
+ import type { CodexBridgeEvent } from './codex-transcript.js';
2
+ export interface MtrTranscriptSource {
3
+ dbPath: string;
4
+ sessionId: string;
5
+ }
6
+ export declare function mtrDbCandidates(dataDir?: string): string[];
7
+ export declare function findMtrSessionById(sessionId: string | undefined, dbPaths?: string[]): MtrTranscriptSource | undefined;
8
+ export declare function findLatestMtrSessionByDirectory(directory: string | undefined, dbPaths?: string[]): MtrTranscriptSource | undefined;
9
+ export declare function drainMtrSession(source: MtrTranscriptSource | undefined, fromOffset: number): {
10
+ events: CodexBridgeEvent[];
11
+ newOffset: number;
12
+ };
13
+ export declare function currentMtrSessionOffset(source: MtrTranscriptSource | undefined): number;
14
+ //# sourceMappingURL=mtr-transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mtr-transcript.d.ts","sourceRoot":"","sources":["../../src/services/mtr-transcript.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAM9D,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AA0MD,wBAAgB,eAAe,CAAC,OAAO,SAAe,GAAG,MAAM,EAAE,CAUhE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,WAAoB,GAAG,mBAAmB,GAAG,SAAS,CAY9H;AAED,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,WAAoB,GAAG,mBAAmB,GAAG,SAAS,CAc3I;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAoC9I;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,CAGvF"}
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Reader for MTR's OpenCode-compatible SQLite session store.
3
+ *
4
+ * MTR persists conversations under ~/.local/share/opencode/mtr*.db. The schema
5
+ * stores message role/finish metadata in message.data and visible text in the
6
+ * part table. This reader maps completed user/assistant turns into the same
7
+ * bridge event shape used by Codex/CoCo/Hermes.
8
+ */
9
+ import { existsSync, readdirSync, statSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { spawnSync } from 'node:child_process';
13
+ const MTR_DATA_DIR = join(homedir(), '.local', 'share', 'opencode');
14
+ const MTR_DB_RE = /^mtr(?:-[A-Za-z0-9._-]+)?\.db$/;
15
+ const MTR_CURSOR_LOOKBACK_MS = 5_000;
16
+ function runPythonJson(script) {
17
+ const proc = spawnSync('python3', ['-c', script], { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
18
+ if (proc.status !== 0)
19
+ throw new Error((proc.stderr || proc.error?.message || 'python3 sqlite query failed').trim());
20
+ const stdout = proc.stdout.trim();
21
+ return (stdout ? JSON.parse(stdout) : []);
22
+ }
23
+ function jsonParseObject(raw) {
24
+ if (!raw)
25
+ return {};
26
+ try {
27
+ const parsed = JSON.parse(raw);
28
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ function numberValue(value) {
35
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
36
+ }
37
+ function messageTimestampMs(message, assistantFinal) {
38
+ const time = message.data.time;
39
+ if (time && typeof time === 'object') {
40
+ const t = time;
41
+ const completed = numberValue(t.completed);
42
+ if (assistantFinal && completed !== undefined)
43
+ return completed;
44
+ const created = numberValue(t.created);
45
+ if (created !== undefined)
46
+ return created;
47
+ }
48
+ return message.timeUpdated || message.timeCreated || Date.now();
49
+ }
50
+ function textFromParts(parts) {
51
+ const out = [];
52
+ for (const part of parts) {
53
+ if (part.data.type !== 'text')
54
+ continue;
55
+ if (part.data.ignored === true)
56
+ continue;
57
+ const text = part.data.text;
58
+ if (typeof text === 'string' && text.trim())
59
+ out.push(text);
60
+ }
61
+ return out.join('');
62
+ }
63
+ function groupRows(rows) {
64
+ const map = new Map();
65
+ for (const row of rows) {
66
+ let msg = map.get(row.message_id);
67
+ if (!msg) {
68
+ msg = {
69
+ id: row.message_id,
70
+ sessionId: row.session_id,
71
+ timeCreated: row.message_time_created ?? 0,
72
+ timeUpdated: row.message_time_updated ?? 0,
73
+ data: jsonParseObject(row.message_data),
74
+ parts: [],
75
+ };
76
+ map.set(row.message_id, msg);
77
+ }
78
+ if (typeof row.message_time_updated === 'number' && row.message_time_updated > msg.timeUpdated) {
79
+ msg.timeUpdated = row.message_time_updated;
80
+ }
81
+ if (row.part_id && row.part_data) {
82
+ msg.parts.push({
83
+ id: row.part_id,
84
+ timeUpdated: row.part_time_updated ?? 0,
85
+ data: jsonParseObject(row.part_data),
86
+ });
87
+ }
88
+ }
89
+ return Array.from(map.values()).sort((a, b) => (a.timeCreated - b.timeCreated) || a.id.localeCompare(b.id));
90
+ }
91
+ function queryChangedRows(source, offset) {
92
+ // MTR only gives us timestamp cursors. Re-read a small overlap so rows that
93
+ // share the previous max timestamp but commit after the last poll are not
94
+ // skipped by the exclusive SQL predicate. The bridge queue de-dupes by uuid.
95
+ const lowerBound = Math.max(0, offset - MTR_CURSOR_LOOKBACK_MS);
96
+ const script = `
97
+ import json
98
+ import sqlite3
99
+ conn = sqlite3.connect(${JSON.stringify(source.dbPath)})
100
+ conn.row_factory = sqlite3.Row
101
+ rows = conn.execute(
102
+ """
103
+ WITH changed AS (
104
+ SELECT m.id
105
+ FROM message m
106
+ LEFT JOIN part p ON p.message_id = m.id
107
+ WHERE m.session_id = ?
108
+ AND (m.time_updated > ? OR COALESCE(p.time_updated, 0) > ?)
109
+ GROUP BY m.id
110
+ )
111
+ SELECT
112
+ m.id AS message_id,
113
+ m.session_id AS session_id,
114
+ m.time_created AS message_time_created,
115
+ m.time_updated AS message_time_updated,
116
+ m.data AS message_data,
117
+ p.id AS part_id,
118
+ p.time_created AS part_time_created,
119
+ p.time_updated AS part_time_updated,
120
+ p.data AS part_data
121
+ FROM message m
122
+ LEFT JOIN part p ON p.message_id = m.id
123
+ WHERE m.id IN (SELECT id FROM changed)
124
+ ORDER BY m.time_created, m.id, p.time_created, p.id
125
+ """,
126
+ (${JSON.stringify(source.sessionId)}, ${JSON.stringify(lowerBound)}, ${JSON.stringify(lowerBound)}),
127
+ ).fetchall()
128
+ print(json.dumps([dict(r) for r in rows], ensure_ascii=False))
129
+ `;
130
+ return runPythonJson(script);
131
+ }
132
+ function currentOffset(source) {
133
+ const script = `
134
+ import sqlite3
135
+ conn = sqlite3.connect(${JSON.stringify(source.dbPath)})
136
+ row = conn.execute(
137
+ """
138
+ SELECT COALESCE(MAX(value), 0) FROM (
139
+ SELECT time_updated AS value FROM message WHERE session_id = ?
140
+ UNION ALL
141
+ SELECT time_updated AS value FROM part WHERE session_id = ?
142
+ )
143
+ """,
144
+ (${JSON.stringify(source.sessionId)}, ${JSON.stringify(source.sessionId)}),
145
+ ).fetchone()
146
+ print(row[0] or 0)
147
+ `;
148
+ const proc = spawnSync('python3', ['-c', script], { encoding: 'utf8' });
149
+ if (proc.status !== 0)
150
+ return 0;
151
+ return Number.parseInt(proc.stdout.trim(), 10) || 0;
152
+ }
153
+ function querySessionById(dbPath, sessionId) {
154
+ const script = `
155
+ import json
156
+ import sqlite3
157
+ conn = sqlite3.connect(${JSON.stringify(dbPath)})
158
+ conn.row_factory = sqlite3.Row
159
+ row = conn.execute(
160
+ "SELECT id, time_updated FROM session WHERE id = ? LIMIT 1",
161
+ (${JSON.stringify(sessionId)},),
162
+ ).fetchone()
163
+ print(json.dumps(dict(row), ensure_ascii=False) if row else "null")
164
+ `;
165
+ const row = runPythonJson(script);
166
+ return row || undefined;
167
+ }
168
+ function queryLatestSessionByDirectory(dbPath, directory) {
169
+ const script = `
170
+ import json
171
+ import sqlite3
172
+ conn = sqlite3.connect(${JSON.stringify(dbPath)})
173
+ conn.row_factory = sqlite3.Row
174
+ row = conn.execute(
175
+ """
176
+ SELECT id, time_updated
177
+ FROM session
178
+ WHERE directory = ?
179
+ ORDER BY time_updated DESC
180
+ LIMIT 1
181
+ """,
182
+ (${JSON.stringify(directory)},),
183
+ ).fetchone()
184
+ print(json.dumps(dict(row), ensure_ascii=False) if row else "null")
185
+ `;
186
+ const row = runPythonJson(script);
187
+ return row || undefined;
188
+ }
189
+ export function mtrDbCandidates(dataDir = MTR_DATA_DIR) {
190
+ if (!existsSync(dataDir))
191
+ return [];
192
+ let names;
193
+ try {
194
+ names = readdirSync(dataDir);
195
+ }
196
+ catch {
197
+ return [];
198
+ }
199
+ return names
200
+ .filter(name => MTR_DB_RE.test(name))
201
+ .map(name => join(dataDir, name))
202
+ .filter(path => {
203
+ try {
204
+ return statSync(path).isFile();
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ });
210
+ }
211
+ export function findMtrSessionById(sessionId, dbPaths = mtrDbCandidates()) {
212
+ if (!sessionId)
213
+ return undefined;
214
+ for (const dbPath of dbPaths) {
215
+ if (!existsSync(dbPath))
216
+ continue;
217
+ try {
218
+ const row = querySessionById(dbPath, sessionId);
219
+ if (row)
220
+ return { dbPath, sessionId: row.id };
221
+ }
222
+ catch {
223
+ continue;
224
+ }
225
+ }
226
+ return undefined;
227
+ }
228
+ export function findLatestMtrSessionByDirectory(directory, dbPaths = mtrDbCandidates()) {
229
+ if (!directory)
230
+ return undefined;
231
+ let best;
232
+ for (const dbPath of dbPaths) {
233
+ if (!existsSync(dbPath))
234
+ continue;
235
+ try {
236
+ const row = queryLatestSessionByDirectory(dbPath, directory);
237
+ if (!row)
238
+ continue;
239
+ if (!best || (row.time_updated ?? 0) > (best.row.time_updated ?? 0))
240
+ best = { dbPath, row };
241
+ }
242
+ catch {
243
+ continue;
244
+ }
245
+ }
246
+ return best ? { dbPath: best.dbPath, sessionId: best.row.id } : undefined;
247
+ }
248
+ export function drainMtrSession(source, fromOffset) {
249
+ if (!source || !existsSync(source.dbPath))
250
+ return { events: [], newOffset: fromOffset };
251
+ const rows = queryChangedRows(source, fromOffset);
252
+ let newOffset = fromOffset;
253
+ const events = [];
254
+ for (const msg of groupRows(rows)) {
255
+ newOffset = Math.max(newOffset, msg.timeUpdated, ...msg.parts.map(part => part.timeUpdated));
256
+ const role = msg.data.role;
257
+ if (role === 'user') {
258
+ const text = textFromParts(msg.parts);
259
+ if (!text)
260
+ continue;
261
+ events.push({
262
+ uuid: `mtr:${source.dbPath}:${msg.id}`,
263
+ timestampMs: messageTimestampMs(msg, false),
264
+ kind: 'user',
265
+ text,
266
+ sourceSessionId: msg.sessionId,
267
+ });
268
+ }
269
+ else if (role === 'assistant') {
270
+ if (msg.data.finish !== 'stop')
271
+ continue;
272
+ const text = textFromParts(msg.parts);
273
+ if (!text)
274
+ continue;
275
+ events.push({
276
+ uuid: `mtr:${source.dbPath}:${msg.id}`,
277
+ timestampMs: messageTimestampMs(msg, true),
278
+ kind: 'assistant_final',
279
+ text,
280
+ sourceSessionId: msg.sessionId,
281
+ });
282
+ }
283
+ }
284
+ return { events, newOffset };
285
+ }
286
+ export function currentMtrSessionOffset(source) {
287
+ if (!source || !existsSync(source.dbPath))
288
+ return 0;
289
+ return currentOffset(source);
290
+ }
291
+ //# sourceMappingURL=mtr-transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mtr-transcript.js","sourceRoot":"","sources":["../../src/services/mtr-transcript.ts"],"names":[],"mappings":"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"}
package/dist/worker.js CHANGED
@@ -23,6 +23,7 @@ import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
23
23
  import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn } from './services/codex-transcript.js';
24
24
  import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
25
25
  import { currentHermesStateOffset, drainHermesStateDb } from './services/hermes-transcript.js';
26
+ import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
26
27
  import { baselineJsonlCursor } from './services/jsonl-cursor.js';
27
28
  import { dirname } from 'node:path';
28
29
  import { createServer as createHttpServer } from 'node:http';
@@ -31,6 +32,7 @@ import { TerminalRenderer } from './utils/terminal-renderer.js';
31
32
  import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
32
33
  import { createCliAdapterSync } from './adapters/cli/registry.js';
33
34
  import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds } from './adapters/cli/claude-code.js';
35
+ import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
34
36
  import { TmuxBackend } from './adapters/backend/tmux-backend.js';
35
37
  import { TmuxPipeBackend } from './adapters/backend/tmux-pipe-backend.js';
36
38
  import { selectSessionBackend } from './adapters/backend/session-backend-selector.js';
@@ -202,6 +204,9 @@ let codexBridgeWatcher = null;
202
204
  let codexBridgeTimer = null;
203
205
  let hermesBridgeOffset = 0;
204
206
  let hermesBridgeBaselineDone = false;
207
+ let mtrBridgeSource;
208
+ let mtrBridgeOffset = 0;
209
+ let mtrBridgeBaselineDone = false;
205
210
  /** Codex sessionId we received via writeInput but haven't yet resolved a
206
211
  * rollout file for. The poller keeps retrying — the file appears on
207
212
  * Codex's first user submit, but with some race delay after our submit
@@ -1332,7 +1337,7 @@ function drainPathInto(path, fromOffset) {
1332
1337
  function codexBridgeFallbackActive() {
1333
1338
  // True for transcript-backed CLIs whose final output can be harvested
1334
1339
  // when the model forgets to call `botmux send`.
1335
- return lastInitConfig?.cliId === 'codex' || lastInitConfig?.cliId === 'coco' || lastInitConfig?.cliId === 'hermes';
1340
+ return lastInitConfig?.cliId === 'codex' || lastInitConfig?.cliId === 'coco' || lastInitConfig?.cliId === 'hermes' || lastInitConfig?.cliId === 'mtr';
1336
1341
  }
1337
1342
  function structuredBridgeIsCodex() {
1338
1343
  return lastInitConfig?.cliId === 'codex';
@@ -1340,6 +1345,9 @@ function structuredBridgeIsCodex() {
1340
1345
  function structuredBridgeIsHermes() {
1341
1346
  return lastInitConfig?.cliId === 'hermes';
1342
1347
  }
1348
+ function structuredBridgeIsMtr() {
1349
+ return lastInitConfig?.cliId === 'mtr';
1350
+ }
1343
1351
  function structuredBridgeIngestPath(path, offset) {
1344
1352
  if (structuredBridgeIsCodex())
1345
1353
  return drainCodexRollout(path, offset);
@@ -1373,6 +1381,23 @@ function codexBridgeStartTimer() {
1373
1381
  emitReadyCodexTurns();
1374
1382
  return;
1375
1383
  }
1384
+ if (structuredBridgeIsMtr()) {
1385
+ if (!mtrBridgeSource) {
1386
+ const source = findMtrSessionById(codexBridgePendingSessionId)
1387
+ ?? (lastInitConfig?.adoptMode
1388
+ ? findLatestMtrSessionByDirectory(lastInitConfig.adoptCwd ?? lastInitConfig.workingDir)
1389
+ : undefined);
1390
+ if (source) {
1391
+ codexBridgePendingSessionId = undefined;
1392
+ codexAdoptPendingPid = undefined;
1393
+ mtrBridgeAttach(source, lastInitConfig?.adoptMode ? 'split-live' : 'fresh-empty');
1394
+ }
1395
+ }
1396
+ mtrBridgeIngest();
1397
+ if (isPromptReady)
1398
+ emitReadyCodexTurns();
1399
+ return;
1400
+ }
1376
1401
  if (!codexBridgeRolloutPath) {
1377
1402
  // Two discovery paths, in order: cliSessionId (known via writeInput
1378
1403
  // result for non-adopt or daemon-side probe for adopt) → exact
@@ -1442,6 +1467,44 @@ function hermesBridgeIngest() {
1442
1467
  idleDetector?.fireIdle();
1443
1468
  }
1444
1469
  }
1470
+ function mtrBridgeAttach(source, mode) {
1471
+ mtrBridgeSource = source;
1472
+ if (mode === 'split-live') {
1473
+ const result = drainMtrSession(source, 0);
1474
+ const cutoff = (codexAdoptStartMs ?? Date.now()) - 5_000;
1475
+ const { history, live } = splitCodexEventsByCutoff(result.events, cutoff);
1476
+ codexBridgeQueue.absorb(history);
1477
+ codexBridgeQueue.ingest(live);
1478
+ mtrBridgeOffset = result.newOffset;
1479
+ mtrBridgeBaselineDone = true;
1480
+ log(`MTR bridge split-live: ${source.dbPath}#${source.sessionId} (history=${history.length}, live=${live.length}, cutoff=${cutoff}, offset=${mtrBridgeOffset})`);
1481
+ maybeEmitCodexAdoptPreamble(history);
1482
+ }
1483
+ else if (mode === 'baseline-existing') {
1484
+ const baseline = currentMtrSessionOffset(source);
1485
+ const result = drainMtrSession(source, baseline);
1486
+ codexBridgeQueue.absorb(result.events);
1487
+ mtrBridgeOffset = Math.max(baseline, result.newOffset);
1488
+ mtrBridgeBaselineDone = true;
1489
+ log(`MTR bridge baselined: ${source.dbPath}#${source.sessionId} (offset=${mtrBridgeOffset}, absorbed=${result.events.length})`);
1490
+ }
1491
+ else {
1492
+ mtrBridgeOffset = 0;
1493
+ mtrBridgeBaselineDone = true;
1494
+ log(`MTR bridge fresh-empty: ${source.dbPath}#${source.sessionId}`);
1495
+ }
1496
+ codexBridgeStartTimer();
1497
+ }
1498
+ function mtrBridgeIngest() {
1499
+ if (!mtrBridgeBaselineDone || !mtrBridgeSource)
1500
+ return;
1501
+ const result = drainMtrSession(mtrBridgeSource, mtrBridgeOffset);
1502
+ mtrBridgeOffset = result.newOffset;
1503
+ codexBridgeQueue.ingest(result.events);
1504
+ if (result.events.some(e => e.kind === 'assistant_final')) {
1505
+ idleDetector?.fireIdle();
1506
+ }
1507
+ }
1445
1508
  function codexBridgeAttach(rolloutPath, mode) {
1446
1509
  codexBridgeRolloutPath = rolloutPath;
1447
1510
  if (mode === 'fresh-empty') {
@@ -1525,6 +1588,18 @@ function codexBridgeAttach(rolloutPath, mode) {
1525
1588
  function codexBridgeNotifyCliSessionId(cliSessionId) {
1526
1589
  if (!codexBridgeFallbackActive() || codexBridgeRolloutPath)
1527
1590
  return;
1591
+ if (structuredBridgeIsMtr()) {
1592
+ const source = findMtrSessionById(cliSessionId);
1593
+ if (source) {
1594
+ codexBridgePendingSessionId = undefined;
1595
+ mtrBridgeAttach(source, 'fresh-empty');
1596
+ }
1597
+ else {
1598
+ codexBridgePendingSessionId = cliSessionId;
1599
+ codexBridgeStartTimer();
1600
+ }
1601
+ return;
1602
+ }
1528
1603
  const path = findCodexRolloutBySessionId(cliSessionId);
1529
1604
  if (path) {
1530
1605
  codexBridgePendingSessionId = undefined;
@@ -1540,6 +1615,10 @@ function codexBridgeIngest() {
1540
1615
  hermesBridgeIngest();
1541
1616
  return;
1542
1617
  }
1618
+ if (structuredBridgeIsMtr()) {
1619
+ mtrBridgeIngest();
1620
+ return;
1621
+ }
1543
1622
  if (!codexBridgeRolloutPath || !codexBridgeBaselineDone)
1544
1623
  return;
1545
1624
  const result = structuredBridgeIngestPath(codexBridgeRolloutPath, codexBridgeOffset);
@@ -1569,7 +1648,7 @@ function codexBridgeMarkPendingTurn(messageText) {
1569
1648
  function codexBridgeDrainAndMaybeEmit() {
1570
1649
  if (!codexBridgeFallbackActive())
1571
1650
  return;
1572
- if (structuredBridgeIsHermes() || (codexBridgeRolloutPath && codexBridgeBaselineDone)) {
1651
+ if (structuredBridgeIsHermes() || structuredBridgeIsMtr() || (codexBridgeRolloutPath && codexBridgeBaselineDone)) {
1573
1652
  try {
1574
1653
  codexBridgeIngest();
1575
1654
  }
@@ -1648,6 +1727,9 @@ function stopCodexBridge() {
1648
1727
  codexBridgeBaselineDone = false;
1649
1728
  hermesBridgeOffset = 0;
1650
1729
  hermesBridgeBaselineDone = false;
1730
+ mtrBridgeSource = undefined;
1731
+ mtrBridgeOffset = 0;
1732
+ mtrBridgeBaselineDone = false;
1651
1733
  codexBridgeQueue.clearPending();
1652
1734
  codexBridgeQueue.setLocalTurns(false);
1653
1735
  codexBridgePendingSessionId = undefined;
@@ -2767,13 +2849,29 @@ function spawnCli(cfg) {
2767
2849
  // and is idempotent (no-op if already started).
2768
2850
  codexBridgeStartTimer();
2769
2851
  }
2852
+ else if (cfg.cliId === 'mtr') {
2853
+ const adoptStartMs = Date.now();
2854
+ codexAdoptStartMs = adoptStartMs;
2855
+ codexBridgeQueue.setLocalTurns(true, adoptStartMs);
2856
+ if (cfg.cliSessionId)
2857
+ codexBridgePendingSessionId = cfg.cliSessionId;
2858
+ const source = findMtrSessionById(cfg.cliSessionId)
2859
+ ?? findLatestMtrSessionByDirectory(cfg.adoptCwd ?? cfg.workingDir);
2860
+ if (source) {
2861
+ codexBridgePendingSessionId = undefined;
2862
+ mtrBridgeAttach(source, 'split-live');
2863
+ }
2864
+ else {
2865
+ codexBridgeStartTimer();
2866
+ }
2867
+ }
2770
2868
  // Idle detection. In bridge mode we use the adopted CLI's real
2771
2869
  // completion/ready patterns (e.g. "Worked for Xs") so tool-execution
2772
2870
  // pauses don't trigger a premature emit. Other adopt cases keep the
2773
2871
  // minimal output-quiescence-only detector.
2774
2872
  const idleAdapter = cfg.bridgeJsonlPath
2775
2873
  ? createCliAdapterSync('claude-code', undefined)
2776
- : cfg.cliId === 'codex' || cfg.cliId === 'coco'
2874
+ : cfg.cliId === 'codex' || cfg.cliId === 'coco' || cfg.cliId === 'mtr'
2777
2875
  ? createCliAdapterSync(cfg.cliId, undefined)
2778
2876
  : { completionPattern: undefined, readyPattern: undefined };
2779
2877
  idleDetector = new IdleDetector(idleAdapter);
@@ -2789,6 +2887,9 @@ function spawnCli(cfg) {
2789
2887
  if (cfg.cliId === 'codex') {
2790
2888
  cliAdapter = createCliAdapterSync('codex', cfg.cliPathOverride);
2791
2889
  }
2890
+ else if (cfg.cliId === 'mtr') {
2891
+ cliAdapter = createCliAdapterSync('mtr', cfg.cliPathOverride);
2892
+ }
2792
2893
  idleDetector.onIdle(() => {
2793
2894
  log('Prompt detected (idle) — adopt mode');
2794
2895
  try {
@@ -2960,8 +3061,8 @@ function spawnCli(cfg) {
2960
3061
  // calling `botmux send`, harvest the final answer from the CLI transcript
2961
3062
  // and post it to Lark. Codex needs late attach because its rollout id is
2962
3063
  // discovered after the first submit; CoCo's events path is deterministic
2963
- // from botmux sessionId. Hermes uses a global SQLite store, so baseline its
2964
- // row id at spawn and poll for rows after each queued prompt is flushed.
3064
+ // from botmux sessionId. Hermes and MTR use SQLite stores, so baseline the
3065
+ // relevant cursor at spawn and poll for rows after each queued prompt flushes.
2965
3066
  if (cfg.cliId === 'hermes') {
2966
3067
  hermesBridgeAttach(cfg.resume ? 'baseline-existing' : 'fresh-empty');
2967
3068
  }
@@ -2985,6 +3086,17 @@ function spawnCli(cfg) {
2985
3086
  codexBridgeAttach(eventsPath, cfg.resume ? 'baseline-existing' : 'fresh-empty');
2986
3087
  codexBridgeStartTimer();
2987
3088
  }
3089
+ else if (cfg.cliId === 'mtr') {
3090
+ const mtrSessionId = cfg.cliSessionId ?? mtrSessionIdForBotmuxSession(cfg.sessionId);
3091
+ codexBridgePendingSessionId = mtrSessionId;
3092
+ const source = findMtrSessionById(mtrSessionId);
3093
+ if (source) {
3094
+ mtrBridgeAttach(source, cfg.resume ? 'baseline-existing' : 'fresh-empty');
3095
+ }
3096
+ else {
3097
+ codexBridgeStartTimer();
3098
+ }
3099
+ }
2988
3100
  // Set up idle detection
2989
3101
  idleDetector = new IdleDetector(cliAdapter);
2990
3102
  idleDetector.onIdle(() => {