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
@@ -1,16 +1,45 @@
1
- import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
1
+ import { existsSync, statSync, openSync, readSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { resolveCommand } from './registry.js';
5
+ import { findJsonlContainingFingerprint, jsonlContainsFingerprint, normaliseForFingerprint } from '../../services/claude-transcript.js';
6
+ /** Resolve cwd to its canonical (symlink-free) absolute path for project-hash
7
+ * computation. Claude Code itself runs `process.cwd()` which the kernel returns
8
+ * already realpath'd via getcwd(3) — so its on-disk project hash always reflects
9
+ * the realpath, not the symlink we may have spawned it under. We must mirror
10
+ * that here, otherwise a deployment whose `workingDir` is a symlink (e.g.
11
+ * `/home/user` → `/data00/home/user`) computes the wrong project dir, the
12
+ * bridge watcher tails a non-existent file, submit-confirm never sees the
13
+ * user line, and the no-`botmux send` fallback never emits. realpathSync
14
+ * throws on non-existent paths — fall back to the raw cwd in that case so a
15
+ * pre-existence check upstream can still report a useful error. */
16
+ function realpathCwd(cwd) {
17
+ try {
18
+ return realpathSync(cwd);
19
+ }
20
+ catch {
21
+ return cwd;
22
+ }
23
+ }
5
24
  /** Resolve the JSONL transcript path Claude Code writes user/assistant turns to.
6
- * Claude Code's project-hash scheme replaces both `/` and `.` with `-`. */
25
+ * Claude Code's project-hash scheme replaces every non-[A-Za-z0-9-] char with `-`
26
+ * (observed: `/foo/life_workspace` → `-foo-life-workspace`; `/`, `.`, `_` all become `-`).
27
+ * Always operates on realpath(cwd) — see realpathCwd above. */
7
28
  export function claudeJsonlPathForSession(sessionId, cwd) {
8
- const projectHash = cwd.replace(/[/.]/g, '-');
29
+ const projectHash = realpathCwd(cwd).replace(/[^A-Za-z0-9-]/g, '-');
9
30
  return join(homedir(), '.claude', 'projects', projectHash, `${sessionId}.jsonl`);
10
31
  }
11
- /** Substring that appears on every real user submission line and NOT on tool-result
12
- * lines (those have array content: `"content":[{...}]`). */
13
- const USER_SUBMIT_MARKER = '"role":"user","content":"';
32
+ /** Substrings that indicate Claude Code received our submit. We accept either:
33
+ * - `"role":"user","content":"` — direct submission while idle (the canonical
34
+ * user-message line; tool-result lines have array content `"content":[{...`
35
+ * so they never match).
36
+ * - `"operation":"enqueue"` — type-ahead submission while Claude is busy.
37
+ * Claude Code logs a `{"type":"queue-operation","operation":"enqueue",...}`
38
+ * line at the moment of submit and only later (after the current turn ends)
39
+ * promotes it to a `queued_command` attachment — never to a `role:user`
40
+ * string-content line. Without this marker, every type-ahead submit would
41
+ * falsely report failure. */
42
+ const SUBMIT_MARKERS = ['"role":"user","content":"', '"operation":"enqueue"'];
14
43
  function currentFileSize(path) {
15
44
  if (!existsSync(path))
16
45
  return 0;
@@ -21,7 +50,7 @@ function currentFileSize(path) {
21
50
  return 0;
22
51
  }
23
52
  }
24
- function deltaHasUserSubmit(path, fromByte) {
53
+ function deltaHasSubmit(path, fromByte) {
25
54
  if (!existsSync(path))
26
55
  return false;
27
56
  let size;
@@ -42,108 +71,500 @@ function deltaHasUserSubmit(path, fromByte) {
42
71
  finally {
43
72
  closeSync(fd);
44
73
  }
45
- return buf.toString('utf8').includes(USER_SUBMIT_MARKER);
74
+ const text = buf.toString('utf8');
75
+ return SUBMIT_MARKERS.some(m => text.includes(m));
46
76
  }
47
- async function waitForUserSubmit(path, baseByte, timeoutMs) {
77
+ async function waitForSubmit(path, baseByte, timeoutMs) {
48
78
  const deadline = Date.now() + timeoutMs;
49
79
  while (Date.now() < deadline) {
50
- if (deltaHasUserSubmit(path, baseByte))
80
+ if (deltaHasSubmit(path, baseByte))
51
81
  return true;
52
82
  await new Promise(r => setTimeout(r, 100));
53
83
  }
54
84
  return false;
55
85
  }
56
- const COMPLETION_RE = /\u2733\s*(?:Worked|Crunched|Cogitated|Cooked|Churned|Saut[eé]ed) for \d+[smh]/;
86
+ function makeSubmitFingerprint(content, len = 30) {
87
+ const collapsed = normaliseForFingerprint(content);
88
+ return collapsed.length > 0 ? collapsed.substring(0, len) : undefined;
89
+ }
90
+ const SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
91
+ /** Returns the absolute path to Claude Code's per-process session state file.
92
+ * Claude writes `{pid, sessionId, cwd, procStart, status, updatedAt, ...}`
93
+ * here. Empirical scope (Claude Code 2.1.123): `status` and `updatedAt`
94
+ * refresh on every state change, but `sessionId` is written ONCE at
95
+ * process start. `--resume` is a fresh spawn → fresh pid file with the
96
+ * resumed id; in-pane `/clear` does NOT rewrite the pid file's
97
+ * `sessionId` even though it rotates the on-disk jsonl. Callers that
98
+ * rely on this for rotation tracking must therefore treat a "matching
99
+ * sessionId" answer as "no spawn-time rotation observed", not "no
100
+ * rotation at all" — the latter requires fingerprint corroboration. */
101
+ export function claudePidStatePath(pid) {
102
+ return join(homedir(), '.claude', 'sessions', `${pid}.json`);
103
+ }
104
+ /** Linux-only: read /proc/<pid>/stat field 22 (starttime). Returns null when
105
+ * /proc isn't available or the stat line is unreadable/malformed; callers
106
+ * decide whether to fail closed or skip validation for their platform. */
107
+ function readProcStarttime(pid) {
108
+ try {
109
+ const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
110
+ // pid (comm) state ppid pgrp ... — comm may contain spaces/parens, so
111
+ // anchor on the LAST ')' before splitting the remaining fields.
112
+ const closeParen = raw.lastIndexOf(')');
113
+ if (closeParen < 0)
114
+ return null;
115
+ const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
116
+ // Post-')' field 1 is state; starttime is field 22 → index 19 here.
117
+ return fields[19] ?? null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /** Resolve Claude Code's authoritative current session id via
124
+ * ~/.claude/sessions/<pid>.json. Validates pid + sessionId UUID + cwd so a
125
+ * stale or unrelated pid file can't redirect us to the wrong jsonl. On Linux
126
+ * also matches procStart against /proc/<pid>/stat to reject PID reuse. If
127
+ * procStart is present but cannot be verified on Linux, fail closed; callers
128
+ * fall back to fingerprint detection. */
129
+ export function resolveJsonlFromPid(pid, expectedCwd) {
130
+ if (!Number.isInteger(pid) || pid <= 0)
131
+ return null;
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(readFileSync(claudePidStatePath(pid), 'utf8'));
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ if (!parsed || typeof parsed !== 'object')
140
+ return null;
141
+ if (parsed.pid !== pid)
142
+ return null;
143
+ if (typeof parsed.sessionId !== 'string' || !SESSION_UUID_RE.test(parsed.sessionId))
144
+ return null;
145
+ if (typeof parsed.cwd !== 'string')
146
+ return null;
147
+ // Identity check: procStart matching against /proc/<pid>/stat field 22 is
148
+ // the strong signal that this pid file belongs to the live process (rules
149
+ // out pid reuse). When that holds, Claude's recorded cwd is authoritative
150
+ // even if it disagrees with `expectedCwd` — the worker's cliCwd can drift
151
+ // (e.g. a schedule resumes a session with a different workingDir than the
152
+ // original spawn, but Claude itself loads the session with its own cwd).
153
+ // When procStart is unavailable/unverifiable, fall back to cwd equality as
154
+ // the only remaining sanity check. Realpath both sides so a symlinked
155
+ // workingDir (/home/x → /data00/home/x) still matches Claude's canonical
156
+ // cwd from getcwd(3).
157
+ let procStartVerified = false;
158
+ if (typeof parsed.procStart === 'string') {
159
+ const live = readProcStarttime(pid);
160
+ if (live === null && process.platform === 'linux')
161
+ return null;
162
+ if (live !== null) {
163
+ if (live !== parsed.procStart)
164
+ return null;
165
+ procStartVerified = true;
166
+ }
167
+ }
168
+ if (!procStartVerified && realpathCwd(parsed.cwd) !== realpathCwd(expectedCwd))
169
+ return null;
170
+ return {
171
+ path: claudeJsonlPathForSession(parsed.sessionId, parsed.cwd),
172
+ cliSessionId: parsed.sessionId,
173
+ };
174
+ }
175
+ /** Linux-only: probe `/proc/<pid>/fd` for any signal that reveals Claude's
176
+ * CURRENT sessionId — not the spawn-time one the pid file records. Two
177
+ * signals are checked:
178
+ * 1. Direct `.jsonl` symlinks under `~/.claude/projects/...` — Claude
179
+ * opens-writes-closes per event, so this only hits if the probe
180
+ * lands during a write window.
181
+ * 2. `~/.claude/tasks/<sessionId>(/...)` symlinks — Claude holds the
182
+ * tasks directory and its `.lock` file open continuously for the
183
+ * duration of the active session, so this signal is reliable even
184
+ * between writes. This is the path that catches in-pane `/clear`
185
+ * rotations the pid file can't see (pid file's `sessionId` is set
186
+ * once at process start; tasks dir tracks every rotation).
187
+ * Returns deduplicated sessionIds in arbitrary order; caller picks one
188
+ * (typically by mtime of the corresponding jsonl). Returns [] on
189
+ * non-Linux platforms or if /proc lookup fails. */
190
+ export function findOpenClaudeSessionIds(pid) {
191
+ if (!Number.isInteger(pid) || pid <= 0)
192
+ return [];
193
+ if (process.platform !== 'linux')
194
+ return [];
195
+ let entries;
196
+ try {
197
+ entries = readdirSync(`/proc/${pid}/fd`);
198
+ }
199
+ catch {
200
+ return [];
201
+ }
202
+ const tasksPrefix = join(homedir(), '.claude', 'tasks') + '/';
203
+ const projectsInfix = '/.claude/projects/';
204
+ const out = new Set();
205
+ for (const name of entries) {
206
+ let target;
207
+ try {
208
+ target = readlinkSync(`/proc/${pid}/fd/${name}`);
209
+ }
210
+ catch {
211
+ continue;
212
+ }
213
+ if (target.startsWith(tasksPrefix)) {
214
+ const sid = target.slice(tasksPrefix.length).split('/')[0];
215
+ if (sid && SESSION_UUID_RE.test(sid))
216
+ out.add(sid);
217
+ continue;
218
+ }
219
+ if (target.endsWith('.jsonl') && target.includes(projectsInfix)) {
220
+ const base = target.split('/').pop() ?? '';
221
+ const sid = base.endsWith('.jsonl') ? base.slice(0, -'.jsonl'.length) : '';
222
+ if (sid && SESSION_UUID_RE.test(sid))
223
+ out.add(sid);
224
+ }
225
+ }
226
+ return [...out];
227
+ }
228
+ /** Fingerprint search that fans out from the pinned project dir to every
229
+ * sibling under `~/.claude/projects/`. Used as the writeInput fallback
230
+ * when the pinned `claudeJsonlPath` doesn't contain the submit marker —
231
+ * Claude may have written to a different project hash than the worker
232
+ * expected (e.g. a schedule resumed the session with a workingDir that
233
+ * differs from Claude's internal cwd, so the worker computes the wrong
234
+ * -project-hash- but Claude appends to the original session's hash dir).
235
+ * Tries the primary dir first (fast path, unchanged behavior); only fans
236
+ * out when no match is found there. Per-dir, `findJsonlContainingFingerprint`
237
+ * still applies its newest-first ordering and the minMtimeMs guard, so a
238
+ * stale historical match in some unrelated project can't false-positive. */
239
+ function findJsonlAcrossProjectsRoot(searchPath, fingerprint, options) {
240
+ const primaryDir = dirname(searchPath);
241
+ const primary = findJsonlContainingFingerprint(primaryDir, fingerprint, {
242
+ excludePath: searchPath,
243
+ ...options,
244
+ });
245
+ if (primary)
246
+ return primary;
247
+ const projectsRoot = dirname(primaryDir);
248
+ if (!existsSync(projectsRoot))
249
+ return null;
250
+ let siblings;
251
+ try {
252
+ siblings = readdirSync(projectsRoot);
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ for (const name of siblings) {
258
+ const sib = join(projectsRoot, name);
259
+ if (sib === primaryDir)
260
+ continue;
261
+ const matched = findJsonlContainingFingerprint(sib, fingerprint, {
262
+ excludePath: searchPath,
263
+ ...options,
264
+ });
265
+ if (matched)
266
+ return matched;
267
+ }
268
+ return null;
269
+ }
270
+ const COMPLETION_RE = /\u2733\s*(?:Worked|Crunched|Cogitated|Cooked|Churned|Saut[eé]ed|Baked|Brewed) for \d+[smh]/;
57
271
  export function createClaudeCodeAdapter(pathOverride) {
58
272
  const bin = resolveCommand(pathOverride ?? 'claude');
59
273
  return {
60
274
  id: 'claude-code',
61
275
  resolvedBin: bin,
62
276
  supportsTypeAhead: true,
63
- buildArgs({ sessionId, resume, botName, botOpenId }) {
277
+ buildResumeCommand({ sessionId, cliSessionId }) {
278
+ // Claude resumes by reading <id>.jsonl, so we need the most recently
279
+ // observed CLI-native id (rotation can happen mid-run); fall back to the
280
+ // botmux sessionId for the first-turn case where they coincide.
281
+ return `claude --resume ${cliSessionId ?? sessionId}`;
282
+ },
283
+ buildArgs({ sessionId, resume, resumeSessionId, botName, botOpenId }) {
64
284
  const args = [];
65
285
  if (resume) {
66
- args.push('--resume', sessionId);
286
+ // Prefer Claude's most recently observed internal session id when we
287
+ // have one — `--resume` reads `<id>.jsonl`, and after a previous run
288
+ // rotated the id (which we now persist via the pid-file resolver) the
289
+ // botmux sessionId no longer matches Claude's actual transcript file.
290
+ args.push('--resume', resumeSessionId ?? sessionId);
67
291
  }
68
292
  else {
69
293
  args.push('--session-id', sessionId);
70
294
  }
71
295
  args.push('--dangerously-skip-permissions');
72
296
  args.push('--disallowed-tools', 'EnterPlanMode,ExitPlanMode');
73
- const identityLines = botName || botOpenId
297
+ const identityBlock = botName || botOpenId
74
298
  ? [
75
299
  '',
76
- '你的身份:',
77
- `- 名字:${botName ?? '(未知)'}`,
78
- `- open_id:${botOpenId ?? '(未知)'}`,
79
- '同一群里可能有多个机器人同时被 @,消息里会以 `@名字` 和 `open_id` 区分。',
80
- '判断本条消息是不是分派给你:对照上面的名字和 open_id',
81
- '- 只执行明确分给自己的那部分,别抢别的机器人的活',
82
- '- 整条消息都指派给别的机器人时,保持沉默不要回复',
83
- '- 需要找对端协作时先用 `botmux bots list` 查 open_id,再用 `botmux send --mention <open_id>` @ 对方',
300
+ '<identity>',
301
+ ` <name>${botName ?? '(未知)'}</name>`,
302
+ ` <open_id>${botOpenId ?? '(未知)'}</open_id>`,
303
+ ' <routing_rules>',
304
+ ' 群里可能有多个机器人,消息里用 `@名字` 和 `open_id` 区分接收方。对照上面的 name/open_id 判断本条消息归属:',
305
+ ' - 只执行明确分给自己的那部分,别抢别的机器人的活',
306
+ ' - 整条消息都指派给别的机器人时,保持沉默不要回复',
307
+ ' - **默认不主动拉别的 bot 进来**。除非用户明确要求、或某段任务只能由对方做,否则一个人做完自己的部分就行。',
308
+ '',
309
+ ' **和别的机器人协作的硬性物理事实**:飞书话题群里其他 bot **默认收不到** 你 `botmux send` 出去的消息——',
310
+ ' 要让某个 bot 接力干活,**必须** 显式 `--mention <对方 bot 的 open_id>`,不 --mention 对方 bot 完全不会被触发。',
311
+ ' - 协作伙伴的 open_id 会列在每条用户消息附带的 `<available_bots>` 块里,也可以 `botmux bots list` 查',
312
+ ' - 用法:`botmux send --mention ou_xxx "消息内容"`(多个 bot 重复 `--mention`);正文里写 `@对方名字` 时 botmux 也会自动补上 --mention,但显式传更稳',
313
+ ' - 该 --mention 的场景:用户明确要求让对方接力、把任务的某段交给对方、需要对方给最终结论或做独立操作',
314
+ ' - 不必 --mention 的场景:纯状态更新/确认/感谢——尽量合并到下一次有内容的消息里再带上,避免互相 ping 触发空转',
315
+ ' </routing_rules>',
316
+ '</identity>',
84
317
  ]
85
318
  : [];
86
319
  args.push('--append-system-prompt', [
320
+ '<botmux_routing>',
87
321
  '你连接到了飞书(Lark)话题群。用户在飞书上阅读,看不到你的终端输出。',
88
322
  '想让用户看到的内容必须通过 `botmux send` 命令发送,终端输出不会到达聊天。',
89
323
  '',
90
324
  '使用指南:',
91
325
  '- 用 `botmux send` 发送:关键结论、方案(等用户确认再执行)、最终结果、进度更新。',
92
- '- 发送纯文本即可:`botmux send "消息"` 或用 heredoc 传多行。格式自动处理。',
326
+ '- 发送纯文本即可:`botmux send "消息"`。格式自动处理。',
327
+ '- 多行消息必须用 heredoc,禁止写成 `botmux send "第一行\\n第二行"`;否则 `\\n` 可能按字面量显示在飞书里。',
328
+ " 正确多行示例:\n```bash\nbotmux send <<'EOF'\n第一行\n第二行\nEOF\n```",
93
329
  '- 附带图片:`botmux send --images /path/to/img.png "说明文字"`',
94
330
  '- 附带文件:`botmux send --files /path/to/file.pdf "请查收"`',
95
- '- 需要上下文时用 `botmux thread messages` 读取之前的对话。',
331
+ '- 需要上下文时用 `botmux history` 读取之前的对话。',
96
332
  '- 查看可协作的机器人:`botmux bots list`',
97
- ...identityLines,
333
+ '</botmux_routing>',
334
+ ...identityBlock,
98
335
  ].join('\n'));
99
336
  return args;
100
337
  },
101
338
  injectsSessionContext: true,
102
339
  async writeInput(pty, content) {
103
- // Always use bracketed paste: Claude Code's paste-burst heuristic can
104
- // swallow a trailing Enter sent via send-keys -l + send-keys Enter,
105
- // leaving content in the input box. Bracketed paste marks an explicit
106
- // \x1b[201~ boundary so the post-paste Enter is unambiguously submit.
107
- // Past that, we still can't trust fixed-delay timing (slow disk, big
108
- // paste, image loading). The robust signal is Claude Code's session
109
- // JSONL: each real user submit appends a line if no new line appears,
110
- // Enter was swallowed and we re-send it.
340
+ // Type content like a human: literal text via send-keys -l, and each
341
+ // newline replaced by `\` + Enter (Claude Code's documented soft-newline
342
+ // idiom — keeps content in the input box without submitting). The final
343
+ // Enter at the bottom is the unambiguous submit. This sidesteps tmux
344
+ // bracketed-paste mode entirely, which was unreliable: Claude Code can
345
+ // toggle bracketed-paste off mid-session (after slash commands etc.),
346
+ // making tmux's paste-buffer drop the markers and turning embedded \r
347
+ // into Enters that fragment the message into multiple submits.
348
+ //
349
+ // Each tmux send-keys is throttled so the cumulative input rate stays
350
+ // below Claude Code's paste-burst threshold — otherwise on long messages
351
+ // (~1300+ chars / ~25+ lines) Ink flips into paste mode mid-stream and
352
+ // subsequent `\` + Enter pairs are kept as literal `\\\r` in the
353
+ // submitted content instead of being consumed as soft-newline markers.
354
+ //
355
+ // Trailing Enter is still subject to Claude Code's paste-burst heuristic
356
+ // (rapid input followed by Enter can be coalesced as paste), so we keep
357
+ // the JSONL retry loop below as the source of truth for "did it submit".
111
358
  const hasImagePath = /\.(jpe?g|png|gif|webp|svg|bmp)\b/i.test(content);
112
359
  const submitDelay = hasImagePath ? 800 : 500;
360
+ const TYPING_THROTTLE_MS = 30;
361
+ const tick = () => new Promise(r => setTimeout(r, TYPING_THROTTLE_MS));
113
362
  const sendEnter = () => {
114
363
  if (pty.sendSpecialKeys)
115
364
  pty.sendSpecialKeys('Enter');
116
365
  else
117
366
  pty.write('\r');
118
367
  };
119
- const baseByte = pty.claudeJsonlPath ? currentFileSize(pty.claudeJsonlPath) : 0;
120
- if (pty.pasteText && pty.sendSpecialKeys) {
121
- pty.pasteText(content);
368
+ // Pid-state path resolver: ~/.claude/sessions/<pid>.json carries
369
+ // the spawn-time sessionId (written once at process start; see
370
+ // claudePidStatePath). Read it first so byte accounting locks onto
371
+ // the resume target right away when Claude was started with
372
+ // `--resume`. In-pane `/clear` won't appear here — that's covered
373
+ // by the fingerprint-based mid-flight rotation check below.
374
+ let observedCliSessionId;
375
+ const applyResolved = (resolved) => {
376
+ if (resolved.cliSessionId !== observedCliSessionId)
377
+ observedCliSessionId = resolved.cliSessionId;
378
+ if (resolved.path !== pty.claudeJsonlPath) {
379
+ pty.claudeJsonlPath = resolved.path;
380
+ return true;
381
+ }
382
+ return false;
383
+ };
384
+ if (pty.cliPid && pty.cliCwd) {
385
+ const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
386
+ if (resolved)
387
+ applyResolved(resolved);
388
+ }
389
+ // baseByte is recomputed at this point (after any entry-time path swap)
390
+ // so future writes are measured against the right transcript. Inside
391
+ // confirmSubmit a mid-flight rotation does NOT advance baseByte — the
392
+ // submit may already be in the rotated jsonl from before our re-resolve.
393
+ let baseByte = pty.claudeJsonlPath ? currentFileSize(pty.claudeJsonlPath) : 0;
394
+ const submitFingerprint = makeSubmitFingerprint(content);
395
+ const submitSearchMinMtime = Date.now() - 60_000;
396
+ if (pty.sendText && pty.sendSpecialKeys) {
397
+ const lines = content.split('\n');
398
+ for (let i = 0; i < lines.length; i++) {
399
+ if (lines[i].length > 0) {
400
+ pty.sendText(lines[i]);
401
+ await tick();
402
+ }
403
+ if (i < lines.length - 1) {
404
+ // Soft-newline: backslash + Enter inserts a newline in Claude
405
+ // Code's input box without submitting.
406
+ pty.sendText('\\');
407
+ await tick();
408
+ pty.sendSpecialKeys('Enter');
409
+ await tick();
410
+ }
411
+ }
122
412
  }
123
413
  else {
414
+ // Non-tmux fallback (raw PTY): bracketed paste is reliable here since
415
+ // we control the markers directly.
124
416
  pty.write('\x1b[200~' + content + '\x1b[201~');
125
417
  }
126
418
  await new Promise(r => setTimeout(r, submitDelay));
127
419
  sendEnter();
128
420
  // Without a JSONL path we can't verify — trust the fixed delay and return.
129
- if (!pty.claudeJsonlPath)
130
- return;
421
+ // Still surface any sessionId we observed via the pid resolver so the
422
+ // worker can persist it even on this unverified path.
423
+ if (!pty.claudeJsonlPath) {
424
+ return observedCliSessionId ? { submitted: true, cliSessionId: observedCliSessionId } : undefined;
425
+ }
426
+ const confirmSubmit = async (timeoutMs) => {
427
+ const startPath = pty.claudeJsonlPath;
428
+ if (!startPath)
429
+ return false;
430
+ // First check: did our submit land past baseByte on the currently
431
+ // pinned path? Fast path for the common case (no rotation).
432
+ if (await waitForSubmit(startPath, baseByte, timeoutMs))
433
+ return true;
434
+ // Second: did Claude rotate sessionId mid-flight? The pid file
435
+ // is rewritten by `--resume` (fresh spawn) but NOT by in-pane
436
+ // `/clear` — so this catches the resume case. We re-read and
437
+ // check both:
438
+ // a) the rotated jsonl already contains our submit (the rotation
439
+ // happened between our type+Enter and this resolve — the
440
+ // content lives in the new file from before we knew about it),
441
+ // b) the rotated jsonl is empty / pre-existing but a fresh
442
+ // append is on its way (briefly poll).
443
+ // We do NOT overwrite the original baseByte before the fingerprint
444
+ // check because (a) requires matching content that may already be in
445
+ // the rotated file. For (b), poll from the rotated file's own current
446
+ // size so an older, larger startPath cannot hide a delayed append.
447
+ if (pty.cliPid && pty.cliCwd) {
448
+ const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
449
+ if (resolved) {
450
+ const switched = applyResolved(resolved);
451
+ const newPath = pty.claudeJsonlPath;
452
+ const rotatedBaseByte = switched && newPath ? currentFileSize(newPath) : baseByte;
453
+ if (switched && newPath && submitFingerprint) {
454
+ if (jsonlContainsFingerprint(newPath, submitFingerprint, { includeQueueOperations: true })) {
455
+ // Sync baseByte to end-of-file so subsequent confirms in
456
+ // this writeInput pass don't re-trigger on the same line.
457
+ baseByte = currentFileSize(newPath);
458
+ return true;
459
+ }
460
+ }
461
+ if (newPath) {
462
+ if (await waitForSubmit(newPath, rotatedBaseByte, switched ? 200 : 0)) {
463
+ if (switched)
464
+ baseByte = currentFileSize(newPath);
465
+ return true;
466
+ }
467
+ }
468
+ }
469
+ }
470
+ // Final fallback when the pid file is unavailable / fails validation:
471
+ // scan the pinned project dir for a recently-written jsonl whose
472
+ // tail contains our content fingerprint. Stricter than mtime-based
473
+ // detection so a sibling pane in the same dir can't hijack us.
474
+ // Per-attempt scope is intentionally narrow (dirname only) — the
475
+ // cross-project fan-out only runs once at end-of-writeInput and in
476
+ // the recheck closure, not per retry, to keep the worst case bounded.
477
+ if (submitFingerprint) {
478
+ const searchPath = pty.claudeJsonlPath ?? startPath;
479
+ const matched = findJsonlContainingFingerprint(dirname(searchPath), submitFingerprint, {
480
+ excludePath: searchPath,
481
+ minMtimeMs: submitSearchMinMtime,
482
+ includeQueueOperations: true,
483
+ });
484
+ if (matched) {
485
+ pty.claudeJsonlPath = matched;
486
+ return true;
487
+ }
488
+ }
489
+ return false;
490
+ };
491
+ const buildResult = (submitted) => {
492
+ return observedCliSessionId
493
+ ? { submitted, cliSessionId: observedCliSessionId }
494
+ : { submitted };
495
+ };
131
496
  // Retry budget: up to 2 extra Enters (3 sends total), each followed by
132
- // an 800ms wait for the JSONL append. If the user is concurrently typing
133
- // in the web terminal, a stray Enter may submit their half-typed text
134
- // but we only retry when the JSONL is provably unchanged, so the race
135
- // window is bounded to cases where submit really did fail.
497
+ // an 800ms wait for the JSONL to record either a direct user-submit line
498
+ // or a type-ahead enqueue line. If the user is concurrently typing in the
499
+ // web terminal, a stray Enter may submit their half-typed text but we
500
+ // only retry when the JSONL is provably unchanged, so the race window is
501
+ // bounded to cases where submit really did fail.
136
502
  for (let attempt = 0; attempt < 3; attempt++) {
137
- if (await waitForUserSubmit(pty.claudeJsonlPath, baseByte, 800))
138
- return;
503
+ if (await confirmSubmit(800)) {
504
+ return observedCliSessionId ? buildResult(true) : undefined;
505
+ }
139
506
  sendEnter();
140
507
  }
141
508
  // Final grace check.
142
- if (await waitForUserSubmit(pty.claudeJsonlPath, baseByte, 800))
143
- return;
144
- // All retries exhausted and still no user line in JSONL. Signal failure
509
+ if (await confirmSubmit(800)) {
510
+ return observedCliSessionId ? buildResult(true) : undefined;
511
+ }
512
+ // Last-resort cross-project fan-out, run ONCE before declaring failure:
513
+ // catches the case where workingDir/cwd drift made every per-attempt
514
+ // scan look in the wrong project dir AND the pid resolver also failed
515
+ // (e.g. pid file missing, /proc unavailable). minMtimeMs filtering and
516
+ // newest-first ordering keep the cost bounded — only jsonls touched in
517
+ // the last 60s are actually read, which is typically a handful even
518
+ // across all sibling project dirs. Per-attempt scans stay narrow
519
+ // (dirname only) so this work doesn't repeat 4×.
520
+ if (submitFingerprint && pty.claudeJsonlPath) {
521
+ const matched = findJsonlAcrossProjectsRoot(pty.claudeJsonlPath, submitFingerprint, {
522
+ minMtimeMs: submitSearchMinMtime,
523
+ includeQueueOperations: true,
524
+ });
525
+ if (matched) {
526
+ pty.claudeJsonlPath = matched;
527
+ return observedCliSessionId ? buildResult(true) : undefined;
528
+ }
529
+ }
530
+ // All retries exhausted and still no submit marker in JSONL. Signal failure
145
531
  // so the worker can notify the user in Lark instead of silently dropping.
146
- return { submitted: false };
532
+ // We still surface observedCliSessionId so the worker can persist Claude's
533
+ // current id even when this particular submit didn't land.
534
+ //
535
+ // Attach a recheck closure: the in-band budget (4 × 800ms) is too short
536
+ // for cold-start sessions and for environments where a slow third-party
537
+ // UserPromptSubmit / SessionStart hook (e.g. superpowers) defers Claude's
538
+ // jsonl append by 5–15s. The worker calls recheck() after a delay, and
539
+ // suppresses the user-facing warning when the line shows up by then.
540
+ const recheck = () => {
541
+ if (!submitFingerprint)
542
+ return false;
543
+ // Latest pid → path; covers post-failure rotations (/clear, /resume).
544
+ if (pty.cliPid && pty.cliCwd) {
545
+ const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
546
+ if (resolved)
547
+ applyResolved(resolved);
548
+ }
549
+ const currentPath = pty.claudeJsonlPath;
550
+ if (currentPath && jsonlContainsFingerprint(currentPath, submitFingerprint, { includeQueueOperations: true })) {
551
+ return true;
552
+ }
553
+ // Fan out to sibling jsonls in the project dir, then across every
554
+ // sibling project dir under `~/.claude/projects/` (catches workingDir
555
+ // drift like worker thinking `-foo-bar/` while Claude actually appends
556
+ // to `-foo-bar-baz/`). Same minMtime guard as the in-band fingerprint
557
+ // fallback so a stale historical match can't suppress the warning.
558
+ const searchPath = currentPath ?? pty.claudeJsonlPath;
559
+ if (!searchPath)
560
+ return false;
561
+ const matched = findJsonlAcrossProjectsRoot(searchPath, submitFingerprint, {
562
+ minMtimeMs: submitSearchMinMtime,
563
+ includeQueueOperations: true,
564
+ });
565
+ return !!matched;
566
+ };
567
+ return { ...buildResult(false), recheck };
147
568
  },
148
569
  completionPattern: COMPLETION_RE,
149
570
  readyPattern: /❯/,