botmux 2.60.0 → 2.60.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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Reader for Cursor Agent's per-chat transcript JSONL.
3
+ *
4
+ * `cursor-agent` keeps each chat's authoritative store in a SQLite file
5
+ * ~/.cursor/chats/<projectHash>/<chatId>/store.db
6
+ * (held open via fd for the whole session) and, in parallel, mirrors the
7
+ * conversation into an append-only JSONL transcript at
8
+ * ~/.cursor/projects/<projectSlug>/agent-transcripts/<chatId>/<chatId>.jsonl
9
+ *
10
+ * The bridge reads the JSONL (not the SQLite store) because it's append-only
11
+ * plain text — the same integration surface the Codex/CoCo bridges use. Each
12
+ * line is `{ role: 'user' | 'assistant', message: { content: [...] } }` where
13
+ * a content block is either `{ type: 'text', text }` or `{ type: 'tool_use',
14
+ * name, input }`. Tool *results* are not recorded.
15
+ *
16
+ * Where Cursor sits between the two existing bridge transcript shapes:
17
+ * - Claude is a STREAMING event stream — one role:user event, then a run of
18
+ * role:assistant events whose text grows incrementally; a turn has no
19
+ * explicit terminator, so the bridge queue tracks the in-flight turn with
20
+ * a `collecting` pointer.
21
+ * - Codex is DISCRETE complete events — exactly one user_message and one
22
+ * assistant_final per turn, each carrying the full text, with a definite
23
+ * terminator (phase=final_answer).
24
+ * Cursor is a hybrid: each JSONL line is a DISCRETE, complete event (verified
25
+ * empirically — assistant lines are never growing prefixes of one another, so
26
+ * there is no Claude-style snapshot replay risk), but a turn is composed of
27
+ * MANY assistant lines (one per step). Crucially it still has a definite
28
+ * terminator: every intermediate step pairs its narration with a `tool_use`
29
+ * block, and the agent loop only stops when the model returns a message with
30
+ * NO tool_use. So a `text`-only assistant line is the end-of-turn final reply:
31
+ * - role=user → the user's prompt
32
+ * - role=assistant, text & no tool_use → the model's final reply
33
+ * Every line carrying a tool_use block is an intermediate step and is dropped.
34
+ * This lets the reader distill Cursor's multi-event turn down to Codex's
35
+ * two-event (user, assistant_final) shape, so it can reuse the proven
36
+ * CodexBridgeQueue attribution as-is rather than a Claude-style streaming
37
+ * accumulator.
38
+ *
39
+ * Consequences of that distillation (intentional):
40
+ * - Only the final wrap-up text is forwarded; the short per-step narrations
41
+ * ("Let me read…", "Now I'll check…") are deliberately not relayed to Lark.
42
+ * - An interrupted turn (process killed / Esc mid-tool, leaving no text-only
43
+ * line) emits NOTHING rather than a half-answer — the safe failure mode.
44
+ *
45
+ * Cursor's JSONL carries no per-event timestamp, so the worker baselines this
46
+ * transcript by byte offset at adopt time (history is behind the offset and
47
+ * never re-ingested) and stamps live events with the drain wall-clock. That's
48
+ * why every emitted event uses `Date.now()` for `timestampMs` — enough for the
49
+ * shared CodexBridgeQueue's freshness gates given the offset baseline.
50
+ *
51
+ * Pure I/O. Attribution belongs in CodexBridgeQueue.
52
+ */
53
+ import { existsSync, statSync, openSync, readSync, closeSync, readdirSync, readlinkSync } from 'node:fs';
54
+ import { execSync } from 'node:child_process';
55
+ import { homedir, platform } from 'node:os';
56
+ import { join } from 'node:path';
57
+ const IS_LINUX = platform() === 'linux';
58
+ const CHAT_ID_RE = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
59
+ /** Default `~/.cursor/projects` root. Overridable by callers (tests) so the
60
+ * scan doesn't depend on a real home directory. */
61
+ export function cursorProjectsRoot() {
62
+ return join(homedir(), '.cursor', 'projects');
63
+ }
64
+ /** Extract the chatId encoded in a Cursor store.db path of the shape
65
+ * `.../.cursor/chats/<projectHash>/<chatId>/store.db` (also matches the
66
+ * `-wal` / `-shm` sidecar files SQLite keeps open). The chatId is the same
67
+ * UUID used to name the agent-transcript JSONL, so it's the bridge between
68
+ * the open fd and the transcript file. Returns undefined for non-matching
69
+ * paths. */
70
+ export function cursorChatIdFromStoreDbPath(path) {
71
+ const re = new RegExp(`/\\.cursor/chats/[^/]+/(${CHAT_ID_RE})/store\\.db(?:-wal|-shm)?$`, 'i');
72
+ const m = re.exec(path);
73
+ return m ? m[1] : undefined;
74
+ }
75
+ /** Find the chatId of an externally-running cursor-agent process by reading
76
+ * the store.db file it keeps open. cursor-agent holds an fd on its current
77
+ * chat's SQLite store for the whole session lifetime, which makes this the
78
+ * authoritative pid→chatId binding — far more reliable than scanning chat
79
+ * dirs by mtime (which would race with sibling cursor-agent panes).
80
+ *
81
+ * Linux: `/proc/<pid>/fd/*` fast path. macOS / BSD: `lsof -p <pid> -Fn`
82
+ * fallback (same shape as codex-transcript.findCodexRolloutByPid). */
83
+ export function findCursorChatIdByPid(pid) {
84
+ if (!Number.isInteger(pid) || pid <= 0)
85
+ return undefined;
86
+ if (IS_LINUX) {
87
+ const fdDir = `/proc/${pid}/fd`;
88
+ if (existsSync(fdDir)) {
89
+ let entries;
90
+ try {
91
+ entries = readdirSync(fdDir);
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ for (const fd of entries) {
97
+ let target;
98
+ try {
99
+ target = readlinkSync(join(fdDir, fd));
100
+ }
101
+ catch {
102
+ continue;
103
+ }
104
+ const chatId = cursorChatIdFromStoreDbPath(target);
105
+ if (chatId)
106
+ return chatId;
107
+ }
108
+ return undefined;
109
+ }
110
+ // /proc unreadable — fall through to lsof.
111
+ }
112
+ let out;
113
+ try {
114
+ out = execSync(`lsof -p ${pid} -Fn`, {
115
+ encoding: 'utf-8',
116
+ stdio: ['pipe', 'pipe', 'pipe'],
117
+ });
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ for (const line of out.split('\n')) {
123
+ if (!line.startsWith('n/'))
124
+ continue;
125
+ const chatId = cursorChatIdFromStoreDbPath(line.slice(1));
126
+ if (chatId)
127
+ return chatId;
128
+ }
129
+ return undefined;
130
+ }
131
+ /** Locate the agent-transcript JSONL for a given chatId. The chatId is a
132
+ * globally-unique UUID, so a one-shot scan of the (small) projects root for
133
+ * `<slug>/agent-transcripts/<chatId>/<chatId>.jsonl` is unambiguous and
134
+ * avoids having to reproduce Cursor's opaque cwd→slug hashing. */
135
+ export function findCursorTranscriptByChatId(chatId, projectsRoot = cursorProjectsRoot()) {
136
+ if (!chatId || !existsSync(projectsRoot))
137
+ return undefined;
138
+ let slugs;
139
+ try {
140
+ slugs = readdirSync(projectsRoot);
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ for (const slug of slugs) {
146
+ const candidate = join(projectsRoot, slug, 'agent-transcripts', chatId, `${chatId}.jsonl`);
147
+ if (existsSync(candidate))
148
+ return candidate;
149
+ }
150
+ return undefined;
151
+ }
152
+ /** Resolve the transcript path for an externally-running cursor-agent pid:
153
+ * pid → open store.db → chatId → agent-transcript JSONL. Returns both the
154
+ * path and the chatId so the caller can remember the chatId for a later
155
+ * retry if the JSONL isn't on disk yet. */
156
+ export function findCursorTranscriptByPid(pid, projectsRoot = cursorProjectsRoot()) {
157
+ const chatId = findCursorChatIdByPid(pid);
158
+ if (!chatId)
159
+ return undefined;
160
+ const path = findCursorTranscriptByChatId(chatId, projectsRoot);
161
+ return path ? { path, chatId } : undefined;
162
+ }
163
+ /** Concatenate the text of all `type:'text'` blocks. Cursor uses the same
164
+ * `{type:'text', text}` shape for both user prompts and assistant replies;
165
+ * `tool_use` (and any other) blocks are ignored — the bridge only forwards
166
+ * text. Tolerates a bare-string content for defensiveness. */
167
+ function joinTextBlocks(content) {
168
+ if (typeof content === 'string')
169
+ return content;
170
+ if (!Array.isArray(content))
171
+ return '';
172
+ const parts = [];
173
+ for (const block of content) {
174
+ if (block && typeof block === 'object' && block.type === 'text') {
175
+ const text = block.text;
176
+ if (typeof text === 'string')
177
+ parts.push(text);
178
+ }
179
+ }
180
+ return parts.join('\n');
181
+ }
182
+ /** True when an assistant content array contains at least one tool_use block,
183
+ * i.e. this is a mid-turn step rather than the final reply. */
184
+ function hasToolUse(content) {
185
+ if (!Array.isArray(content))
186
+ return false;
187
+ return content.some(b => b && typeof b === 'object' && b.type === 'tool_use');
188
+ }
189
+ const CURSOR_REASONING_LEAK_HEADING_RE = new RegExp([
190
+ '\\n{2,}\\*\\*(?:',
191
+ [
192
+ 'Considering',
193
+ 'Thinking',
194
+ 'Planning',
195
+ 'Inspecting',
196
+ 'Exploring',
197
+ 'Reviewing',
198
+ 'Troubleshooting',
199
+ 'Diagnosing',
200
+ 'Evaluating',
201
+ 'Running',
202
+ 'Checking',
203
+ 'Reading',
204
+ 'Understanding',
205
+ 'Analyzing',
206
+ 'Debugging',
207
+ 'Responding',
208
+ ].join('|'),
209
+ ')\\b[^*\\n]{0,80}\\*\\*\\n{2,}',
210
+ ].join(''));
211
+ function stripCursorReasoningLeak(text) {
212
+ // Cursor's mirror can append the model's internal planning/debug summary to
213
+ // the same text-only assistant line that otherwise represents the final user
214
+ // reply. The leak starts with a bold English activity heading after a blank
215
+ // paragraph, e.g. "**Considering user response**".
216
+ const marker = CURSOR_REASONING_LEAK_HEADING_RE.exec(text);
217
+ if (!marker || marker.index <= 0)
218
+ return text;
219
+ return text.slice(0, marker.index).trimEnd();
220
+ }
221
+ function eventFromLine(path, lineStart, obj, timestampMs) {
222
+ const role = obj?.role ?? obj?.message?.role;
223
+ const content = obj?.message?.content;
224
+ if (role === 'user') {
225
+ const t = joinTextBlocks(content);
226
+ if (!t)
227
+ return undefined;
228
+ return { uuid: `${path}:${lineStart}`, timestampMs, kind: 'user', text: t };
229
+ }
230
+ if (role === 'assistant') {
231
+ // A turn ends with a text-only assistant line; any line carrying a
232
+ // tool_use block is an intermediate step and must not be forwarded.
233
+ if (hasToolUse(content))
234
+ return undefined;
235
+ const t = stripCursorReasoningLeak(joinTextBlocks(content));
236
+ if (!t)
237
+ return undefined;
238
+ return { uuid: `${path}:${lineStart}`, timestampMs, kind: 'assistant_final', text: t };
239
+ }
240
+ return undefined;
241
+ }
242
+ /** Increment-read the transcript from `fromOffset`. Mirrors the byte-offset
243
+ * contract of codex-transcript.drainCodexRollout so the worker can reuse the
244
+ * same fs.watch / poll wakeup machinery and the shared CodexBridgeQueue. */
245
+ export function drainCursorTranscript(path, fromOffset) {
246
+ if (!existsSync(path))
247
+ return { events: [], newOffset: fromOffset, pendingTail: '' };
248
+ let size;
249
+ try {
250
+ size = statSync(path).size;
251
+ }
252
+ catch {
253
+ return { events: [], newOffset: fromOffset, pendingTail: '' };
254
+ }
255
+ let start = fromOffset;
256
+ // Cursor's mirror can briefly disappear / shrink while it rewrites. Do not
257
+ // reset to 0 here: replaying the full history pollutes attribution state and
258
+ // can wedge a live turn behind old events. Wait for the mirror to grow past
259
+ // the last consumed byte instead.
260
+ if (size < start)
261
+ return { events: [], newOffset: fromOffset, pendingTail: '' };
262
+ if (size === start)
263
+ return { events: [], newOffset: start, pendingTail: '' };
264
+ const len = size - start;
265
+ const buf = Buffer.alloc(len);
266
+ const fd = openSync(path, 'r');
267
+ try {
268
+ readSync(fd, buf, 0, len, start);
269
+ }
270
+ finally {
271
+ closeSync(fd);
272
+ }
273
+ const text = buf.toString('utf8');
274
+ const lastNl = text.lastIndexOf('\n');
275
+ const completeText = lastNl >= 0 ? text.slice(0, lastNl + 1) : '';
276
+ let pendingTail = lastNl >= 0 ? text.slice(lastNl + 1) : text;
277
+ let newOffset = start + Buffer.byteLength(completeText, 'utf8');
278
+ const events = [];
279
+ // Track byte offset within the file so synthetic uuids are stable across
280
+ // re-drains (the transcript is append-only).
281
+ let cursor = start;
282
+ for (const line of completeText.split('\n')) {
283
+ if (line.length === 0) {
284
+ cursor += 1; // the \n after an empty line
285
+ continue;
286
+ }
287
+ const lineByteLen = Buffer.byteLength(line, 'utf8') + 1; // include \n
288
+ const lineStart = cursor;
289
+ cursor += lineByteLen;
290
+ let obj;
291
+ try {
292
+ obj = JSON.parse(line);
293
+ }
294
+ catch {
295
+ continue;
296
+ }
297
+ // No per-event timestamp in Cursor's JSONL — stamp with the drain
298
+ // wall-clock. Combined with byte-offset baselining at attach, this keeps
299
+ // the CodexBridgeQueue freshness gates happy without a real timestamp.
300
+ const timestampMs = Date.now();
301
+ const ev = eventFromLine(path, lineStart, obj, timestampMs);
302
+ if (ev)
303
+ events.push(ev);
304
+ }
305
+ // Cursor frequently leaves the final JSON object at EOF without a trailing
306
+ // newline until the next turn mutates the mirror. If the tail is already a
307
+ // complete JSON object, consume it now; otherwise keep it pending.
308
+ if (pendingTail.length > 0) {
309
+ try {
310
+ const lineStart = newOffset;
311
+ const obj = JSON.parse(pendingTail);
312
+ const ev = eventFromLine(path, lineStart, obj, Date.now());
313
+ if (ev)
314
+ events.push(ev);
315
+ newOffset = size;
316
+ pendingTail = '';
317
+ }
318
+ catch {
319
+ // Still being written.
320
+ }
321
+ }
322
+ return { events, newOffset, pendingTail };
323
+ }
324
+ //# sourceMappingURL=cursor-transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-transcript.js","sourceRoot":"","sources":["../../src/services/cursor-transcript.ts"],"names":[],"mappings":"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"}
package/dist/worker.js CHANGED
@@ -26,6 +26,7 @@ import { findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/t
26
26
  import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
27
27
  import { currentHermesStateOffset, drainHermesStateDb } from './services/hermes-transcript.js';
28
28
  import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
29
+ import { drainCursorTranscript, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
29
30
  import { baselineJsonlCursor } from './services/jsonl-cursor.js';
30
31
  import { dirname } from 'node:path';
31
32
  import { createServer as createHttpServer } from 'node:http';
@@ -1433,7 +1434,16 @@ function drainPathInto(path, fromOffset) {
1433
1434
  function codexBridgeFallbackActive() {
1434
1435
  // True for transcript-backed CLIs whose final output can be harvested
1435
1436
  // when the model forgets to call `botmux send`.
1436
- return lastInitConfig?.cliId === 'codex' || lastInitConfig?.cliId === 'traex' || lastInitConfig?.cliId === 'coco' || lastInitConfig?.cliId === 'hermes' || lastInitConfig?.cliId === 'mtr';
1437
+ const id = lastInitConfig?.cliId;
1438
+ if (id === 'codex' || id === 'traex' || id === 'coco' || id === 'hermes' || id === 'mtr')
1439
+ return true;
1440
+ // Cursor only harvests its transcript in adopt mode: a botmux-spawned
1441
+ // cursor session carries the botmux skill and replies via `botmux send`,
1442
+ // and we never resolve a transcript path for it — so leave that flow
1443
+ // (screen capture + botmux send) untouched and scope the bridge to adopt.
1444
+ if (id === 'cursor')
1445
+ return lastInitConfig?.adoptMode === true;
1446
+ return false;
1437
1447
  }
1438
1448
  // Both Codex and TRAE share the same rollout JSONL layout (response_item
1439
1449
  // messages), so drainCodexRollout works for both.
@@ -1446,9 +1456,14 @@ function structuredBridgeIsHermes() {
1446
1456
  function structuredBridgeIsMtr() {
1447
1457
  return lastInitConfig?.cliId === 'mtr';
1448
1458
  }
1459
+ function codexBridgeIsCursor() {
1460
+ return lastInitConfig?.cliId === 'cursor';
1461
+ }
1449
1462
  function structuredBridgeIngestPath(path, offset) {
1450
1463
  if (structuredBridgeIsCodex())
1451
1464
  return drainCodexRollout(path, offset);
1465
+ if (codexBridgeIsCursor())
1466
+ return drainCursorTranscript(path, offset);
1452
1467
  if (structuredBridgeIsHermes()) {
1453
1468
  const result = drainHermesStateDb(offset);
1454
1469
  return { events: result.events, newOffset: result.newOffset, pendingTail: '' };
@@ -1496,6 +1511,30 @@ function codexBridgeStartTimer() {
1496
1511
  emitReadyCodexTurns();
1497
1512
  return;
1498
1513
  }
1514
+ if (codexBridgeIsCursor()) {
1515
+ // Late-attach: the transcript usually exists at adopt time (the
1516
+ // session is already running), so cursorBridgeAttach in setup wins.
1517
+ // This covers the rare race where pid→chatId resolved but the JSONL
1518
+ // hadn't been created yet. Resolution order: chatId (cliSessionId) →
1519
+ // path; then adopt pid → store.db fd → chatId → path.
1520
+ if (!codexBridgeRolloutPath) {
1521
+ let path = codexBridgePendingSessionId
1522
+ ? findCursorTranscriptByChatId(codexBridgePendingSessionId)
1523
+ : undefined;
1524
+ if (!path && codexAdoptPendingPid) {
1525
+ path = findCursorTranscriptByPid(codexAdoptPendingPid)?.path;
1526
+ }
1527
+ if (path) {
1528
+ codexBridgePendingSessionId = undefined;
1529
+ codexAdoptPendingPid = undefined;
1530
+ cursorBridgeAttach(path, cursorLateAttachMode(path));
1531
+ }
1532
+ }
1533
+ codexBridgeIngest();
1534
+ if (isPromptReady)
1535
+ emitReadyCodexTurns();
1536
+ return;
1537
+ }
1499
1538
  if (!codexBridgeRolloutPath) {
1500
1539
  // Two discovery paths, in order: cliSessionId (known via writeInput
1501
1540
  // result for non-adopt or daemon-side probe for adopt) → exact
@@ -1661,6 +1700,17 @@ function codexBridgeAttach(rolloutPath, mode) {
1661
1700
  codexBridgeBaselineDone = true;
1662
1701
  log(`Codex bridge split-live degraded to fresh (file missing): ${rolloutPath}`);
1663
1702
  }
1703
+ else if (mode === 'baseline-existing-skip-tail' && existsSync(rolloutPath)) {
1704
+ let size = 0;
1705
+ try {
1706
+ size = statSync(rolloutPath).size;
1707
+ }
1708
+ catch { /* degrade below */ }
1709
+ codexBridgeOffset = size;
1710
+ codexBridgePendingTail = '';
1711
+ codexBridgeBaselineDone = true;
1712
+ log(`Codex bridge baselined: ${rolloutPath} (offset=${codexBridgeOffset}, skipTail=true)`);
1713
+ }
1664
1714
  else if (existsSync(rolloutPath)) {
1665
1715
  const cursor = baselineJsonlCursor(rolloutPath);
1666
1716
  codexBridgeOffset = cursor.newOffset;
@@ -1696,6 +1746,39 @@ function codexBridgeAttach(rolloutPath, mode) {
1696
1746
  // (codexBridgeIngest 在 offset 未推进时是 no-op)。
1697
1747
  codexBridgeStartTimer();
1698
1748
  }
1749
+ function cursorLateAttachMode(path) {
1750
+ const start = codexAdoptStartMs;
1751
+ if (start !== undefined) {
1752
+ try {
1753
+ const birthtimeMs = statSync(path).birthtimeMs;
1754
+ // Cursor often creates the agent-transcript file lazily on the first
1755
+ // post-adopt submit. In that case the first user line is live and must
1756
+ // be ingested from byte 0 rather than swallowed as history.
1757
+ if (Number.isFinite(birthtimeMs) && birthtimeMs >= start - 5_000)
1758
+ return 'fresh-empty';
1759
+ }
1760
+ catch { /* fall back to history-safe baseline */ }
1761
+ }
1762
+ return 'baseline-existing';
1763
+ }
1764
+ /** Attach the Cursor adopt bridge. Cursor's JSONL has no per-event
1765
+ * timestamp, so existing transcripts are baselined by byte offset. Cursor
1766
+ * restore intentionally skips any partial tail present at attach time: it is
1767
+ * old in-flight output and must not be attributed to the next Lark turn. If
1768
+ * the transcript is created after /adopt, attach fresh so the first
1769
+ * post-adopt Lark/user turn can still be attributed. */
1770
+ function cursorBridgeAttach(path, mode = 'baseline-existing') {
1771
+ if (mode === 'baseline-existing' && existsSync(path)) {
1772
+ try {
1773
+ const full = drainCursorTranscript(path, 0);
1774
+ maybeEmitCodexAdoptPreamble(full.events);
1775
+ }
1776
+ catch (err) {
1777
+ log(`Cursor bridge preamble drain failed: ${err.message}`);
1778
+ }
1779
+ }
1780
+ codexBridgeAttach(path, mode === 'baseline-existing' ? 'baseline-existing-skip-tail' : mode);
1781
+ }
1699
1782
  /** Called from flushPending after writeInput first returns a cliSessionId.
1700
1783
  * Tries to locate the rollout file immediately; if it's not on disk yet,
1701
1784
  * remembers the sid so the 1s poller can keep retrying. */
@@ -1714,6 +1797,20 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
1714
1797
  }
1715
1798
  return;
1716
1799
  }
1800
+ if (codexBridgeIsCursor()) {
1801
+ // Cursor's cliSessionId is the chatId — the same UUID naming the
1802
+ // agent-transcript JSONL, so it resolves the path directly.
1803
+ const cursorPath = findCursorTranscriptByChatId(cliSessionId);
1804
+ if (cursorPath) {
1805
+ codexBridgePendingSessionId = undefined;
1806
+ cursorBridgeAttach(cursorPath, cursorLateAttachMode(cursorPath));
1807
+ }
1808
+ else {
1809
+ codexBridgePendingSessionId = cliSessionId;
1810
+ codexBridgeStartTimer();
1811
+ }
1812
+ return;
1813
+ }
1717
1814
  const path = lastInitConfig?.cliId === 'traex'
1718
1815
  ? findTraexRolloutBySessionId(cliSessionId)
1719
1816
  : findCodexRolloutBySessionId(cliSessionId);
@@ -2986,6 +3083,37 @@ function setupAdoptTranscriptBridges(cfg) {
2986
3083
  codexBridgeStartTimer();
2987
3084
  }
2988
3085
  }
3086
+ else if (cfg.cliId === 'cursor') {
3087
+ const adoptStartMs = Date.now();
3088
+ codexAdoptStartMs = adoptStartMs;
3089
+ // Cursor JSONL lacks per-event timestamps, but adopt still needs parity
3090
+ // with other transcript bridges: direct terminal input should be surfaced
3091
+ // as a local-turn card in Lark. Baseline/offset handling above keeps
3092
+ // pre-adopt history out of the queue; worst-case mirror replay is a
3093
+ // duplicate local-turn message rather than lost local input.
3094
+ codexBridgeQueue.setLocalTurns(true, adoptStartMs);
3095
+ // Resolve the transcript: cliSessionId (= Cursor chatId) when discovery
3096
+ // captured it, else the adopt pid via its open store.db fd. Cursor lacks
3097
+ // per-event timestamps, so cursorBridgeAttach baselines by byte offset
3098
+ // rather than the timestamp-cutoff split-live the other CLIs use.
3099
+ let path;
3100
+ if (cfg.cliSessionId)
3101
+ path = findCursorTranscriptByChatId(cfg.cliSessionId);
3102
+ if (!path && cfg.adoptCliPid) {
3103
+ const probed = findCursorTranscriptByPid(cfg.adoptCliPid);
3104
+ if (probed)
3105
+ path = probed.path;
3106
+ }
3107
+ if (path) {
3108
+ cursorBridgeAttach(path);
3109
+ }
3110
+ else {
3111
+ if (cfg.cliSessionId)
3112
+ codexBridgePendingSessionId = cfg.cliSessionId;
3113
+ codexAdoptPendingPid = cfg.adoptCliPid;
3114
+ codexBridgeStartTimer();
3115
+ }
3116
+ }
2989
3117
  }
2990
3118
  function adoptIdleAdapter(cfg) {
2991
3119
  return cfg.bridgeJsonlPath
@@ -4120,11 +4248,24 @@ process.on('message', async (raw) => {
4120
4248
  // in-flight events from a local-typed prior turn close before
4121
4249
  // this Lark turn's fingerprint window opens. Mark works even
4122
4250
  // pre-attach (queue is path-agnostic).
4123
- try {
4124
- codexBridgeIngest();
4251
+ if (codexBridgeIsCursor()) {
4252
+ // Cursor may append the current Lark/user line to its transcript
4253
+ // before this IPC message is handled. Mark first so that preexisting
4254
+ // current-line can still fingerprint-match instead of being marked
4255
+ // seen as an unmatched event.
4256
+ codexBridgeMarkPendingTurn(content);
4257
+ try {
4258
+ codexBridgeIngest();
4259
+ }
4260
+ catch { /* best effort */ }
4261
+ }
4262
+ else {
4263
+ try {
4264
+ codexBridgeIngest();
4265
+ }
4266
+ catch { /* best effort */ }
4267
+ codexBridgeMarkPendingTurn(content);
4125
4268
  }
4126
- catch { /* best effort */ }
4127
- codexBridgeMarkPendingTurn(content);
4128
4269
  }
4129
4270
  // Adopt mode write:
4130
4271
  // - codex routes through cliAdapter.writeInput so the adapter's