botmux 2.9.1 → 2.9.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.
Files changed (330) hide show
  1. package/README.en.md +140 -76
  2. package/README.md +134 -75
  3. package/dist/adapters/backend/pty-backend.d.ts +6 -0
  4. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/pty-backend.js +10 -0
  6. package/dist/adapters/backend/pty-backend.js.map +1 -1
  7. package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
  8. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
  9. package/dist/adapters/backend/session-backend-selector.js +26 -0
  10. package/dist/adapters/backend/session-backend-selector.js.map +1 -0
  11. package/dist/adapters/backend/tmux-backend.d.ts +80 -3
  12. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  13. package/dist/adapters/backend/tmux-backend.js +301 -49
  14. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  15. package/dist/adapters/backend/tmux-pipe-backend.d.ts +100 -0
  16. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -0
  17. package/dist/adapters/backend/tmux-pipe-backend.js +473 -0
  18. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -0
  19. package/dist/adapters/cli/aiden.d.ts.map +1 -1
  20. package/dist/adapters/cli/aiden.js +5 -0
  21. package/dist/adapters/cli/aiden.js.map +1 -1
  22. package/dist/adapters/cli/claude-code.d.ts +40 -1
  23. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  24. package/dist/adapters/cli/claude-code.js +470 -49
  25. package/dist/adapters/cli/claude-code.js.map +1 -1
  26. package/dist/adapters/cli/coco.d.ts.map +1 -1
  27. package/dist/adapters/cli/coco.js +191 -9
  28. package/dist/adapters/cli/coco.js.map +1 -1
  29. package/dist/adapters/cli/codex.d.ts.map +1 -1
  30. package/dist/adapters/cli/codex.js +94 -17
  31. package/dist/adapters/cli/codex.js.map +1 -1
  32. package/dist/adapters/cli/shared-hints.d.ts +2 -2
  33. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  34. package/dist/adapters/cli/shared-hints.js +7 -5
  35. package/dist/adapters/cli/shared-hints.js.map +1 -1
  36. package/dist/adapters/cli/types.d.ts +38 -1
  37. package/dist/adapters/cli/types.d.ts.map +1 -1
  38. package/dist/autostart.d.ts +14 -0
  39. package/dist/autostart.d.ts.map +1 -0
  40. package/dist/autostart.js +357 -0
  41. package/dist/autostart.js.map +1 -0
  42. package/dist/bot-registry.d.ts +29 -3
  43. package/dist/bot-registry.d.ts.map +1 -1
  44. package/dist/bot-registry.js +91 -12
  45. package/dist/bot-registry.js.map +1 -1
  46. package/dist/cli/arg-utils.d.ts +11 -0
  47. package/dist/cli/arg-utils.d.ts.map +1 -0
  48. package/dist/cli/arg-utils.js +25 -0
  49. package/dist/cli/arg-utils.js.map +1 -0
  50. package/dist/cli/create-group-resolver.d.ts +32 -0
  51. package/dist/cli/create-group-resolver.d.ts.map +1 -0
  52. package/dist/cli/create-group-resolver.js +70 -0
  53. package/dist/cli/create-group-resolver.js.map +1 -0
  54. package/dist/cli/quoted-render.d.ts +30 -0
  55. package/dist/cli/quoted-render.d.ts.map +1 -0
  56. package/dist/cli/quoted-render.js +29 -0
  57. package/dist/cli/quoted-render.js.map +1 -0
  58. package/dist/cli.js +916 -272
  59. package/dist/cli.js.map +1 -1
  60. package/dist/config.d.ts +6 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +18 -8
  63. package/dist/config.js.map +1 -1
  64. package/dist/core/command-handler.d.ts +43 -0
  65. package/dist/core/command-handler.d.ts.map +1 -1
  66. package/dist/core/command-handler.js +167 -64
  67. package/dist/core/command-handler.js.map +1 -1
  68. package/dist/core/dashboard-events.d.ts +57 -0
  69. package/dist/core/dashboard-events.d.ts.map +1 -0
  70. package/dist/core/dashboard-events.js +23 -0
  71. package/dist/core/dashboard-events.js.map +1 -0
  72. package/dist/core/dashboard-ipc-server.d.ts +43 -0
  73. package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
  74. package/dist/core/dashboard-ipc-server.js +481 -0
  75. package/dist/core/dashboard-ipc-server.js.map +1 -0
  76. package/dist/core/dashboard-locate.d.ts +20 -0
  77. package/dist/core/dashboard-locate.d.ts.map +1 -0
  78. package/dist/core/dashboard-locate.js +26 -0
  79. package/dist/core/dashboard-locate.js.map +1 -0
  80. package/dist/core/dashboard-rows.d.ts +31 -0
  81. package/dist/core/dashboard-rows.d.ts.map +1 -0
  82. package/dist/core/dashboard-rows.js +65 -0
  83. package/dist/core/dashboard-rows.js.map +1 -0
  84. package/dist/core/inherit-peer.d.ts +14 -0
  85. package/dist/core/inherit-peer.d.ts.map +1 -0
  86. package/dist/core/inherit-peer.js +32 -0
  87. package/dist/core/inherit-peer.js.map +1 -0
  88. package/dist/core/scheduler.d.ts +24 -0
  89. package/dist/core/scheduler.d.ts.map +1 -1
  90. package/dist/core/scheduler.js +93 -2
  91. package/dist/core/scheduler.js.map +1 -1
  92. package/dist/core/session-activity.d.ts +3 -0
  93. package/dist/core/session-activity.d.ts.map +1 -0
  94. package/dist/core/session-activity.js +20 -0
  95. package/dist/core/session-activity.js.map +1 -0
  96. package/dist/core/session-discovery.d.ts +39 -0
  97. package/dist/core/session-discovery.d.ts.map +1 -1
  98. package/dist/core/session-discovery.js +114 -21
  99. package/dist/core/session-discovery.js.map +1 -1
  100. package/dist/core/session-manager.d.ts +72 -0
  101. package/dist/core/session-manager.d.ts.map +1 -1
  102. package/dist/core/session-manager.js +396 -106
  103. package/dist/core/session-manager.js.map +1 -1
  104. package/dist/core/types.d.ts +27 -2
  105. package/dist/core/types.d.ts.map +1 -1
  106. package/dist/core/types.js +14 -3
  107. package/dist/core/types.js.map +1 -1
  108. package/dist/core/worker-pool.d.ts +72 -3
  109. package/dist/core/worker-pool.d.ts.map +1 -1
  110. package/dist/core/worker-pool.js +459 -38
  111. package/dist/core/worker-pool.js.map +1 -1
  112. package/dist/daemon.d.ts.map +1 -1
  113. package/dist/daemon.js +645 -314
  114. package/dist/daemon.js.map +1 -1
  115. package/dist/dashboard/aggregator.d.ts +41 -0
  116. package/dist/dashboard/aggregator.d.ts.map +1 -0
  117. package/dist/dashboard/aggregator.js +125 -0
  118. package/dist/dashboard/aggregator.js.map +1 -0
  119. package/dist/dashboard/auth.d.ts +23 -0
  120. package/dist/dashboard/auth.d.ts.map +1 -0
  121. package/dist/dashboard/auth.js +66 -0
  122. package/dist/dashboard/auth.js.map +1 -0
  123. package/dist/dashboard/operator-selector.d.ts +20 -0
  124. package/dist/dashboard/operator-selector.d.ts.map +1 -0
  125. package/dist/dashboard/operator-selector.js +39 -0
  126. package/dist/dashboard/operator-selector.js.map +1 -0
  127. package/dist/dashboard/registry.d.ts +35 -0
  128. package/dist/dashboard/registry.d.ts.map +1 -0
  129. package/dist/dashboard/registry.js +74 -0
  130. package/dist/dashboard/registry.js.map +1 -0
  131. package/dist/dashboard/web/app.d.ts +2 -0
  132. package/dist/dashboard/web/app.d.ts.map +1 -0
  133. package/dist/dashboard/web/app.js +45 -0
  134. package/dist/dashboard/web/app.js.map +1 -0
  135. package/dist/dashboard/web/bot-defaults.d.ts +2 -0
  136. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -0
  137. package/dist/dashboard/web/bot-defaults.js +201 -0
  138. package/dist/dashboard/web/bot-defaults.js.map +1 -0
  139. package/dist/dashboard/web/groups.d.ts +16 -0
  140. package/dist/dashboard/web/groups.d.ts.map +1 -0
  141. package/dist/dashboard/web/groups.js +584 -0
  142. package/dist/dashboard/web/groups.js.map +1 -0
  143. package/dist/dashboard/web/schedules.d.ts +2 -0
  144. package/dist/dashboard/web/schedules.d.ts.map +1 -0
  145. package/dist/dashboard/web/schedules.js +105 -0
  146. package/dist/dashboard/web/schedules.js.map +1 -0
  147. package/dist/dashboard/web/sessions.d.ts +2 -0
  148. package/dist/dashboard/web/sessions.d.ts.map +1 -0
  149. package/dist/dashboard/web/sessions.js +374 -0
  150. package/dist/dashboard/web/sessions.js.map +1 -0
  151. package/dist/dashboard/web/store.d.ts +23 -0
  152. package/dist/dashboard/web/store.d.ts.map +1 -0
  153. package/dist/dashboard/web/store.js +82 -0
  154. package/dist/dashboard/web/store.js.map +1 -0
  155. package/dist/dashboard-web/app.js +263 -0
  156. package/dist/dashboard-web/index.html +23 -0
  157. package/dist/dashboard-web/style.css +93 -0
  158. package/dist/dashboard.d.ts +2 -0
  159. package/dist/dashboard.d.ts.map +1 -0
  160. package/dist/dashboard.js +639 -0
  161. package/dist/dashboard.js.map +1 -0
  162. package/dist/im/lark/card-builder.d.ts +18 -1
  163. package/dist/im/lark/card-builder.d.ts.map +1 -1
  164. package/dist/im/lark/card-builder.js +70 -9
  165. package/dist/im/lark/card-builder.js.map +1 -1
  166. package/dist/im/lark/card-handler.d.ts.map +1 -1
  167. package/dist/im/lark/card-handler.js +123 -109
  168. package/dist/im/lark/card-handler.js.map +1 -1
  169. package/dist/im/lark/client.d.ts +35 -0
  170. package/dist/im/lark/client.d.ts.map +1 -1
  171. package/dist/im/lark/client.js +114 -11
  172. package/dist/im/lark/client.js.map +1 -1
  173. package/dist/im/lark/event-dispatcher.d.ts +88 -6
  174. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  175. package/dist/im/lark/event-dispatcher.js +398 -62
  176. package/dist/im/lark/event-dispatcher.js.map +1 -1
  177. package/dist/im/lark/forwarded-renderer.d.ts +23 -0
  178. package/dist/im/lark/forwarded-renderer.d.ts.map +1 -0
  179. package/dist/im/lark/forwarded-renderer.js +105 -0
  180. package/dist/im/lark/forwarded-renderer.js.map +1 -0
  181. package/dist/im/lark/md-card.d.ts +73 -0
  182. package/dist/im/lark/md-card.d.ts.map +1 -0
  183. package/dist/im/lark/md-card.js +332 -0
  184. package/dist/im/lark/md-card.js.map +1 -0
  185. package/dist/im/lark/merge-forward.d.ts +32 -0
  186. package/dist/im/lark/merge-forward.d.ts.map +1 -0
  187. package/dist/im/lark/merge-forward.js +110 -0
  188. package/dist/im/lark/merge-forward.js.map +1 -0
  189. package/dist/im/lark/message-parser.d.ts +9 -3
  190. package/dist/im/lark/message-parser.d.ts.map +1 -1
  191. package/dist/im/lark/message-parser.js +48 -13
  192. package/dist/im/lark/message-parser.js.map +1 -1
  193. package/dist/im/lark/quote-hint.d.ts +18 -0
  194. package/dist/im/lark/quote-hint.d.ts.map +1 -0
  195. package/dist/im/lark/quote-hint.js +23 -0
  196. package/dist/im/lark/quote-hint.js.map +1 -0
  197. package/dist/services/bridge-fallback-gate.d.ts +42 -0
  198. package/dist/services/bridge-fallback-gate.d.ts.map +1 -0
  199. package/dist/services/bridge-fallback-gate.js +12 -0
  200. package/dist/services/bridge-fallback-gate.js.map +1 -0
  201. package/dist/services/bridge-rotation-policy.d.ts +139 -0
  202. package/dist/services/bridge-rotation-policy.d.ts.map +1 -0
  203. package/dist/services/bridge-rotation-policy.js +125 -0
  204. package/dist/services/bridge-rotation-policy.js.map +1 -0
  205. package/dist/services/bridge-turn-queue.d.ts +154 -0
  206. package/dist/services/bridge-turn-queue.d.ts.map +1 -0
  207. package/dist/services/bridge-turn-queue.js +316 -0
  208. package/dist/services/bridge-turn-queue.js.map +1 -0
  209. package/dist/services/chat-first-seen-store.d.ts +27 -0
  210. package/dist/services/chat-first-seen-store.d.ts.map +1 -0
  211. package/dist/services/chat-first-seen-store.js +114 -0
  212. package/dist/services/chat-first-seen-store.js.map +1 -0
  213. package/dist/services/claude-transcript.d.ts +268 -0
  214. package/dist/services/claude-transcript.d.ts.map +1 -0
  215. package/dist/services/claude-transcript.js +798 -0
  216. package/dist/services/claude-transcript.js.map +1 -0
  217. package/dist/services/coco-transcript.d.ts +35 -0
  218. package/dist/services/coco-transcript.d.ts.map +1 -0
  219. package/dist/services/coco-transcript.js +192 -0
  220. package/dist/services/coco-transcript.js.map +1 -0
  221. package/dist/services/codex-bridge-queue.d.ts +56 -0
  222. package/dist/services/codex-bridge-queue.d.ts.map +1 -0
  223. package/dist/services/codex-bridge-queue.js +150 -0
  224. package/dist/services/codex-bridge-queue.js.map +1 -0
  225. package/dist/services/codex-transcript.d.ts +84 -0
  226. package/dist/services/codex-transcript.d.ts.map +1 -0
  227. package/dist/services/codex-transcript.js +298 -0
  228. package/dist/services/codex-transcript.js.map +1 -0
  229. package/dist/services/group-creator.d.ts +23 -0
  230. package/dist/services/group-creator.d.ts.map +1 -0
  231. package/dist/services/group-creator.js +75 -0
  232. package/dist/services/group-creator.js.map +1 -0
  233. package/dist/services/groups-store.d.ts +98 -0
  234. package/dist/services/groups-store.d.ts.map +1 -0
  235. package/dist/services/groups-store.js +213 -0
  236. package/dist/services/groups-store.js.map +1 -0
  237. package/dist/services/oncall-store.d.ts +80 -8
  238. package/dist/services/oncall-store.d.ts.map +1 -1
  239. package/dist/services/oncall-store.js +265 -55
  240. package/dist/services/oncall-store.js.map +1 -1
  241. package/dist/services/project-scanner.d.ts +1 -2
  242. package/dist/services/project-scanner.d.ts.map +1 -1
  243. package/dist/services/project-scanner.js +118 -68
  244. package/dist/services/project-scanner.js.map +1 -1
  245. package/dist/services/schedule-store.d.ts +5 -0
  246. package/dist/services/schedule-store.d.ts.map +1 -1
  247. package/dist/services/schedule-store.js +77 -1
  248. package/dist/services/schedule-store.js.map +1 -1
  249. package/dist/services/session-store.d.ts +22 -0
  250. package/dist/services/session-store.d.ts.map +1 -1
  251. package/dist/services/session-store.js +62 -4
  252. package/dist/services/session-store.js.map +1 -1
  253. package/dist/setup/bots-store.d.ts +3 -0
  254. package/dist/setup/bots-store.d.ts.map +1 -0
  255. package/dist/setup/bots-store.js +24 -0
  256. package/dist/setup/bots-store.js.map +1 -0
  257. package/dist/setup/detect-platform.d.ts +14 -0
  258. package/dist/setup/detect-platform.d.ts.map +1 -0
  259. package/dist/setup/detect-platform.js +139 -0
  260. package/dist/setup/detect-platform.js.map +1 -0
  261. package/dist/setup/ensure-fonts.d.ts +13 -0
  262. package/dist/setup/ensure-fonts.d.ts.map +1 -0
  263. package/dist/setup/ensure-fonts.js +225 -0
  264. package/dist/setup/ensure-fonts.js.map +1 -0
  265. package/dist/setup/ensure-tmux.d.ts +60 -0
  266. package/dist/setup/ensure-tmux.d.ts.map +1 -0
  267. package/dist/setup/ensure-tmux.js +236 -0
  268. package/dist/setup/ensure-tmux.js.map +1 -0
  269. package/dist/setup/index.d.ts +9 -0
  270. package/dist/setup/index.d.ts.map +1 -0
  271. package/dist/setup/index.js +46 -0
  272. package/dist/setup/index.js.map +1 -0
  273. package/dist/setup/lark-scopes.json +301 -0
  274. package/dist/setup/register-app.d.ts +52 -0
  275. package/dist/setup/register-app.d.ts.map +1 -0
  276. package/dist/setup/register-app.js +91 -0
  277. package/dist/setup/register-app.js.map +1 -0
  278. package/dist/setup/verify-permissions.d.ts +115 -0
  279. package/dist/setup/verify-permissions.d.ts.map +1 -0
  280. package/dist/setup/verify-permissions.js +207 -0
  281. package/dist/setup/verify-permissions.js.map +1 -0
  282. package/dist/skills/definitions.d.ts +4 -0
  283. package/dist/skills/definitions.d.ts.map +1 -1
  284. package/dist/skills/definitions.js +133 -19
  285. package/dist/skills/definitions.js.map +1 -1
  286. package/dist/skills/installer.d.ts +3 -1
  287. package/dist/skills/installer.d.ts.map +1 -1
  288. package/dist/skills/installer.js +18 -3
  289. package/dist/skills/installer.js.map +1 -1
  290. package/dist/types.d.ts +44 -0
  291. package/dist/types.d.ts.map +1 -1
  292. package/dist/utils/bot-routing.d.ts +6 -0
  293. package/dist/utils/bot-routing.d.ts.map +1 -0
  294. package/dist/utils/bot-routing.js +50 -0
  295. package/dist/utils/bot-routing.js.map +1 -0
  296. package/dist/utils/file-lock.d.ts +2 -0
  297. package/dist/utils/file-lock.d.ts.map +1 -0
  298. package/dist/utils/file-lock.js +114 -0
  299. package/dist/utils/file-lock.js.map +1 -0
  300. package/dist/utils/font-installer.js +1 -1
  301. package/dist/utils/font-installer.js.map +1 -1
  302. package/dist/utils/idle-detector.d.ts +6 -0
  303. package/dist/utils/idle-detector.d.ts.map +1 -1
  304. package/dist/utils/idle-detector.js +25 -4
  305. package/dist/utils/idle-detector.js.map +1 -1
  306. package/dist/utils/logger.d.ts +10 -0
  307. package/dist/utils/logger.d.ts.map +1 -1
  308. package/dist/utils/logger.js +60 -8
  309. package/dist/utils/logger.js.map +1 -1
  310. package/dist/utils/render-dimensions.d.ts +48 -0
  311. package/dist/utils/render-dimensions.d.ts.map +1 -0
  312. package/dist/utils/render-dimensions.js +55 -0
  313. package/dist/utils/render-dimensions.js.map +1 -0
  314. package/dist/utils/screen-analyzer.d.ts.map +1 -1
  315. package/dist/utils/screen-analyzer.js +24 -0
  316. package/dist/utils/screen-analyzer.js.map +1 -1
  317. package/dist/utils/screenshot-renderer.d.ts.map +1 -1
  318. package/dist/utils/screenshot-renderer.js +67 -23
  319. package/dist/utils/screenshot-renderer.js.map +1 -1
  320. package/dist/utils/terminal-renderer.d.ts +16 -0
  321. package/dist/utils/terminal-renderer.d.ts.map +1 -1
  322. package/dist/utils/terminal-renderer.js +40 -23
  323. package/dist/utils/terminal-renderer.js.map +1 -1
  324. package/dist/utils/transient-snapshot.d.ts +28 -0
  325. package/dist/utils/transient-snapshot.d.ts.map +1 -0
  326. package/dist/utils/transient-snapshot.js +96 -0
  327. package/dist/utils/transient-snapshot.js.map +1 -0
  328. package/dist/worker.js +2248 -83
  329. package/dist/worker.js.map +1 -1
  330. package/package.json +12 -5
@@ -0,0 +1,798 @@
1
+ /**
2
+ * Incremental reader for Claude Code transcript JSONL files.
3
+ *
4
+ * Used by the adopt-bridge pipeline (worker.ts) to:
5
+ * 1. baseline the transcript at attach time so historical messages aren't
6
+ * replayed to Lark.
7
+ * 2. drain newly-appended assistant messages between user turns.
8
+ * 3. tolerate truncation, rotation, half-written JSON lines, and races with
9
+ * Claude Code's writer.
10
+ *
11
+ * The functions are pure (no fs.watch — that's the worker's wakeup concern)
12
+ * to keep them unit-testable.
13
+ */
14
+ import { existsSync, openSync, readSync, closeSync, statSync, readdirSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ /** Extract the user-typed prompt text for a "turn start" event — works for
17
+ * both legacy `role:user` events (text in `message.content`) and the
18
+ * type-ahead `attachment(queued_command)` form (text in `attachment.prompt`).
19
+ * Returns '' when neither shape carries usable content. Used at three
20
+ * layers: BridgeTurnQueue.ingest (fingerprint-match the right pending Lark
21
+ * turn), worker emit (local-turn user-text resolution), and tests. */
22
+ export function extractTurnStartText(ev) {
23
+ if (!ev || typeof ev !== 'object')
24
+ return '';
25
+ if (ev.type === 'attachment' && ev.attachment?.type === 'queued_command') {
26
+ const prompt = ev.attachment.prompt;
27
+ if (typeof prompt === 'string')
28
+ return prompt;
29
+ return stringifyUserContent(prompt);
30
+ }
31
+ return stringifyUserContent(ev.message?.content);
32
+ }
33
+ /**
34
+ * Read everything from `path` starting at `fromOffset` and return parsed
35
+ * JSONL events plus the new file offset.
36
+ *
37
+ * - Returns `{ events: [], newOffset: 0, pendingTail: '' }` if the file
38
+ * doesn't exist (caller treats this as "nothing yet").
39
+ * - Detects truncation (size < fromOffset): resets to 0 and re-drains so a
40
+ * rotated/cleared transcript doesn't silently swallow new lines.
41
+ * - Skips malformed JSON lines (logs nothing — robustness over noise).
42
+ * - The trailing partial line (no `\n` yet) is *not* parsed and *not*
43
+ * counted toward `newOffset`, so the next drain re-reads it.
44
+ */
45
+ export function drainTranscript(path, fromOffset) {
46
+ if (!existsSync(path)) {
47
+ return { events: [], newOffset: 0, pendingTail: '' };
48
+ }
49
+ let size;
50
+ try {
51
+ size = statSync(path).size;
52
+ }
53
+ catch {
54
+ return { events: [], newOffset: fromOffset, pendingTail: '' };
55
+ }
56
+ let start = fromOffset;
57
+ if (size < start) {
58
+ // Truncated/rotated — re-read from the top.
59
+ start = 0;
60
+ }
61
+ if (size === start) {
62
+ return { events: [], newOffset: start, pendingTail: '' };
63
+ }
64
+ const len = size - start;
65
+ const buf = Buffer.alloc(len);
66
+ let read = 0;
67
+ const fd = openSync(path, 'r');
68
+ try {
69
+ read = readSync(fd, buf, 0, len, start);
70
+ }
71
+ finally {
72
+ closeSync(fd);
73
+ }
74
+ const text = buf.subarray(0, read).toString('utf8');
75
+ // Find the last '\n' — anything after it is a partial line we shouldn't
76
+ // commit yet. Adjust newOffset to exclude the partial tail so the next
77
+ // drain re-reads it.
78
+ const lastNl = text.lastIndexOf('\n');
79
+ let toParse;
80
+ let pendingTail;
81
+ let newOffset;
82
+ if (lastNl < 0) {
83
+ // No complete line at all — treat the whole buffer as pending.
84
+ toParse = '';
85
+ pendingTail = text;
86
+ newOffset = start;
87
+ }
88
+ else {
89
+ toParse = text.substring(0, lastNl);
90
+ pendingTail = text.substring(lastNl + 1);
91
+ newOffset = start + Buffer.byteLength(text.substring(0, lastNl + 1), 'utf8');
92
+ }
93
+ const events = [];
94
+ if (toParse) {
95
+ for (const line of toParse.split('\n')) {
96
+ const trimmed = line.trim();
97
+ if (!trimmed)
98
+ continue;
99
+ try {
100
+ const obj = JSON.parse(trimmed);
101
+ if (obj && typeof obj === 'object')
102
+ events.push(obj);
103
+ }
104
+ catch {
105
+ // Malformed line — skip silently. Claude Code's writer is atomic per
106
+ // line, so this means a debug/non-JSON line snuck in; not our concern.
107
+ }
108
+ }
109
+ }
110
+ return { events, newOffset, pendingTail };
111
+ }
112
+ /**
113
+ * Filter to assistant text events. Returns only events where:
114
+ * - type === 'assistant' OR message.role === 'assistant'
115
+ * - content has at least one text block
116
+ * - uuid is present
117
+ *
118
+ * Sub-agent / sidechain events (isSidechain === true) are excluded so that
119
+ * spawn-internal Task agent chatter doesn't leak to Lark.
120
+ */
121
+ export function pickAssistantTextEvents(events) {
122
+ return events.filter(e => {
123
+ if (!e || typeof e !== 'object')
124
+ return false;
125
+ if (e.isSidechain === true)
126
+ return false;
127
+ const role = e.message?.role ?? e.type;
128
+ if (role !== 'assistant')
129
+ return false;
130
+ if (!e.uuid)
131
+ return false;
132
+ const content = e.message?.content;
133
+ if (!content)
134
+ return false;
135
+ if (typeof content === 'string')
136
+ return content.length > 0;
137
+ if (Array.isArray(content))
138
+ return content.some(b => b && b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
139
+ return false;
140
+ });
141
+ }
142
+ /**
143
+ * Extract the visible text from one assistant event. Walks all `type:'text'`
144
+ * blocks in `message.content` (or the bare string) and joins them with
145
+ * blank lines. Returns '' if no text blocks.
146
+ */
147
+ export function extractAssistantText(event) {
148
+ const content = event.message?.content;
149
+ if (!content)
150
+ return '';
151
+ if (typeof content === 'string')
152
+ return content;
153
+ if (!Array.isArray(content))
154
+ return '';
155
+ const parts = [];
156
+ for (const block of content) {
157
+ if (block && block.type === 'text' && typeof block.text === 'string' && block.text.length > 0) {
158
+ parts.push(block.text);
159
+ }
160
+ }
161
+ return parts.join('\n\n');
162
+ }
163
+ /** Convenience: filter+extract a list of events into a single concatenated string. */
164
+ export function joinAssistantText(events) {
165
+ return pickAssistantTextEvents(events)
166
+ .map(extractAssistantText)
167
+ .filter(s => s.length > 0)
168
+ .join('\n\n');
169
+ }
170
+ /** XML wrappers Claude Code uses for synthetic user events that aren't real
171
+ * prompts (slash command invocation, local-command output caveat, etc.).
172
+ * These should usually carry `isMeta:true` and we'd filter on that — this
173
+ * list is a defense-in-depth check for jsonls where the flag is absent. */
174
+ const SYNTHETIC_USER_PREFIXES = [
175
+ '<command-name>',
176
+ '<command-message>',
177
+ '<command-args>',
178
+ '<local-command-caveat>',
179
+ '<local-command-stdout>',
180
+ '<local-command-stderr>',
181
+ ];
182
+ /** True when a `type:'user'` (or `message.role:'user'`) event represents a
183
+ * *real* prompt the human typed — not Claude Code's internal machinery
184
+ * (tool_result, slash-command wrappers, isMeta/isCompactSummary markers,
185
+ * sidechain spawn events). The bridge attribution queue and the adopt
186
+ * preamble extractor share this predicate to ensure they're seeing the
187
+ * same notion of "user input". */
188
+ export function isMeaningfulUserEvent(ev) {
189
+ if (!ev || typeof ev !== 'object')
190
+ return false;
191
+ const role = ev.message?.role ?? ev.type;
192
+ if (role !== 'user')
193
+ return false;
194
+ const flags = ev;
195
+ if (flags.isMeta === true)
196
+ return false;
197
+ if (flags.isCompactSummary === true)
198
+ return false;
199
+ if (flags.isSidechain === true)
200
+ return false;
201
+ const content = ev.message?.content;
202
+ if (isPureToolResultUserEvent(content))
203
+ return false;
204
+ const text = normaliseForFingerprint(stringifyUserContent(content));
205
+ if (text.length === 0)
206
+ return false;
207
+ if (SYNTHETIC_USER_PREFIXES.some(p => text.startsWith(p)))
208
+ return false;
209
+ return true;
210
+ }
211
+ /** True when a `type:'attachment'` line carries a queued-command payload
212
+ * representing a real submitted prompt. Claude writes one of these when it
213
+ * dequeues a type-ahead submission (right before the assistant's reply for
214
+ * that turn starts streaming) — the bridge attribution queue treats it
215
+ * exactly like a `role:user` event for turn-start purposes. Filters mirror
216
+ * isMeaningfulUserEvent's defenses (sidechain, empty / synthetic-prefix
217
+ * prompts) so a queued slash command can't false-start a Lark turn. */
218
+ export function isMeaningfulQueuedCommand(ev) {
219
+ if (!ev || typeof ev !== 'object')
220
+ return false;
221
+ if (ev.type !== 'attachment')
222
+ return false;
223
+ if (ev.attachment?.type !== 'queued_command')
224
+ return false;
225
+ if (ev.isSidechain === true)
226
+ return false;
227
+ const text = normaliseForFingerprint(extractTurnStartText(ev));
228
+ if (text.length === 0)
229
+ return false;
230
+ if (SYNTHETIC_USER_PREFIXES.some(p => text.startsWith(p)))
231
+ return false;
232
+ return true;
233
+ }
234
+ /** Walk the events forward and return the last *completed* user/assistant
235
+ * exchange. "Completed" here means: a meaningful user prompt followed by
236
+ * at least one assistant event with visible text. tool_use / tool_result
237
+ * events do NOT reset the turn — they're intra-turn machinery, so a
238
+ * prompt → tool_use → tool_result → assistant text sequence still counts
239
+ * as a single turn. Returns null when there's no meaningful user yet, or
240
+ * the last user wasn't followed by any visible assistant text (Claude is
241
+ * mid-tool-use when /adopt fired).
242
+ *
243
+ * Used by adopt-bridge to surface "the previous round" to the Lark thread
244
+ * so the user has context for continuing the conversation. */
245
+ export function extractLastAssistantTurn(events) {
246
+ let userText = null;
247
+ let assistantTexts = [];
248
+ for (const ev of events) {
249
+ if (!ev || typeof ev !== 'object')
250
+ continue;
251
+ if (isMeaningfulUserEvent(ev)) {
252
+ // New turn boundary — reset the assistant accumulator.
253
+ userText = stringifyUserContent(ev.message?.content);
254
+ assistantTexts = [];
255
+ continue;
256
+ }
257
+ const role = ev.message?.role ?? ev.type;
258
+ if (role !== 'assistant')
259
+ continue;
260
+ if (ev.isSidechain === true)
261
+ continue;
262
+ const text = extractAssistantText(ev);
263
+ if (text.length === 0)
264
+ continue;
265
+ if (userText !== null)
266
+ assistantTexts.push(text);
267
+ }
268
+ if (userText === null || assistantTexts.length === 0)
269
+ return null;
270
+ return {
271
+ userText,
272
+ assistantText: assistantTexts.join('\n\n'),
273
+ };
274
+ }
275
+ /**
276
+ * True when a user-role event carries ONLY tool_result blocks — Claude
277
+ * Code's representation of "tool returned this output" between an
278
+ * assistant tool_use and the assistant's continuation. Both the bridge
279
+ * attribution queue and the on-disk fingerprint search must skip these:
280
+ *
281
+ * - the queue would treat tool output as fresh local input and disable
282
+ * collection mid-turn,
283
+ * - the fingerprint search would false-positive on log content that
284
+ * happens to contain the Lark fingerprint substring (e.g. a short
285
+ * "hello" message hijacked by an unrelated jsonl whose tool_result
286
+ * dumped a log line containing "hello"). Re-exported by
287
+ * bridge-turn-queue.ts so both consumers share the same predicate
288
+ * and never drift apart.
289
+ */
290
+ export function isPureToolResultUserEvent(content) {
291
+ if (!Array.isArray(content) || content.length === 0)
292
+ return false;
293
+ return content.every((block) => block?.type === 'tool_result');
294
+ }
295
+ /**
296
+ * Stringify a transcript user event's content to a flat string. Handles
297
+ * both legacy bare-string content and the array-of-blocks form.
298
+ *
299
+ * Lives here (not in bridge-turn-queue.ts) so the in-process attribution
300
+ * state machine and the on-disk fingerprint search use *exactly* the
301
+ * same text — otherwise multi-line / array-content Lark messages stop
302
+ * matching one path or the other and bridges silently break.
303
+ */
304
+ export function stringifyUserContent(content) {
305
+ if (typeof content === 'string')
306
+ return content;
307
+ if (!Array.isArray(content))
308
+ return '';
309
+ const parts = [];
310
+ for (const block of content) {
311
+ if (typeof block?.text === 'string')
312
+ parts.push(block.text);
313
+ else if (typeof block?.content === 'string')
314
+ parts.push(block.content);
315
+ }
316
+ return parts.join('\n');
317
+ }
318
+ /**
319
+ * Collapse whitespace + trim. Same normalisation applied on both sides
320
+ * of the fingerprint compare (the Lark message that produces the
321
+ * fingerprint, and the transcript user content we search through),
322
+ * so newlines / tabs / double-spaces don't break the match.
323
+ */
324
+ export function normaliseForFingerprint(s) {
325
+ return s.replace(/\s+/g, ' ').trim();
326
+ }
327
+ /**
328
+ * Find the most recently-modified `.jsonl` file in a Claude Code project
329
+ * directory.
330
+ *
331
+ * `acceptCandidate` lets callers narrow the candidate set — the bridge's
332
+ * quiet-mtime fallback passes a trust-set predicate so a sibling Claude
333
+ * pane writing in the same project dir cannot hijack the watcher.
334
+ * Without it any actively-written sibling jsonl wins the mtime race and
335
+ * the bridge enters a flap loop with the pid resolver pulling it back.
336
+ *
337
+ * Returns null when the directory doesn't exist, has no jsonl files, or
338
+ * every candidate was rejected by `acceptCandidate`.
339
+ */
340
+ export function findLatestJsonl(dir, opts) {
341
+ if (!existsSync(dir))
342
+ return null;
343
+ let entries;
344
+ try {
345
+ entries = readdirSync(dir);
346
+ }
347
+ catch {
348
+ return null;
349
+ }
350
+ const accept = opts?.acceptCandidate;
351
+ let latestPath = null;
352
+ let latestMtime = -Infinity;
353
+ for (const name of entries) {
354
+ if (!name.endsWith('.jsonl'))
355
+ continue;
356
+ const full = join(dir, name);
357
+ if (accept && !accept(full))
358
+ continue;
359
+ try {
360
+ const st = statSync(full);
361
+ if (!st.isFile())
362
+ continue;
363
+ if (st.mtimeMs > latestMtime) {
364
+ latestMtime = st.mtimeMs;
365
+ latestPath = full;
366
+ }
367
+ }
368
+ catch {
369
+ // File disappeared between readdir and stat — ignore.
370
+ }
371
+ }
372
+ return latestPath;
373
+ }
374
+ /** Scan a single jsonl file's tail for a Lark message fingerprint. Same
375
+ * parsing rules as `findJsonlContainingFingerprint` (decode role:user content,
376
+ * optionally also queue-operation/enqueue, normalise whitespace, then
377
+ * substring-match the fingerprint). Used by the claude-code adapter when
378
+ * the pid resolver has just switched to a rotated jsonl that may already
379
+ * contain the just-submitted user event. */
380
+ export function jsonlContainsFingerprint(path, fingerprint, opts) {
381
+ if (fingerprint.length === 0 || !existsSync(path))
382
+ return false;
383
+ let size;
384
+ try {
385
+ size = statSync(path).size;
386
+ }
387
+ catch {
388
+ return false;
389
+ }
390
+ if (size === 0)
391
+ return false;
392
+ const includeQueueOps = opts?.includeQueueOperations ?? false;
393
+ const minEventTimestampMs = opts?.minEventTimestampMs;
394
+ const len = Math.min(size, 1024 * 1024);
395
+ let buf;
396
+ try {
397
+ const fd = openSync(path, 'r');
398
+ try {
399
+ buf = Buffer.alloc(len);
400
+ readSync(fd, buf, 0, len, size - len);
401
+ }
402
+ finally {
403
+ closeSync(fd);
404
+ }
405
+ }
406
+ catch {
407
+ return false;
408
+ }
409
+ const text = buf.toString('utf8');
410
+ const lines = text.split('\n');
411
+ // Skip the leading partial line when we read a strict tail (size > len).
412
+ const startIdx = size > len ? 1 : 0;
413
+ for (let i = startIdx; i < lines.length; i++) {
414
+ const line = lines[i].trim();
415
+ if (!line)
416
+ continue;
417
+ let ev;
418
+ try {
419
+ ev = JSON.parse(line);
420
+ }
421
+ catch {
422
+ continue;
423
+ }
424
+ if (!ev || typeof ev !== 'object')
425
+ continue;
426
+ // Per-event timestamp guard: short fingerprints would otherwise
427
+ // false-match old user events in unrelated sibling jsonls (file
428
+ // mtime can be recent if a sibling Claude pane is actively writing
429
+ // its own turns). We compare against `event.timestamp` rather than
430
+ // file mtime to be precise.
431
+ if (minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
432
+ const evMs = Date.parse(ev.timestamp);
433
+ if (Number.isFinite(evMs) && evMs < minEventTimestampMs)
434
+ continue;
435
+ }
436
+ const role = ev.message?.role ?? ev.type;
437
+ let lineText = '';
438
+ if (role === 'user') {
439
+ // Skip pure tool_result events — Claude Code records them as
440
+ // role:user but they're internal turn machinery, not the user's
441
+ // actual prompt. A tool_result that dumps log output containing
442
+ // the fingerprint substring would otherwise hijack the search.
443
+ if (isPureToolResultUserEvent(ev.message?.content))
444
+ continue;
445
+ lineText = stringifyUserContent(ev.message?.content);
446
+ }
447
+ else if (includeQueueOps &&
448
+ ev.type === 'queue-operation' &&
449
+ ev.operation === 'enqueue') {
450
+ lineText = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
451
+ }
452
+ else {
453
+ continue;
454
+ }
455
+ const normalisedText = normaliseForFingerprint(lineText);
456
+ if (normalisedText.length > 0 && normalisedText.includes(fingerprint))
457
+ return true;
458
+ }
459
+ return false;
460
+ }
461
+ export function findJsonlContainingFingerprint(dir, fingerprint, excludePathOrOptions) {
462
+ if (!existsSync(dir) || fingerprint.length === 0)
463
+ return null;
464
+ const opts = typeof excludePathOrOptions === 'string'
465
+ ? { excludePath: excludePathOrOptions }
466
+ : (excludePathOrOptions ?? {});
467
+ let entries;
468
+ try {
469
+ entries = readdirSync(dir);
470
+ }
471
+ catch {
472
+ return null;
473
+ }
474
+ // Walk newest-first so a recently-rotated jsonl is found before older
475
+ // ones; if two files contain the fingerprint (rare, e.g. user pasted
476
+ // the same message into two panes) we prefer the more recent.
477
+ const candidates = [];
478
+ for (const name of entries) {
479
+ if (!name.endsWith('.jsonl'))
480
+ continue;
481
+ const full = join(dir, name);
482
+ if (opts.excludePath && full === opts.excludePath)
483
+ continue;
484
+ try {
485
+ const st = statSync(full);
486
+ if (!st.isFile())
487
+ continue;
488
+ if (opts.minMtimeMs !== undefined && st.mtimeMs < opts.minMtimeMs)
489
+ continue;
490
+ candidates.push({ path: full, mtime: st.mtimeMs });
491
+ }
492
+ catch { /* ignore */ }
493
+ }
494
+ candidates.sort((a, b) => b.mtime - a.mtime);
495
+ for (const { path } of candidates) {
496
+ try {
497
+ const fd = openSync(path, 'r');
498
+ try {
499
+ const size = statSync(path).size;
500
+ // Read at most the trailing 1MB — fingerprints land near the end
501
+ // of the jsonl when Claude just wrote them. Cheaper than reading
502
+ // an entire long-lived session.
503
+ const len = Math.min(size, 1024 * 1024);
504
+ const buf = Buffer.alloc(len);
505
+ readSync(fd, buf, 0, len, size - len);
506
+ const text = buf.toString('utf8');
507
+ // We must NOT do a raw includes() here: Claude writes user content
508
+ // as a JSON-encoded string, so any newline in the Lark message is
509
+ // serialized as `\n` on disk while our fingerprint has it
510
+ // collapsed to a single space. Parse each complete jsonl line,
511
+ // pick role:user events, and apply the same stringify+normalise
512
+ // we use in BridgeTurnQueue.ingest. Skip the leading partial line
513
+ // when we read a strict tail (size > len), since it likely begins
514
+ // mid-line.
515
+ const lines = text.split('\n');
516
+ const startIdx = size > len ? 1 : 0;
517
+ for (let i = startIdx; i < lines.length; i++) {
518
+ const line = lines[i].trim();
519
+ if (!line)
520
+ continue;
521
+ let ev;
522
+ try {
523
+ ev = JSON.parse(line);
524
+ }
525
+ catch {
526
+ continue;
527
+ }
528
+ if (!ev || typeof ev !== 'object')
529
+ continue;
530
+ // Per-event timestamp guard — see jsonlContainsFingerprint for
531
+ // the full rationale. Required to keep short fingerprints
532
+ // ("hello", "test") from matching old user lines in unrelated
533
+ // sibling jsonls.
534
+ if (opts.minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
535
+ const evMs = Date.parse(ev.timestamp);
536
+ if (Number.isFinite(evMs) && evMs < opts.minEventTimestampMs)
537
+ continue;
538
+ }
539
+ const role = ev.message?.role ?? ev.type;
540
+ let text = '';
541
+ if (role === 'user') {
542
+ // Skip pure tool_result events — see jsonlContainsFingerprint
543
+ // for the full rationale; in short, tool_result content is
544
+ // log output, not user input, and would false-match short
545
+ // fingerprints like "hello" in unrelated jsonls.
546
+ if (isPureToolResultUserEvent(ev.message?.content))
547
+ continue;
548
+ text = stringifyUserContent(ev.message?.content);
549
+ }
550
+ else if (opts.includeQueueOperations &&
551
+ ev.type === 'queue-operation' &&
552
+ ev.operation === 'enqueue') {
553
+ text = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
554
+ }
555
+ else {
556
+ continue;
557
+ }
558
+ const normalisedText = normaliseForFingerprint(text);
559
+ if (normalisedText.length > 0 && normalisedText.includes(fingerprint)) {
560
+ // Allow caller to veto this candidate (e.g., sibling-pane
561
+ // hijack guard rejecting an untrusted sessionId). On veto,
562
+ // break out of the line loop so we move to the next, older
563
+ // candidate instead of returning `null` after the first
564
+ // fingerprint hit.
565
+ if (opts.acceptCandidate && !opts.acceptCandidate(path)) {
566
+ break;
567
+ }
568
+ return path;
569
+ }
570
+ }
571
+ }
572
+ finally {
573
+ closeSync(fd);
574
+ }
575
+ }
576
+ catch { /* unreadable — skip */ }
577
+ }
578
+ return null;
579
+ }
580
+ /**
581
+ * Stronger sibling-pane recovery anchor than the substring fingerprint
582
+ * search. Walks every `.jsonl` in `dir` and returns the paths whose
583
+ * trailing 1MB contains a user/queue event whose normalised text is
584
+ * EXACTLY equal to `normalisedContent` (not a substring), respecting
585
+ * `excludePath`, `minMtimeMs`, `minEventTimestampMs`,
586
+ * `includeQueueOperations`, and `acceptCandidate` the same way as
587
+ * `findJsonlContainingFingerprint`.
588
+ *
589
+ * Returns *all* matches in mtime-descending order — callers must
590
+ * abstain when the result has length > 1, since multiple files containing
591
+ * the same exact normalised content cannot be disambiguated without
592
+ * stronger evidence (and forcing a switch would risk picking the wrong
593
+ * pane). The caller's typical pattern is:
594
+ *
595
+ * - 1 match → switch to it (legitimate post-/clear recovery)
596
+ * - 0 matches → no recovery this tick; wait for stronger signal
597
+ * - >1 match → log and abstain; surface a diagnostic to the user
598
+ *
599
+ * Used by the bridge fingerprint fallback's recovery path for in-pane
600
+ * `/clear`: substring matches risk hijacking on short fingerprints (the
601
+ * literal text "test" matches "run tests" / "test bridge"), but full
602
+ * equality on a Lark message we just wrote is a much stronger anchor.
603
+ */
604
+ export function findJsonlsContainingExactContent(dir, normalisedContent, options) {
605
+ if (!existsSync(dir) || normalisedContent.length === 0)
606
+ return [];
607
+ const opts = options ?? {};
608
+ let entries;
609
+ try {
610
+ entries = readdirSync(dir);
611
+ }
612
+ catch {
613
+ return [];
614
+ }
615
+ const candidates = [];
616
+ for (const name of entries) {
617
+ if (!name.endsWith('.jsonl'))
618
+ continue;
619
+ const full = join(dir, name);
620
+ if (opts.excludePath && full === opts.excludePath)
621
+ continue;
622
+ try {
623
+ const st = statSync(full);
624
+ if (!st.isFile())
625
+ continue;
626
+ if (opts.minMtimeMs !== undefined && st.mtimeMs < opts.minMtimeMs)
627
+ continue;
628
+ candidates.push({ path: full, mtime: st.mtimeMs });
629
+ }
630
+ catch { /* ignore */ }
631
+ }
632
+ candidates.sort((a, b) => b.mtime - a.mtime);
633
+ const matches = [];
634
+ for (const { path } of candidates) {
635
+ if (opts.acceptCandidate && !opts.acceptCandidate(path))
636
+ continue;
637
+ try {
638
+ const fd = openSync(path, 'r');
639
+ try {
640
+ const size = statSync(path).size;
641
+ const len = Math.min(size, 1024 * 1024);
642
+ const buf = Buffer.alloc(len);
643
+ readSync(fd, buf, 0, len, size - len);
644
+ const text = buf.toString('utf8');
645
+ const lines = text.split('\n');
646
+ const startIdx = size > len ? 1 : 0;
647
+ let hit = false;
648
+ for (let i = startIdx; i < lines.length; i++) {
649
+ const line = lines[i].trim();
650
+ if (!line)
651
+ continue;
652
+ let ev;
653
+ try {
654
+ ev = JSON.parse(line);
655
+ }
656
+ catch {
657
+ continue;
658
+ }
659
+ if (!ev || typeof ev !== 'object')
660
+ continue;
661
+ if (opts.minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
662
+ const evMs = Date.parse(ev.timestamp);
663
+ if (Number.isFinite(evMs) && evMs < opts.minEventTimestampMs)
664
+ continue;
665
+ }
666
+ const role = ev.message?.role ?? ev.type;
667
+ let raw = '';
668
+ if (role === 'user') {
669
+ if (isPureToolResultUserEvent(ev.message?.content))
670
+ continue;
671
+ raw = stringifyUserContent(ev.message?.content);
672
+ }
673
+ else if (opts.includeQueueOperations &&
674
+ ev.type === 'queue-operation' &&
675
+ ev.operation === 'enqueue') {
676
+ raw = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
677
+ }
678
+ else {
679
+ continue;
680
+ }
681
+ const normalised = normaliseForFingerprint(raw);
682
+ if (normalised === normalisedContent) {
683
+ hit = true;
684
+ break;
685
+ }
686
+ }
687
+ if (hit)
688
+ matches.push(path);
689
+ }
690
+ finally {
691
+ closeSync(fd);
692
+ }
693
+ }
694
+ catch { /* unreadable — skip */ }
695
+ }
696
+ return matches;
697
+ }
698
+ /**
699
+ * Partition transcript events into history (timestamp ≤ cutoff) and live
700
+ * (timestamp > cutoff, or no parseable timestamp). Used by the bridge
701
+ * watcher when it switches to a new jsonl that may contain pre-existing
702
+ * conversation: anything older than the cutoff (e.g. iTerm-typed turns
703
+ * the user produced before the Lark mark fired) belongs in the seen-set
704
+ * via `BridgeTurnQueue.absorb` so the worker doesn't replay them as
705
+ * "🖥️ 终端本地对话" cards. Anything newer is fed through `ingest()` so
706
+ * the freshly-written Lark user event can match its pending fingerprint.
707
+ *
708
+ * Events with malformed / missing timestamps fall into `live`: better
709
+ * to forward an unattributable event once than to silently drop a real
710
+ * reply because Claude omitted a timestamp.
711
+ */
712
+ export function splitTranscriptEventsByCutoff(events, cutoffMs) {
713
+ const history = [];
714
+ const live = [];
715
+ for (const ev of events) {
716
+ let evMs = Number.NaN;
717
+ if (typeof ev.timestamp === 'string')
718
+ evMs = Date.parse(ev.timestamp);
719
+ if (Number.isFinite(evMs) && evMs <= cutoffMs)
720
+ history.push(ev);
721
+ else
722
+ live.push(ev);
723
+ }
724
+ return { history, live };
725
+ }
726
+ /**
727
+ * Read the first event timestamp out of a jsonl. Reads only the leading
728
+ * 4 KB — Claude's `file-history-snapshot` and `SessionStart` events both
729
+ * land in the first few hundred bytes. Returns the parsed millis, or
730
+ * undefined when no parseable timestamp is found in the leading chunk
731
+ * (corrupted file, partial first line, format change).
732
+ *
733
+ * NOTE: not currently wired into the bridge rotation flow. The bridge
734
+ * fingerprint fallback (`decideFingerprintSwitch` in
735
+ * `bridge-rotation-policy.ts`) deliberately rejects candidates outside
736
+ * the pid-derived trust set rather than relying on freshness heuristics
737
+ * — file-creation timestamps cannot prove ownership across panes in
738
+ * the same project dir. Kept here as a reusable primitive for
739
+ * diagnostics and future /clear-recovery work.
740
+ */
741
+ export function readFirstEventTimestamp(path) {
742
+ let fd;
743
+ try {
744
+ fd = openSync(path, 'r');
745
+ }
746
+ catch {
747
+ return undefined;
748
+ }
749
+ try {
750
+ const len = 4096;
751
+ const buf = Buffer.alloc(len);
752
+ let bytesRead = 0;
753
+ try {
754
+ bytesRead = readSync(fd, buf, 0, len, 0);
755
+ }
756
+ catch {
757
+ return undefined;
758
+ }
759
+ if (bytesRead <= 0)
760
+ return undefined;
761
+ const text = buf.subarray(0, bytesRead).toString('utf8');
762
+ const lines = text.split('\n');
763
+ // Drop the trailing partial line if we read exactly `len` bytes — it
764
+ // may not be a complete JSON object. When the whole file is shorter
765
+ // than `len` bytes the last line is complete and we keep it.
766
+ const usable = bytesRead === len ? lines.slice(0, -1) : lines;
767
+ for (const line of usable) {
768
+ const trimmed = line.trim();
769
+ if (!trimmed)
770
+ continue;
771
+ let ev;
772
+ try {
773
+ ev = JSON.parse(trimmed);
774
+ }
775
+ catch {
776
+ continue;
777
+ }
778
+ // Top-level `timestamp` field — covers both regular events
779
+ // (user/assistant/attachment) and `file-history-snapshot` records
780
+ // whose `timestamp` lives under `snapshot.timestamp` instead.
781
+ const tsStr = typeof ev?.timestamp === 'string'
782
+ ? ev.timestamp
783
+ : typeof ev?.snapshot?.timestamp === 'string'
784
+ ? ev.snapshot.timestamp
785
+ : undefined;
786
+ if (!tsStr)
787
+ continue;
788
+ const ms = Date.parse(tsStr);
789
+ if (Number.isFinite(ms))
790
+ return ms;
791
+ }
792
+ return undefined;
793
+ }
794
+ finally {
795
+ closeSync(fd);
796
+ }
797
+ }
798
+ //# sourceMappingURL=claude-transcript.js.map