kimaki 0.26.0 → 0.27.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 (214) hide show
  1. package/dist/agent-model.e2e.test.js +25 -122
  2. package/dist/anthropic-account-identity.js +1 -0
  3. package/dist/anthropic-auth-plugin.js +1 -0
  4. package/dist/anthropic-auth-state.js +1 -0
  5. package/dist/bin.js +73 -19
  6. package/dist/cli-commands/maintenance.js +46 -7
  7. package/dist/cli-commands/send.js +24 -0
  8. package/dist/cli-commands/session.js +183 -8
  9. package/dist/cli-commands/task.js +13 -1
  10. package/dist/cli-parsing.test.js +59 -3
  11. package/dist/cli-runner.js +28 -4
  12. package/dist/cli-runner.test.js +27 -0
  13. package/dist/cli-send-thread.e2e.test.js +1 -1
  14. package/dist/cli.js +32 -0
  15. package/dist/commands/abort.js +5 -1
  16. package/dist/commands/agent.js +6 -8
  17. package/dist/commands/ask-question.js +21 -32
  18. package/dist/commands/ask-question.test.js +43 -1
  19. package/dist/commands/login.js +6 -0
  20. package/dist/commands/merge-worktree.js +36 -10
  21. package/dist/commands/model-variant.js +7 -1
  22. package/dist/commands/model.js +14 -8
  23. package/dist/commands/multioauth.js +1 -0
  24. package/dist/commands/new-worktree.js +170 -102
  25. package/dist/commands/user-command.js +6 -8
  26. package/dist/commands/worktrees.js +5 -13
  27. package/dist/config.js +16 -1
  28. package/dist/database.js +124 -3
  29. package/dist/db.js +22 -0
  30. package/dist/db.test.js +167 -1
  31. package/dist/discord-bot.js +34 -32
  32. package/dist/discord-command-registration.js +9 -1
  33. package/dist/discord-utils.js +13 -5
  34. package/dist/discord-utils.test.js +17 -1
  35. package/dist/errors.js +5 -0
  36. package/dist/exec-async.js +17 -5
  37. package/dist/external-directory-permissions.e2e.test.js +2 -2
  38. package/dist/file-edit-log.js +197 -0
  39. package/dist/file-edit-log.test.js +258 -0
  40. package/dist/format-tables.js +52 -0
  41. package/dist/format-tables.test.js +93 -0
  42. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  43. package/dist/gateway-proxy.e2e.test.js +5 -5
  44. package/dist/git-worktree-core.js +182 -17
  45. package/dist/ipc-tools-plugin.js +63 -3
  46. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  47. package/dist/kimaki-opencode-plugin.js +7 -3
  48. package/dist/kimaki-workspace-adaptor.js +33 -10
  49. package/dist/logger.js +10 -1
  50. package/dist/markdown.js +3 -1
  51. package/dist/markdown.test.js +11 -3
  52. package/dist/message-formatting.js +18 -7
  53. package/dist/message-formatting.test.js +56 -9
  54. package/dist/oauth-rotation-shared.js +10 -0
  55. package/dist/openai-auth-plugin.js +1 -0
  56. package/dist/openai-auth-state.js +1 -0
  57. package/dist/opencode-command.test.js +60 -0
  58. package/dist/opencode.js +53 -12
  59. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  60. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  61. package/dist/queue-advanced-abort.e2e.test.js +1 -1
  62. package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
  63. package/dist/queue-advanced-e2e-setup.js +78 -5
  64. package/dist/queue-advanced-footer.e2e.test.js +40 -246
  65. package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
  66. package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
  67. package/dist/queue-advanced-question.e2e.test.js +31 -26
  68. package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
  69. package/dist/queue-delete-message.e2e.test.js +1 -1
  70. package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
  71. package/dist/queue-interrupt-drain.e2e.test.js +112 -2
  72. package/dist/queue-question-select-drain.e2e.test.js +30 -128
  73. package/dist/runtime-lifecycle.e2e.test.js +10 -37
  74. package/dist/schema.js +34 -0
  75. package/dist/session-handler/event-stream-state.js +267 -50
  76. package/dist/session-handler/event-stream-state.test.js +415 -8
  77. package/dist/session-handler/model-utils.js +159 -5
  78. package/dist/session-handler/model-utils.test.js +345 -0
  79. package/dist/session-handler/thread-session-runtime.js +252 -182
  80. package/dist/session-sleep.test.js +224 -0
  81. package/dist/session-title-rename.test.js +7 -39
  82. package/dist/sleep.e2e.test.js +491 -0
  83. package/dist/startup-time.e2e.test.js +2 -1
  84. package/dist/store.js +3 -0
  85. package/dist/system-message.js +59 -6
  86. package/dist/system-message.test.js +76 -6
  87. package/dist/task-runner.js +94 -4
  88. package/dist/task-schedule.js +56 -0
  89. package/dist/task-schedule.test.js +71 -1
  90. package/dist/thread-message-queue.e2e.test.js +10 -169
  91. package/dist/undo-redo.e2e.test.js +2 -2
  92. package/dist/voice-attachment.js +23 -1
  93. package/dist/voice-message.e2e.test.js +10 -10
  94. package/dist/voice.test.js +55 -29
  95. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  96. package/dist/worktree-lifecycle.e2e.test.js +29 -10
  97. package/dist/worktrees.js +226 -443
  98. package/dist/worktrees.test.js +435 -58
  99. package/dist/xai-auth-plugin.js +1 -0
  100. package/dist/xai-auth-state.js +1 -0
  101. package/package.json +8 -6
  102. package/skills/errore/SKILL.md +18 -0
  103. package/skills/goke/SKILL.md +150 -10
  104. package/skills/holocron/SKILL.md +37 -0
  105. package/skills/npm-package/SKILL.md +55 -35
  106. package/skills/profano/SKILL.md +17 -2
  107. package/skills/spiceflow/SKILL.md +39 -3
  108. package/skills/strada/SKILL.md +39 -0
  109. package/skills/zele/SKILL.md +19 -0
  110. package/src/agent-model.e2e.test.ts +25 -151
  111. package/src/anthropic-account-identity.ts +1 -0
  112. package/src/anthropic-auth-plugin.ts +1 -0
  113. package/src/anthropic-auth-state.ts +1 -0
  114. package/src/bin.ts +78 -26
  115. package/src/cli-commands/maintenance.ts +59 -13
  116. package/src/cli-commands/send.ts +25 -0
  117. package/src/cli-commands/session.ts +203 -9
  118. package/src/cli-commands/task.ts +13 -1
  119. package/src/cli-parsing.test.ts +69 -3
  120. package/src/cli-runner.test.ts +49 -0
  121. package/src/cli-runner.ts +28 -4
  122. package/src/cli-send-thread.e2e.test.ts +1 -1
  123. package/src/cli.ts +53 -0
  124. package/src/commands/abort.ts +6 -1
  125. package/src/commands/agent.ts +6 -6
  126. package/src/commands/ask-question.test.ts +48 -1
  127. package/src/commands/ask-question.ts +21 -32
  128. package/src/commands/login.ts +6 -0
  129. package/src/commands/merge-worktree.ts +43 -12
  130. package/src/commands/model-variant.ts +8 -1
  131. package/src/commands/model.ts +18 -8
  132. package/src/commands/multioauth.ts +1 -0
  133. package/src/commands/new-worktree.ts +138 -55
  134. package/src/commands/user-command.ts +6 -6
  135. package/src/commands/worktrees.ts +5 -12
  136. package/src/config.ts +18 -1
  137. package/src/database.ts +195 -3
  138. package/src/db.test.ts +188 -0
  139. package/src/db.ts +26 -0
  140. package/src/discord-bot.ts +35 -29
  141. package/src/discord-command-registration.ts +15 -1
  142. package/src/discord-utils.test.ts +19 -0
  143. package/src/discord-utils.ts +21 -6
  144. package/src/errors.ts +5 -0
  145. package/src/exec-async.ts +20 -11
  146. package/src/external-directory-permissions.e2e.test.ts +2 -2
  147. package/src/file-edit-log.test.ts +287 -0
  148. package/src/file-edit-log.ts +283 -0
  149. package/src/format-tables.test.ts +101 -0
  150. package/src/format-tables.ts +75 -0
  151. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  152. package/src/gateway-proxy.e2e.test.ts +5 -5
  153. package/src/git-worktree-core.ts +247 -20
  154. package/src/ipc-tools-plugin.ts +75 -5
  155. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  156. package/src/kimaki-opencode-plugin.ts +7 -3
  157. package/src/kimaki-workspace-adaptor.ts +40 -11
  158. package/src/logger.ts +13 -4
  159. package/src/markdown.test.ts +13 -3
  160. package/src/markdown.ts +4 -1
  161. package/src/message-formatting.test.ts +86 -12
  162. package/src/message-formatting.ts +19 -7
  163. package/src/oauth-rotation-shared.ts +10 -0
  164. package/src/openai-auth-plugin.ts +1 -0
  165. package/src/openai-auth-state.ts +1 -0
  166. package/src/opencode-command.test.ts +81 -0
  167. package/src/opencode.ts +71 -11
  168. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  169. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  170. package/src/queue-advanced-abort.e2e.test.ts +1 -1
  171. package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
  172. package/src/queue-advanced-e2e-setup.ts +83 -4
  173. package/src/queue-advanced-footer.e2e.test.ts +47 -294
  174. package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
  175. package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
  176. package/src/queue-advanced-question.e2e.test.ts +35 -29
  177. package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
  178. package/src/queue-delete-message.e2e.test.ts +1 -1
  179. package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
  180. package/src/queue-interrupt-drain.e2e.test.ts +123 -2
  181. package/src/queue-question-select-drain.e2e.test.ts +32 -157
  182. package/src/runtime-lifecycle.e2e.test.ts +10 -41
  183. package/src/schema.sql +14 -1
  184. package/src/schema.ts +36 -0
  185. package/src/session-handler/event-stream-state.test.ts +437 -7
  186. package/src/session-handler/event-stream-state.ts +389 -53
  187. package/src/session-handler/model-utils.test.ts +415 -0
  188. package/src/session-handler/model-utils.ts +249 -5
  189. package/src/session-handler/thread-session-runtime.ts +300 -215
  190. package/src/session-sleep.test.ts +294 -0
  191. package/src/session-title-rename.test.ts +9 -48
  192. package/src/sleep.e2e.test.ts +562 -0
  193. package/src/startup-time.e2e.test.ts +5 -1
  194. package/src/store.ts +21 -0
  195. package/src/system-message.test.ts +86 -6
  196. package/src/system-message.ts +66 -7
  197. package/src/task-runner.ts +136 -2
  198. package/src/task-schedule.test.ts +76 -0
  199. package/src/task-schedule.ts +95 -0
  200. package/src/thread-message-queue.e2e.test.ts +10 -202
  201. package/src/undo-redo.e2e.test.ts +2 -2
  202. package/src/voice-attachment.ts +29 -1
  203. package/src/voice-message.e2e.test.ts +10 -10
  204. package/src/voice.test.ts +63 -29
  205. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  206. package/src/worktree-lifecycle.e2e.test.ts +29 -9
  207. package/src/worktrees.test.ts +505 -64
  208. package/src/worktrees.ts +257 -603
  209. package/src/xai-auth-plugin.ts +1 -0
  210. package/src/xai-auth-state.ts +1 -0
  211. package/dist/message-finish-field.e2e.test.js +0 -169
  212. package/dist/subagent-rate-limit-plugin.js +0 -184
  213. package/src/message-finish-field.e2e.test.ts +0 -196
  214. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -25,6 +25,15 @@ afterEach(async () => {
25
25
  })
26
26
 
27
27
  describe('system-message', () => {
28
+ test('requires reading the report-bugs guide before filing kimaki issues', () => {
29
+ const message = getOpencodeSystemMessage({
30
+ sessionId: 'ses_123',
31
+ })
32
+ expect(message).toContain(
33
+ 'Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue',
34
+ )
35
+ })
36
+
28
37
  test('includes callout guidance for important content', () => {
29
38
  const message = getOpencodeSystemMessage({
30
39
  sessionId: 'ses_123',
@@ -48,6 +57,7 @@ describe('system-message', () => {
48
57
  expect(message).toContain('NEVER call `question` before your text')
49
58
  expect(message).toContain('`kimaki_action_buttons`')
50
59
  expect(message).toContain('`kimaki_file_upload`')
60
+ expect(message).toContain('`kimaki_sleep`')
51
61
  })
52
62
 
53
63
  test('persists and reads session system prompt for command path', async () => {
@@ -100,6 +110,22 @@ describe('system-message', () => {
100
110
  ).rejects.toMatchObject({ code: 'EISDIR' })
101
111
  })
102
112
 
113
+ test('includes session title update guidance when scope or goal changed', () => {
114
+ const message = getOpencodeSystemMessage({
115
+ sessionId: 'ses_123',
116
+ threadId: 'thread_123',
117
+ })
118
+ expect(message).toContain('## updating the session title')
119
+ expect(message).toContain(
120
+ "kimaki session title 'Short title' --session ses_123",
121
+ )
122
+ expect(message).toContain(
123
+ 'When the session scope or goal changed from the first message, update the title:',
124
+ )
125
+ expect(message).toContain('thread-name="..."')
126
+ expect(message).toContain('Do not retitle every turn')
127
+ })
128
+
103
129
  test('includes parent session context when parentSessionId is set', () => {
104
130
  const message = getOpencodeSystemMessage({
105
131
  sessionId: 'ses_child',
@@ -151,6 +177,9 @@ describe('system-message', () => {
151
177
  expect(message).toContain(
152
178
  'When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work.',
153
179
  )
180
+ expect(message).toContain(
181
+ 'while kimaki session list --active --exclude ses_123; do sleep 5; done',
182
+ )
154
183
 
155
184
  expect(message).toMatchInlineSnapshot(`
156
185
  "
@@ -180,7 +209,7 @@ describe('system-message', () => {
180
209
  Your current Discord thread ID is: thread_123
181
210
  Your current Discord guild ID is: guild_123
182
211
 
183
- Per-turn Discord metadata like the current user and current agent is delivered in synthetic user message parts.
212
+ Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
184
213
 
185
214
  ## permissions
186
215
 
@@ -203,6 +232,7 @@ describe('system-message', () => {
203
232
 
204
233
  ## debugging kimaki issues
205
234
 
235
+ ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
206
236
  If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the log file at \`<data-dir>/kimaki.log\`. This file contains detailed logs of all bot activity including session creation, event handling, errors, and API calls. The log file is reset every time the bot restarts, so it only contains logs from the current run.
207
237
 
208
238
  ## uploading files to discord
@@ -232,9 +262,16 @@ describe('system-message', () => {
232
262
  To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
233
263
  You MUST call \`kimaki_file_upload\` LAST, after ALL text.
234
264
 
265
+ ## sleeping the session
266
+
267
+ 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
+ Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
269
+ 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.
271
+
235
272
  ## archiving the current thread
236
273
 
237
- To archive the current Discord thread (hide it from sidebar) and stop the session, run:
274
+ To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
238
275
 
239
276
  kimaki session archive thread_123 (or --session ses_123)
240
277
 
@@ -249,6 +286,17 @@ describe('system-message', () => {
249
286
  This stops the AI from processing but keeps the thread visible in Discord.
250
287
  Different from \`kimaki session archive\` which hides the thread.
251
288
 
289
+ ## updating the session title
290
+
291
+ When the session scope or goal changed from the first message, update the title:
292
+
293
+ kimaki session title 'Short title' --session ses_123
294
+
295
+ The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
296
+ This updates the OpenCode title. Discord follows automatically.
297
+ Do not retitle every turn. Discord rate-limits thread renames.
298
+ Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
299
+
252
300
  ## discord user mentions
253
301
 
254
302
  Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
@@ -480,6 +528,9 @@ describe('system-message', () => {
480
528
  kimaki session list
481
529
  kimaki session list --json # machine-readable output
482
530
  kimaki session list --project /path/to/project # specific project
531
+
532
+ # List only in-progress sessions. Exit status is 1 when none remain.
533
+ kimaki session list --active
483
534
  \`\`\`
484
535
 
485
536
  To search past sessions for this project (supports plain text or /regex/flags):
@@ -500,6 +551,29 @@ describe('system-message', () => {
500
551
 
501
552
  Then use grep/read tools on the file to find what you need.
502
553
 
554
+ ### who edited a file
555
+
556
+ To find the session that last edited a file, run:
557
+
558
+ \`\`\`bash
559
+ kimaki session editors src/foo.ts
560
+ kimaki session editors src/foo.ts --json
561
+ \`\`\`
562
+
563
+ Output is newest first. Each row has:
564
+
565
+ - **session ID** (\`ses_xxx\`)
566
+ - **title** (Discord thread name, so you can tell what that session was doing)
567
+ - **time ago** (when that session last edited the file)
568
+
569
+ Use this before a commit when this session did not edit the file. Put the original session ID as the last line of the commit message:
570
+
571
+ \`\`\`
572
+ Session: ses_xxx
573
+ \`\`\`
574
+
575
+ If this session edited the file, use this session ID instead. If several files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
576
+
503
577
  ## cross-project commands
504
578
 
505
579
  When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
@@ -566,7 +640,7 @@ describe('system-message', () => {
566
640
 
567
641
  When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via Bash, then continue from the printed session markdown. Do not tell the user to run the command.
568
642
 
569
- IMPORTANT: if you run \`kimaki send --wait\` or \`kimaki session wait <session_id>\` via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
643
+ IMPORTANT: if you run \`kimaki send --wait\`, \`kimaki session wait <session_id>\`, or the active-session wait loop via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
570
644
 
571
645
  If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
572
646
 
@@ -581,9 +655,14 @@ describe('system-message', () => {
581
655
 
582
656
  # Wait for a session that was already started elsewhere
583
657
  kimaki session wait <session_id>
658
+
659
+ # Wait until every other in-progress session in this project finishes
660
+ while kimaki session list --active --exclude ses_123; do sleep 5; done
584
661
  \`\`\`
585
662
 
586
- The command exits with the session markdown on stdout once the model finishes responding.
663
+ \`session list --active\` exits with status 0 while it finds active sessions and status 1 when none remain. Exclude the current session in a wait loop so the loop does not wait for itself.
664
+
665
+ \`session wait\` exits with the session markdown on stdout once the model finishes responding.
587
666
 
588
667
  Use \`--wait\` when you need to:
589
668
  - **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
@@ -848,7 +927,7 @@ describe('system-message', () => {
848
927
  You MUST call \`question\` LAST, after ALL text parts.
849
928
  NEVER call \`question\` before your text. Discord will hide the message.
850
929
 
851
- The same rule applies to \`kimaki_action_buttons\` and \`kimaki_file_upload\`.
930
+ The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
852
931
  You MUST call them LAST, after ALL text.
853
932
 
854
933
  ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
@@ -878,6 +957,7 @@ describe('system-message', () => {
878
957
  userId: 'user_123',
879
958
  sourceMessageId: 'msg_123',
880
959
  sourceThreadId: 'thread_123',
960
+ threadName: 'Fix auth bug',
881
961
  repliedMessage: {
882
962
  authorUsername: 'alice',
883
963
  text: 'Original replied message',
@@ -891,7 +971,7 @@ describe('system-message', () => {
891
971
  },
892
972
  }),
893
973
  ).toMatchInlineSnapshot(`
894
- "<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" />
974
+ "<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" thread-name="Fix auth bug" />
895
975
 
896
976
  This message was a reply to message
897
977
 
@@ -316,9 +316,13 @@ export type ThreadStartMarker = {
316
316
  /**
317
317
  * OpenCode session ID of the parent session that spawned this thread via
318
318
  * `kimaki send --parent-session`. Exposed in the child system message so the
319
- * child can message the parent only when the user explicitly asks.
320
- */
319
+ * child can message the parent only when the user explicitly asks.
320
+ */
321
321
  parentSessionId?: string
322
+ /** Wake prompt posted after kimaki_sleep. Ingress must not cancel this sleep. */
323
+ sleepWake?: boolean
324
+ /** delivery_id of the session_sleeps row this wake delivers. */
325
+ sleepId?: string
322
326
  }
323
327
 
324
328
  export function isInjectedPromptMarker({
@@ -357,6 +361,7 @@ export function getOpencodePromptContext({
357
361
  userId,
358
362
  sourceMessageId,
359
363
  sourceThreadId,
364
+ threadName,
360
365
  repliedMessage,
361
366
  worktree,
362
367
  currentAgent,
@@ -366,6 +371,7 @@ export function getOpencodePromptContext({
366
371
  userId?: string
367
372
  sourceMessageId?: string
368
373
  sourceThreadId?: string
374
+ threadName?: string
369
375
  repliedMessage?: RepliedMessageContext
370
376
  worktree?: WorktreeInfo
371
377
  currentAgent?: string
@@ -384,6 +390,9 @@ export function getOpencodePromptContext({
384
390
  ...(sourceThreadId
385
391
  ? [` thread-id="${escapePromptAttribute(sourceThreadId)}"`]
386
392
  : []),
393
+ ...(threadName
394
+ ? [` thread-name="${escapePromptAttribute(threadName)}"`]
395
+ : []),
387
396
  ].join('')
388
397
  const repliedMessageXml = repliedMessage
389
398
  ? `This message was a reply to message
@@ -486,7 +495,7 @@ interface BashToolInput {
486
495
 
487
496
  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}
488
497
 
489
- Per-turn Discord metadata like the current user and current agent is delivered in synthetic user message parts.
498
+ Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
490
499
 
491
500
  ## permissions
492
501
 
@@ -509,6 +518,7 @@ Do not restart the bot unless the user explicitly asks for it.
509
518
 
510
519
  ## debugging kimaki issues
511
520
 
521
+ ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
512
522
  If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the log file at \`${getDataDir()}/kimaki.log\`. This file contains detailed logs of all bot activity including session creation, event handling, errors, and API calls. The log file is reset every time the bot restarts, so it only contains logs from the current run.
513
523
 
514
524
  ## uploading files to discord
@@ -538,9 +548,16 @@ see --help for options like voice, speed, etc.
538
548
  To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
539
549
  You MUST call \`kimaki_file_upload\` LAST, after ALL text.
540
550
 
551
+ ## sleeping the session
552
+
553
+ 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
+ Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
555
+ 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
+
541
558
  ## archiving the current thread
542
559
 
543
- To archive the current Discord thread (hide it from sidebar) and stop the session, run:
560
+ To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
544
561
 
545
562
  kimaki session archive ${archiveTarget}
546
563
 
@@ -555,6 +572,17 @@ kimaki session abort <session_id>
555
572
  This stops the AI from processing but keeps the thread visible in Discord.
556
573
  Different from \`kimaki session archive\` which hides the thread.
557
574
 
575
+ ## updating the session title
576
+
577
+ When the session scope or goal changed from the first message, update the title:
578
+
579
+ kimaki session title 'Short title' --session ${sessionId}
580
+
581
+ The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
582
+ This updates the OpenCode title. Discord follows automatically.
583
+ Do not retitle every turn. Discord rate-limits thread renames.
584
+ Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
585
+
558
586
  ## discord user mentions
559
587
 
560
588
  Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
@@ -784,6 +812,9 @@ To list all sessions in this project (shows which were started via kimaki):
784
812
  kimaki session list
785
813
  kimaki session list --json # machine-readable output
786
814
  kimaki session list --project /path/to/project # specific project
815
+
816
+ # List only in-progress sessions. Exit status is 1 when none remain.
817
+ kimaki session list --active
787
818
  \`\`\`
788
819
 
789
820
  To search past sessions for this project (supports plain text or /regex/flags):
@@ -804,6 +835,29 @@ kimaki session read <sessionId> > ./tmp/session.md 2>/dev/null
804
835
 
805
836
  Then use grep/read tools on the file to find what you need.
806
837
 
838
+ ### who edited a file
839
+
840
+ To find the session that last edited a file, run:
841
+
842
+ \`\`\`bash
843
+ kimaki session editors src/foo.ts
844
+ kimaki session editors src/foo.ts --json
845
+ \`\`\`
846
+
847
+ Output is newest first. Each row has:
848
+
849
+ - **session ID** (\`ses_xxx\`)
850
+ - **title** (Discord thread name, so you can tell what that session was doing)
851
+ - **time ago** (when that session last edited the file)
852
+
853
+ Use this before a commit when this session did not edit the file. Put the original session ID as the last line of the commit message:
854
+
855
+ \`\`\`
856
+ Session: ses_xxx
857
+ \`\`\`
858
+
859
+ If this session edited the file, use this session ID instead. If several files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
860
+
807
861
  ## cross-project commands
808
862
 
809
863
  When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
@@ -870,7 +924,7 @@ Use \`--wait\` to block until a session completes and print its full conversatio
870
924
 
871
925
  When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via Bash, then continue from the printed session markdown. Do not tell the user to run the command.
872
926
 
873
- IMPORTANT: if you run \`kimaki send --wait\` or \`kimaki session wait <session_id>\` via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
927
+ IMPORTANT: if you run \`kimaki send --wait\`, \`kimaki session wait <session_id>\`, or the active-session wait loop via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
874
928
 
875
929
  If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
876
930
 
@@ -885,9 +939,14 @@ kimaki send --thread <thread_id> --prompt 'Run the tests' --wait --agent <curren
885
939
 
886
940
  # Wait for a session that was already started elsewhere
887
941
  kimaki session wait <session_id>
942
+
943
+ # Wait until every other in-progress session in this project finishes
944
+ while kimaki session list --active --exclude ${sessionId}; do sleep 5; done
888
945
  \`\`\`
889
946
 
890
- The command exits with the session markdown on stdout once the model finishes responding.
947
+ \`session list --active\` exits with status 0 while it finds active sessions and status 1 when none remain. Exclude the current session in a wait loop so the loop does not wait for itself.
948
+
949
+ \`session wait\` exits with the session markdown on stdout once the model finishes responding.
891
950
 
892
951
  Use \`--wait\` when you need to:
893
952
  - **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
@@ -1001,7 +1060,7 @@ You MUST write ALL user-visible text FIRST.
1001
1060
  You MUST call \`question\` LAST, after ALL text parts.
1002
1061
  NEVER call \`question\` before your text. Discord will hide the message.
1003
1062
 
1004
- The same rule applies to \`kimaki_action_buttons\` and \`kimaki_file_upload\`.
1063
+ The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
1005
1064
  You MUST call them LAST, after ALL text.
1006
1065
 
1007
1066
  ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
@@ -1,6 +1,6 @@
1
- // Scheduled task runner for executing due `send --send-at` jobs in the bot process.
1
+ // Scheduled task runner for due `send --send-at` jobs and session sleep wakes.
2
2
 
3
- import { type REST, Routes } from 'discord.js'
3
+ import { DiscordAPIError, type REST, Routes } from 'discord.js'
4
4
  import { createDiscordRest } from './discord-urls.js'
5
5
  import { ensureThreadMember } from './discord-utils.js'
6
6
  import YAML from 'yaml'
@@ -20,6 +20,11 @@ import {
20
20
  releaseScheduledTaskClaim,
21
21
  setScheduledTaskRunThread,
22
22
  type ScheduledTask,
23
+ type SessionSleep,
24
+ claimSessionSleepAttempt,
25
+ getDueSessionSleeps,
26
+ getThreadIdBySessionId,
27
+ markSessionSleepFailed,
23
28
  } from './database.js'
24
29
  import { execAsync } from './exec-async.js'
25
30
  import { initializeOpencodeForDirectory } from './opencode.js'
@@ -29,6 +34,7 @@ import type { ThreadStartMarker } from './system-message.js'
29
34
  import {
30
35
  type ScheduledTaskPayload,
31
36
  appendTaskCommandOutput,
37
+ formatSessionSleepWakePrompt,
32
38
  getNextCronRun,
33
39
  getPromptPreview,
34
40
  parseScheduledTaskPayload,
@@ -129,6 +135,132 @@ async function executeThreadScheduledTask({
129
135
  return payload.threadId
130
136
  }
131
137
 
138
+ /** Wait before retrying a wake that ingress has not consumed yet. */
139
+ const SLEEP_WAKE_RETRY_AFTER_MS = 30_000
140
+ const SLEEP_WAKE_MAX_ATTEMPTS = 5
141
+
142
+ type SleepWakeFailure = { error: Error; permanent: boolean }
143
+
144
+ export function buildSessionSleepWakeBody({
145
+ deliveryId,
146
+ wakeAt,
147
+ reason,
148
+ }: {
149
+ deliveryId: string
150
+ wakeAt: Date
151
+ reason: string | null
152
+ }) {
153
+ const marker: ThreadStartMarker = {
154
+ start: true,
155
+ sleepWake: true,
156
+ sleepId: deliveryId,
157
+ }
158
+ return {
159
+ content: formatSessionSleepWakePrompt({ wakeAt, reason }),
160
+ embeds: [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }],
161
+ // Discord limits nonces to 25 characters. The full ID stays in the marker.
162
+ // A stable nonce deduplicates retries when a successful response is lost.
163
+ nonce: deliveryId.replaceAll('-', '').slice(0, 25),
164
+ enforce_nonce: true,
165
+ }
166
+ }
167
+
168
+ async function postSessionSleepWake({
169
+ rest,
170
+ sleep,
171
+ threadId,
172
+ }: {
173
+ rest: REST
174
+ sleep: SessionSleep
175
+ threadId: string
176
+ }): Promise<SleepWakeFailure | null> {
177
+ // `.then(() => null)` keeps the success type concrete. Returning the raw post
178
+ // value would widen the union to `unknown` and erase the failure shape.
179
+ return await rest
180
+ .post(Routes.channelMessages(threadId), {
181
+ body: buildSessionSleepWakeBody({
182
+ deliveryId: sleep.delivery_id,
183
+ wakeAt: sleep.wake_at,
184
+ reason: sleep.reason,
185
+ }),
186
+ })
187
+ .then((): SleepWakeFailure | null => {
188
+ return null
189
+ })
190
+ .catch((error): SleepWakeFailure => {
191
+ // Only an unknown channel/message is genuinely unrecoverable. A 403 is
192
+ // often temporary (permissions revoked, thread locked) so it keeps
193
+ // retrying until the attempt budget runs out.
194
+ const permanent =
195
+ error instanceof DiscordAPIError &&
196
+ error.status === 404
197
+ return {
198
+ error: new Error(
199
+ `Failed to post sleep wake for session ${sleep.session_id}`,
200
+ { cause: error },
201
+ ),
202
+ permanent,
203
+ }
204
+ })
205
+ }
206
+
207
+ export async function wakeDueSessionSleeps({
208
+ rest,
209
+ now = new Date(),
210
+ limit = 20,
211
+ }: {
212
+ rest: REST
213
+ now?: Date
214
+ limit?: number
215
+ }): Promise<void> {
216
+ const dueSleeps = await getDueSessionSleeps({
217
+ now,
218
+ retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
219
+ limit,
220
+ })
221
+ for (const sleep of dueSleeps) {
222
+ // Reserves an attempt but stays `planned`. The row only leaves `planned`
223
+ // when ingress consumes the wake, so dying anywhere below costs one retry
224
+ // delay instead of losing the wake.
225
+ const claimed = await claimSessionSleepAttempt({
226
+ sessionId: sleep.session_id,
227
+ deliveryId: sleep.delivery_id,
228
+ now,
229
+ retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
230
+ })
231
+ if (!claimed) continue
232
+
233
+ // Resolved now, not when the tool ran, so a session rebound by /resume
234
+ // wakes in the thread that currently owns it.
235
+ const threadId = await getThreadIdBySessionId(claimed.session_id)
236
+ if (!threadId) {
237
+ await markSessionSleepFailed({
238
+ sessionId: claimed.session_id,
239
+ deliveryId: claimed.delivery_id,
240
+ })
241
+ taskLogger.warn(
242
+ `[task-runner] no thread owns session ${claimed.session_id}, dropping sleep wake`,
243
+ )
244
+ continue
245
+ }
246
+
247
+ // A successful post changes nothing here on purpose: ingress owns the
248
+ // transition out of `planned` when the wake actually becomes a turn.
249
+ const postResult = await postSessionSleepWake({ rest, sleep: claimed, threadId })
250
+ if (!postResult) continue
251
+
252
+ const exhausted = claimed.attempts >= SLEEP_WAKE_MAX_ATTEMPTS
253
+ if (postResult.permanent || exhausted) {
254
+ await markSessionSleepFailed({
255
+ sessionId: claimed.session_id,
256
+ deliveryId: claimed.delivery_id,
257
+ })
258
+ }
259
+ taskLogger.error(`[task-runner] ${formatErrorWithStack(postResult.error)}`)
260
+ void notifyError(postResult.error, 'Session sleep wake failed')
261
+ }
262
+ }
263
+
132
264
  async function executeChannelScheduledTask({
133
265
  rest,
134
266
  task,
@@ -564,6 +696,8 @@ async function runTaskRunnerTick({
564
696
  await previous
565
697
  await processDueTask({ rest, task })
566
698
  }, Promise.resolve())
699
+
700
+ await wakeDueSessionSleeps({ rest })
567
701
  }
568
702
 
569
703
  export function startTaskRunner({
@@ -18,6 +18,7 @@ import {
18
18
  appendTaskCommandOutput,
19
19
  parseScheduledTaskPayload,
20
20
  parseSendAtValue,
21
+ parseSleepWakeAt,
21
22
  } from './task-schedule.js'
22
23
 
23
24
  describe('parseSendAtValue', () => {
@@ -100,6 +101,81 @@ describe('parseSendAtValue', () => {
100
101
  })
101
102
  })
102
103
 
104
+ describe('parseSleepWakeAt', () => {
105
+ const now = new Date('2026-08-19T12:00:00Z')
106
+
107
+ test('adds 2h to now', () => {
108
+ const result = parseSleepWakeAt({ duration: '2h', now })
109
+ expect(result).not.toBeInstanceOf(Error)
110
+ if (result instanceof Error) throw result
111
+ expect(result.toISOString()).toBe('2026-08-19T14:00:00.000Z')
112
+ })
113
+
114
+ test('adds 30s and 1d', () => {
115
+ const thirtySeconds = parseSleepWakeAt({ duration: '30s', now })
116
+ expect(thirtySeconds).not.toBeInstanceOf(Error)
117
+ if (thirtySeconds instanceof Error) throw thirtySeconds
118
+ expect(thirtySeconds.toISOString()).toBe('2026-08-19T12:00:30.000Z')
119
+
120
+ const oneDay = parseSleepWakeAt({ duration: '1d', now })
121
+ expect(oneDay).not.toBeInstanceOf(Error)
122
+ if (oneDay instanceof Error) throw oneDay
123
+ expect(oneDay.toISOString()).toBe('2026-08-20T12:00:00.000Z')
124
+ })
125
+
126
+ test('accepts until UTC ISO date ending with Z', () => {
127
+ const result = parseSleepWakeAt({
128
+ until: '2026-08-20T09:00:00Z',
129
+ now,
130
+ })
131
+ expect(result).not.toBeInstanceOf(Error)
132
+ if (result instanceof Error) throw result
133
+ expect(result.toISOString()).toBe('2026-08-20T09:00:00.000Z')
134
+ })
135
+
136
+ test('rejects past until', () => {
137
+ const result = parseSleepWakeAt({
138
+ until: '2026-08-19T11:59:59Z',
139
+ now,
140
+ })
141
+ expect(result).toBeInstanceOf(Error)
142
+ if (result instanceof Error) {
143
+ expect(result.message).toContain('until must be in the future (UTC)')
144
+ }
145
+ })
146
+
147
+ test('rejects both duration and until', () => {
148
+ const result = parseSleepWakeAt({
149
+ duration: '1h',
150
+ until: '2026-08-20T09:00:00Z',
151
+ now,
152
+ })
153
+ expect(result).toBeInstanceOf(Error)
154
+ if (result instanceof Error) {
155
+ expect(result.message).toContain('either duration or until')
156
+ }
157
+ })
158
+
159
+ test('rejects neither duration nor until', () => {
160
+ const result = parseSleepWakeAt({ now })
161
+ expect(result).toBeInstanceOf(Error)
162
+ if (result instanceof Error) {
163
+ expect(result.message).toContain('duration or until')
164
+ }
165
+ })
166
+
167
+ test('rejects until with a non-UTC offset', () => {
168
+ const result = parseSleepWakeAt({
169
+ until: '2026-08-20T09:00:00+01:00',
170
+ now,
171
+ })
172
+ expect(result).toBeInstanceOf(Error)
173
+ if (result instanceof Error) {
174
+ expect(result.message).toContain('UTC ISO format ending with Z')
175
+ }
176
+ })
177
+ })
178
+
103
179
  describe('scheduled task execution options', () => {
104
180
  const tempDirectories: string[] = []
105
181