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
@@ -0,0 +1,565 @@
1
+ // E2e coverage for the durable kimaki_sleep tool.
2
+ // Verifies the tool persists a wake row, the thread shows the sleep line,
3
+ // the task runner wake resumes the same session, and a user message cancels
4
+ // a planned sleep so the wake never fires.
5
+
6
+ import { describe, test, expect } from 'vitest'
7
+ import type { DeterministicMatcher } from 'opencode-deterministic-provider'
8
+ import {
9
+ setupQueueAdvancedSuite,
10
+ TEST_USER_ID,
11
+ } from './queue-advanced-e2e-setup.js'
12
+ import {
13
+ waitForBotMessageContaining,
14
+ waitForFooterMessage,
15
+ } from './test-utils.js'
16
+ import { getSessionSleep, getThreadSession } from './database.js'
17
+ import { wakeDueSessionSleeps } from './task-runner.js'
18
+
19
+ const TEXT_CHANNEL_ID = '200000000000001031'
20
+
21
+ // Fixed wake instant so the wake message stays snapshot-stable across runs.
22
+ const SLEEP_UNTIL = '2030-01-01T09:00:00Z'
23
+ const AFTER_SLEEP_UNTIL = new Date('2030-01-01T09:00:01Z')
24
+
25
+ function createSleepMatchers(): DeterministicMatcher[] {
26
+ const sleepCallMatcher: DeterministicMatcher = {
27
+ id: 'sleep-tool-call',
28
+ priority: 130,
29
+ when: {
30
+ lastMessageRole: 'user',
31
+ latestUserTextIncludes: 'SLEEP_TOOL_MARKER',
32
+ },
33
+ then: {
34
+ parts: [
35
+ { type: 'stream-start', warnings: [] },
36
+ { type: 'text-start', id: 'sleep-call-text' },
37
+ {
38
+ type: 'text-delta',
39
+ id: 'sleep-call-text',
40
+ delta: 'waiting for the deploy',
41
+ },
42
+ { type: 'text-end', id: 'sleep-call-text' },
43
+ {
44
+ type: 'tool-call',
45
+ toolCallId: 'sleep-call-1',
46
+ toolName: 'kimaki_sleep',
47
+ input: JSON.stringify({
48
+ until: SLEEP_UNTIL,
49
+ reason: 'waiting for the deploy',
50
+ }),
51
+ },
52
+ {
53
+ type: 'finish',
54
+ finishReason: 'tool-calls',
55
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
56
+ },
57
+ ],
58
+ },
59
+ }
60
+
61
+ const sleepAcknowledgeMatcher: DeterministicMatcher = {
62
+ id: 'sleep-tool-ack',
63
+ priority: 129,
64
+ when: {
65
+ latestUserTextIncludes: 'SLEEP_TOOL_MARKER',
66
+ rawPromptIncludes: 'Sleeping until',
67
+ },
68
+ then: {
69
+ parts: [
70
+ { type: 'stream-start', warnings: [] },
71
+ { type: 'text-start', id: 'sleep-ack-text' },
72
+ { type: 'text-delta', id: 'sleep-ack-text', delta: 'sleep-started' },
73
+ { type: 'text-end', id: 'sleep-ack-text' },
74
+ {
75
+ type: 'finish',
76
+ finishReason: 'stop',
77
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
78
+ },
79
+ ],
80
+ },
81
+ }
82
+
83
+ const sleepWakeMatcher: DeterministicMatcher = {
84
+ id: 'sleep-tool-wake',
85
+ priority: 131,
86
+ when: {
87
+ latestUserTextIncludes: 'Woke after sleeping until',
88
+ },
89
+ then: {
90
+ parts: [
91
+ { type: 'stream-start', warnings: [] },
92
+ { type: 'text-start', id: 'sleep-wake-text' },
93
+ { type: 'text-delta', id: 'sleep-wake-text', delta: 'sleep-wake-done' },
94
+ { type: 'text-end', id: 'sleep-wake-text' },
95
+ {
96
+ type: 'finish',
97
+ finishReason: 'stop',
98
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
99
+ },
100
+ ],
101
+ },
102
+ }
103
+
104
+ const sleepCancelCallMatcher: DeterministicMatcher = {
105
+ id: 'sleep-cancel-call',
106
+ priority: 132,
107
+ when: {
108
+ lastMessageRole: 'user',
109
+ latestUserTextIncludes: 'SLEEP_CANCEL_MARKER',
110
+ },
111
+ then: {
112
+ parts: [
113
+ { type: 'stream-start', warnings: [] },
114
+ {
115
+ type: 'tool-call',
116
+ toolCallId: 'sleep-cancel-call-1',
117
+ toolName: 'kimaki_sleep',
118
+ input: JSON.stringify({ duration: '2h', reason: 'cancel me' }),
119
+ },
120
+ {
121
+ type: 'finish',
122
+ finishReason: 'tool-calls',
123
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
124
+ },
125
+ ],
126
+ },
127
+ }
128
+
129
+ const sleepCancelAckMatcher: DeterministicMatcher = {
130
+ id: 'sleep-cancel-ack',
131
+ priority: 133,
132
+ when: {
133
+ latestUserTextIncludes: 'SLEEP_CANCEL_MARKER',
134
+ rawPromptIncludes: 'Sleeping until',
135
+ },
136
+ then: {
137
+ parts: [
138
+ { type: 'stream-start', warnings: [] },
139
+ { type: 'text-start', id: 'sleep-cancel-ack-text' },
140
+ {
141
+ type: 'text-delta',
142
+ id: 'sleep-cancel-ack-text',
143
+ delta: 'cancel-sleep-started',
144
+ },
145
+ { type: 'text-end', id: 'sleep-cancel-ack-text' },
146
+ {
147
+ type: 'finish',
148
+ finishReason: 'stop',
149
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
150
+ },
151
+ ],
152
+ },
153
+ }
154
+
155
+ const sleepQueueCallMatcher: DeterministicMatcher = {
156
+ id: 'sleep-queue-call',
157
+ priority: 135,
158
+ when: {
159
+ lastMessageRole: 'user',
160
+ latestUserTextIncludes: 'SLEEP_QUEUE_MARKER',
161
+ },
162
+ then: {
163
+ parts: [
164
+ { type: 'stream-start', warnings: [] },
165
+ {
166
+ type: 'tool-call',
167
+ toolCallId: 'sleep-queue-call-1',
168
+ toolName: 'kimaki_sleep',
169
+ input: JSON.stringify({ duration: '2h', reason: 'queue supersedes' }),
170
+ },
171
+ {
172
+ type: 'finish',
173
+ finishReason: 'tool-calls',
174
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
175
+ },
176
+ ],
177
+ },
178
+ }
179
+
180
+ const sleepQueueAckMatcher: DeterministicMatcher = {
181
+ id: 'sleep-queue-ack',
182
+ priority: 136,
183
+ when: {
184
+ latestUserTextIncludes: 'SLEEP_QUEUE_MARKER',
185
+ rawPromptIncludes: 'Sleeping until',
186
+ },
187
+ then: {
188
+ parts: [
189
+ { type: 'stream-start', warnings: [] },
190
+ { type: 'text-start', id: 'sleep-queue-ack-text' },
191
+ {
192
+ type: 'text-delta',
193
+ id: 'sleep-queue-ack-text',
194
+ delta: 'queue-sleep-started',
195
+ },
196
+ { type: 'text-end', id: 'sleep-queue-ack-text' },
197
+ {
198
+ type: 'finish',
199
+ finishReason: 'stop',
200
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
201
+ },
202
+ ],
203
+ },
204
+ }
205
+
206
+ const sleepQueuedMessageMatcher: DeterministicMatcher = {
207
+ id: 'sleep-queued-message',
208
+ priority: 137,
209
+ when: {
210
+ lastMessageRole: 'user',
211
+ latestUserTextIncludes: 'SLEEP_QUEUE_FOLLOWUP',
212
+ },
213
+ then: {
214
+ parts: [
215
+ { type: 'stream-start', warnings: [] },
216
+ { type: 'text-start', id: 'sleep-queued-text' },
217
+ {
218
+ type: 'text-delta',
219
+ id: 'sleep-queued-text',
220
+ delta: 'queue-followup-done',
221
+ },
222
+ { type: 'text-end', id: 'sleep-queued-text' },
223
+ {
224
+ type: 'finish',
225
+ finishReason: 'stop',
226
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
227
+ },
228
+ ],
229
+ },
230
+ }
231
+
232
+ const sleepCancelFollowupMatcher: DeterministicMatcher = {
233
+ id: 'sleep-cancel-followup',
234
+ priority: 134,
235
+ when: {
236
+ lastMessageRole: 'user',
237
+ latestUserTextIncludes: 'SLEEP_CANCEL_FOLLOWUP',
238
+ },
239
+ then: {
240
+ parts: [
241
+ { type: 'stream-start', warnings: [] },
242
+ { type: 'text-start', id: 'sleep-cancel-followup-text' },
243
+ {
244
+ type: 'text-delta',
245
+ id: 'sleep-cancel-followup-text',
246
+ delta: 'cancel-followup-done',
247
+ },
248
+ { type: 'text-end', id: 'sleep-cancel-followup-text' },
249
+ {
250
+ type: 'finish',
251
+ finishReason: 'stop',
252
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
253
+ },
254
+ ],
255
+ },
256
+ }
257
+
258
+ return [
259
+ sleepCallMatcher,
260
+ sleepAcknowledgeMatcher,
261
+ sleepWakeMatcher,
262
+ sleepCancelCallMatcher,
263
+ sleepCancelAckMatcher,
264
+ sleepCancelFollowupMatcher,
265
+ sleepQueueCallMatcher,
266
+ sleepQueueAckMatcher,
267
+ sleepQueuedMessageMatcher,
268
+ ]
269
+ }
270
+
271
+ describe('kimaki_sleep', () => {
272
+ const ctx = setupQueueAdvancedSuite({
273
+ channelId: TEXT_CHANNEL_ID,
274
+ channelName: 'sleep-e2e',
275
+ dirName: 'sleep-e2e',
276
+ username: 'sleep-tester',
277
+ extraMatchers: createSleepMatchers(),
278
+ })
279
+
280
+ test(
281
+ 'persists a wake row, then the runner wakes the same session',
282
+ async () => {
283
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
284
+ content: 'SLEEP_TOOL_MARKER wait for the deploy',
285
+ })
286
+
287
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
288
+ timeout: 4_000,
289
+ predicate: (t) => {
290
+ return t.name === 'SLEEP_TOOL_MARKER wait for the deploy'
291
+ },
292
+ })
293
+
294
+ const th = ctx.discord.thread(thread.id)
295
+
296
+ await waitForBotMessageContaining({
297
+ discord: ctx.discord,
298
+ threadId: thread.id,
299
+ text: 'sleep-started',
300
+ timeout: 4_000,
301
+ })
302
+
303
+ // Let the first turn finish emitting its footer before waking, otherwise
304
+ // the footer can land after the wake message and reorder the snapshot.
305
+ await waitForFooterMessage({
306
+ discord: ctx.discord,
307
+ threadId: thread.id,
308
+ timeout: 4_000,
309
+ afterMessageIncludes: 'sleep-started',
310
+ afterAuthorId: ctx.discord.botUserId,
311
+ })
312
+
313
+ const sessionId = await getThreadSession(thread.id)
314
+ if (!sessionId) {
315
+ throw new Error('Expected a thread session after the sleep tool ran')
316
+ }
317
+
318
+ const plannedSleep = await getSessionSleep({ sessionId })
319
+ expect(plannedSleep?.status).toBe('planned')
320
+ expect(plannedSleep?.reason).toBe('waiting for the deploy')
321
+
322
+ // The row is years out, so pass a future `now` instead of waiting on the tick.
323
+ await wakeDueSessionSleeps({
324
+ rest: ctx.botClient.rest,
325
+ now: AFTER_SLEEP_UNTIL,
326
+ })
327
+
328
+ await waitForBotMessageContaining({
329
+ discord: ctx.discord,
330
+ threadId: thread.id,
331
+ text: 'sleep-wake-done',
332
+ timeout: 4_000,
333
+ })
334
+
335
+ await waitForFooterMessage({
336
+ discord: ctx.discord,
337
+ threadId: thread.id,
338
+ timeout: 4_000,
339
+ afterMessageIncludes: 'sleep-wake-done',
340
+ afterAuthorId: ctx.discord.botUserId,
341
+ })
342
+
343
+ expect(await th.text()).toMatchInlineSnapshot(`
344
+ "--- from: user (sleep-tester)
345
+ SLEEP_TOOL_MARKER wait for the deploy
346
+ --- from: assistant (TestBot)
347
+ > *using deterministic-provider/deterministic-v2*
348
+ > waiting for the deploy
349
+
350
+ ┣ kimaki_sleep until 2030-01-01T09:00:00Z _waiting for the deploy_
351
+
352
+ > sleep-started
353
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
354
+ ⬦ Woke after sleeping until 2030-01-01 09:00 UTC
355
+ Reason: waiting for the deploy
356
+ Continue the work you were waiting for.
357
+ [embed]
358
+ > sleep-wake-done
359
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
360
+ `)
361
+
362
+ // The wake must continue the SAME OpenCode session, not start a new one:
363
+ // that is the whole point of sleeping instead of scheduling a new prompt.
364
+ expect(await getThreadSession(thread.id)).toBe(sessionId)
365
+
366
+ // `consumed` proves ingress actually turned the wake into a turn. The row
367
+ // only reaches this state through the ingress commit point.
368
+ const wokenSleep = await getSessionSleep({ sessionId })
369
+ expect(wokenSleep?.status).toBe('consumed')
370
+
371
+ // Replaying the tick must not produce a second wake turn.
372
+ const messagesBeforeReplay = (await th.getMessages()).length
373
+ await wakeDueSessionSleeps({
374
+ rest: ctx.botClient.rest,
375
+ now: AFTER_SLEEP_UNTIL,
376
+ })
377
+ for (let attempt = 0; attempt < 10; attempt++) {
378
+ expect((await th.getMessages()).length).toBe(messagesBeforeReplay)
379
+ await new Promise((resolve) => {
380
+ setTimeout(resolve, 20)
381
+ })
382
+ }
383
+ },
384
+ 20_000,
385
+ )
386
+
387
+ test(
388
+ 'a user message cancels a planned sleep so no wake fires',
389
+ async () => {
390
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
391
+ content: 'SLEEP_CANCEL_MARKER wait for something',
392
+ })
393
+
394
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
395
+ timeout: 4_000,
396
+ predicate: (t) => {
397
+ return t.name === 'SLEEP_CANCEL_MARKER wait for something'
398
+ },
399
+ })
400
+
401
+ const th = ctx.discord.thread(thread.id)
402
+
403
+ await waitForBotMessageContaining({
404
+ discord: ctx.discord,
405
+ threadId: thread.id,
406
+ text: 'cancel-sleep-started',
407
+ timeout: 4_000,
408
+ })
409
+
410
+ await waitForFooterMessage({
411
+ discord: ctx.discord,
412
+ threadId: thread.id,
413
+ timeout: 4_000,
414
+ afterMessageIncludes: 'cancel-sleep-started',
415
+ afterAuthorId: ctx.discord.botUserId,
416
+ })
417
+
418
+ const sessionId = await getThreadSession(thread.id)
419
+ if (!sessionId) {
420
+ throw new Error('Expected a thread session after the sleep tool ran')
421
+ }
422
+ expect((await getSessionSleep({ sessionId }))?.status).toBe('planned')
423
+
424
+ await th.user(TEST_USER_ID).sendMessage({
425
+ content: 'SLEEP_CANCEL_FOLLOWUP never mind, keep going',
426
+ })
427
+
428
+ await waitForBotMessageContaining({
429
+ discord: ctx.discord,
430
+ threadId: thread.id,
431
+ text: 'cancel-followup-done',
432
+ timeout: 4_000,
433
+ })
434
+
435
+ await waitForFooterMessage({
436
+ discord: ctx.discord,
437
+ threadId: thread.id,
438
+ timeout: 4_000,
439
+ afterMessageIncludes: 'cancel-followup-done',
440
+ afterAuthorId: ctx.discord.botUserId,
441
+ })
442
+
443
+ // Cancellation happens inside runtime.enqueueIncoming, so it covers
444
+ // slash commands and CLI-injected prompts, not just chat messages.
445
+ expect((await getSessionSleep({ sessionId }))?.status).toBe('cancelled')
446
+
447
+ await wakeDueSessionSleeps({
448
+ rest: ctx.botClient.rest,
449
+ now: AFTER_SLEEP_UNTIL,
450
+ })
451
+
452
+ // Everything is deterministic, so a short poll proves the wake never posts.
453
+ for (let attempt = 0; attempt < 10; attempt++) {
454
+ const messages = await th.getMessages()
455
+ const woke = messages.some((message) => {
456
+ return message.content.includes('Woke after sleeping until')
457
+ })
458
+ expect(woke).toBe(false)
459
+ await new Promise((resolve) => {
460
+ setTimeout(resolve, 20)
461
+ })
462
+ }
463
+
464
+ expect(await th.text()).toMatchInlineSnapshot(`
465
+ "--- from: user (sleep-tester)
466
+ SLEEP_CANCEL_MARKER wait for something
467
+ --- from: assistant (TestBot)
468
+ > *using deterministic-provider/deterministic-v2*
469
+ ┣ kimaki_sleep for 2h _cancel me_
470
+
471
+ > cancel-sleep-started
472
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
473
+ --- from: user (sleep-tester)
474
+ SLEEP_CANCEL_FOLLOWUP never mind, keep going
475
+ --- from: assistant (TestBot)
476
+ > cancel-followup-done
477
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
478
+ `)
479
+ },
480
+ 20_000,
481
+ )
482
+
483
+ test(
484
+ '/queue also supersedes a pending sleep',
485
+ async () => {
486
+ // Slash commands never reach the Discord MessageCreate handler, so this
487
+ // proves cancellation really lives in the shared ingress path.
488
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
489
+ content: 'SLEEP_QUEUE_MARKER wait for something',
490
+ })
491
+
492
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
493
+ timeout: 4_000,
494
+ predicate: (t) => {
495
+ return t.name === 'SLEEP_QUEUE_MARKER wait for something'
496
+ },
497
+ })
498
+
499
+ const th = ctx.discord.thread(thread.id)
500
+
501
+ await waitForBotMessageContaining({
502
+ discord: ctx.discord,
503
+ threadId: thread.id,
504
+ text: 'queue-sleep-started',
505
+ timeout: 4_000,
506
+ })
507
+
508
+ await waitForFooterMessage({
509
+ discord: ctx.discord,
510
+ threadId: thread.id,
511
+ timeout: 4_000,
512
+ afterMessageIncludes: 'queue-sleep-started',
513
+ afterAuthorId: ctx.discord.botUserId,
514
+ })
515
+
516
+ const sessionId = await getThreadSession(thread.id)
517
+ if (!sessionId) {
518
+ throw new Error('Expected a thread session after the sleep tool ran')
519
+ }
520
+ expect((await getSessionSleep({ sessionId }))?.status).toBe('planned')
521
+
522
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
523
+ .runSlashCommand({
524
+ name: 'queue',
525
+ options: [
526
+ {
527
+ name: 'message',
528
+ type: 3,
529
+ value: 'SLEEP_QUEUE_FOLLOWUP handle this instead',
530
+ },
531
+ ],
532
+ })
533
+ await th.waitForInteractionAck({
534
+ interactionId: queueInteractionId,
535
+ timeout: 4_000,
536
+ })
537
+
538
+ await waitForBotMessageContaining({
539
+ discord: ctx.discord,
540
+ threadId: thread.id,
541
+ text: 'queue-followup-done',
542
+ timeout: 4_000,
543
+ })
544
+
545
+ expect((await getSessionSleep({ sessionId }))?.status).toBe('cancelled')
546
+
547
+ await wakeDueSessionSleeps({
548
+ rest: ctx.botClient.rest,
549
+ now: AFTER_SLEEP_UNTIL,
550
+ })
551
+
552
+ for (let attempt = 0; attempt < 10; attempt++) {
553
+ const messages = await th.getMessages()
554
+ const woke = messages.some((message) => {
555
+ return message.content.includes('Woke after sleeping until')
556
+ })
557
+ expect(woke).toBe(false)
558
+ await new Promise((resolve) => {
559
+ setTimeout(resolve, 20)
560
+ })
561
+ }
562
+ },
563
+ 20_000,
564
+ )
565
+ })
@@ -106,7 +106,11 @@ function createMinimalMatchers(): DeterministicMatcher[] {
106
106
  const TEST_USER_ID = '900000000000000777'
107
107
  const TEXT_CHANNEL_ID = '900000000000000778'
108
108
 
109
- describe('startup time measurement', () => {
109
+ const startupBenchmark = describe.skipIf(
110
+ process.env['KIMAKI_STARTUP_BENCHMARK'] !== '1',
111
+ )
112
+
113
+ startupBenchmark('startup time measurement', () => {
110
114
  let directories: ReturnType<typeof createRunDirectories>
111
115
  let discord: DigitalDiscord
112
116
  let botClient: Client | null = null
package/src/store.ts CHANGED
@@ -25,6 +25,7 @@ export type RegisteredUserCommand = {
25
25
  export type DeterministicTranscriptionConfig = {
26
26
  transcription: string
27
27
  queueMessage: boolean
28
+ sessionAction?: 'btw' | 'new-session'
28
29
  /** Exercise the production API-key prompt before returning the canned result. */
29
30
  requireApiKey?: boolean
30
31
  /** Agent name extracted from voice message. Only set if user explicitly requested an agent. */
@@ -67,6 +68,11 @@ export type KimakiState = {
67
68
  // Read by: system-message.ts (conditionally appends critique instructions).
68
69
  critiqueEnabled: boolean
69
70
 
71
+ // Whether final session footers mention the thread creator.
72
+ // Changes: set once at startup from --skip-footer-mentions.
73
+ // Read by: ThreadSessionRuntime.emitFooter().
74
+ footerMentionsEnabled: boolean
75
+
70
76
  // User-specified skill whitelist. When non-empty, only these skill names
71
77
  // are injected into the model's system prompt (all others are hidden
72
78
  // behind an opencode permission.skill deny-all rule). Mutually exclusive
@@ -96,6 +102,19 @@ export type KimakiState = {
96
102
  // Read by: discord-utils.ts hasKimakiBotPermission().
97
103
  allowAllUsers: boolean
98
104
 
105
+ // Hostname passed to `opencode serve --hostname`. Null means OpenCode's
106
+ // default (127.0.0.1). Set from --opencode-hostname. Kimaki still talks
107
+ // to 127.0.0.1 even when the server binds 0.0.0.0. Does not bind the
108
+ // Kimaki hrana/lock server.
109
+ // Changes: set once at startup from --opencode-hostname CLI flag.
110
+ // Read by: opencode.ts startSingleServer().
111
+ opencodeHostname: string | null
112
+
113
+ // Port passed to `opencode serve --port`. Null means pick a free port.
114
+ // Changes: set once at startup from --opencode-port CLI flag.
115
+ // Read by: opencode.ts startSingleServer().
116
+ opencodePort: number | null
117
+
99
118
  // When true, the agent may only touch the session working directory and a
100
119
  // small set of known-safe paths; anything else triggers an external_directory
101
120
  // permission prompt. When false (default), every directory is allowed and the
@@ -185,10 +204,13 @@ export const store = createStore<KimakiState>(() => ({
185
204
  defaultVerbosity: 'text_and_essential_tools',
186
205
  defaultMentionMode: false,
187
206
  critiqueEnabled: true,
207
+ footerMentionsEnabled: true,
188
208
  enabledSkills: [],
189
209
  disabledSkills: [],
190
210
  allowedMentions: ['users'],
191
211
  allowAllUsers: false,
212
+ opencodeHostname: null,
213
+ opencodePort: null,
192
214
  restrictExternalDirectories: false,
193
215
  permissionTimeoutMs: 10 * 60 * 1000,
194
216
  useWorktrees: false,