kimaki 0.26.0 → 0.28.0

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 (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -24,7 +24,7 @@ import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-u
24
24
  import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js'
25
25
  import { execAsync, validateWorktreeDirectory } from '../worktrees.js'
26
26
  import { upgrade, getCurrentVersion } from '../upgrade.js'
27
- import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload, type ScheduledTaskPayload } from '../task-schedule.js'
27
+ import { applyScheduledTaskUserEdit, getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload, type ScheduledTaskPayload } from '../task-schedule.js'
28
28
  import {
29
29
  EXIT_NO_RESTART,
30
30
  formatMemberLookupUnavailableMessage,
@@ -39,6 +39,7 @@ import {
39
39
  resolveDiscordUserOption,
40
40
  sendDiscordMessageWithOptionalAttachment,
41
41
  } from '../cli-runner.js'
42
+ import { validateCliModelOption } from '../session-handler/model-utils.js'
42
43
 
43
44
  const cliLogger = createLogger(LogPrefix.CLI)
44
45
  const cli = goke()
@@ -192,7 +193,7 @@ cli
192
193
  )
193
194
  .option(
194
195
  '-u, --user <user>',
195
- 'Discord user ID, mention, or username added to the task thread',
196
+ 'Discord user ID, mention, or username added to the task thread (empty string clears)',
196
197
  )
197
198
  .action(async (id, options) => {
198
199
  try {
@@ -202,11 +203,12 @@ cli
202
203
  const hasModel = options.model !== undefined
203
204
  const hasPreRun = options.preRun !== undefined
204
205
  const hasAllowConcurrency = options.allowConcurrency !== undefined
206
+ const hasUser = options.user !== undefined
205
207
 
206
208
  if (
207
209
  !trimmedPrompt &&
208
210
  !options.sendAt &&
209
- !options.user &&
211
+ !hasUser &&
210
212
  !hasAgent &&
211
213
  !hasModel &&
212
214
  !hasPreRun &&
@@ -251,7 +253,7 @@ cli
251
253
  process.exit(EXIT_NO_RESTART)
252
254
  }
253
255
 
254
- const resolvedUser = options.user
256
+ const resolvedUser = hasUser && options.user.trim()
255
257
  ? await resolveTaskUser({
256
258
  user: options.user,
257
259
  channelId: task.channel_id,
@@ -263,24 +265,39 @@ cli
263
265
  }
264
266
 
265
267
  const newPrompt = trimmedPrompt ?? existingPayload.prompt
268
+ const nextModel = hasModel ? options.model!.trim() || null : existingPayload.model
269
+ if (nextModel) {
270
+ const modelCheck = await validateCliModelOption({
271
+ model: nextModel,
272
+ directory: task.project_directory ?? undefined,
273
+ })
274
+ if (modelCheck instanceof Error) {
275
+ cliLogger.error(modelCheck.message)
276
+ process.exit(EXIT_NO_RESTART)
277
+ }
278
+ }
266
279
  // Match send --model/--agent: empty string clears the override (null).
267
280
  const updatedPayload: ScheduledTaskPayload = {
268
281
  ...existingPayload,
269
282
  prompt: newPrompt,
270
- ...(resolvedUser
271
- ? { userId: resolvedUser.id, username: resolvedUser.username || null }
272
- : {}),
273
283
  ...(hasAgent ? { agent: options.agent!.trim() || null } : {}),
274
- ...(hasModel ? { model: options.model!.trim() || null } : {}),
284
+ ...(hasModel ? { model: nextModel } : {}),
275
285
  ...(hasPreRun ? { preRunCommand: options.preRun!.trim() || null } : {}),
276
286
  ...(hasAllowConcurrency
277
287
  ? { allowConcurrency: options.allowConcurrency === 'true' }
278
288
  : {}),
279
289
  }
290
+ const payloadWithUser = hasUser
291
+ ? applyScheduledTaskUserEdit({
292
+ payload: updatedPayload,
293
+ userOption: options.user,
294
+ resolvedUser,
295
+ })
296
+ : updatedPayload
280
297
 
281
298
  const updateData: Parameters<typeof updateScheduledTask>[0] = {
282
299
  taskId,
283
- payloadJson: serializeScheduledTaskPayload(updatedPayload),
300
+ payloadJson: serializeScheduledTaskPayload(payloadWithUser),
284
301
  promptPreview: getPromptPreview(newPrompt),
285
302
  }
286
303
 
@@ -309,20 +326,22 @@ cli
309
326
  }
310
327
 
311
328
  const parts: string[] = [`Updated task ${taskId}`]
312
- if (resolvedUser) {
329
+ if (hasUser) {
313
330
  parts.push(
314
- `user ${resolvedUser.username || resolvedUser.id} will be added to the thread`,
331
+ payloadWithUser.userId
332
+ ? `user ${payloadWithUser.username || payloadWithUser.userId} will be added to the thread`
333
+ : 'user cleared; nobody will be added to the thread',
315
334
  )
316
335
  }
317
336
  if (hasAgent) {
318
- parts.push(`agent=${updatedPayload.agent || '-'}`)
337
+ parts.push(`agent=${payloadWithUser.agent || '-'}`)
319
338
  }
320
339
  if (hasModel) {
321
- parts.push(`model=${updatedPayload.model || '-'}`)
340
+ parts.push(`model=${payloadWithUser.model || '-'}`)
322
341
  }
323
- if (hasPreRun) parts.push(`preRun=${updatedPayload.preRunCommand || '-'}`)
342
+ if (hasPreRun) parts.push(`preRun=${payloadWithUser.preRunCommand || '-'}`)
324
343
  if (hasAllowConcurrency) {
325
- parts.push(`allowConcurrency=${updatedPayload.allowConcurrency}`)
344
+ parts.push(`allowConcurrency=${payloadWithUser.allowConcurrency}`)
326
345
  }
327
346
  cliLogger.log(parts.join(' | '))
328
347
  process.exit(0)
@@ -1,6 +1,7 @@
1
1
  // Regression tests for CLI argument parsing around Discord ID string preservation.
2
2
  import { describe, expect, test } from 'vitest'
3
3
  import { execAsync } from './exec-async.js'
4
+ import maintenanceCommands from './cli-commands/maintenance.js'
4
5
 
5
6
  async function parseWithGoke(argv: string[]) {
6
7
  const script = [
@@ -8,10 +9,12 @@ async function parseWithGoke(argv: string[]) {
8
9
  'const cli = goke(\'kimaki\')',
9
10
  "cli.command('send', 'Send a message').option('-c, --channel <channelId>', 'Discord channel ID').option('--thread <threadId>', 'Thread ID').option('--session <sessionId>', 'Session ID').option('--send-at <schedule>', 'Schedule')",
10
11
  "cli.command('session archive <threadId>', 'Archive a thread')",
11
- "cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path')",
12
+ "cli.command('session title <title>', 'Update title').option('--session <sessionId>', 'Session ID').option('--thread <threadId>', 'Thread ID')",
13
+ "cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path').option('--all', 'Search all registered projects').option('--days <n>', 'Only search recent sessions')",
12
14
  "cli.command('session export-events-jsonl', 'Export in-memory events to JSONL').option('--session <sessionId>', 'Session ID').option('--out <file>', 'Output path')",
13
15
  "cli.command('add-project', 'Add a project').option('-g, --guild <guildId>', 'Discord guild/server ID')",
14
16
  "cli.command('task delete <id>', 'Delete task')",
17
+ "cli.command('task edit <id>', 'Edit task').option('-u, --user <user>', 'Discord user')",
15
18
  "cli.command('multioauth anthropic list', 'List stored Anthropic accounts')",
16
19
  "cli.command('multioauth anthropic remove <indexOrEmail>', 'Remove stored Anthropic account')",
17
20
  "cli.command('multioauth openai list', 'List stored OpenAI accounts')",
@@ -55,7 +58,7 @@ async function parseRootBotOptions(argv: string[]) {
55
58
  const script = [
56
59
  "import { goke } from 'goke'",
57
60
  'const cli = goke(\'kimaki\')',
58
- "cli.command('', 'bot').option('--no-analytics', 'Disable analytics')",
61
+ "cli.command('', 'bot').option('--no-analytics', 'Disable analytics').option('--skip-footer-mentions', 'Do not mention the thread creator in final footers')",
59
62
  `const result = await cli.parse(${JSON.stringify(argv)}, { run: false })`,
60
63
  'process.stdout.write(JSON.stringify({ args: result.args, options: result.options }))',
61
64
  ].join(';')
@@ -106,6 +109,28 @@ describe('goke CLI ID parsing', () => {
106
109
  expect(typeof result.options.guild).toBe('string')
107
110
  })
108
111
 
112
+ test('keeps session title and session ID as strings', async () => {
113
+ const sessionId = '001111222233334444'
114
+ const title = 'Fix queue draining'
115
+
116
+ const result = await parseWithGoke(
117
+ [
118
+ 'node',
119
+ 'kimaki',
120
+ 'session',
121
+ 'title',
122
+ title,
123
+ '--session',
124
+ sessionId,
125
+ ],
126
+ )
127
+
128
+ expect(result.args[0]).toBe(title)
129
+ expect(typeof result.args[0]).toBe('string')
130
+ expect(result.options.session).toBe(sessionId)
131
+ expect(typeof result.options.session).toBe('string')
132
+ })
133
+
109
134
  test('keeps session archive thread ID as string', async () => {
110
135
  const threadId = '0098765432109876543'
111
136
 
@@ -131,6 +156,23 @@ describe('goke CLI ID parsing', () => {
131
156
  expect(typeof result.options.channel).toBe('string')
132
157
  })
133
158
 
159
+ test('parses session search --all as a boolean', async () => {
160
+ const result = await parseWithGoke(
161
+ ['node', 'kimaki', 'session', 'search', 'auth timeout', '--all'],
162
+ )
163
+
164
+ expect(result.args[0]).toBe('auth timeout')
165
+ expect(result.options.all).toBe(true)
166
+ })
167
+
168
+ test('parses session search --days as a string', async () => {
169
+ const result = await parseWithGoke(
170
+ ['node', 'kimaki', 'session', 'search', 'auth timeout', '--days', '0'],
171
+ )
172
+
173
+ expect(result.options.days).toBe('0')
174
+ })
175
+
134
176
  test('keeps session export options as strings', async () => {
135
177
  const sessionId = '001111222233334444'
136
178
  const outPath = './tmp/session-events.jsonl'
@@ -172,6 +214,21 @@ describe('goke CLI ID parsing', () => {
172
214
  expect(typeof result.args[0]).toBe('string')
173
215
  })
174
216
 
217
+ test('parses empty --user on task edit so the stored user can be cleared', async () => {
218
+ const result = await parseWithGoke([
219
+ 'node',
220
+ 'kimaki',
221
+ 'task',
222
+ 'edit',
223
+ '11',
224
+ '--user',
225
+ '',
226
+ ])
227
+
228
+ expect(result.args[0]).toBe('11')
229
+ expect(result.options.user).toBe('')
230
+ })
231
+
175
232
  test('multioauth account remove parses index and email as strings', async () => {
176
233
  const indexResult = await parseWithGoke(
177
234
  ['node', 'kimaki', 'multioauth', 'anthropic', 'remove', '2'],
@@ -194,13 +251,55 @@ describe('goke CLI ID parsing', () => {
194
251
  expect(stdout).toContain('multioauth')
195
252
  })
196
253
 
197
- test('parses --no-analytics as a boolean flag on the root bot command', async () => {
254
+ test('merge-worktree parses strategy and target branch', async () => {
255
+ const result = await maintenanceCommands.parse(
256
+ [
257
+ 'node',
258
+ 'kimaki',
259
+ 'merge-worktree',
260
+ '--strategy',
261
+ 'squash',
262
+ '--target-branch',
263
+ 'release',
264
+ '--thread',
265
+ '123456789012345678',
266
+ '--thread-name',
267
+ 'feature work',
268
+ ],
269
+ { run: false },
270
+ )
271
+
272
+ expect({
273
+ command: maintenanceCommands.matchedCommandName,
274
+ strategy: result.options.strategy,
275
+ targetBranch: result.options.targetBranch,
276
+ thread: result.options.thread,
277
+ threadName: result.options.threadName,
278
+ }).toMatchInlineSnapshot(`
279
+ {
280
+ "command": "merge-worktree",
281
+ "strategy": "squash",
282
+ "targetBranch": "release",
283
+ "thread": "123456789012345678",
284
+ "threadName": "feature work",
285
+ }
286
+ `)
287
+ })
288
+
289
+ test('parses root bot boolean flags', async () => {
198
290
  const result = await parseRootBotOptions([
199
291
  'node',
200
292
  'kimaki',
201
293
  '--no-analytics',
294
+ '--skip-footer-mentions',
202
295
  ])
203
296
 
204
- expect(result.options.noAnalytics).toBe(true)
297
+ expect(result.options).toMatchInlineSnapshot(`
298
+ {
299
+ "--": [],
300
+ "noAnalytics": true,
301
+ "skipFooterMentions": true,
302
+ }
303
+ `)
205
304
  })
206
305
  })
@@ -77,4 +77,53 @@ describe('isTransientNetworkError', () => {
77
77
  })
78
78
  expect(isTransientNetworkError(error)).toBe(true)
79
79
  })
80
+
81
+ test('treats undici connect timeouts as transient', () => {
82
+ const error = Object.assign(
83
+ new Error(
84
+ 'Connect Timeout Error (attempted address: discord-gateway.kimaki.dev:443, timeout: 10000ms)',
85
+ ),
86
+ {
87
+ name: 'ConnectTimeoutError',
88
+ code: 'UND_ERR_CONNECT_TIMEOUT',
89
+ },
90
+ )
91
+ expect(isTransientNetworkError(error)).toBe(true)
92
+ })
93
+
94
+ test('matches undici connect timeouts by message when code is missing', () => {
95
+ expect(
96
+ isTransientNetworkError(
97
+ new Error(
98
+ 'Connect Timeout Error (attempted address: discord-gateway.kimaki.dev:443, timeout: 10000ms)',
99
+ ),
100
+ ),
101
+ ).toBe(true)
102
+ })
103
+
104
+ test('walks cause chains for nested undici connect timeouts', () => {
105
+ const cause = Object.assign(
106
+ new Error(
107
+ 'Connect Timeout Error (attempted address: discord-gateway.kimaki.dev:443, timeout: 10000ms)',
108
+ ),
109
+ {
110
+ name: 'ConnectTimeoutError',
111
+ code: 'UND_ERR_CONNECT_TIMEOUT',
112
+ },
113
+ )
114
+ expect(
115
+ isTransientNetworkError(new Error('Failed to connect to Discord', { cause })),
116
+ ).toBe(true)
117
+ })
118
+
119
+ test('treats other undici timeout and socket codes as transient', () => {
120
+ for (const [name, code] of [
121
+ ['HeadersTimeoutError', 'UND_ERR_HEADERS_TIMEOUT'],
122
+ ['BodyTimeoutError', 'UND_ERR_BODY_TIMEOUT'],
123
+ ['SocketError', 'UND_ERR_SOCKET'],
124
+ ] as const) {
125
+ const error = Object.assign(new Error(name), { name, code })
126
+ expect(isTransientNetworkError(error)).toBe(true)
127
+ }
128
+ })
80
129
  })
package/src/cli-runner.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  getChannelDirectory,
30
30
  startDiscordBot,
31
31
  initializeOpencodeForDirectory,
32
+ assertCompatibleOpencodeVersion,
32
33
  createProjectChannels,
33
34
  createDefaultKimakiChannel,
34
35
  type ChannelWithTags,
@@ -456,6 +457,10 @@ export function formatTaskScheduleLine(schedule: ParsedSendAt): string {
456
457
  }
457
458
 
458
459
  export const EXIT_NO_RESTART = 64
460
+ // Temporary failure (sysexits EX_TEMPFAIL). Wrapper retries forever with
461
+ // backoff and does not count this toward the crash-loop detector. Used when
462
+ // Discord login fails because the network is down.
463
+ export const EXIT_TEMPFAIL = 75
459
464
 
460
465
  export type GuildMemberSearchResult = {
461
466
  user: { id: string; username: string; global_name?: string }
@@ -542,6 +547,20 @@ const TRANSIENT_ERROR_CODES = new Set([
542
547
  'DEPTH_ZERO_SELF_SIGNED_CERT',
543
548
  'SELF_SIGNED_CERT_IN_CHAIN',
544
549
  'ERR_TLS_CERT_ALTNAME_INVALID',
550
+ // undici fetch/WebSocket connect failures when the network is down.
551
+ // Without these, Discord login exits EXIT_NO_RESTART and the wrapper never
552
+ // comes back after a connect timeout to discord-gateway.kimaki.dev.
553
+ 'UND_ERR_CONNECT_TIMEOUT',
554
+ 'UND_ERR_HEADERS_TIMEOUT',
555
+ 'UND_ERR_BODY_TIMEOUT',
556
+ 'UND_ERR_SOCKET',
557
+ ])
558
+
559
+ const TRANSIENT_ERROR_NAMES = new Set([
560
+ 'ConnectTimeoutError',
561
+ 'HeadersTimeoutError',
562
+ 'BodyTimeoutError',
563
+ 'SocketError',
545
564
  ])
546
565
 
547
566
  const TRANSIENT_ERROR_MESSAGE_PATTERNS = [
@@ -549,12 +568,16 @@ const TRANSIENT_ERROR_MESSAGE_PATTERNS = [
549
568
  /certificate has expired/i,
550
569
  /self[- ]signed certificate/i,
551
570
  /unable to get local issuer certificate/i,
571
+ /connect timeout error/i,
572
+ /headers timeout error/i,
573
+ /body timeout error/i,
552
574
  ]
553
575
 
554
576
  export function isTransientNetworkError(error: unknown): boolean {
555
577
  if (!(error instanceof Error)) return false
556
578
  const code = (error as NodeJS.ErrnoException).code
557
579
  if (code && TRANSIENT_ERROR_CODES.has(code)) return true
580
+ if (TRANSIENT_ERROR_NAMES.has(error.name)) return true
558
581
  // Fallback when code is stripped by wrappers (discord.js sometimes rethrows by message only).
559
582
  if (
560
583
  TRANSIENT_ERROR_MESSAGE_PATTERNS.some((pattern) =>
@@ -1082,7 +1105,7 @@ export function showReadyMessage({
1082
1105
  }
1083
1106
 
1084
1107
  /**
1085
- * Create the default kimaki channel in each guild and send a welcome message.
1108
+ * Create default channels in locally configured guilds, or proxy-authorized gateway guilds.
1086
1109
  * Idempotent: skips guilds that already have the channel.
1087
1110
  * Extracted so both the interactive and headless startup paths share the same logic.
1088
1111
  */
@@ -1101,9 +1124,15 @@ export async function ensureDefaultChannelsWithWelcome({
1101
1124
  }): Promise<{ name: string; id: string; guildId: string }[]> {
1102
1125
  if (process.env['KIMAKI_NO_DEFAULT_CHANNEL'] === '1') return []
1103
1126
 
1127
+ const localMappings = isGatewayMode ? [] : await findChannelsByDirectory({})
1104
1128
  const created: { name: string; id: string; guildId: string }[] = []
1105
1129
  for (const guild of guilds) {
1106
1130
  try {
1131
+ if (!isGatewayMode) {
1132
+ // Match live channel IDs: older local mappings have no guild_id.
1133
+ const channels = await guild.channels.fetch()
1134
+ if (!localMappings.some((row) => channels.has(row.channel_id))) continue
1135
+ }
1107
1136
  const result = await createDefaultKimakiChannel({
1108
1137
  guild,
1109
1138
  botName: discordClient.user?.username,
@@ -1586,6 +1615,12 @@ export async function run({
1586
1615
  }),
1587
1616
  ])
1588
1617
 
1618
+ const opencodeVersionCheck = await assertCompatibleOpencodeVersion()
1619
+ if (opencodeVersionCheck instanceof Error) {
1620
+ cliLogger.error(opencodeVersionCheck.message)
1621
+ process.exit(EXIT_NO_RESTART)
1622
+ }
1623
+
1589
1624
 
1590
1625
  if (store.getState().autoUpgradeEnabled) {
1591
1626
  void backgroundUpgradeKimaki()
@@ -1757,12 +1792,14 @@ export async function run({
1757
1792
  cliLogger.error(
1758
1793
  'Error: ' + (error instanceof Error ? error.stack : String(error)),
1759
1794
  )
1760
- // Transient network errors (DNS down, gateway unreachable) should allow
1761
- // the bin.ts wrapper to restart us after a delay. Only truly fatal errors
1762
- // (bad token, invalid intent, etc.) should use EXIT_NO_RESTART.
1795
+ // Transient network errors (DNS down, gateway unreachable, connect
1796
+ // timeout) should allow the bin.ts wrapper to restart us after a delay.
1797
+ // EXIT_TEMPFAIL skips the crash-loop detector so a long outage cannot
1798
+ // permanently stop the bot. Only truly fatal errors (bad token, invalid
1799
+ // intent, etc.) should use EXIT_NO_RESTART.
1763
1800
  if (isTransientNetworkError(error)) {
1764
1801
  cliLogger.error('Transient network error, exiting for wrapper restart...')
1765
- process.exit(1)
1802
+ process.exit(EXIT_TEMPFAIL)
1766
1803
  }
1767
1804
  process.exit(EXIT_NO_RESTART)
1768
1805
  }
@@ -1823,7 +1860,7 @@ export async function run({
1823
1860
  )
1824
1861
  }
1825
1862
 
1826
- // Create default kimaki channel + welcome message in each guild.
1863
+ // Create default channels only in locally configured or gateway-authorized guilds.
1827
1864
  // Runs after channel sync so existing channels are detected correctly.
1828
1865
  try {
1829
1866
  await ensureDefaultChannelsWithWelcome({
@@ -2030,7 +2067,7 @@ export async function run({
2030
2067
  }
2031
2068
 
2032
2069
  // Create default kimaki channel for general-purpose tasks.
2033
- // Runs for every guild the bot is in, idempotent (skips if already exists).
2070
+ // Only locally configured or gateway-authorized guilds are eligible.
2034
2071
  const defaultChannelResults = await ensureDefaultChannelsWithWelcome({
2035
2072
  guilds,
2036
2073
  discordClient,
@@ -410,9 +410,9 @@ describe('kimaki send --channel thread creation', () => {
410
410
  "--- from: user (cli-send-tester)
411
411
  thread empty content seed
412
412
  --- from: assistant (TestBot)
413
- *using deterministic-provider/deterministic-v2*
414
- ⬥ caught-by-model
415
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
413
+ > *using deterministic-provider/deterministic-v2*
414
+ > caught-by-model
415
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000830>
416
416
  --- from: user (cli-send-tester)
417
417
  --- from: assistant (TestBot)
418
418
  I can see you sent a message, but Discord did not include its text.
@@ -570,13 +570,13 @@ describe('kimaki send --channel thread creation', () => {
570
570
  Routes.threadMembers(threadData.id, TEST_USER_ID),
571
571
  )
572
572
 
573
- // Wait for the bot to reply with the ⬥ prefix (proves ThreadCreate
574
- // handler picked up the starter message and started a session)
573
+ // Wait for the bot to reply (proves ThreadCreate handler picked up
574
+ // the starter message and started a session)
575
575
  await waitForBotMessageContaining({
576
576
  discord,
577
577
  threadId: threadData.id,
578
578
  userId: discord.botUserId,
579
- text: '⬥',
579
+ text: 'ok',
580
580
  timeout: 4_000,
581
581
  })
582
582
 
@@ -585,7 +585,7 @@ describe('kimaki send --channel thread creation', () => {
585
585
  discord,
586
586
  threadId: threadData.id,
587
587
  timeout: 4_000,
588
- afterMessageIncludes: '⬥',
588
+ afterMessageIncludes: 'ok',
589
589
  afterAuthorId: discord.botUserId,
590
590
  })
591
591
 
@@ -598,11 +598,9 @@ describe('kimaki send --channel thread creation', () => {
598
598
  })
599
599
  expect(errorMessages).toHaveLength(0)
600
600
 
601
- // Verify at least one ⬥ reply exists (session produced output)
601
+ // Verify at least one reply exists (session produced output)
602
602
  const botReplies = messages.filter((m) => {
603
- return (
604
- m.author.id === discord.botUserId && m.content.startsWith('⬥')
605
- )
603
+ return m.author.id === discord.botUserId
606
604
  })
607
605
  expect(botReplies.length).toBeGreaterThanOrEqual(1)
608
606
  },
package/src/cli.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  } from './config.js'
17
17
  import { getCurrentVersion } from './upgrade.js'
18
18
  import { store } from './store.js'
19
+ import { publicOpencodeBindRequiresPassword } from './opencode.js'
19
20
  import multioauthCommands from './commands/multioauth.js'
20
21
  import botCommands from './cli-commands/bot.js'
21
22
  import maintenanceCommands from './cli-commands/maintenance.js'
@@ -73,6 +74,10 @@ cli
73
74
  '--no-critique',
74
75
  'Disable automatic diff upload to critique.work in system prompts',
75
76
  )
77
+ .option(
78
+ '--skip-footer-mentions',
79
+ 'Do not mention the thread creator in final session footers',
80
+ )
76
81
  .option(
77
82
  '--auto-restart',
78
83
  'Automatically restart the bot on crash or OOM kill',
@@ -98,6 +103,14 @@ cli
98
103
  'Disable anonymous product analytics (Strada). Same as KIMAKI_STRADA_ENABLED=0',
99
104
  )
100
105
  .option('--no-auto-upgrade', 'Disable background auto-upgrade on startup')
106
+ .option(
107
+ '--opencode-hostname <host>',
108
+ 'Hostname the OpenCode server listens on (default: 127.0.0.1). Use 0.0.0.0 to expose it on a VPS',
109
+ )
110
+ .option(
111
+ '--opencode-port <port>',
112
+ 'Port the OpenCode server listens on (default: a random free port)',
113
+ )
101
114
  .option(
102
115
  '--gateway',
103
116
  'Force gateway mode (use the gateway Kimaki bot instead of a self-hosted bot)',
@@ -145,6 +158,7 @@ cli
145
158
  verbosity?: string
146
159
  mentionMode?: boolean
147
160
  noCritique?: boolean
161
+ skipFooterMentions?: boolean
148
162
  allowAllUsers?: boolean
149
163
  restrictDirectories?: boolean
150
164
  permissionTimeoutMinutes?: string
@@ -157,6 +171,8 @@ cli
157
171
  allowMention?: Array<'users' | 'roles' | 'everyone'>
158
172
  enableSkill?: string[]
159
173
  disableSkill?: string[]
174
+ opencodeHostname?: string
175
+ opencodePort?: string
160
176
  }) => {
161
177
  // Guard: only one kimaki bot process can run per lock port. Agents may run
162
178
  // a second dev bot only when they explicitly choose a different lock port.
@@ -269,12 +285,36 @@ cli
269
285
  return parsed * 60_000
270
286
  })()
271
287
 
288
+ const opencodeHostname = options.opencodeHostname?.trim() || undefined
289
+ const opencodePort = (() => {
290
+ if (!options.opencodePort) return undefined
291
+ const parsed = Number(options.opencodePort)
292
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
293
+ cliLogger.error(
294
+ `Invalid --opencode-port: ${options.opencodePort}. Must be an integer between 1 and 65535.`,
295
+ )
296
+ process.exit(EXIT_NO_RESTART)
297
+ }
298
+ return parsed
299
+ })()
300
+
301
+ if (
302
+ publicOpencodeBindRequiresPassword({ hostname: opencodeHostname }) &&
303
+ !process.env.OPENCODE_SERVER_PASSWORD
304
+ ) {
305
+ cliLogger.error(
306
+ `OPENCODE_SERVER_PASSWORD is required when --opencode-hostname is ${opencodeHostname}. Binding the OpenCode server on a public address without a password lets anyone control the agent.`,
307
+ )
308
+ process.exit(EXIT_NO_RESTART)
309
+ }
310
+
272
311
  store.setState({
273
312
  ...(defaultVerbosity && {
274
313
  defaultVerbosity,
275
314
  }),
276
315
  ...(options.mentionMode && { defaultMentionMode: true }),
277
316
  ...(options.noCritique && { critiqueEnabled: false }),
317
+ ...(options.skipFooterMentions && { footerMentionsEnabled: false }),
278
318
  ...(options.allowAllUsers && { allowAllUsers: true }),
279
319
  ...(options.restrictDirectories && { restrictExternalDirectories: true }),
280
320
  ...(permissionTimeoutMs !== undefined && { permissionTimeoutMs }),
@@ -283,6 +323,8 @@ cli
283
323
  ...(enabledSkills.length > 0 && { enabledSkills }),
284
324
  ...(disabledSkills.length > 0 && { disabledSkills }),
285
325
  ...(options.allowMention && { allowedMentions: options.allowMention }),
326
+ ...(opencodeHostname && { opencodeHostname }),
327
+ ...(opencodePort !== undefined && { opencodePort }),
286
328
  })
287
329
 
288
330
  if (enabledSkills.length > 0) {
@@ -323,6 +365,11 @@ cli
323
365
  'Critique disabled: diffs will not be auto-uploaded to critique.work',
324
366
  )
325
367
  }
368
+ if (options.skipFooterMentions) {
369
+ cliLogger.log(
370
+ 'Footer mentions disabled: final session footers will not mention thread creators',
371
+ )
372
+ }
326
373
  if (options.noAutoUpgrade) {
327
374
  cliLogger.log(
328
375
  'Auto-upgrade disabled: kimaki will not check for updates on startup',
@@ -339,6 +386,12 @@ cli
339
386
  'Anonymous product analytics disabled (--no-analytics)',
340
387
  )
341
388
  }
389
+ if (opencodeHostname) {
390
+ cliLogger.log(`OpenCode server hostname: ${opencodeHostname}`)
391
+ }
392
+ if (opencodePort !== undefined) {
393
+ cliLogger.log(`OpenCode server port: ${opencodePort}`)
394
+ }
342
395
 
343
396
  if (options.installUrl) {
344
397
  await printDiscordInstallUrlAndExit({
@@ -7,7 +7,7 @@ import {
7
7
  type ThreadChannel,
8
8
  } from 'discord.js'
9
9
  import type { CommandContext } from './types.js'
10
- import { getThreadSession } from '../database.js'
10
+ import { cancelSessionSleepForThread, getThreadSession } from '../database.js'
11
11
  import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js'
12
12
  import {
13
13
  resolveWorkingDirectory,
@@ -66,6 +66,11 @@ export async function handleAbortCommand({
66
66
  return
67
67
  }
68
68
 
69
+ // Aborting is an explicit "stop waiting" from the user, and it never goes
70
+ // through enqueueIncoming, so cancel any pending sleep here too. Otherwise the
71
+ // wake would still fire later and restart a session the user just stopped.
72
+ await cancelSessionSleepForThread({ threadId: channel.id })
73
+
69
74
  // abortActiveRun delegates to session.abort(), run settlement stays event-driven.
70
75
  const runtime = getRuntime(channel.id)
71
76
  if (runtime) {
@@ -19,6 +19,7 @@ import {
19
19
  sendThreadMessage,
20
20
  } from '../discord-utils.js'
21
21
  import { createLogger } from '../logger.js'
22
+ import { QUEUE_PREFIX } from '../message-formatting.js'
22
23
  import { notifyError } from '../sentry.js'
23
24
  import {
24
25
  getOrCreateRuntime,
@@ -331,7 +332,7 @@ export async function handleActionButton(
331
332
 
332
333
  await sendThreadMessage(
333
334
  thread,
334
- `» **${username}:** ${button.label}`,
335
+ `${QUEUE_PREFIX}**${username}:** ${button.label}`,
335
336
  )
336
337
 
337
338
  try {