kimaki 0.4.82 → 0.4.84

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 (264) hide show
  1. package/LICENSE +21 -0
  2. package/dist/anthropic-auth-plugin.js +7 -0
  3. package/dist/cli.js +51 -7
  4. package/dist/commands/abort.js +5 -16
  5. package/dist/commands/action-buttons.js +3 -3
  6. package/dist/commands/add-project.js +1 -1
  7. package/dist/commands/ask-question.js +3 -3
  8. package/dist/commands/context-usage.js +1 -1
  9. package/dist/commands/create-new-project.js +1 -1
  10. package/dist/commands/fork.js +11 -8
  11. package/dist/commands/merge-worktree.js +1 -1
  12. package/dist/commands/new-worktree.js +63 -44
  13. package/dist/commands/remove-project.js +1 -1
  14. package/dist/commands/resume.js +11 -8
  15. package/dist/commands/screenshare.js +14 -6
  16. package/dist/commands/screenshare.test.js +20 -0
  17. package/dist/commands/session.js +1 -1
  18. package/dist/commands/undo-redo.js +91 -7
  19. package/dist/commands/user-command.js +1 -1
  20. package/dist/config.js +16 -1
  21. package/dist/database.js +53 -2
  22. package/dist/db.js +6 -0
  23. package/dist/discord-bot.js +48 -85
  24. package/dist/discord-command-registration.js +1 -1
  25. package/dist/external-opencode-sync.js +515 -0
  26. package/dist/external-opencode-sync.test.js +151 -0
  27. package/dist/gateway-proxy.e2e.test.js +8 -5
  28. package/dist/genai.js +1 -1
  29. package/dist/generated/enums.js +4 -0
  30. package/dist/generated/internal/class.js +4 -4
  31. package/dist/generated/internal/prismaNamespace.js +1 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  33. package/dist/generated/models/external_session_pending_prompts.js +1 -0
  34. package/dist/hrana-server.js +14 -285
  35. package/dist/hrana-server.test.js +4 -2
  36. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
  37. package/dist/kimaki-opencode-plugin.js +2 -0
  38. package/dist/kitty-graphics-parser.js +3 -0
  39. package/dist/kitty-graphics-parser.test.js +276 -0
  40. package/dist/kitty-graphics-plugin.js +3 -0
  41. package/dist/markdown.js +4 -4
  42. package/dist/markdown.test.js +1 -1
  43. package/dist/message-formatting.js +54 -15
  44. package/dist/onboarding-tutorial.js +1 -1
  45. package/dist/openai-realtime.js +9 -13
  46. package/dist/opencode.js +28 -5
  47. package/dist/queue-advanced-e2e-setup.js +89 -0
  48. package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
  49. package/dist/queue-advanced-typing.e2e.test.js +9 -22
  50. package/dist/queue-question-select-drain.e2e.test.js +117 -0
  51. package/dist/session-handler/event-stream-state.js +101 -7
  52. package/dist/session-handler/event-stream-state.test.js +7 -3
  53. package/dist/session-handler/thread-session-runtime.js +120 -9
  54. package/dist/store.js +1 -0
  55. package/dist/system-message.js +22 -4
  56. package/dist/system-message.test.js +19 -0
  57. package/dist/task-runner.js +1 -1
  58. package/dist/thread-message-queue.e2e.test.js +8 -14
  59. package/dist/tools.js +1 -1
  60. package/dist/undo-redo.e2e.test.js +20 -25
  61. package/package.json +10 -6
  62. package/schema.prisma +6 -0
  63. package/skills/errore/SKILL.md +40 -13
  64. package/skills/goke/SKILL.md +12 -0
  65. package/skills/lintcn/SKILL.md +868 -0
  66. package/skills/npm-package/SKILL.md +1 -0
  67. package/skills/proxyman/SKILL.md +215 -0
  68. package/skills/spiceflow/SKILL.md +1 -1
  69. package/skills/usecomputer/SKILL.md +339 -0
  70. package/src/ai-tool-to-genai.ts +1 -0
  71. package/src/anthropic-auth-plugin.ts +7 -0
  72. package/src/cli.ts +59 -6
  73. package/src/commands/abort.ts +6 -16
  74. package/src/commands/action-buttons.ts +5 -1
  75. package/src/commands/add-project.ts +1 -1
  76. package/src/commands/ask-question.ts +5 -2
  77. package/src/commands/context-usage.ts +1 -1
  78. package/src/commands/create-new-project.ts +1 -1
  79. package/src/commands/fork.ts +12 -11
  80. package/src/commands/merge-worktree.ts +1 -1
  81. package/src/commands/new-worktree.ts +74 -55
  82. package/src/commands/remove-project.ts +1 -1
  83. package/src/commands/resume.ts +12 -10
  84. package/src/commands/screenshare.test.ts +30 -0
  85. package/src/commands/screenshare.ts +18 -6
  86. package/src/commands/session.ts +1 -1
  87. package/src/commands/undo-redo.ts +108 -10
  88. package/src/commands/user-command.ts +1 -1
  89. package/src/config.ts +19 -1
  90. package/src/database.ts +72 -3
  91. package/src/db.ts +8 -0
  92. package/src/discord-bot.ts +58 -93
  93. package/src/discord-command-registration.ts +1 -1
  94. package/src/external-opencode-sync.ts +729 -0
  95. package/src/gateway-proxy.e2e.test.ts +9 -5
  96. package/src/genai.ts +3 -3
  97. package/src/generated/commonInputTypes.ts +34 -0
  98. package/src/generated/enums.ts +8 -0
  99. package/src/generated/internal/class.ts +4 -4
  100. package/src/generated/internal/prismaNamespace.ts +8 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  102. package/src/generated/models/thread_sessions.ts +53 -1
  103. package/src/hrana-server.test.ts +8 -2
  104. package/src/hrana-server.ts +18 -390
  105. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
  106. package/src/kimaki-opencode-plugin.ts +2 -0
  107. package/src/markdown.test.ts +1 -1
  108. package/src/markdown.ts +4 -4
  109. package/src/message-formatting.ts +66 -17
  110. package/src/onboarding-tutorial.ts +1 -1
  111. package/src/openai-realtime.ts +6 -10
  112. package/src/opencode.ts +31 -7
  113. package/src/queue-advanced-e2e-setup.ts +92 -0
  114. package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
  115. package/src/queue-advanced-typing.e2e.test.ts +9 -22
  116. package/src/queue-question-select-drain.e2e.test.ts +149 -0
  117. package/src/schema.sql +1 -0
  118. package/src/session-handler/event-stream-state.test.ts +7 -2
  119. package/src/session-handler/event-stream-state.ts +128 -7
  120. package/src/session-handler/thread-runtime-state.ts +5 -0
  121. package/src/session-handler/thread-session-runtime.ts +153 -11
  122. package/src/store.ts +8 -0
  123. package/src/system-message.ts +27 -4
  124. package/src/task-runner.ts +1 -1
  125. package/src/thread-message-queue.e2e.test.ts +8 -14
  126. package/src/tools.ts +1 -1
  127. package/src/undo-redo.e2e.test.ts +28 -26
  128. package/skills/jitter/node_modules/.bin/esbuild +0 -21
  129. package/skills/jitter/node_modules/.bin/tsc +0 -21
  130. package/skills/jitter/node_modules/.bin/tsserver +0 -21
  131. package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
  132. package/skills/jitter/node_modules/typescript/README.md +0 -50
  133. package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
  134. package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
  135. package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
  136. package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
  137. package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
  138. package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
  139. package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  140. package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  141. package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  142. package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  143. package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  144. package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  145. package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  146. package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  147. package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
  148. package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
  149. package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
  150. package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
  151. package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
  152. package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
  153. package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
  154. package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
  155. package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
  156. package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
  157. package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
  158. package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
  159. package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
  160. package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
  161. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
  162. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
  163. package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
  164. package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
  165. package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
  166. package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
  167. package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
  168. package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
  169. package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
  170. package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
  171. package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
  172. package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
  173. package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
  174. package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
  175. package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
  176. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
  177. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
  178. package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
  179. package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
  180. package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
  181. package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
  182. package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
  183. package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
  184. package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
  185. package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
  186. package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
  187. package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
  188. package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
  189. package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
  190. package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
  191. package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
  192. package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
  193. package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
  194. package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
  195. package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
  196. package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
  197. package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
  198. package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
  199. package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
  200. package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
  201. package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
  202. package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
  203. package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
  204. package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
  205. package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
  206. package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
  207. package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
  208. package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
  209. package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
  210. package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
  211. package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
  212. package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
  213. package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
  214. package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
  215. package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
  216. package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
  217. package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
  218. package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
  219. package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
  220. package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
  221. package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
  222. package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
  223. package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
  224. package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
  225. package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
  226. package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
  227. package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
  228. package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
  229. package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
  230. package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
  231. package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
  232. package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
  233. package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
  234. package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
  235. package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
  236. package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
  237. package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
  238. package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
  239. package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
  240. package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
  241. package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
  242. package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
  243. package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
  244. package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
  245. package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
  246. package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
  247. package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  248. package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  249. package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  250. package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  251. package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
  252. package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
  253. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
  254. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  255. package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
  256. package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
  257. package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
  258. package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
  259. package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
  260. package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  261. package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  262. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
  263. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
  264. package/skills/jitter/node_modules/typescript/package.json +0 -120
package/src/opencode.ts CHANGED
@@ -67,6 +67,10 @@ import {
67
67
 
68
68
  const opencodeLogger = createLogger(LogPrefix.OPENCODE)
69
69
 
70
+ // Tracks directories that have been initialized, to avoid repeated log spam
71
+ // from the external sync polling loop.
72
+ const initializedDirectories = new Set<string>()
73
+
70
74
  const STARTUP_STDERR_TAIL_LIMIT = 30
71
75
  const STARTUP_STDERR_LINE_MAX_LENGTH = 120
72
76
  const STARTUP_ERROR_REASON_MAX_LENGTH = 1500
@@ -514,6 +518,20 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
514
518
  opencodeLogger.warn(kimakiShimDirectory.message)
515
519
  }
516
520
  const gatewayToken = store.getState().gatewayToken
521
+ const vitestOpencodeEnv = (() => {
522
+ if (process.env.KIMAKI_VITEST !== '1') {
523
+ return {}
524
+ }
525
+ const root = path.join(getDataDir(), 'opencode-vitest-home')
526
+ return {
527
+ OPENCODE_TEST_HOME: root,
528
+ OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
529
+ XDG_CONFIG_HOME: path.join(root, '.config'),
530
+ XDG_DATA_HOME: path.join(root, '.local', 'share'),
531
+ XDG_CACHE_HOME: path.join(root, '.cache'),
532
+ XDG_STATE_HOME: path.join(root, '.local', 'state'),
533
+ }
534
+ })()
517
535
 
518
536
  const serverProcess = spawn(
519
537
  spawnCommand,
@@ -574,6 +592,7 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
574
592
  ...(process.env.KIMAKI_SENTRY_DSN && {
575
593
  KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
576
594
  }),
595
+ ...vitestOpencodeEnv,
577
596
  ...(pathEnv && { [pathEnvKey]: pathEnv }),
578
597
  },
579
598
  },
@@ -644,10 +663,12 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
644
663
  clientCache.clear()
645
664
  notifyServerLifecycle({ type: 'stopped' })
646
665
 
647
- // Intentional kills (SIGTERM from cleanup/restart) should not trigger
648
- // auto-restart. Only unexpected crashes (non-zero exit without signal)
649
- // get retried.
650
- if (signal === 'SIGTERM') {
666
+ // Intentional kills should not trigger auto-restart:
667
+ // - SIGTERM from our cleanup/restart code
668
+ // - SIGINT propagated from Ctrl+C (parent process group signal)
669
+ // - any exit during bot shutdown (shuttingDown flag)
670
+ // Only unexpected crashes (non-zero exit without signal) get retried.
671
+ if (signal === 'SIGTERM' || signal === 'SIGINT' || (global as any).shuttingDown) {
651
672
  serverRetryCount = 0
652
673
  return
653
674
  }
@@ -781,9 +802,12 @@ export async function initializeOpencodeForDirectory(
781
802
  return server
782
803
  }
783
804
 
784
- opencodeLogger.log(
785
- `Using shared server on port ${server.port} for directory: ${directory}`,
786
- )
805
+ if (!initializedDirectories.has(directory)) {
806
+ initializedDirectories.add(directory)
807
+ opencodeLogger.log(
808
+ `Using shared server on port ${server.port} for directory: ${directory}`,
809
+ )
810
+ }
787
811
 
788
812
  return () => {
789
813
  if (!singleServer) {
@@ -330,6 +330,42 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
330
330
  },
331
331
  }
332
332
 
333
+ // Question tool for select+queue drain test: model asks a question via dropdown,
334
+ // user answers via select menu while a message is queued.
335
+ const questionSelectQueueMatcher: DeterministicMatcher = {
336
+ id: 'question-select-queue-marker',
337
+ priority: 107,
338
+ when: {
339
+ lastMessageRole: 'user',
340
+ latestUserTextIncludes: 'QUESTION_SELECT_QUEUE_MARKER',
341
+ },
342
+ then: {
343
+ parts: [
344
+ { type: 'stream-start', warnings: [] },
345
+ {
346
+ type: 'tool-call',
347
+ toolCallId: 'question-select-queue-call',
348
+ toolName: 'question',
349
+ input: JSON.stringify({
350
+ questions: [{
351
+ question: 'How to proceed?',
352
+ header: 'Select action',
353
+ options: [
354
+ { label: 'Alpha', description: 'Alpha option' },
355
+ { label: 'Beta', description: 'Beta option' },
356
+ ],
357
+ }],
358
+ }),
359
+ },
360
+ {
361
+ type: 'finish',
362
+ finishReason: 'tool-calls',
363
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
364
+ },
365
+ ],
366
+ },
367
+ }
368
+
333
369
  // Model responds with text + tool call, then after tool result the
334
370
  // follow-up matcher responds with text. This creates two assistant messages:
335
371
  // first with finish="tool-calls" + completed, second with finish="stop".
@@ -389,6 +425,59 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
389
425
  },
390
426
  }
391
427
 
428
+ const undoFileMatcher: DeterministicMatcher = {
429
+ id: 'undo-file-marker',
430
+ priority: 111,
431
+ when: {
432
+ lastMessageRole: 'user',
433
+ latestUserTextIncludes: 'UNDO_FILE_MARKER',
434
+ },
435
+ then: {
436
+ parts: [
437
+ { type: 'stream-start', warnings: [] },
438
+ { type: 'text-start', id: 'undo-file-text' },
439
+ { type: 'text-delta', id: 'undo-file-text', delta: 'creating undo file' },
440
+ { type: 'text-end', id: 'undo-file-text' },
441
+ {
442
+ type: 'tool-call',
443
+ toolCallId: 'undo-file-bash',
444
+ toolName: 'bash',
445
+ input: JSON.stringify({
446
+ command: 'mkdir -p tmp && printf created > tmp/undo-marker.txt',
447
+ description: 'Create undo marker file',
448
+ }),
449
+ },
450
+ {
451
+ type: 'finish',
452
+ finishReason: 'tool-calls',
453
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
454
+ },
455
+ ],
456
+ },
457
+ }
458
+
459
+ const undoFileFollowupMatcher: DeterministicMatcher = {
460
+ id: 'undo-file-followup',
461
+ priority: 112,
462
+ when: {
463
+ latestUserTextIncludes: 'UNDO_FILE_MARKER',
464
+ rawPromptIncludes: 'creating undo file',
465
+ },
466
+ then: {
467
+ parts: [
468
+ { type: 'stream-start', warnings: [] },
469
+ { type: 'text-start', id: 'undo-file-followup' },
470
+ { type: 'text-delta', id: 'undo-file-followup', delta: 'undo file created' },
471
+ { type: 'text-end', id: 'undo-file-followup' },
472
+ {
473
+ type: 'finish',
474
+ finishReason: 'stop',
475
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
476
+ },
477
+ ],
478
+ },
479
+ }
480
+
392
481
  // Multi-step tool chain: model emits text + 3 parallel tool calls in one
393
482
  // response (finish="tool-calls"). All tools complete, then the follow-up
394
483
  // matcher responds with final text (finish="stop"). This creates 2 assistant
@@ -601,10 +690,13 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
601
690
  pluginTimeoutSleepMatcher,
602
691
  actionButtonClickFollowupMatcher,
603
692
  questionToolMatcher,
693
+ questionSelectQueueMatcher,
604
694
  permissionTypingMatcher,
605
695
  permissionTypingFollowupMatcher,
606
696
  multiToolMatcher,
607
697
  multiToolFollowupMatcher,
698
+ undoFileMatcher,
699
+ undoFileFollowupMatcher,
608
700
  multiStepChainInitMatcher,
609
701
  multiStepChainStep2Matcher,
610
702
  multiStepChainStep3Matcher,
@@ -62,7 +62,7 @@ describe('queue advanced: typing around permissions', () => {
62
62
 
63
63
  const th = ctx.discord.thread(thread.id)
64
64
 
65
- await th.waitForTypingEvent({ timeout: 1_000 })
65
+ await th.waitForTypingEvent({ timeout: 4_000 })
66
66
 
67
67
  const pending = await waitForPendingPermission({
68
68
  threadId: thread.id,
@@ -181,7 +181,7 @@ describe('queue advanced: typing around permissions', () => {
181
181
  discord: ctx.discord,
182
182
  threadId: thread.id,
183
183
  text: 'Permission dismissed - user sent a new message.',
184
- timeout: 4_000,
184
+ timeout: 8_000,
185
185
  })
186
186
 
187
187
  await waitForBotReplyAfterUserMessage({
@@ -189,7 +189,7 @@ describe('queue advanced: typing around permissions', () => {
189
189
  threadId: thread.id,
190
190
  userId: TEST_USER_ID,
191
191
  userMessageIncludes: 'post-permission-user-message',
192
- timeout: 4_000,
192
+ timeout: 8_000,
193
193
  })
194
194
 
195
195
  await waitForBotMessageContaining({
@@ -198,13 +198,13 @@ describe('queue advanced: typing around permissions', () => {
198
198
  userId: TEST_USER_ID,
199
199
  text: 'ok',
200
200
  afterUserMessageIncludes: 'post-permission-user-message',
201
- timeout: 4_000,
201
+ timeout: 8_000,
202
202
  })
203
203
 
204
204
  await waitForFooterMessage({
205
205
  discord: ctx.discord,
206
206
  threadId: thread.id,
207
- timeout: 4_000,
207
+ timeout: 8_000,
208
208
  afterMessageIncludes: 'ok',
209
209
  afterAuthorId: ctx.discord.botUserId,
210
210
  })
@@ -72,14 +72,11 @@ e2eTest('queue advanced: typing lifecycle', () => {
72
72
  })
73
73
 
74
74
  const timeline = await th.text({ showTyping: true })
75
- expect(timeline).toMatchInlineSnapshot(`
76
- "--- from: user (queue-advanced-tester)
77
- Reply with exactly: typing-stop-normal
78
- [bot typing]
79
- --- from: assistant (TestBot)
80
- ⬥ ok
81
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
82
- `)
75
+ expect(timeline).toContain('Reply with exactly: typing-stop-normal')
76
+ expect(timeline).toContain('⬥ ok')
77
+ expect(timeline).toContain('*project main ⋅')
78
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
79
+ expect(typingCount).toBeGreaterThanOrEqual(1)
83
80
  expect(replyIndex).toBeGreaterThanOrEqual(0)
84
81
  expect(footerIndex).toBeGreaterThan(replyIndex)
85
82
  expect(messages[footerIndex]).toBeDefined()
@@ -175,20 +172,10 @@ e2eTest('queue advanced: typing lifecycle', () => {
175
172
  })
176
173
 
177
174
  const timeline = await th.text({ showTyping: true })
178
- expect(timeline).toMatchInlineSnapshot(`
179
- "--- from: user (queue-advanced-tester)
180
- Reply with exactly: typing-thread-reply-setup
181
- --- from: assistant (TestBot)
182
- ⬥ ok
183
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
184
- --- from: user (queue-advanced-tester)
185
- TYPING_REPULSE_MARKER
186
- [bot typing]
187
- --- from: assistant (TestBot)
188
- ⬥ repulse-first
189
- [bot typing]
190
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
191
- `)
175
+ expect(timeline).toContain('TYPING_REPULSE_MARKER')
176
+ expect(timeline).toContain('⬥ repulse-first')
177
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
178
+ expect(typingCount).toBeGreaterThanOrEqual(2)
192
179
 
193
180
  const followupUserIndex = messages.findIndex((message) => {
194
181
  return message.author.id === TEST_USER_ID
@@ -0,0 +1,149 @@
1
+ // E2e test: queued message must drain after the user answers a pending question
2
+ // via the Discord dropdown select menu. Reproduces a bug where answering via
3
+ // select (not text) leaves queued messages stuck because the session continues
4
+ // processing after the answer and may enter another blocking state.
5
+
6
+ import { describe, test, expect } from 'vitest'
7
+ import {
8
+ setupQueueAdvancedSuite,
9
+ TEST_USER_ID,
10
+ } from './queue-advanced-e2e-setup.js'
11
+ import {
12
+ waitForBotMessageContaining,
13
+ waitForFooterMessage,
14
+ } from './test-utils.js'
15
+ import { pendingQuestionContexts } from './commands/ask-question.js'
16
+
17
+ const TEXT_CHANNEL_ID = '200000000000001030'
18
+
19
+ async function waitForPendingQuestion({
20
+ threadId,
21
+ timeoutMs,
22
+ }: {
23
+ threadId: string
24
+ timeoutMs: number
25
+ }): Promise<{ contextHash: string }> {
26
+ const start = Date.now()
27
+ while (Date.now() - start < timeoutMs) {
28
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
29
+ return context.thread.id === threadId
30
+ })
31
+ if (entry) {
32
+ return { contextHash: entry[0] }
33
+ }
34
+ await new Promise<void>((resolve) => {
35
+ setTimeout(resolve, 100)
36
+ })
37
+ }
38
+ throw new Error('Timed out waiting for pending question context')
39
+ }
40
+
41
+ describe('queue drain after question select answer', () => {
42
+ const ctx = setupQueueAdvancedSuite({
43
+ channelId: TEXT_CHANNEL_ID,
44
+ channelName: 'qa-question-select-drain',
45
+ dirName: 'qa-question-select-drain',
46
+ username: 'question-select-tester',
47
+ })
48
+
49
+ test(
50
+ 'queued message drains after answering question via dropdown select',
51
+ async () => {
52
+ // 1. Send a message that triggers the question tool
53
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
54
+ content: 'QUESTION_SELECT_QUEUE_MARKER',
55
+ })
56
+
57
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
58
+ timeout: 4_000,
59
+ predicate: (t) => {
60
+ return t.name === 'QUESTION_SELECT_QUEUE_MARKER'
61
+ },
62
+ })
63
+
64
+ const th = ctx.discord.thread(thread.id)
65
+
66
+ // 2. Wait for the question dropdown to appear
67
+ const pending = await waitForPendingQuestion({
68
+ threadId: thread.id,
69
+ timeoutMs: 4_000,
70
+ })
71
+ expect(pending.contextHash).toBeTruthy()
72
+
73
+ // Verify dropdown message appeared
74
+ const questionMessages = await waitForBotMessageContaining({
75
+ discord: ctx.discord,
76
+ threadId: thread.id,
77
+ text: 'How to proceed?',
78
+ timeout: 4_000,
79
+ })
80
+ const questionMsg = questionMessages.find((m) => {
81
+ return m.content.includes('How to proceed?')
82
+ })!
83
+ expect(questionMsg).toBeTruthy()
84
+
85
+ // 3. Queue a message while question is pending
86
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
87
+ .runSlashCommand({
88
+ name: 'queue',
89
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
90
+ })
91
+
92
+ const queueAck = await th.waitForInteractionAck({
93
+ interactionId: queueInteractionId,
94
+ timeout: 4_000,
95
+ })
96
+ if (!queueAck.messageId) {
97
+ throw new Error('Expected /queue response message id')
98
+ }
99
+
100
+ // 4. Answer the question via dropdown select (pick first option "Alpha")
101
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
102
+ messageId: questionMsg.id,
103
+ customId: `ask_question:${pending.contextHash}:0`,
104
+ values: ['0'],
105
+ })
106
+
107
+ await th.waitForInteractionAck({
108
+ interactionId: interaction.id,
109
+ timeout: 4_000,
110
+ })
111
+
112
+ // 5. Queued message should be handed off to OpenCode's own prompt queue
113
+ // after the question reply, so the dispatch indicator appears without
114
+ // waiting for a later natural idle.
115
+ await waitForBotMessageContaining({
116
+ discord: ctx.discord,
117
+ threadId: thread.id,
118
+ text: '» **question-select-tester:** Reply with exactly: post-question-drain',
119
+ timeout: 4_000,
120
+ })
121
+
122
+ // 6. Wait for footer from the drained queued message
123
+ await waitForFooterMessage({
124
+ discord: ctx.discord,
125
+ threadId: thread.id,
126
+ timeout: 4_000,
127
+ afterMessageIncludes: '» **question-select-tester:**',
128
+ afterAuthorId: ctx.discord.botUserId,
129
+ })
130
+
131
+ const timeline = await th.text({ showInteractions: true })
132
+ expect(timeline).toMatchInlineSnapshot(`
133
+ "--- from: user (question-select-tester)
134
+ QUESTION_SELECT_QUEUE_MARKER
135
+ --- from: assistant (TestBot)
136
+ **Select action**
137
+ How to proceed?
138
+ ✓ _Alpha_
139
+ [user interaction]
140
+ Queued message (position 1)
141
+ [user selects dropdown: 0]
142
+ » **question-select-tester:** Reply with exactly: post-question-drain
143
+ ⬥ ok
144
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
145
+ `)
146
+ },
147
+ 20_000,
148
+ )
149
+ })
package/src/schema.sql CHANGED
@@ -2,6 +2,7 @@
2
2
  CREATE TABLE IF NOT EXISTS "thread_sessions" (
3
3
  "thread_id" TEXT NOT NULL PRIMARY KEY,
4
4
  "session_id" TEXT NOT NULL,
5
+ "source" TEXT NOT NULL DEFAULT 'kimaki',
5
6
  "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP
6
7
  );
7
8
  CREATE TABLE IF NOT EXISTS "session_events" (
@@ -16,6 +16,7 @@ import {
16
16
  getLatestAssistantMessageIdForLatestUserTurn,
17
17
  getLatestRunInfo,
18
18
  hasAssistantMessageCompletedBefore,
19
+ doesLatestUserTurnHaveNaturalCompletion,
19
20
  isAssistantMessageInLatestUserTurn,
20
21
  isAssistantMessageNaturalCompletion,
21
22
  isSessionBusy,
@@ -240,7 +241,11 @@ describe('session-concurrent-messages-serialized', () => {
240
241
  sessionId,
241
242
  })
242
243
 
243
- test('fixture ends idle and latest assistant completed naturally', () => {
244
+ test('fixture latest turn is still incomplete even though an older turn completed', () => {
245
+ expect(doesLatestUserTurnHaveNaturalCompletion({
246
+ events,
247
+ sessionId,
248
+ })).toBe(false)
244
249
  if (!latestAssistantMessageId) {
245
250
  throw new Error('Expected latest assistant message')
246
251
  }
@@ -249,7 +254,7 @@ describe('session-concurrent-messages-serialized', () => {
249
254
  sessionId,
250
255
  messageId: latestAssistantMessageId,
251
256
  })
252
- expect(isAssistantMessageNaturalCompletion({ message })).toBe(true)
257
+ expect(message.id).toBe(latestAssistantMessageId)
253
258
  })
254
259
  })
255
260
 
@@ -164,6 +164,7 @@ export function getLatestUserMessage({
164
164
  upToIndex?: number
165
165
  }): UserMessage | undefined {
166
166
  const end = upToIndex ?? events.length - 1
167
+ let latestUserMessage: UserMessage | undefined
167
168
  for (let i = end; i >= 0; i--) {
168
169
  const entry = events[i]
169
170
  if (!entry) {
@@ -177,9 +178,15 @@ export function getLatestUserMessage({
177
178
  if (info.sessionID !== sessionId || info.role !== 'user') {
178
179
  continue
179
180
  }
180
- return info
181
+ if (!latestUserMessage) {
182
+ latestUserMessage = info
183
+ continue
184
+ }
185
+ if (info.time.created > latestUserMessage.time.created) {
186
+ latestUserMessage = info
187
+ }
181
188
  }
182
- return undefined
189
+ return latestUserMessage
183
190
  }
184
191
 
185
192
  export function getCurrentTurnStartTime({
@@ -314,6 +321,9 @@ export function getLatestAssistantMessageIdForLatestUserTurn({
314
321
  return undefined
315
322
  }
316
323
  const end = upToIndex ?? events.length - 1
324
+ let latestAssistantMessage:
325
+ | Extract<OpenCodeMessage, { role: 'assistant' }>
326
+ | undefined
317
327
  for (let i = end; i >= 0; i--) {
318
328
  const entry = events[i]
319
329
  if (!entry) {
@@ -327,11 +337,99 @@ export function getLatestAssistantMessageIdForLatestUserTurn({
327
337
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
328
338
  continue
329
339
  }
330
- if (info.parentID === latestUserMessage.id) {
331
- return info.id
340
+ if (info.parentID !== latestUserMessage.id) {
341
+ continue
342
+ }
343
+ if (!latestAssistantMessage) {
344
+ latestAssistantMessage = info
345
+ continue
346
+ }
347
+ if (info.time.created > latestAssistantMessage.time.created) {
348
+ latestAssistantMessage = info
332
349
  }
333
350
  }
334
- return undefined
351
+ return latestAssistantMessage?.id
352
+ }
353
+
354
+ type EventBufferedAssistantMessage = AssistantMessage & {
355
+ partsSummary?: Array<{ id: string; type: string }>
356
+ }
357
+
358
+ function hasRenderablePartSummary(message: EventBufferedAssistantMessage): boolean {
359
+ if (!('partsSummary' in message) || !Array.isArray(message.partsSummary)) {
360
+ return false
361
+ }
362
+ return message.partsSummary.some((part) => {
363
+ return part.type === 'text' || part.type === 'tool'
364
+ })
365
+ }
366
+
367
+ function hasAssistantPartEvidence({
368
+ events,
369
+ sessionId,
370
+ messageId,
371
+ upToIndex,
372
+ }: {
373
+ events: EventBufferEntry[]
374
+ sessionId: string
375
+ messageId: string
376
+ upToIndex?: number
377
+ }): boolean {
378
+ const end = upToIndex ?? events.length - 1
379
+ for (let i = end; i >= 0; i--) {
380
+ const entry = events[i]
381
+ if (!entry) {
382
+ continue
383
+ }
384
+ const event = entry.event
385
+ if (event.type === 'message.updated') {
386
+ const info = event.properties.info as EventBufferedAssistantMessage
387
+ if (info.sessionID !== sessionId || info.role !== 'assistant' || info.id !== messageId) {
388
+ continue
389
+ }
390
+ if (hasRenderablePartSummary(info)) {
391
+ return true
392
+ }
393
+ continue
394
+ }
395
+ if (event.type !== 'message.part.updated') {
396
+ continue
397
+ }
398
+ const { part } = event.properties
399
+ if (part.messageID !== messageId) {
400
+ continue
401
+ }
402
+ if (part.type === 'text' || part.type === 'tool') {
403
+ return true
404
+ }
405
+ }
406
+ return false
407
+ }
408
+
409
+ function hasAssistantStepFinished({
410
+ events,
411
+ messageId,
412
+ upToIndex,
413
+ }: {
414
+ events: EventBufferEntry[]
415
+ messageId: string
416
+ upToIndex?: number
417
+ }): boolean {
418
+ const end = upToIndex ?? events.length - 1
419
+ for (let i = end; i >= 0; i--) {
420
+ const entry = events[i]
421
+ if (!entry || entry.event.type !== 'message.part.updated') {
422
+ continue
423
+ }
424
+ const { part } = entry.event.properties
425
+ if (part.messageID !== messageId) {
426
+ continue
427
+ }
428
+ if (part.type === 'step-finish') {
429
+ return true
430
+ }
431
+ }
432
+ return false
335
433
  }
336
434
 
337
435
  export function doesLatestUserTurnHaveNaturalCompletion({
@@ -353,6 +451,7 @@ export function doesLatestUserTurnHaveNaturalCompletion({
353
451
  }
354
452
 
355
453
  const end = upToIndex ?? events.length - 1
454
+ let latestAssistantMessage: EventBufferedAssistantMessage | undefined
356
455
  for (let i = end; i >= 0; i--) {
357
456
  const entry = events[i]
358
457
  if (!entry) {
@@ -369,10 +468,32 @@ export function doesLatestUserTurnHaveNaturalCompletion({
369
468
  if (info.id !== latestAssistantMessageId) {
370
469
  continue
371
470
  }
372
- return isAssistantMessageNaturalCompletion({ message: info })
471
+ latestAssistantMessage = info as EventBufferedAssistantMessage
472
+ if (isAssistantMessageNaturalCompletion({ message: info })) {
473
+ return true
474
+ }
475
+ break
373
476
  }
374
477
 
375
- return false
478
+ if (!latestAssistantMessage) {
479
+ return false
480
+ }
481
+ if (latestAssistantMessage.error) {
482
+ return false
483
+ }
484
+ if (latestAssistantMessage.finish === 'tool-calls') {
485
+ return false
486
+ }
487
+ return hasAssistantStepFinished({
488
+ events,
489
+ messageId: latestAssistantMessageId,
490
+ upToIndex,
491
+ }) && hasAssistantPartEvidence({
492
+ events,
493
+ sessionId,
494
+ messageId: latestAssistantMessageId,
495
+ upToIndex,
496
+ })
376
497
  }
377
498
 
378
499
  export function isAssistantMessageInLatestUserTurn({
@@ -42,6 +42,11 @@ export type QueuedMessage = {
42
42
  // Raw permission rule strings ("tool:action" or "tool:pattern:action").
43
43
  // Parsed and merged into session permissions on creation.
44
44
  permissions?: string[]
45
+ // Discord message ID and thread ID of the source message. Embedded in
46
+ // <discord-user> synthetic context so the external sync loop can detect
47
+ // messages that originated from Discord and skip re-mirroring them.
48
+ sourceMessageId?: string
49
+ sourceThreadId?: string
45
50
  // Tracking fields for scheduled tasks. Stored in the DB via
46
51
  // setSessionStartSource() after the session is created, so the session
47
52
  // list can show which sessions were started by scheduled tasks.