@otto-assistant/otto 0.1.1 → 0.7.15

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