botmux 2.9.0 → 2.9.2

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 +601 -309
  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 +36 -0
  170. package/dist/im/lark/client.d.ts.map +1 -1
  171. package/dist/im/lark/client.js +119 -13
  172. package/dist/im/lark/client.js.map +1 -1
  173. package/dist/im/lark/event-dispatcher.d.ts +92 -8
  174. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  175. package/dist/im/lark/event-dispatcher.js +410 -89
  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 +2220 -83
  329. package/dist/worker.js.map +1 -1
  330. package/package.json +12 -5
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Adopt-bridge turn attribution state machine.
3
+ *
4
+ * Pure (no fs / IPC / timers) so the worker can wrap it with watchers and
5
+ * tests can drive it deterministically. The worker feeds it transcript
6
+ * events (already drained from JSONL) and Lark-message markers; this class
7
+ * decides which assistant uuids belong to which Lark turn.
8
+ *
9
+ * Attribution rule:
10
+ * - mark() — pushes a new pending turn entry (state: not started)
11
+ * - ingest(events) — for each new user/assistant event:
12
+ * * user event → the earliest unstarted pending turn whose fingerprint
13
+ * matches becomes 'started' (its assistantUuids will collect from
14
+ * now on). A user event that does NOT match any pending fingerprint
15
+ * (or arrives with no pending Lark turn at all) is treated as
16
+ * **local terminal input**: a synthetic local turn is created on
17
+ * the spot, started immediately, and inserted ahead of any
18
+ * still-unstarted Lark turns so emit ordering reflects when the
19
+ * user event actually landed in the transcript. The local turn is
20
+ * emitted with `isLocal: true` so the worker can format it with a
21
+ * "user typed in the terminal" marker for the Lark thread.
22
+ * * assistant text event (non-sidechain) → appended to the
23
+ * currently-collecting turn (Lark or local), if any.
24
+ * - drainEmittable() — pops any leading turn that has been started AND has
25
+ * accumulated at least one visible assistant-text uuid. Started turns with no text
26
+ * yet (Claude is mid-tool-use) stay queued for the next idle.
27
+ *
28
+ * Baseline (`absorb()`) takes a batch of historical events and registers
29
+ * their uuids as already-seen so future ingest doesn't double-attribute.
30
+ */
31
+ import { normaliseForFingerprint, isMeaningfulUserEvent, isMeaningfulQueuedCommand, extractTurnStartText } from './claude-transcript.js';
32
+ // Re-export so existing callers (worker.ts, tests) don't need to change
33
+ // their import path now that these helpers live in claude-transcript.ts.
34
+ export { normaliseForFingerprint };
35
+ function assistantHasVisibleText(content) {
36
+ if (typeof content === 'string')
37
+ return content.length > 0;
38
+ if (!Array.isArray(content))
39
+ return false;
40
+ return content.some((block) => block?.type === 'text' && typeof block.text === 'string' && block.text.length > 0);
41
+ }
42
+ /** Trim a Lark message into a stable fingerprint. Keeps a leading window
43
+ * of non-whitespace-collapsed content; long enough to disambiguate, short
44
+ * enough that minor formatting differences (newlines, attachment hints
45
+ * appended below) don't break the match. */
46
+ export function makeFingerprint(message, len = 30) {
47
+ if (typeof message !== 'string')
48
+ return undefined;
49
+ const collapsed = normaliseForFingerprint(message);
50
+ if (collapsed.length === 0)
51
+ return undefined;
52
+ return collapsed.substring(0, len);
53
+ }
54
+ export class BridgeTurnQueue {
55
+ seen = new Set();
56
+ queue = [];
57
+ collecting = null;
58
+ /** Register events as historical — their uuids are now considered seen
59
+ * but no attribution happens. Used at attach time to baseline. */
60
+ absorb(events) {
61
+ for (const ev of events) {
62
+ if (ev.uuid)
63
+ this.seen.add(ev.uuid);
64
+ }
65
+ }
66
+ /** Push a new pending turn for the next Lark message. `contentFingerprint`
67
+ * (when set) restricts which user event can start this turn — only a
68
+ * user event whose content contains the fingerprint qualifies. Pass
69
+ * `undefined` to start on the next user event regardless (legacy).
70
+ *
71
+ * `markTimeMs` is captured here so the rotation fallback can bound its
72
+ * fingerprint scan to events written after this point — protects short
73
+ * fingerprints from matching old history in unrelated sibling jsonls. */
74
+ mark(turnId, contentFingerprint, markTimeMs = Date.now(), contentNormalized) {
75
+ this.queue.push({
76
+ turnId,
77
+ started: false,
78
+ assistantUuids: [],
79
+ contentFingerprint,
80
+ contentNormalized,
81
+ markTimeMs,
82
+ });
83
+ return turnId;
84
+ }
85
+ /** Drop all pending turns. Used when the worker discovers it can't
86
+ * reliably attribute future events (e.g. baseline raced with a turn
87
+ * already in flight) and wants to clear the slate. */
88
+ clearPending() {
89
+ const dropped = this.queue.splice(0);
90
+ if (this.collecting && dropped.includes(this.collecting))
91
+ this.collecting = null;
92
+ return dropped;
93
+ }
94
+ /** Drop a specific pending turn by turnId iff it has not yet started
95
+ * collecting assistant text. Returns the dropped turn or null if not
96
+ * found / already started. Used by the worker when a writeInput's
97
+ * deferred recheck conclusively fails — the user has been notified
98
+ * the message was lost, so keeping a fingerprint-bearing mark around
99
+ * only fuels the per-tick rotation-fallback scan that already
100
+ * spammed 99% CPU once (no jsonl line will ever match). */
101
+ dropPendingTurn(turnId) {
102
+ const idx = this.queue.findIndex(t => t.turnId === turnId && !t.started);
103
+ if (idx === -1)
104
+ return null;
105
+ const [dropped] = this.queue.splice(idx, 1);
106
+ return dropped;
107
+ }
108
+ /** Sweep pending (unstarted) turns whose mark is older than `maxAgeMs`.
109
+ * Returns the dropped turns for logging. Belt-and-braces backstop for
110
+ * any future code path that leaves an unstarted mark stranded — without
111
+ * it, `maybeSwitchBridgeJsonl` would keep doing full-directory jsonl
112
+ * scans every poll tick until the worker restarts. Started turns are
113
+ * never expired here: once Claude actually wrote the user line, the
114
+ * turn is collecting assistant text and we want to wait however long
115
+ * the model takes. */
116
+ pruneExpired(maxAgeMs, now = Date.now()) {
117
+ const dropped = [];
118
+ this.queue = this.queue.filter(t => {
119
+ if (t.started)
120
+ return true;
121
+ if (t.markTimeMs === undefined)
122
+ return true;
123
+ if (now - t.markTimeMs <= maxAgeMs)
124
+ return true;
125
+ dropped.push(t);
126
+ return false;
127
+ });
128
+ return dropped;
129
+ }
130
+ /** Process newly-appended events. Idempotent on uuid: events with seen
131
+ * uuids are skipped, so callers can safely replay.
132
+ *
133
+ * `sourceJsonlPath` (when provided) is stamped onto a turn at the moment
134
+ * it transitions from "pending" to "started" — so that emit-time text
135
+ * resolution reads the same transcript file the user/assistant uuids
136
+ * were originally observed in. Without this, a sessionId rotation
137
+ * between ingest and emit would silently drop the reply, since the
138
+ * global current jsonl path would no longer contain those uuids. */
139
+ ingest(events, sourceJsonlPath) {
140
+ for (const ev of events) {
141
+ const uuid = ev.uuid;
142
+ if (!uuid || this.seen.has(uuid))
143
+ continue;
144
+ this.seen.add(uuid);
145
+ const role = ev.message?.role ?? ev.type;
146
+ if (role === 'user') {
147
+ // Skip ALL non-meaningful user events: tool_result (intra-turn
148
+ // machinery), `<command-name>/clear</command-name>` and other
149
+ // slash-command wrappers (Claude rewrites them after /clear /
150
+ // /resume — same in-process rotation that broke bridge tracking
151
+ // before), isMeta / isCompactSummary markers, sidechain spawns,
152
+ // empty content. These are NOT real user input; treating them as
153
+ // turn boundaries would (a) drop `collecting` mid-stream and lose
154
+ // assistant text after them, and (b) let a synthetic line that
155
+ // accidentally contains the fingerprint substring start the
156
+ // wrong turn.
157
+ if (!isMeaningfulUserEvent(ev))
158
+ continue;
159
+ this.handleTurnStart(uuid, ev, sourceJsonlPath);
160
+ }
161
+ else if (ev.type === 'attachment' && ev.attachment?.type === 'queued_command') {
162
+ // Type-ahead path: Claude writes `attachment(queued_command)` the
163
+ // moment it dequeues a queued submit, immediately before the
164
+ // assistant text for that turn starts streaming. Equivalent to
165
+ // role:user for turn-start purposes; share the same handler so
166
+ // fingerprint-match / HOL-block-drop / local-turn fallback all
167
+ // apply identically. Without this the bridge attribution queue
168
+ // would skip queued_command events and the type-ahead'd turn's
169
+ // assistant text would either be dropped or attributed to a
170
+ // sibling turn.
171
+ if (!isMeaningfulQueuedCommand(ev))
172
+ continue;
173
+ this.handleTurnStart(uuid, ev, sourceJsonlPath);
174
+ }
175
+ else if (role === 'assistant') {
176
+ if (ev.isSidechain === true)
177
+ continue;
178
+ if (!assistantHasVisibleText(ev.message?.content))
179
+ continue;
180
+ if (!this.collecting) {
181
+ // Headless local turn: assistant text arrived without any
182
+ // collecting context. Typical trigger: daemon restart cut off
183
+ // an in-flight model stream — baseline absorbed the original
184
+ // user event (uuid added to `seen`) and the worker process lost
185
+ // its in-memory `collecting` pointer. Without this synthesis the
186
+ // continuation would be silently dropped (assistantHasVisibleText
187
+ // events with no `collecting` were just skipped before).
188
+ // Headless turns have no userUuid; emit-side formatting omits
189
+ // the user block. Inserted at the head of the unstarted region
190
+ // so a subsequent normal turn doesn't get reordered ahead of it.
191
+ const headless = {
192
+ turnId: `local-headless-${uuid}`,
193
+ started: true,
194
+ isLocal: true,
195
+ userUuid: undefined,
196
+ assistantUuids: [],
197
+ sourceJsonlPath,
198
+ markTimeMs: Date.now(),
199
+ };
200
+ const insertAt = this.queue.findIndex(t => !t.started);
201
+ if (insertAt === -1)
202
+ this.queue.push(headless);
203
+ else
204
+ this.queue.splice(insertAt, 0, headless);
205
+ this.collecting = headless;
206
+ }
207
+ this.collecting.assistantUuids.push(uuid);
208
+ }
209
+ }
210
+ }
211
+ /** Shared turn-start handler. Called for both `role:user` and
212
+ * `attachment(queued_command)` events once meaningfulness has been
213
+ * established by the caller. Encapsulates:
214
+ * 1. HOL-block drop of the previous collecting turn when it got no
215
+ * assistant text (Claude moved on).
216
+ * 2. Fingerprint-gated start of the earliest unstarted Lark turn,
217
+ * falling through to local-turn synthesis on mismatch.
218
+ * 3. markTimeMs override to the transcript event's own timestamp —
219
+ * critical for type-ahead, where the original markTimeMs (set when
220
+ * the worker wrote to PTY) can be many seconds earlier than the
221
+ * moment Claude actually dequeues and starts processing the turn.
222
+ * The bridge-fallback gate's [markTimeMs, nextBoundaryMs) window
223
+ * MUST anchor on the latter, otherwise a `botmux send` from the
224
+ * previous turn can leak into the next turn's window and the
225
+ * suppression decision flips to the wrong turn (real reply
226
+ * suppressed, fallback shown — exactly what the type-ahead-disable
227
+ * in commit b2d9791 was protecting against). */
228
+ handleTurnStart(uuid, ev, sourceJsonlPath) {
229
+ // Head-of-line block drop: previous turn never produced any visible
230
+ // assistant text and a new meaningful turn-start has arrived → Claude
231
+ // is single-threaded over the PTY, so the old turn will never get
232
+ // text. Applies to both Lark and local turns.
233
+ if (this.collecting && this.collecting.assistantUuids.length === 0) {
234
+ const idx = this.queue.indexOf(this.collecting);
235
+ if (idx >= 0)
236
+ this.queue.splice(idx, 1);
237
+ this.collecting = null;
238
+ }
239
+ const tsParsed = ev.timestamp ? Date.parse(ev.timestamp) : NaN;
240
+ const eventTimeMs = Number.isFinite(tsParsed) ? tsParsed : Date.now();
241
+ const next = this.queue.find(t => !t.started);
242
+ let consumedNext = false;
243
+ if (next) {
244
+ if (next.contentFingerprint) {
245
+ // Both sides normalised (whitespace-collapsed + trimmed) before
246
+ // the substring check so a transcript line that preserved newlines
247
+ // still matches a fingerprint built from the same text.
248
+ const userText = normaliseForFingerprint(extractTurnStartText(ev));
249
+ if (userText.includes(next.contentFingerprint)) {
250
+ next.started = true;
251
+ if (!next.sourceJsonlPath)
252
+ next.sourceJsonlPath = sourceJsonlPath;
253
+ next.markTimeMs = eventTimeMs;
254
+ this.collecting = next;
255
+ consumedNext = true;
256
+ }
257
+ // Mismatch falls through to local-turn synthesis below.
258
+ }
259
+ else {
260
+ // Legacy mark() with no fingerprint — start on the next turn-start.
261
+ next.started = true;
262
+ if (!next.sourceJsonlPath)
263
+ next.sourceJsonlPath = sourceJsonlPath;
264
+ next.markTimeMs = eventTimeMs;
265
+ this.collecting = next;
266
+ consumedNext = true;
267
+ }
268
+ }
269
+ if (!consumedNext) {
270
+ // Local-terminal input (or a queued_command whose prompt didn't
271
+ // match any pending Lark fingerprint). Synthesise a started turn
272
+ // ahead of any unstarted Lark turn so chronological order matches
273
+ // transcript order at emit time.
274
+ const localTurn = {
275
+ turnId: `local-${uuid}`,
276
+ started: true,
277
+ isLocal: true,
278
+ userUuid: uuid,
279
+ assistantUuids: [],
280
+ sourceJsonlPath,
281
+ markTimeMs: eventTimeMs,
282
+ };
283
+ const insertAt = this.queue.findIndex(t => !t.started);
284
+ if (insertAt === -1)
285
+ this.queue.push(localTurn);
286
+ else
287
+ this.queue.splice(insertAt, 0, localTurn);
288
+ this.collecting = localTurn;
289
+ }
290
+ }
291
+ /** Pop FIFO any leading turn that's started AND has assistant text.
292
+ * Returns the popped turns in order; the caller is responsible for
293
+ * rebuilding the text payload from the assistant uuids. */
294
+ drainEmittable() {
295
+ const out = [];
296
+ while (this.queue.length > 0) {
297
+ const head = this.queue[0];
298
+ if (!head.started || head.assistantUuids.length === 0)
299
+ break;
300
+ this.queue.shift();
301
+ if (this.collecting === head)
302
+ this.collecting = null;
303
+ out.push(head);
304
+ }
305
+ return out;
306
+ }
307
+ /** Number of queued (not-yet-emitted) Lark turns. */
308
+ size() {
309
+ return this.queue.length;
310
+ }
311
+ /** Test helper — peek the queue without mutating. */
312
+ peek() {
313
+ return this.queue;
314
+ }
315
+ }
316
+ //# sourceMappingURL=bridge-turn-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge-turn-queue.js","sourceRoot":"","sources":["../../src/services/bridge-turn-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ export declare function init(appId: string): void;
2
+ /** True iff init() has been called with an appId. Callers that may run before
3
+ * daemon startup (e.g. the dashboard IPC server invoked from tests) can use
4
+ * this to skip persistence rather than crash the request. */
5
+ export declare function isInitialised(): boolean;
6
+ /**
7
+ * Per-chat result of a markSeen call. `preexisting=false` means we stamped
8
+ * this chat for the first time in *this* call — callers that need provenance
9
+ * (e.g. the defaultOncall auto-bind judge) must treat freshly-stamped chats
10
+ * as "unknown age" rather than "new", because a missed backfill manifests
11
+ * exactly the same way as a real first observation.
12
+ */
13
+ export interface SeenEntry {
14
+ firstSeenAt: number;
15
+ preexisting: boolean;
16
+ }
17
+ /**
18
+ * Record `Date.now()` for each chatId not yet known, return per-chat
19
+ * (firstSeenAt, preexisting) for the requested ids. Batches the write so a
20
+ * fresh 44-chat listChats only hits disk once. Empty map is returned when
21
+ * the store hasn't been init()'d (test harnesses that spin the IPC server up
22
+ * without a daemon backing it).
23
+ */
24
+ export declare function markSeenBulkDetailed(chatIds: readonly string[]): Map<string, SeenEntry>;
25
+ /** Back-compat shim — same semantics as before, returns only the timestamps. */
26
+ export declare function markSeenBulk(chatIds: readonly string[]): Map<string, number>;
27
+ //# sourceMappingURL=chat-first-seen-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-first-seen-store.d.ts","sourceRoot":"","sources":["../../src/services/chat-first-seen-store.ts"],"names":[],"mappings":"AAwBA,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIxC;AAOD;;8DAE8D;AAC9D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAiCD;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IAAG,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE;AAExE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAqBvF;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAK5E"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Per-bot "first seen" timestamps for chats — the dashboard sorts Groups & Bots
3
+ * by these timestamps so newly-added chats surface at the top. Lark's chat
4
+ * APIs do not return chat create_time, so we approximate creation order with
5
+ * the moment our daemon first observed each chat in `im.v1.chat.list`.
6
+ *
7
+ * On a fresh install all existing chats get the same backfill timestamp on
8
+ * first enumeration, so their relative order is undefined (the dashboard
9
+ * tie-breaks by name). From that point on, every newly-added chat gets its
10
+ * own real timestamp and rises above the bunch.
11
+ *
12
+ * File layout mirrors session-store: one file per bot at
13
+ * `${config.session.dataDir}/chat-first-seen-${appId}.json`, written
14
+ * atomically via tmp + rename.
15
+ */
16
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
17
+ import { join, dirname } from 'node:path';
18
+ import { config } from '../config.js';
19
+ import { logger } from '../utils/logger.js';
20
+ let firstSeen = new Map();
21
+ let loaded = false;
22
+ let currentAppId;
23
+ export function init(appId) {
24
+ currentAppId = appId;
25
+ loaded = false;
26
+ firstSeen = new Map();
27
+ }
28
+ function getFilePath() {
29
+ if (!currentAppId)
30
+ throw new Error('chat-first-seen-store not initialised (call init(appId) first)');
31
+ return join(config.session.dataDir, `chat-first-seen-${currentAppId}.json`);
32
+ }
33
+ /** True iff init() has been called with an appId. Callers that may run before
34
+ * daemon startup (e.g. the dashboard IPC server invoked from tests) can use
35
+ * this to skip persistence rather than crash the request. */
36
+ export function isInitialised() {
37
+ return !!currentAppId;
38
+ }
39
+ function ensureDir() {
40
+ const dir = dirname(getFilePath());
41
+ if (!existsSync(dir))
42
+ mkdirSync(dir, { recursive: true });
43
+ }
44
+ function load() {
45
+ if (loaded)
46
+ return;
47
+ ensureDir();
48
+ const fp = getFilePath();
49
+ if (existsSync(fp)) {
50
+ try {
51
+ const data = JSON.parse(readFileSync(fp, 'utf-8'));
52
+ firstSeen = new Map(Object.entries(data));
53
+ }
54
+ catch (err) {
55
+ logger.error(`[chat-first-seen] failed to load ${fp}: ${err}`);
56
+ firstSeen = new Map();
57
+ }
58
+ }
59
+ loaded = true;
60
+ }
61
+ function save() {
62
+ ensureDir();
63
+ const fp = getFilePath();
64
+ const tmpFp = fp + '.tmp';
65
+ const obj = {};
66
+ for (const [k, v] of firstSeen)
67
+ obj[k] = v;
68
+ writeFileSync(tmpFp, JSON.stringify(obj, null, 2), 'utf-8');
69
+ renameSync(tmpFp, fp);
70
+ }
71
+ /**
72
+ * Record `Date.now()` for each chatId not yet known, return per-chat
73
+ * (firstSeenAt, preexisting) for the requested ids. Batches the write so a
74
+ * fresh 44-chat listChats only hits disk once. Empty map is returned when
75
+ * the store hasn't been init()'d (test harnesses that spin the IPC server up
76
+ * without a daemon backing it).
77
+ */
78
+ export function markSeenBulkDetailed(chatIds) {
79
+ if (!currentAppId)
80
+ return new Map();
81
+ load();
82
+ const now = Date.now();
83
+ let dirty = false;
84
+ const out = new Map();
85
+ for (const id of chatIds) {
86
+ const prior = firstSeen.get(id);
87
+ if (prior !== undefined) {
88
+ out.set(id, { firstSeenAt: prior, preexisting: true });
89
+ }
90
+ else {
91
+ firstSeen.set(id, now);
92
+ dirty = true;
93
+ out.set(id, { firstSeenAt: now, preexisting: false });
94
+ }
95
+ }
96
+ if (dirty) {
97
+ try {
98
+ save();
99
+ }
100
+ catch (err) {
101
+ logger.error(`[chat-first-seen] save failed: ${err}`);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /** Back-compat shim — same semantics as before, returns only the timestamps. */
107
+ export function markSeenBulk(chatIds) {
108
+ const detailed = markSeenBulkDetailed(chatIds);
109
+ const out = new Map();
110
+ for (const [id, e] of detailed)
111
+ out.set(id, e.firstSeenAt);
112
+ return out;
113
+ }
114
+ //# sourceMappingURL=chat-first-seen-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-first-seen-store.js","sourceRoot":"","sources":["../../src/services/chat-first-seen-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,IAAI,SAAS,GAAwB,IAAI,GAAG,EAAE,CAAC;AAC/C,IAAI,MAAM,GAAG,KAAK,CAAC;AACnB,IAAI,YAAgC,CAAC;AAErC,MAAM,UAAU,IAAI,CAAC,KAAa;IAChC,YAAY,GAAG,KAAK,CAAC;IACrB,MAAM,GAAG,KAAK,CAAC;IACf,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACrG,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,mBAAmB,YAAY,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED;;8DAE8D;AAC9D,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,CAAC,YAAY,CAAC;AACxB,CAAC;AAED,SAAS,SAAS;IAChB,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACnC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,IAAI;IACX,IAAI,MAAM;QAAE,OAAO;IACnB,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC;IACzB,IAAI,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,CAA2B,CAAC;YAC7E,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC;YAC/D,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IACD,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,IAAI;IACX,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC;IAC1B,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3C,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5D,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACxB,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA0B;IAC7D,IAAI,CAAC,YAAY;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IACpC,IAAI,EAAE,CAAC;IACP,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAqB,CAAC;IACzC,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACvB,KAAK,GAAG,IAAI,CAAC;YACb,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YAAC,IAAI,EAAE,CAAC;QAAC,CAAC;QACf,OAAO,GAAG,EAAE,CAAC;YAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,GAAG,EAAE,CAAC,CAAC;QAAC,CAAC;IACxE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,OAA0B;IACrD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,QAAQ;QAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC;IAC3D,OAAO,GAAG,CAAC;AACb,CAAC"}