kimaki 0.27.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 (221) hide show
  1. package/dist/agent-model.e2e.test.js +247 -64
  2. package/dist/anthropic-account-identity.js +8 -1
  3. package/dist/anthropic-auth-plugin.js +9 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/bash-tool-schema-plugin.js +32 -0
  6. package/dist/bash-tool-schema-plugin.test.js +49 -0
  7. package/dist/cli-commands/project.js +11 -27
  8. package/dist/cli-commands/send.js +6 -1
  9. package/dist/cli-commands/session.js +139 -82
  10. package/dist/cli-commands/task.js +21 -14
  11. package/dist/cli-parsing.test.js +24 -1
  12. package/dist/cli-runner.js +16 -4
  13. package/dist/cli-send-thread.e2e.test.js +9 -9
  14. package/dist/commands/action-buttons.js +2 -1
  15. package/dist/commands/add-project.js +4 -7
  16. package/dist/commands/agent.js +158 -24
  17. package/dist/commands/ask-question.js +2 -1
  18. package/dist/commands/btw.js +28 -6
  19. package/dist/commands/file-upload.js +2 -1
  20. package/dist/commands/fork-subagent.js +2 -4
  21. package/dist/commands/fork.js +4 -4
  22. package/dist/commands/last-sessions.js +17 -10
  23. package/dist/commands/merge-worktree.js +13 -6
  24. package/dist/commands/model-variant.js +3 -2
  25. package/dist/commands/model.js +4 -3
  26. package/dist/commands/queue.js +3 -2
  27. package/dist/commands/resume.js +4 -4
  28. package/dist/commands/session.js +78 -63
  29. package/dist/cpu-profiler.js +211 -0
  30. package/dist/cpu-profiler.test.js +160 -0
  31. package/dist/database.js +28 -0
  32. package/dist/db.js +11 -0
  33. package/dist/db.test.js +100 -0
  34. package/dist/default-channel-provisioning.test.js +148 -0
  35. package/dist/discord-bot.js +501 -474
  36. package/dist/discord-command-registration.js +16 -9
  37. package/dist/discord-command-registration.test.js +17 -0
  38. package/dist/discord-utils.js +84 -0
  39. package/dist/discord-utils.test.js +135 -1
  40. package/dist/errors.js +5 -0
  41. package/dist/external-directory-permissions.e2e.test.js +11 -7
  42. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  43. package/dist/external-opencode-sync.js +94 -17
  44. package/dist/external-opencode-sync.test.js +206 -0
  45. package/dist/extract-sdk-error-message.test.js +13 -0
  46. package/dist/gateway-proxy.e2e.test.js +18 -18
  47. package/dist/interaction-handler.js +416 -398
  48. package/dist/ipc-tools-plugin.js +12 -7
  49. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
  50. package/dist/kimaki-opencode-plugin.js +2 -0
  51. package/dist/logger.js +1 -0
  52. package/dist/message-formatting.js +163 -44
  53. package/dist/message-formatting.test.js +473 -13
  54. package/dist/message-preprocessing.js +105 -1
  55. package/dist/opencode-command.js +28 -0
  56. package/dist/opencode-command.test.js +49 -1
  57. package/dist/opencode-interrupt-plugin.js +4 -0
  58. package/dist/opencode-interrupt-plugin.test.js +24 -0
  59. package/dist/opencode.js +29 -3
  60. package/dist/project-add.test.js +62 -0
  61. package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
  62. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  63. package/dist/question-text-before-dropdown.e2e.test.js +5 -5
  64. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  65. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  66. package/dist/queue-advanced-footer.e2e.test.js +29 -32
  67. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  68. package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
  69. package/dist/queue-advanced-question.e2e.test.js +2 -2
  70. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  71. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  72. package/dist/queue-delete-message.e2e.test.js +3 -3
  73. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  74. package/dist/queue-interrupt-drain.e2e.test.js +20 -20
  75. package/dist/queue-question-select-drain.e2e.test.js +7 -7
  76. package/dist/runtime-lifecycle.e2e.test.js +16 -19
  77. package/dist/schema.js +1 -1
  78. package/dist/session-handler/event-stream-state.js +124 -0
  79. package/dist/session-handler/event-stream-state.test.js +322 -1
  80. package/dist/session-handler/model-utils.js +16 -3
  81. package/dist/session-handler/model-utils.test.js +103 -3
  82. package/dist/session-handler/opencode-session-event-log.js +30 -1
  83. package/dist/session-handler/thread-ingress.test.js +41 -0
  84. package/dist/session-handler/thread-runtime-state.js +2 -0
  85. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  86. package/dist/session-handler/thread-session-runtime.js +340 -135
  87. package/dist/session-search.js +123 -4
  88. package/dist/session-search.test.js +248 -1
  89. package/dist/session-title-rename.test.js +30 -0
  90. package/dist/sleep.e2e.test.js +14 -11
  91. package/dist/system-message.js +45 -12
  92. package/dist/system-message.test.js +117 -10
  93. package/dist/task-runner.js +2 -1
  94. package/dist/task-schedule.js +32 -2
  95. package/dist/task-schedule.test.js +79 -1
  96. package/dist/test-utils.js +17 -9
  97. package/dist/thinking-utils.js +10 -0
  98. package/dist/thinking-utils.test.js +35 -0
  99. package/dist/thread-message-queue.e2e.test.js +65 -54
  100. package/dist/undo-redo.e2e.test.js +17 -9
  101. package/dist/voice-handler.js +21 -4
  102. package/dist/voice-message.e2e.test.js +144 -30
  103. package/dist/voice-routing-input.e2e.test.js +195 -0
  104. package/dist/voice-routing-question.e2e.test.js +125 -0
  105. package/dist/voice.js +28 -8
  106. package/dist/voice.test.js +41 -1
  107. package/dist/worktree-lifecycle.e2e.test.js +30 -30
  108. package/package.json +8 -8
  109. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  110. package/skills/critique/SKILL.md +4 -4
  111. package/skills/errore/SKILL.md +56 -232
  112. package/skills/holocron/SKILL.md +117 -17
  113. package/src/agent-model.e2e.test.ts +293 -66
  114. package/src/anthropic-account-identity.ts +19 -1
  115. package/src/anthropic-auth-plugin.test.ts +19 -0
  116. package/src/anthropic-auth-plugin.ts +11 -16
  117. package/src/bash-tool-schema-plugin.test.ts +55 -0
  118. package/src/bash-tool-schema-plugin.ts +46 -0
  119. package/src/cli-commands/project.ts +14 -35
  120. package/src/cli-commands/send.ts +6 -1
  121. package/src/cli-commands/session.ts +171 -105
  122. package/src/cli-commands/task.ts +22 -15
  123. package/src/cli-parsing.test.ts +34 -1
  124. package/src/cli-runner.ts +16 -3
  125. package/src/cli-send-thread.e2e.test.ts +9 -11
  126. package/src/commands/action-buttons.ts +2 -1
  127. package/src/commands/add-project.ts +4 -8
  128. package/src/commands/agent.ts +199 -23
  129. package/src/commands/ask-question.ts +2 -1
  130. package/src/commands/btw.ts +40 -5
  131. package/src/commands/file-upload.ts +2 -1
  132. package/src/commands/fork-subagent.ts +2 -3
  133. package/src/commands/fork.ts +4 -3
  134. package/src/commands/last-sessions.ts +10 -4
  135. package/src/commands/merge-worktree.ts +13 -6
  136. package/src/commands/model-variant.ts +3 -2
  137. package/src/commands/model.ts +4 -3
  138. package/src/commands/queue.ts +3 -2
  139. package/src/commands/resume.ts +4 -3
  140. package/src/commands/session.ts +94 -70
  141. package/src/cpu-profiler.test.ts +183 -0
  142. package/src/cpu-profiler.ts +293 -0
  143. package/src/database.ts +40 -0
  144. package/src/db.test.ts +105 -0
  145. package/src/db.ts +12 -0
  146. package/src/default-channel-provisioning.test.ts +155 -0
  147. package/src/discord-bot.ts +43 -7
  148. package/src/discord-command-registration.test.ts +20 -0
  149. package/src/discord-command-registration.ts +27 -11
  150. package/src/discord-utils.test.ts +157 -0
  151. package/src/discord-utils.ts +131 -1
  152. package/src/errors.ts +7 -0
  153. package/src/external-directory-permissions.e2e.test.ts +11 -7
  154. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  155. package/src/external-opencode-sync.test.ts +248 -0
  156. package/src/external-opencode-sync.ts +122 -19
  157. package/src/extract-sdk-error-message.test.ts +18 -0
  158. package/src/gateway-proxy.e2e.test.ts +18 -17
  159. package/src/interaction-handler.ts +36 -13
  160. package/src/ipc-tools-plugin.ts +16 -9
  161. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
  162. package/src/kimaki-opencode-plugin.ts +2 -0
  163. package/src/logger.ts +1 -0
  164. package/src/message-formatting.test.ts +586 -13
  165. package/src/message-formatting.ts +223 -46
  166. package/src/message-preprocessing.ts +116 -1
  167. package/src/opencode-command.test.ts +59 -0
  168. package/src/opencode-command.ts +38 -0
  169. package/src/opencode-interrupt-plugin.test.ts +32 -0
  170. package/src/opencode-interrupt-plugin.ts +3 -0
  171. package/src/opencode.ts +44 -4
  172. package/src/project-add.test.ts +80 -0
  173. package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
  174. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  175. package/src/question-text-before-dropdown.e2e.test.ts +5 -5
  176. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  177. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  178. package/src/queue-advanced-footer.e2e.test.ts +30 -31
  179. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  180. package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
  181. package/src/queue-advanced-question.e2e.test.ts +2 -2
  182. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  183. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  184. package/src/queue-delete-message.e2e.test.ts +3 -3
  185. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  186. package/src/queue-interrupt-drain.e2e.test.ts +20 -20
  187. package/src/queue-question-select-drain.e2e.test.ts +7 -7
  188. package/src/runtime-lifecycle.e2e.test.ts +16 -18
  189. package/src/schema.sql +1 -1
  190. package/src/schema.ts +1 -1
  191. package/src/session-handler/event-stream-state.test.ts +353 -0
  192. package/src/session-handler/event-stream-state.ts +179 -0
  193. package/src/session-handler/model-utils.test.ts +125 -3
  194. package/src/session-handler/model-utils.ts +36 -4
  195. package/src/session-handler/opencode-session-event-log.ts +31 -1
  196. package/src/session-handler/thread-ingress.test.ts +54 -0
  197. package/src/session-handler/thread-runtime-state.ts +3 -0
  198. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  199. package/src/session-handler/thread-session-runtime.ts +427 -158
  200. package/src/session-search.test.ts +281 -0
  201. package/src/session-search.ts +241 -7
  202. package/src/session-title-rename.test.ts +45 -0
  203. package/src/sleep.e2e.test.ts +14 -11
  204. package/src/store.ts +1 -0
  205. package/src/system-message.test.ts +141 -10
  206. package/src/system-message.ts +66 -11
  207. package/src/task-runner.ts +2 -1
  208. package/src/task-schedule.test.ts +94 -0
  209. package/src/task-schedule.ts +47 -2
  210. package/src/test-utils.ts +21 -10
  211. package/src/thinking-utils.test.ts +46 -0
  212. package/src/thinking-utils.ts +21 -0
  213. package/src/thread-message-queue.e2e.test.ts +65 -53
  214. package/src/undo-redo.e2e.test.ts +17 -9
  215. package/src/voice-handler.ts +23 -4
  216. package/src/voice-message.e2e.test.ts +149 -29
  217. package/src/voice-routing-input.e2e.test.ts +203 -0
  218. package/src/voice-routing-question.e2e.test.ts +131 -0
  219. package/src/voice.test.ts +44 -0
  220. package/src/voice.ts +34 -6
  221. package/src/worktree-lifecycle.e2e.test.ts +30 -30
@@ -25,6 +25,15 @@ afterEach(async () => {
25
25
  })
26
26
 
27
27
  describe('system-message', () => {
28
+ test('requires kimaki upload for Discord images, not markdown', () => {
29
+ const message = getOpencodeSystemMessage({
30
+ sessionId: 'ses_123',
31
+ })
32
+ expect(message).toContain('NEVER show images with markdown')
33
+ expect(message).toContain('Discord does not render local markdown images')
34
+ expect(message).toContain('ALWAYS upload them with `kimaki upload-to-discord`')
35
+ })
36
+
28
37
  test('requires reading the report-bugs guide before filing kimaki issues', () => {
29
38
  const message = getOpencodeSystemMessage({
30
39
  sessionId: 'ses_123',
@@ -48,6 +57,30 @@ describe('system-message', () => {
48
57
  expect(message).toContain('<callout accent="#f59e0b">')
49
58
  })
50
59
 
60
+ test('tells the model the sleep tool result is not a wake', () => {
61
+ const message = getOpencodeSystemMessage({
62
+ sessionId: 'ses_123',
63
+ })
64
+ expect(message).toContain('The tool result is not a wake')
65
+ expect(message).toContain('Woke after sleeping until')
66
+ expect(message).toContain(
67
+ 'If you still need to wake later after answering, call `kimaki_sleep` again',
68
+ )
69
+ expect(message).not.toContain('After wake, continue the wait reason')
70
+ })
71
+
72
+ test('tells the model to stay quiet between tool calls', () => {
73
+ const message = getOpencodeSystemMessage({
74
+ sessionId: 'ses_123',
75
+ })
76
+ expect(message).toContain('## Discord output')
77
+ expect(message).toContain('Be concise')
78
+ expect(message).toContain('Do not narrate between tool calls')
79
+ expect(message).toContain(
80
+ 'Do not output text until you are ready to give the user the final answer for this turn',
81
+ )
82
+ })
83
+
51
84
  test('requires interactive tools after all text, using exact tool names', () => {
52
85
  const message = getOpencodeSystemMessage({
53
86
  sessionId: 'ses_123',
@@ -110,6 +143,18 @@ describe('system-message', () => {
110
143
  ).rejects.toMatchObject({ code: 'EISDIR' })
111
144
  })
112
145
 
146
+ test('includes all-projects session search example', () => {
147
+ const message = getOpencodeSystemMessage({
148
+ sessionId: 'ses_123',
149
+ channelId: 'chan_123',
150
+ })
151
+ expect(message).toContain('kimaki session search "auth timeout" --all')
152
+ expect(message).toContain('kimaki session search "auth timeout" --days 0')
153
+ expect(message).toContain(
154
+ 'Defaults to this project and the last 14 days. Use `--days 0` for all time. Use `--all` to search every locally registered project',
155
+ )
156
+ })
157
+
113
158
  test('includes session title update guidance when scope or goal changed', () => {
114
159
  const message = getOpencodeSystemMessage({
115
160
  sessionId: 'ses_123',
@@ -142,6 +187,78 @@ describe('system-message', () => {
142
187
  expect(message).toContain('--parent-session ses_child')
143
188
  })
144
189
 
190
+ test('scheduled cron task section shows task id, cron and no-sleep rule', () => {
191
+ const message = getOpencodeSystemMessage({
192
+ sessionId: 'ses_task',
193
+ channelId: 'chan_123',
194
+ threadId: 'thread_123',
195
+ scheduledTask: {
196
+ taskId: 23,
197
+ scheduleKind: 'cron',
198
+ cronExpr: '0 6,14 * * *',
199
+ timezone: 'Europe/Rome',
200
+ },
201
+ })
202
+ expect(message).toContain('## scheduled task session')
203
+ expect(message).toContain('kimaki scheduled task #23')
204
+ expect(message).toContain('Schedule: cron `0 6,14 * * *` in Europe/Rome.')
205
+ expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run')
206
+ expect(message).toContain('starts a fresh session automatically')
207
+ const section = message.slice(
208
+ message.indexOf('## scheduled task session'),
209
+ message.indexOf('## archiving the current thread'),
210
+ )
211
+ expect(section).not.toContain('archive')
212
+ })
213
+
214
+ test('cron task without timezone says UTC', () => {
215
+ const message = getOpencodeSystemMessage({
216
+ sessionId: 'ses_task_utc',
217
+ scheduledTask: { taskId: 7, scheduleKind: 'cron', cronExpr: '0 9 * * 1' },
218
+ })
219
+ expect(message).toContain('Schedule: cron `0 9 * * 1` in UTC.')
220
+ })
221
+
222
+ test('one-shot at task says it does not repeat and skips sleep', () => {
223
+ const message = getOpencodeSystemMessage({
224
+ sessionId: 'ses_oneshot',
225
+ scheduledTask: { scheduleKind: 'at' },
226
+ })
227
+ expect(message).toContain('## scheduled task session')
228
+ expect(message).toContain('a one-time kimaki scheduled task')
229
+ expect(message).toContain('This task runs once and does not repeat.')
230
+ expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run')
231
+ expect(message).not.toContain('Schedule: cron')
232
+ })
233
+
234
+ test('no scheduled task section by default (normal sessions)', () => {
235
+ const message = getOpencodeSystemMessage({
236
+ sessionId: 'ses_normal',
237
+ channelId: 'chan_123',
238
+ threadId: 'thread_123',
239
+ })
240
+ expect(message).not.toContain('## scheduled task session')
241
+ })
242
+
243
+ test('lets agents omit --user for quiet scheduled work', () => {
244
+ const message = getOpencodeSystemMessage({
245
+ sessionId: 'ses_123',
246
+ channelId: 'chan_123',
247
+ threadId: 'thread_123',
248
+ })
249
+ expect(message).not.toContain(
250
+ 'ALWAYS pass `--user` when scheduling a task.',
251
+ )
252
+ expect(message).not.toContain('(always pass this)')
253
+ expect(message).toContain("kimaki task edit <id> --user ''")
254
+ expect(message).toContain(
255
+ "kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123",
256
+ )
257
+ expect(message).not.toContain(
258
+ "kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'",
259
+ )
260
+ })
261
+
145
262
  test('omits parent session system block by default (btw/task/fork cache)', () => {
146
263
  // /btw, task subagents, and normal sessions must not get a parent block in
147
264
  // the system message. That block is opt-in via --parent-session only.
@@ -185,6 +302,12 @@ describe('system-message', () => {
185
302
  "
186
303
  The user is reading your messages from inside Discord, via kimaki.dev
187
304
 
305
+ ## Discord output
306
+
307
+ Be concise. Do not narrate between tool calls. Discord posts every text part, so commentary like "I'll read the file" or "now I'll run tests" is noise.
308
+ Do not output text until you are ready to give the user the final answer for this turn. Tool calls can run with no preceding text.
309
+ Exceptions: when a tool requires user-visible text first (\`question\`, \`kimaki_action_buttons\`, \`kimaki_file_upload\`, \`kimaki_sleep\`), write that required text, then call the tool.
310
+
188
311
  ## bash tool
189
312
 
190
313
  When calling the bash tool, always include these extra fields alongside \`command\`:
@@ -201,7 +324,7 @@ describe('system-message', () => {
201
324
  }
202
325
  \`\`\`
203
326
 
204
- \`description\` is shown to the user in Discord as a summary of the bash call.
327
+ \`description\` is shown in Discord when the bash command is longer than 50 characters.
205
328
  \`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
206
329
 
207
330
  Your current OpenCode session ID is: ses_123
@@ -241,6 +364,8 @@ describe('system-message', () => {
241
364
 
242
365
  kimaki upload-to-discord --session ses_123 <file1> [file2] ...
243
366
 
367
+ NEVER show images with markdown like \`![alt](/tmp/file.png)\` or \`![alt](file://...)\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
368
+
244
369
  ## generating audio from text
245
370
 
246
371
  When the user asks you to generate audio of some text so they can listen instead of reading, use \`kimaki tts\` to create a speech file and \`kimaki upload-to-discord\` to send it to the thread. Only use this when the user explicitly asks for audio.
@@ -267,7 +392,9 @@ describe('system-message', () => {
267
392
  Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
268
393
  Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
269
394
  You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
270
- A new user message cancels the sleep. After wake, continue the wait reason.
395
+ A new user message cancels the sleep. If you still need to wake later after answering, call \`kimaki_sleep\` again with \`until\` set to the original UTC time.
396
+ The tool result is not a wake. After it succeeds, write one short line that you are waiting, then stop. Do not continue the wait reason and do not pretend time has passed.
397
+ Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
271
398
 
272
399
  ## archiving the current thread
273
400
 
@@ -397,13 +524,13 @@ describe('system-message', () => {
397
524
  Use \`--send-at\` to schedule a one-time or recurring task:
398
525
 
399
526
  kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
400
- kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
527
+ kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
401
528
 
402
529
  Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
403
530
 
404
531
  Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
405
532
 
406
- **ALWAYS pass \`--user\` when scheduling a task.** Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member, so the task can fire completely unnoticed if the user never joined the thread or already left it. This applies to \`--channel\` and \`--thread\` scheduling alike.
533
+ Pass \`--user\` when the user should see or act on the thread. Omit \`--user\` for routine autonomous work (digests, monitors, housekeeping) so the thread does not appear in the user's Discord sidebar. Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member. This applies to \`--channel\` and \`--thread\` scheduling alike.
407
534
 
408
535
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
409
536
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -415,14 +542,14 @@ describe('system-message', () => {
415
542
  - \`--pre-run\` to start only when a project command exits with code 0
416
543
  - \`--allow-concurrency\` to permit overlapping runs from the same task
417
544
  - \`--parent-session\` to pass this session as parent of the scheduled child
418
- - \`--user\` to add a specific user to the scheduled thread (always pass this)
545
+ - \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
419
546
 
420
547
  \`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
421
548
 
422
549
  Keep scheduled task prompts **short**. The prompt text becomes the first message in the Discord thread, so long prompts clutter the channel. Instead of inlining the full task description in \`--prompt\`, write a markdown file in the project's \`tasks/\` folder and reference it:
423
550
 
424
551
  \`\`\`bash
425
- kimaki send --channel chan_123 --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
552
+ kimaki send --channel chan_123 --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
426
553
  \`\`\`
427
554
 
428
555
  The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
@@ -441,7 +568,7 @@ describe('system-message', () => {
441
568
  Notification strategy:
442
569
  - NEVER use \`@username\` (e.g. \`@Tommy\`) directly in task prompts. The prompt text becomes the first message in the thread, so a raw \`@\` mention triggers an actual Discord ping every time the task fires. Instead, wrap it in inline code like \`\\\`@Tommy\\\`\`, or use Discord user ID mentions like \`<@USER_ID>\` only in the body of the prompt where the agent will process it, not in the opening line.
443
570
  - If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
444
- - With \`--user\`, the user is added to the thread and receives thread-level notifications.
571
+ - With \`--user\`, the user is added to the thread and receives thread-level notifications. Omit \`--user\` when the work should stay out of the sidebar.
445
572
  - If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive thread_123 (or --session ses_123)\`
446
573
 
447
574
  Manage scheduled tasks with:
@@ -450,7 +577,7 @@ describe('system-message', () => {
450
577
  kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
451
578
  kimaki task delete <id>
452
579
 
453
- \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Fix it with \`kimaki task edit <id> --user '<discord-user-id>'\` instead of deleting and recreating the task. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
580
+ \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Add a user with \`kimaki task edit <id> --user '<discord-user-id>'\`. Clear a stored user with \`kimaki task edit <id> --user ''\`. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
454
581
 
455
582
  \`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
456
583
 
@@ -533,13 +660,17 @@ describe('system-message', () => {
533
660
  kimaki session list --active
534
661
  \`\`\`
535
662
 
536
- To search past sessions for this project (supports plain text or /regex/flags):
663
+ Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
664
+
665
+ To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last 14 days. Use \`--days 0\` for all time. Use \`--all\` to search every locally registered project:
537
666
 
538
667
  \`\`\`bash
539
668
  kimaki session search "auth timeout"
669
+ kimaki session search "auth timeout" --days 0
540
670
  kimaki session search "/error\\s+42/i"
541
671
  kimaki session search "rate limit" --project /path/to/project
542
672
  kimaki session search "/panic|crash/i" --channel <channel_id>
673
+ kimaki session search "auth timeout" --all
543
674
  \`\`\`
544
675
 
545
676
  To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
@@ -749,7 +880,7 @@ describe('system-message', () => {
749
880
 
750
881
  ## running dev servers with tunnel access
751
882
 
752
- ALWAYS use \`kimaki tunnel\` when starting any dev server. NEVER run \`pnpm dev\`, \`npm run dev\`, or any dev server command without wrapping it in \`kimaki tunnel\`. Always invoke Kimaki directly as \`kimaki\`, never via \`npx\` or \`bunx\`. The user is on Discord, not at the terminal — localhost URLs are useless to them. They need a tunnel URL to access the site.
883
+ When starting a local dev server that the Discord user should open in a browser, prefer wrapping it in \`kimaki tunnel\` so they get a public URL. Localhost URLs are useless from Discord. Invoke Kimaki directly as \`kimaki\`, not via \`npx\` or \`bunx\`.
753
884
 
754
885
  Use \`bunx tuistory\` to run the tunnel + dev server combo in the background so it persists across commands. This is preferable to raw shell backgrounding because you can wait for real output, read logs, and interact with the running process.
755
886
 
@@ -13,6 +13,7 @@ import fs from 'node:fs'
13
13
  import path from 'node:path'
14
14
  import { getDataDir } from './config.js'
15
15
  import { store } from './store.js'
16
+ import { SESSION_SEARCH_DEFAULT_DAYS } from './session-search.js'
16
17
 
17
18
  /** Subfolder under the kimaki data dir for session system prompt side-channel files. */
18
19
  export const SESSION_SYSTEM_PROMPT_DIR = 'session-system'
@@ -175,7 +176,7 @@ this feature by restarting kimaki with the \`--no-critique\` flag.
175
176
  const KIMAKI_TUNNEL_INSTRUCTIONS = `
176
177
  ## running dev servers with tunnel access
177
178
 
178
- ALWAYS use \`kimaki tunnel\` when starting any dev server. NEVER run \`pnpm dev\`, \`npm run dev\`, or any dev server command without wrapping it in \`kimaki tunnel\`. Always invoke Kimaki directly as \`kimaki\`, never via \`npx\` or \`bunx\`. The user is on Discord, not at the terminal — localhost URLs are useless to them. They need a tunnel URL to access the site.
179
+ When starting a local dev server that the Discord user should open in a browser, prefer wrapping it in \`kimaki tunnel\` so they get a public URL. Localhost URLs are useless from Discord. Invoke Kimaki directly as \`kimaki\`, not via \`npx\` or \`bunx\`.
179
180
 
180
181
  Use \`bunx tuistory\` to run the tunnel + dev server combo in the background so it persists across commands. This is preferable to raw shell backgrounding because you can wait for real output, read logs, and interact with the running process.
181
182
 
@@ -341,6 +342,40 @@ export type AgentInfo = {
341
342
  description?: string
342
343
  }
343
344
 
345
+ /**
346
+ * Info about the scheduled task that started this session, resolved once per
347
+ * session and kept stable across turns so the system prompt prefix stays
348
+ * cacheable.
349
+ */
350
+ export type ScheduledTaskSystemContext = {
351
+ /** Scheduled task ID. Missing for one-shot 'at' tasks (deleted after run). */
352
+ taskId?: number
353
+ scheduleKind: 'at' | 'cron'
354
+ /** Cron expression in the task's timezone. Only set for 'cron' tasks. */
355
+ cronExpr?: string | null
356
+ /** IANA timezone the cron fires in. Defaults to UTC when unset. */
357
+ timezone?: string | null
358
+ }
359
+
360
+ function getScheduledTaskSection(context: ScheduledTaskSystemContext): string {
361
+ const origin = context.taskId
362
+ ? `kimaki scheduled task #${context.taskId}`
363
+ : 'a one-time kimaki scheduled task'
364
+ const schedule = context.scheduleKind === 'cron' && context.cronExpr
365
+ ? `Schedule: cron \`${context.cronExpr}\`${context.timezone ? ` in ${context.timezone}` : ' in UTC'}.`
366
+ : context.scheduleKind === 'at'
367
+ ? 'This task runs once and does not repeat.'
368
+ : ''
369
+ return `
370
+ ## scheduled task session
371
+
372
+ This session was started automatically by ${origin}.
373
+ ${schedule}
374
+ When your run is done, just stop: the task fires again on its schedule and starts a fresh session automatically.
375
+ Do NOT use \`kimaki_sleep\` to wait for the next run. Sleeping pins this session and never triggers the next one; each firing of the task starts a new session on its own.
376
+ `
377
+ }
378
+
344
379
  function escapePromptAttribute(value: string): string {
345
380
  return value
346
381
  .replaceAll('&', '&amp;')
@@ -431,6 +466,7 @@ export function getOpencodeSystemMessage({
431
466
  agents,
432
467
  userId,
433
468
  parentSessionId,
469
+ scheduledTask,
434
470
  }: {
435
471
  sessionId: string
436
472
  channelId?: string
@@ -449,6 +485,11 @@ export function getOpencodeSystemMessage({
449
485
  * per-parent block. Never auto-derive this from OpenCode parent session IDs.
450
486
  */
451
487
  parentSessionId?: string
488
+ /**
489
+ * Set only when the session was started by a scheduled task. Resolved from
490
+ * the session_start_sources row, so it stays identical across turns.
491
+ */
492
+ scheduledTask?: ScheduledTaskSystemContext
452
493
  }) {
453
494
  const userArg = ` --user '${userId || '<discord-user-id>'}'`
454
495
  const parentSessionArg = ` --parent-session ${sessionId}`
@@ -474,6 +515,12 @@ export function getOpencodeSystemMessage({
474
515
  return `
475
516
  The user is reading your messages from inside Discord, via kimaki.dev
476
517
 
518
+ ## Discord output
519
+
520
+ Be concise. Do not narrate between tool calls. Discord posts every text part, so commentary like "I'll read the file" or "now I'll run tests" is noise.
521
+ Do not output text until you are ready to give the user the final answer for this turn. Tool calls can run with no preceding text.
522
+ Exceptions: when a tool requires user-visible text first (\`question\`, \`kimaki_action_buttons\`, \`kimaki_file_upload\`, \`kimaki_sleep\`), write that required text, then call the tool.
523
+
477
524
  ## bash tool
478
525
 
479
526
  When calling the bash tool, always include these extra fields alongside \`command\`:
@@ -490,7 +537,7 @@ interface BashToolInput {
490
537
  }
491
538
  \`\`\`
492
539
 
493
- \`description\` is shown to the user in Discord as a summary of the bash call.
540
+ \`description\` is shown in Discord when the bash command is longer than 50 characters.
494
541
  \`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
495
542
 
496
543
  Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}${parentSessionContext}
@@ -527,6 +574,8 @@ To upload files to the Discord thread (images, screenshots, long files that woul
527
574
 
528
575
  kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
529
576
 
577
+ NEVER show images with markdown like \`![alt](/tmp/file.png)\` or \`![alt](file://...)\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
578
+
530
579
  ## generating audio from text
531
580
 
532
581
  When the user asks you to generate audio of some text so they can listen instead of reading, use \`kimaki tts\` to create a speech file and \`kimaki upload-to-discord\` to send it to the thread. Only use this when the user explicitly asks for audio.
@@ -553,8 +602,10 @@ You MUST call \`kimaki_file_upload\` LAST, after ALL text.
553
602
  Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
554
603
  Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
555
604
  You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
556
- A new user message cancels the sleep. After wake, continue the wait reason.
557
-
605
+ A new user message cancels the sleep. If you still need to wake later after answering, call \`kimaki_sleep\` again with \`until\` set to the original UTC time.
606
+ The tool result is not a wake. After it succeeds, write one short line that you are waiting, then stop. Do not continue the wait reason and do not pretend time has passed.
607
+ Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
608
+ ${scheduledTask ? getScheduledTaskSection(scheduledTask) : ''}
558
609
  ## archiving the current thread
559
610
 
560
611
  To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
@@ -681,13 +732,13 @@ kimaki send --thread <thread_id> --prompt '/<agentname>-agent' --agent <current_
681
732
  Use \`--send-at\` to schedule a one-time or recurring task:
682
733
 
683
734
  kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${parentSessionArg}${userArg}
684
- kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
735
+ kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
685
736
 
686
737
  Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
687
738
 
688
739
  Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
689
740
 
690
- **ALWAYS pass \`--user\` when scheduling a task.** Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member, so the task can fire completely unnoticed if the user never joined the thread or already left it. This applies to \`--channel\` and \`--thread\` scheduling alike.
741
+ Pass \`--user\` when the user should see or act on the thread. Omit \`--user\` for routine autonomous work (digests, monitors, housekeeping) so the thread does not appear in the user's Discord sidebar. Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member. This applies to \`--channel\` and \`--thread\` scheduling alike.
691
742
 
692
743
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
693
744
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -699,14 +750,14 @@ When the user specifies a time without a timezone, ask them to confirm their tim
699
750
  - \`--pre-run\` to start only when a project command exits with code 0
700
751
  - \`--allow-concurrency\` to permit overlapping runs from the same task
701
752
  - \`--parent-session\` to pass this session as parent of the scheduled child
702
- - \`--user\` to add a specific user to the scheduled thread (always pass this)
753
+ - \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
703
754
 
704
755
  \`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
705
756
 
706
757
  Keep scheduled task prompts **short**. The prompt text becomes the first message in the Discord thread, so long prompts clutter the channel. Instead of inlining the full task description in \`--prompt\`, write a markdown file in the project's \`tasks/\` folder and reference it:
707
758
 
708
759
  \`\`\`bash
709
- kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
760
+ kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
710
761
  \`\`\`
711
762
 
712
763
  The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
@@ -725,7 +776,7 @@ For simple reminders and notifications (\`--notify-only\`), inline the prompt di
725
776
  Notification strategy:
726
777
  - NEVER use \`@username\` (e.g. \`@Tommy\`) directly in task prompts. The prompt text becomes the first message in the thread, so a raw \`@\` mention triggers an actual Discord ping every time the task fires. Instead, wrap it in inline code like \`\\\`@Tommy\\\`\`, or use Discord user ID mentions like \`<@USER_ID>\` only in the body of the prompt where the agent will process it, not in the opening line.
727
778
  - If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
728
- - With \`--user\`, the user is added to the thread and receives thread-level notifications.
779
+ - With \`--user\`, the user is added to the thread and receives thread-level notifications. Omit \`--user\` when the work should stay out of the sidebar.
729
780
  - If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive ${archiveTarget}\`
730
781
 
731
782
  Manage scheduled tasks with:
@@ -734,7 +785,7 @@ kimaki task list
734
785
  kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
735
786
  kimaki task delete <id>
736
787
 
737
- \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Fix it with \`kimaki task edit <id> --user '<discord-user-id>'\` instead of deleting and recreating the task. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
788
+ \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Add a user with \`kimaki task edit <id> --user '<discord-user-id>'\`. Clear a stored user with \`kimaki task edit <id> --user ''\`. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
738
789
 
739
790
  \`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
740
791
 
@@ -817,13 +868,17 @@ kimaki session list --project /path/to/project # specific project
817
868
  kimaki session list --active
818
869
  \`\`\`
819
870
 
820
- To search past sessions for this project (supports plain text or /regex/flags):
871
+ Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
872
+
873
+ To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last ${SESSION_SEARCH_DEFAULT_DAYS} days. Use \`--days 0\` for all time. Use \`--all\` to search every locally registered project:
821
874
 
822
875
  \`\`\`bash
823
876
  kimaki session search "auth timeout"
877
+ kimaki session search "auth timeout" --days 0
824
878
  kimaki session search "/error\\s+42/i"
825
879
  kimaki session search "rate limit" --project /path/to/project
826
880
  kimaki session search "/panic|crash/i" --channel <channel_id>
881
+ kimaki session search "auth timeout" --all
827
882
  \`\`\`
828
883
 
829
884
  To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
@@ -27,6 +27,7 @@ import {
27
27
  markSessionSleepFailed,
28
28
  } from './database.js'
29
29
  import { execAsync } from './exec-async.js'
30
+ import { QUEUE_PREFIX } from './message-formatting.js'
30
31
  import { initializeOpencodeForDirectory } from './opencode.js'
31
32
  import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
32
33
  import { notifyError } from './sentry.js'
@@ -99,7 +100,7 @@ async function executeThreadScheduledTask({
99
100
  const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
100
101
  // Newline between prefix and prompt so leading /command detection can
101
102
  // find the command on its own line.
102
- const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`
103
+ const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`
103
104
 
104
105
  // Re-join the user before posting, so the message they get notified about is
105
106
  // in a thread they are already a member of. Works on archived threads too;
@@ -16,7 +16,10 @@ import { closeDb } from './db.js'
16
16
  import { runTaskCommand } from './task-runner.js'
17
17
  import {
18
18
  appendTaskCommandOutput,
19
+ applyScheduledTaskUserEdit,
19
20
  parseScheduledTaskPayload,
21
+ formatSessionSleepToolOutput,
22
+ formatSessionSleepWakePrompt,
20
23
  parseSendAtValue,
21
24
  parseSleepWakeAt,
22
25
  } from './task-schedule.js'
@@ -176,6 +179,42 @@ describe('parseSleepWakeAt', () => {
176
179
  })
177
180
  })
178
181
 
182
+ describe('formatSessionSleepToolOutput', () => {
183
+ const wakeAt = new Date('2026-09-07T13:21:00Z')
184
+
185
+ test('starts with Sleeping until and tells the model to wait', () => {
186
+ const output = formatSessionSleepToolOutput({
187
+ wakeAt,
188
+ reason: 'Wait 1 day, then check mail',
189
+ })
190
+
191
+ expect(output).toBe(
192
+ 'Sleeping until 2026-09-07 13:21 UTC. Reason: Wait 1 day, then check mail. This tool result is not a wake. Do not continue the waited work. Do not call more tools. Reply with one short line that you are waiting until that time, then stop. The real wake is a later Discord message that starts with "Woke after sleeping until". Only then continue the wait reason. A new user message in this thread cancels the sleep. If you still need that later wake after answering, call kimaki_sleep again with until set to the same UTC time.',
193
+ )
194
+ expect(output.startsWith('Sleeping until ')).toBe(true)
195
+ expect(output).toContain('This tool result is not a wake')
196
+ expect(output).not.toContain('You will be woken')
197
+ expect(output).not.toContain('Stop now')
198
+ })
199
+
200
+ test('omits reason when none is passed', () => {
201
+ const output = formatSessionSleepToolOutput({ wakeAt })
202
+ expect(output.startsWith('Sleeping until 2026-09-07 13:21 UTC.')).toBe(true)
203
+ expect(output).not.toContain('Reason:')
204
+ })
205
+
206
+ test('wake prompt stays a separate later message', () => {
207
+ expect(
208
+ formatSessionSleepWakePrompt({
209
+ wakeAt,
210
+ reason: 'Wait 1 day, then check mail',
211
+ }),
212
+ ).toBe(
213
+ '⬦ Woke after sleeping until 2026-09-07 13:21 UTC\nReason: Wait 1 day, then check mail\nContinue the work you were waiting for.',
214
+ )
215
+ })
216
+ })
217
+
179
218
  describe('scheduled task execution options', () => {
180
219
  const tempDirectories: string[] = []
181
220
 
@@ -210,6 +249,61 @@ describe('scheduled task execution options', () => {
210
249
  `)
211
250
  })
212
251
 
252
+ test('clears the stored user when task edit passes an empty --user', () => {
253
+ const payload = parseScheduledTaskPayload(JSON.stringify({
254
+ kind: 'thread',
255
+ threadId: 'thread-1',
256
+ prompt: 'Handle support requests',
257
+ userId: '535922349652836367',
258
+ username: 'Tommy',
259
+ }))
260
+ expect(payload).not.toBeInstanceOf(Error)
261
+ if (payload instanceof Error) throw payload
262
+
263
+ expect(applyScheduledTaskUserEdit({
264
+ payload,
265
+ userOption: '',
266
+ })).toMatchInlineSnapshot(`
267
+ {
268
+ "agent": null,
269
+ "allowConcurrency": false,
270
+ "injectionGuardPatterns": null,
271
+ "kind": "thread",
272
+ "model": null,
273
+ "parentSessionId": null,
274
+ "permissions": null,
275
+ "preRunCommand": null,
276
+ "prompt": "Handle support requests",
277
+ "threadId": "thread-1",
278
+ "userId": null,
279
+ "username": null,
280
+ }
281
+ `)
282
+ expect(applyScheduledTaskUserEdit({
283
+ payload,
284
+ userOption: ' ',
285
+ }).userId).toBeNull()
286
+ })
287
+
288
+ test('sets the stored user when task edit passes a resolved --user', () => {
289
+ const payload = parseScheduledTaskPayload(JSON.stringify({
290
+ kind: 'channel',
291
+ channelId: 'chan-1',
292
+ prompt: 'Daily digest',
293
+ }))
294
+ expect(payload).not.toBeInstanceOf(Error)
295
+ if (payload instanceof Error) throw payload
296
+
297
+ expect(applyScheduledTaskUserEdit({
298
+ payload,
299
+ userOption: '535922349652836367',
300
+ resolvedUser: { id: '535922349652836367', username: 'Tommy' },
301
+ })).toMatchObject({
302
+ userId: '535922349652836367',
303
+ username: 'Tommy',
304
+ })
305
+ })
306
+
213
307
  test('adds command stdout to the prompt', () => {
214
308
  expect(appendTaskCommandOutput({
215
309
  prompt: 'Handle support requests',