@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,645 @@
1
+ // Fixture-driven tests for pure event-stream derivation helpers.
2
+ // Focuses on assistant message completion boundaries instead of session.idle.
3
+
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import type { Message as OpenCodeMessage } from '@opencode-ai/sdk/v2'
7
+ import { describe, expect, test } from 'vitest'
8
+ import {
9
+ getOpencodeEventSessionId,
10
+ type OpencodeEventLogEntry,
11
+ } from './opencode-session-event-log.js'
12
+ import {
13
+ getAssistantMessageIdsForLatestUserTurn,
14
+ getCurrentTurnStartTime,
15
+ getDerivedSubtaskIndex,
16
+ getLatestAssistantMessageIdForLatestUserTurn,
17
+ getLatestRunInfo,
18
+ hasAssistantMessageCompletedBefore,
19
+ doesLatestUserTurnHaveNaturalCompletion,
20
+ isAssistantMessageInLatestUserTurn,
21
+ isAssistantMessageNaturalCompletion,
22
+ isSessionBusy,
23
+ type EventBufferEntry,
24
+ } from './event-stream-state.js'
25
+
26
+ const fixturesDir = path.join(import.meta.dirname, 'event-stream-fixtures')
27
+ type AssistantMessage = Extract<OpenCodeMessage, { role: 'assistant' }>
28
+
29
+ function loadFixture(filename: string): EventBufferEntry[] {
30
+ const content = fs.readFileSync(path.join(fixturesDir, filename), 'utf8')
31
+ return content
32
+ .split('\n')
33
+ .filter(Boolean)
34
+ .map((line) => {
35
+ const parsed = JSON.parse(line) as OpencodeEventLogEntry
36
+ return { event: parsed.event, timestamp: parsed.timestamp }
37
+ })
38
+ }
39
+
40
+ function getSessionId(events: EventBufferEntry[]): string {
41
+ for (const entry of events) {
42
+ const sessionId = getOpencodeEventSessionId(entry.event)
43
+ if (sessionId) {
44
+ return sessionId
45
+ }
46
+ }
47
+ throw new Error('No sessionId found in fixture')
48
+ }
49
+
50
+ function getAssistantMessages(events: EventBufferEntry[], sessionId: string) {
51
+ const messagesById = new Map<string, AssistantMessage>()
52
+ events.forEach((entry) => {
53
+ if (entry.event.type !== 'message.updated') {
54
+ return
55
+ }
56
+ const info = entry.event.properties.info
57
+ if (info.sessionID !== sessionId || info.role !== 'assistant') {
58
+ return
59
+ }
60
+ messagesById.set(info.id, info)
61
+ })
62
+ return [...messagesById.values()]
63
+ }
64
+
65
+ function getAssistantMessageById({
66
+ events,
67
+ sessionId,
68
+ messageId,
69
+ }: {
70
+ events: EventBufferEntry[]
71
+ sessionId: string
72
+ messageId: string
73
+ }): AssistantMessage {
74
+ const message = getAssistantMessages(events, sessionId).find((candidate) => {
75
+ return candidate.id === messageId
76
+ })
77
+ if (!message) {
78
+ throw new Error(`Assistant message ${messageId} not found`)
79
+ }
80
+ return message
81
+ }
82
+
83
+ function findAssistantCompletionEventIndex({
84
+ events,
85
+ sessionId,
86
+ messageId,
87
+ }: {
88
+ events: EventBufferEntry[]
89
+ sessionId: string
90
+ messageId: string
91
+ }): number {
92
+ const index = events.findIndex((entry) => {
93
+ if (entry.event.type !== 'message.updated') {
94
+ return false
95
+ }
96
+ const info = entry.event.properties.info
97
+ return info.sessionID === sessionId
98
+ && info.role === 'assistant'
99
+ && info.id === messageId
100
+ && typeof info.time.completed === 'number'
101
+ })
102
+ if (index === -1) {
103
+ throw new Error(`Completed assistant message ${messageId} not found`)
104
+ }
105
+ return index
106
+ }
107
+
108
+ describe('session-normal-completion', () => {
109
+ const events = loadFixture('session-normal-completion.jsonl')
110
+ const sessionId = getSessionId(events)
111
+ const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
112
+ events,
113
+ sessionId,
114
+ })
115
+
116
+ test('latest assistant message completes naturally', () => {
117
+ if (!latestAssistantMessageId) {
118
+ throw new Error('Expected latest assistant message')
119
+ }
120
+ const message = getAssistantMessageById({
121
+ events,
122
+ sessionId,
123
+ messageId: latestAssistantMessageId,
124
+ })
125
+ expect(isAssistantMessageNaturalCompletion({ message })).toBe(true)
126
+ })
127
+
128
+ test('latest user turn start time comes from the latest user message', () => {
129
+ expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636294845)
130
+ })
131
+
132
+ test('completion history only appears after the completed update lands', () => {
133
+ if (!latestAssistantMessageId) {
134
+ throw new Error('Expected latest assistant message')
135
+ }
136
+ const completionIndex = findAssistantCompletionEventIndex({
137
+ events,
138
+ sessionId,
139
+ messageId: latestAssistantMessageId,
140
+ })
141
+ expect(hasAssistantMessageCompletedBefore({
142
+ events,
143
+ sessionId,
144
+ messageId: latestAssistantMessageId,
145
+ upToIndex: completionIndex - 1,
146
+ })).toBe(false)
147
+ expect(hasAssistantMessageCompletedBefore({
148
+ events,
149
+ sessionId,
150
+ messageId: latestAssistantMessageId,
151
+ })).toBe(true)
152
+ })
153
+
154
+ test('getLatestRunInfo', () => {
155
+ expect(getLatestRunInfo({ events, sessionId })).toEqual({
156
+ model: 'deterministic-v2',
157
+ providerID: 'deterministic-provider',
158
+ agent: 'build',
159
+ tokensUsed: 2,
160
+ })
161
+ })
162
+ })
163
+
164
+ describe('session-explicit-abort', () => {
165
+ const events = loadFixture('session-explicit-abort.jsonl')
166
+ const sessionId = getSessionId(events)
167
+ const assistantMessages = getAssistantMessages(events, sessionId)
168
+ const latestAssistant = assistantMessages[assistantMessages.length - 1]
169
+
170
+ test('aborted assistant message is not a natural completion', () => {
171
+ if (!latestAssistant) {
172
+ throw new Error('Expected assistant message in fixture')
173
+ }
174
+ expect(isAssistantMessageNaturalCompletion({ message: latestAssistant })).toBe(false)
175
+ })
176
+ })
177
+
178
+ describe('session-user-interruption', () => {
179
+ const events = loadFixture('session-user-interruption.jsonl')
180
+ const sessionId = getSessionId(events)
181
+ const firstAssistantId = 'msg_cb95be135001I1vqtzLtT4Q1iQ'
182
+ const slowSleepAssistantId = 'msg_cb95be39e001huREyY2wfjgV1M'
183
+ const followupAssistantId = 'msg_cb95beeb8001MuEOER9WprXsPC'
184
+
185
+ test('latest user turn only includes the follow-up assistant message', () => {
186
+ expect(isAssistantMessageInLatestUserTurn({
187
+ events,
188
+ sessionId,
189
+ messageId: firstAssistantId,
190
+ })).toBe(false)
191
+ expect(isAssistantMessageInLatestUserTurn({
192
+ events,
193
+ sessionId,
194
+ messageId: slowSleepAssistantId,
195
+ })).toBe(false)
196
+ expect(isAssistantMessageInLatestUserTurn({
197
+ events,
198
+ sessionId,
199
+ messageId: followupAssistantId,
200
+ })).toBe(true)
201
+ })
202
+
203
+ test('latest user turn start time follows the follow-up user message', () => {
204
+ expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636335777)
205
+ })
206
+ })
207
+
208
+ describe('session-two-completions-same-session', () => {
209
+ const events = loadFixture('session-two-completions-same-session.jsonl')
210
+ const sessionId = getSessionId(events)
211
+ const assistantMessages = getAssistantMessages(events, sessionId)
212
+ const firstAssistant = assistantMessages[0]
213
+ const secondAssistant = assistantMessages[1]
214
+
215
+ test('latest user turn points at the second completion only', () => {
216
+ if (!firstAssistant || !secondAssistant) {
217
+ throw new Error('Expected two assistant messages in fixture')
218
+ }
219
+ expect(isAssistantMessageInLatestUserTurn({
220
+ events,
221
+ sessionId,
222
+ messageId: firstAssistant.id,
223
+ })).toBe(false)
224
+ expect(isAssistantMessageInLatestUserTurn({
225
+ events,
226
+ sessionId,
227
+ messageId: secondAssistant.id,
228
+ })).toBe(true)
229
+ expect(getLatestAssistantMessageIdForLatestUserTurn({
230
+ events,
231
+ sessionId,
232
+ })).toBe(secondAssistant.id)
233
+ })
234
+ })
235
+
236
+ describe('session-concurrent-messages-serialized', () => {
237
+ const events = loadFixture('session-concurrent-messages-serialized.jsonl')
238
+ const sessionId = getSessionId(events)
239
+ const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
240
+ events,
241
+ sessionId,
242
+ })
243
+
244
+ test('fixture latest turn is still incomplete even though an older turn completed', () => {
245
+ expect(doesLatestUserTurnHaveNaturalCompletion({
246
+ events,
247
+ sessionId,
248
+ })).toBe(false)
249
+ if (!latestAssistantMessageId) {
250
+ throw new Error('Expected latest assistant message')
251
+ }
252
+ const message = getAssistantMessageById({
253
+ events,
254
+ sessionId,
255
+ messageId: latestAssistantMessageId,
256
+ })
257
+ expect(message.id).toBe(latestAssistantMessageId)
258
+ })
259
+ })
260
+
261
+ describe('session-tool-call-noisy-stream', () => {
262
+ const events = loadFixture('session-tool-call-noisy-stream.jsonl')
263
+ const sessionId = getSessionId(events)
264
+ const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
265
+ events,
266
+ sessionId,
267
+ })
268
+
269
+ test('fixture ends busy on a tool-call handoff message', () => {
270
+ expect(isSessionBusy({ events, sessionId })).toBe(true)
271
+ if (!latestAssistantMessageId) {
272
+ throw new Error('Expected latest assistant message')
273
+ }
274
+ const message = getAssistantMessageById({
275
+ events,
276
+ sessionId,
277
+ messageId: latestAssistantMessageId,
278
+ })
279
+ expect(isAssistantMessageNaturalCompletion({ message })).toBe(false)
280
+ })
281
+
282
+ test('getLatestRunInfo still works through dense tool events', () => {
283
+ expect(getLatestRunInfo({ events, sessionId })).toEqual({
284
+ model: 'deterministic-v2',
285
+ providerID: 'deterministic-provider',
286
+ agent: 'build',
287
+ tokensUsed: 0,
288
+ })
289
+ })
290
+ })
291
+
292
+ describe('session-voice-queued-followup', () => {
293
+ const events = loadFixture('session-voice-queued-followup.jsonl')
294
+ const sessionId = getSessionId(events)
295
+
296
+ test('latest user turn start moves to the queued follow-up', () => {
297
+ expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636414577)
298
+ })
299
+ })
300
+
301
+ describe('synthetic-question-followup', () => {
302
+ const sessionId = 'ses_question'
303
+ const events: EventBufferEntry[] = [
304
+ {
305
+ timestamp: 1,
306
+ event: {
307
+ type: 'message.updated',
308
+ properties: {
309
+ sessionID: sessionId,
310
+ info: {
311
+ id: 'msg_user_1',
312
+ sessionID: sessionId,
313
+ role: 'user',
314
+ time: { created: 1 },
315
+ agent: 'build',
316
+ model: {
317
+ providerID: 'deterministic-provider',
318
+ modelID: 'deterministic-v2',
319
+ },
320
+ },
321
+ },
322
+ },
323
+ },
324
+ {
325
+ timestamp: 2,
326
+ event: {
327
+ type: 'message.updated',
328
+ properties: {
329
+ sessionID: sessionId,
330
+ info: {
331
+ id: 'msg_asst_1',
332
+ sessionID: sessionId,
333
+ role: 'assistant',
334
+ time: { created: 2, completed: 3 },
335
+ parentID: 'msg_user_1',
336
+ modelID: 'deterministic-v2',
337
+ providerID: 'deterministic-provider',
338
+ mode: 'build',
339
+ agent: 'build',
340
+ path: { cwd: '/test', root: '/test' },
341
+ cost: 0,
342
+ tokens: {
343
+ input: 1,
344
+ output: 1,
345
+ reasoning: 0,
346
+ cache: { read: 0, write: 0 },
347
+ },
348
+ finish: 'stop',
349
+ },
350
+ },
351
+ },
352
+ },
353
+ {
354
+ timestamp: 4,
355
+ event: {
356
+ type: 'message.updated',
357
+ properties: {
358
+ sessionID: sessionId,
359
+ info: {
360
+ id: 'msg_user_2',
361
+ sessionID: sessionId,
362
+ role: 'user',
363
+ time: { created: 4 },
364
+ agent: 'build',
365
+ model: {
366
+ providerID: 'deterministic-provider',
367
+ modelID: 'deterministic-v2',
368
+ },
369
+ },
370
+ },
371
+ },
372
+ },
373
+ ]
374
+
375
+ test('latest user turn flips immediately after the follow-up user message', () => {
376
+ expect(isAssistantMessageInLatestUserTurn({
377
+ events,
378
+ sessionId,
379
+ messageId: 'msg_asst_1',
380
+ })).toBe(false)
381
+ expect(getCurrentTurnStartTime({ events, sessionId })).toBe(4)
382
+ })
383
+ })
384
+
385
+ describe('real-session-task-normal', () => {
386
+ const events = loadFixture('real-session-task-normal.jsonl')
387
+ const sessionId = getSessionId(events)
388
+ const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
389
+ events,
390
+ sessionId,
391
+ })
392
+
393
+ test('latest assistant completion is terminal', () => {
394
+ if (!latestAssistantMessageId) {
395
+ throw new Error('Expected latest assistant message')
396
+ }
397
+ const message = getAssistantMessageById({
398
+ events,
399
+ sessionId,
400
+ messageId: latestAssistantMessageId,
401
+ })
402
+ expect(isAssistantMessageNaturalCompletion({ message })).toBe(true)
403
+ })
404
+
405
+ test('getLatestRunInfo has model info', () => {
406
+ expect(getLatestRunInfo({ events, sessionId })).toEqual({
407
+ model: 'gemini-2.5-flash',
408
+ providerID: 'cached-google-real-events',
409
+ agent: 'build',
410
+ tokensUsed: 39025,
411
+ })
412
+ })
413
+ })
414
+
415
+ describe('real-session-task-user-interruption', () => {
416
+ const events = loadFixture('real-session-task-user-interruption.jsonl')
417
+ const sessionId = getSessionId(events)
418
+ const childSessionId = 'ses_3464f3a1dffeBBD0d15EqnGjAh'
419
+ const firstAssistantId = 'msg_cb9b0ba96001SpPjgzxWPmRuW9'
420
+ const secondAssistantId = 'msg_cb9b1ae5c001E5G3Ql6aXNpst2'
421
+
422
+ test('tool-call handoff assistant is not a natural completion but the resumed reply is', () => {
423
+ const firstAssistant = getAssistantMessageById({
424
+ events,
425
+ sessionId,
426
+ messageId: firstAssistantId,
427
+ })
428
+ const secondAssistant = getAssistantMessageById({
429
+ events,
430
+ sessionId,
431
+ messageId: secondAssistantId,
432
+ })
433
+ // The first message finished with tool-calls — not a natural completion
434
+ // (footer is deferred to session.idle). The second message IS natural.
435
+ expect(isAssistantMessageNaturalCompletion({ message: firstAssistant })).toBe(false)
436
+ expect(isAssistantMessageNaturalCompletion({ message: secondAssistant })).toBe(true)
437
+ })
438
+
439
+ test('latest user turn keeps both assistant messages for the same user turn', () => {
440
+ const assistantIds = getAssistantMessageIdsForLatestUserTurn({ events, sessionId })
441
+ expect(assistantIds.has(firstAssistantId)).toBe(true)
442
+ expect(assistantIds.has(secondAssistantId)).toBe(true)
443
+ expect(getLatestAssistantMessageIdForLatestUserTurn({
444
+ events,
445
+ sessionId,
446
+ })).toBe(secondAssistantId)
447
+ })
448
+
449
+ test('getDerivedSubtaskIndex starts at 1 for first task of assistant message', () => {
450
+ expect(getDerivedSubtaskIndex({
451
+ events,
452
+ mainSessionId: sessionId,
453
+ candidateSessionId: childSessionId,
454
+ })).toBe(1)
455
+ })
456
+
457
+ test('getDerivedSubtaskIndex restarts at 1 for a newer assistant message', () => {
458
+ const firstTaskEvent = events.find((entry) => {
459
+ if (entry.event.type !== 'message.part.updated') {
460
+ return false
461
+ }
462
+ const part = entry.event.properties.part
463
+ if (part.sessionID !== sessionId) {
464
+ return false
465
+ }
466
+ if (part.type !== 'tool' || part.tool !== 'task') {
467
+ return false
468
+ }
469
+ if (part.state.status !== 'running' && part.state.status !== 'completed') {
470
+ return false
471
+ }
472
+ return part.state.metadata?.sessionId === childSessionId
473
+ })
474
+ if (!firstTaskEvent) {
475
+ throw new Error('Expected to find task tool event in fixture')
476
+ }
477
+
478
+ const secondChildSessionId = 'ses_synthetic_child_2'
479
+ const thirdChildSessionId = 'ses_synthetic_child_3'
480
+ const syntheticAssistantMessageId = 'msg_synthetic_new_assistant'
481
+
482
+ const secondTaskEvent = structuredClone(firstTaskEvent)
483
+ if (secondTaskEvent.event.type !== 'message.part.updated') {
484
+ throw new Error('Expected message.part.updated event')
485
+ }
486
+ const secondTaskPart = secondTaskEvent.event.properties.part
487
+ if (secondTaskPart.type !== 'tool' || secondTaskPart.tool !== 'task') {
488
+ throw new Error('Expected task tool part')
489
+ }
490
+ if (secondTaskPart.state.status !== 'completed') {
491
+ throw new Error('Expected completed task tool part')
492
+ }
493
+ secondTaskPart.id = `${secondTaskPart.id}-synthetic-2`
494
+ secondTaskPart.messageID = syntheticAssistantMessageId
495
+ secondTaskPart.state = {
496
+ ...secondTaskPart.state,
497
+ metadata: {
498
+ ...(secondTaskPart.state.metadata || {}),
499
+ sessionId: secondChildSessionId,
500
+ },
501
+ output: `task_id: ${secondChildSessionId}`,
502
+ }
503
+
504
+ const thirdTaskEvent = structuredClone(secondTaskEvent)
505
+ if (thirdTaskEvent.event.type !== 'message.part.updated') {
506
+ throw new Error('Expected message.part.updated event')
507
+ }
508
+ const thirdTaskPart = thirdTaskEvent.event.properties.part
509
+ if (thirdTaskPart.type !== 'tool' || thirdTaskPart.tool !== 'task') {
510
+ throw new Error('Expected task tool part')
511
+ }
512
+ if (thirdTaskPart.state.status !== 'completed') {
513
+ throw new Error('Expected completed task tool part')
514
+ }
515
+ thirdTaskPart.id = `${thirdTaskPart.id}-synthetic-3`
516
+ thirdTaskPart.messageID = syntheticAssistantMessageId
517
+ thirdTaskPart.state = {
518
+ ...thirdTaskPart.state,
519
+ metadata: {
520
+ ...(thirdTaskPart.state.metadata || {}),
521
+ sessionId: thirdChildSessionId,
522
+ },
523
+ output: `task_id: ${thirdChildSessionId}`,
524
+ }
525
+
526
+ const lastTimestamp = events[events.length - 1]?.timestamp || 0
527
+ const augmentedEvents: EventBufferEntry[] = [
528
+ ...events,
529
+ {
530
+ timestamp: lastTimestamp + 1,
531
+ event: secondTaskEvent.event,
532
+ },
533
+ {
534
+ timestamp: lastTimestamp + 2,
535
+ event: thirdTaskEvent.event,
536
+ },
537
+ ]
538
+
539
+ expect(getDerivedSubtaskIndex({
540
+ events: augmentedEvents,
541
+ mainSessionId: sessionId,
542
+ candidateSessionId: childSessionId,
543
+ })).toBe(1)
544
+ expect(getDerivedSubtaskIndex({
545
+ events: augmentedEvents,
546
+ mainSessionId: sessionId,
547
+ candidateSessionId: secondChildSessionId,
548
+ })).toBe(1)
549
+ expect(getDerivedSubtaskIndex({
550
+ events: augmentedEvents,
551
+ mainSessionId: sessionId,
552
+ candidateSessionId: thirdChildSessionId,
553
+ })).toBe(2)
554
+ })
555
+
556
+ test('getDerivedSubtaskIndex returns undefined for unknown session', () => {
557
+ expect(getDerivedSubtaskIndex({
558
+ events,
559
+ mainSessionId: sessionId,
560
+ candidateSessionId: 'ses_nonexistent',
561
+ })).toBe(undefined)
562
+ })
563
+ })
564
+
565
+ describe('real-session-action-buttons', () => {
566
+ const events = loadFixture('real-session-action-buttons.jsonl')
567
+ const sessionId = getSessionId(events)
568
+ const toolCallAssistantId = 'msg_cb9b55c3b001hXC9qxjVxLMypM'
569
+ const finalAssistantId = 'msg_cb9b5ddd1001FALqKNM6xW98u6'
570
+
571
+ test('tool-call handoff assistant is not a natural completion but final reply is', () => {
572
+ const toolCallAssistant = getAssistantMessageById({
573
+ events,
574
+ sessionId,
575
+ messageId: toolCallAssistantId,
576
+ })
577
+ const finalAssistant = getAssistantMessageById({
578
+ events,
579
+ sessionId,
580
+ messageId: finalAssistantId,
581
+ })
582
+ // The tool-call message has finish="tool-calls" — not a natural completion
583
+ // (footer is deferred to session.idle). The final text message IS natural.
584
+ expect(isAssistantMessageNaturalCompletion({ message: toolCallAssistant })).toBe(false)
585
+ expect(isAssistantMessageNaturalCompletion({ message: finalAssistant })).toBe(true)
586
+ })
587
+
588
+ test('latest user turn keeps both assistant messages for the same user turn', () => {
589
+ const assistantIds = getAssistantMessageIdsForLatestUserTurn({ events, sessionId })
590
+ expect(assistantIds.has(toolCallAssistantId)).toBe(true)
591
+ expect(assistantIds.has(finalAssistantId)).toBe(true)
592
+ expect(getLatestAssistantMessageIdForLatestUserTurn({
593
+ events,
594
+ sessionId,
595
+ })).toBe(finalAssistantId)
596
+ })
597
+ })
598
+
599
+ describe('real-session-permission-external-file', () => {
600
+ const events = loadFixture('real-session-permission-external-file.jsonl')
601
+ const sessionId = getSessionId(events)
602
+
603
+ test('permission flow has no terminal assistant completion yet', () => {
604
+ const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
605
+ events,
606
+ sessionId,
607
+ })
608
+ expect(latestAssistantMessageId).toBeDefined()
609
+ if (!latestAssistantMessageId) {
610
+ return
611
+ }
612
+ const message = getAssistantMessageById({
613
+ events,
614
+ sessionId,
615
+ messageId: latestAssistantMessageId,
616
+ })
617
+ expect(isAssistantMessageNaturalCompletion({ message })).toBe(false)
618
+ })
619
+ })
620
+
621
+ describe('real-session-footer-suppressed-on-pre-idle-interrupt', () => {
622
+ const events = loadFixture('real-session-footer-suppressed-on-pre-idle-interrupt.jsonl')
623
+ const sessionId = getSessionId(events)
624
+ const oldAssistantId = 'msg_cbda8f408001VATHNUi9l05XqA'
625
+ const abortedAssistantId = 'msg_cbda90cef001GOQW8EQxkUz9b5'
626
+ const latestAssistantId = 'msg_cbda91463001DvEB6YMCXayZNj'
627
+
628
+ test('latest user turn ignores stale assistant messages from the interrupted turn', () => {
629
+ expect(isAssistantMessageInLatestUserTurn({
630
+ events,
631
+ sessionId,
632
+ messageId: oldAssistantId,
633
+ })).toBe(false)
634
+ expect(isAssistantMessageInLatestUserTurn({
635
+ events,
636
+ sessionId,
637
+ messageId: abortedAssistantId,
638
+ })).toBe(false)
639
+ expect(isAssistantMessageInLatestUserTurn({
640
+ events,
641
+ sessionId,
642
+ messageId: latestAssistantId,
643
+ })).toBe(true)
644
+ })
645
+ })