kimaki 1.3.0 → 1.5.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 (346) hide show
  1. package/assets/fonts/Geist-Bold.ttf +0 -0
  2. package/assets/fonts/Geist-Regular.ttf +0 -0
  3. package/assets/fonts/GeistMono-Regular.ttf +0 -0
  4. package/assets/fonts/OFL.txt +92 -0
  5. package/dist/agent-ui.e2e.test.js +27 -13
  6. package/dist/agent-ui.e2e.test.js.map +1 -1
  7. package/dist/agent-ui.js +2 -1
  8. package/dist/agent-ui.js.map +1 -1
  9. package/dist/analytics.e2e.test.js +1 -1
  10. package/dist/analytics.e2e.test.js.map +1 -1
  11. package/dist/autostart.d.ts +4 -0
  12. package/dist/autostart.js +55 -10
  13. package/dist/autostart.js.map +1 -1
  14. package/dist/bot.d.ts +13 -6
  15. package/dist/bot.js +17 -15
  16. package/dist/bot.js.map +1 -1
  17. package/dist/btw.e2e.test.js +3 -3
  18. package/dist/builtin-skills.d.ts +3 -0
  19. package/dist/builtin-skills.js +29 -0
  20. package/dist/builtin-skills.js.map +1 -1
  21. package/dist/builtin-skills.test.js +72 -1
  22. package/dist/builtin-skills.test.js.map +1 -1
  23. package/dist/catalog-refresh.e2e.test.js +2 -2
  24. package/dist/cli/bot.d.ts +1 -1
  25. package/dist/cli/bot.js +33 -18
  26. package/dist/cli/bot.js.map +1 -1
  27. package/dist/cli/discord.js +32 -6
  28. package/dist/cli/discord.js.map +1 -1
  29. package/dist/cli/schedule.js +23 -9
  30. package/dist/cli/schedule.js.map +1 -1
  31. package/dist/cli/session.js +37 -12
  32. package/dist/cli/session.js.map +1 -1
  33. package/dist/cli/shared.d.ts +5 -3
  34. package/dist/cli/shared.js +45 -10
  35. package/dist/cli/shared.js.map +1 -1
  36. package/dist/cli-discord.e2e.test.js +13 -8
  37. package/dist/cli-discord.e2e.test.js.map +1 -1
  38. package/dist/cli-lock.e2e.test.js +87 -20
  39. package/dist/cli-lock.e2e.test.js.map +1 -1
  40. package/dist/cli-read.e2e.test.js +18 -9
  41. package/dist/cli-read.e2e.test.js.map +1 -1
  42. package/dist/cli-sessions.e2e.test.js +4 -3
  43. package/dist/cli-sessions.e2e.test.js.map +1 -1
  44. package/dist/cli.js +2 -0
  45. package/dist/cli.js.map +1 -1
  46. package/dist/commands/preference-commands.d.ts +4 -0
  47. package/dist/commands/preference-commands.js +38 -4
  48. package/dist/commands/preference-commands.js.map +1 -1
  49. package/dist/commands/session-commands.js +11 -8
  50. package/dist/commands/session-commands.js.map +1 -1
  51. package/dist/commands/thread-commands.js +94 -24
  52. package/dist/commands/thread-commands.js.map +1 -1
  53. package/dist/commands/worktree-commands.d.ts +5 -6
  54. package/dist/commands/worktree-commands.js +8 -7
  55. package/dist/commands/worktree-commands.js.map +1 -1
  56. package/dist/commands.e2e.test.js +25 -21
  57. package/dist/commands.e2e.test.js.map +1 -1
  58. package/dist/coordinator-agents.e2e.test.d.ts +1 -0
  59. package/dist/coordinator-agents.e2e.test.js +113 -0
  60. package/dist/coordinator-agents.e2e.test.js.map +1 -0
  61. package/dist/coordinator-idle.e2e.test.d.ts +1 -0
  62. package/dist/coordinator-idle.e2e.test.js +100 -0
  63. package/dist/coordinator-idle.e2e.test.js.map +1 -0
  64. package/dist/coordinator-queue.e2e.test.d.ts +1 -0
  65. package/dist/coordinator-queue.e2e.test.js +244 -0
  66. package/dist/coordinator-queue.e2e.test.js.map +1 -0
  67. package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
  68. package/dist/coordinator-warmup.e2e.test.js +131 -0
  69. package/dist/coordinator-warmup.e2e.test.js.map +1 -0
  70. package/dist/coordinator.d.ts +6 -0
  71. package/dist/coordinator.e2e.test.d.ts +1 -0
  72. package/dist/coordinator.e2e.test.js +505 -0
  73. package/dist/coordinator.e2e.test.js.map +1 -0
  74. package/dist/coordinator.js +164 -0
  75. package/dist/coordinator.js.map +1 -0
  76. package/dist/db.d.ts +78 -3
  77. package/dist/db.test.js +1 -0
  78. package/dist/db.test.js.map +1 -1
  79. package/dist/diagram.d.ts +11 -0
  80. package/dist/diagram.js +232 -0
  81. package/dist/diagram.js.map +1 -0
  82. package/dist/diagram.test.d.ts +1 -0
  83. package/dist/diagram.test.js +52 -0
  84. package/dist/diagram.test.js.map +1 -0
  85. package/dist/duration.js +13 -5
  86. package/dist/duration.js.map +1 -1
  87. package/dist/effects.d.ts +6 -1
  88. package/dist/effects.js +97 -26
  89. package/dist/effects.js.map +1 -1
  90. package/dist/event-loop.d.ts +2 -3
  91. package/dist/event-loop.js +74 -44
  92. package/dist/event-loop.js.map +1 -1
  93. package/dist/format-parts.d.ts +21 -4
  94. package/dist/format-parts.js +40 -23
  95. package/dist/format-parts.js.map +1 -1
  96. package/dist/gateway-replay.e2e.test.js +4 -4
  97. package/dist/harness.e2e.test.js +2 -2
  98. package/dist/harness.e2e.test.js.map +1 -1
  99. package/dist/hello-thread.e2e.test.js +56 -5
  100. package/dist/hello-thread.e2e.test.js.map +1 -1
  101. package/dist/ingress.js +47 -12
  102. package/dist/ingress.js.map +1 -1
  103. package/dist/interaction-context.d.ts +4 -4
  104. package/dist/interaction-context.js +9 -7
  105. package/dist/interaction-context.js.map +1 -1
  106. package/dist/interrupt.e2e.test.js +8 -5
  107. package/dist/interrupt.e2e.test.js.map +1 -1
  108. package/dist/legacy.e2e.test.js +1 -1
  109. package/dist/lock-routes.d.ts +83 -31
  110. package/dist/lock-routes.js +68 -8
  111. package/dist/lock-routes.js.map +1 -1
  112. package/dist/lock-server.d.ts +3 -2
  113. package/dist/lock-server.js +23 -7
  114. package/dist/lock-server.js.map +1 -1
  115. package/dist/main.d.ts +6 -3
  116. package/dist/main.js +14 -4
  117. package/dist/main.js.map +1 -1
  118. package/dist/mainmast-idle.e2e.test.d.ts +1 -0
  119. package/dist/mainmast-idle.e2e.test.js +99 -0
  120. package/dist/mainmast-idle.e2e.test.js.map +1 -0
  121. package/dist/mainmast-queue.e2e.test.d.ts +1 -0
  122. package/dist/mainmast-queue.e2e.test.js +244 -0
  123. package/dist/mainmast-queue.e2e.test.js.map +1 -0
  124. package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
  125. package/dist/mainmast-warmup.e2e.test.js +130 -0
  126. package/dist/mainmast-warmup.e2e.test.js.map +1 -0
  127. package/dist/mainmast.d.ts +6 -0
  128. package/dist/mainmast.e2e.test.d.ts +1 -0
  129. package/dist/mainmast.e2e.test.js +428 -0
  130. package/dist/mainmast.e2e.test.js.map +1 -0
  131. package/dist/mainmast.js +156 -0
  132. package/dist/mainmast.js.map +1 -0
  133. package/dist/markdown/markdown.test.js +14 -4
  134. package/dist/markdown/markdown.test.js.map +1 -1
  135. package/dist/markdown/render-markdown.js +9 -6
  136. package/dist/markdown/render-markdown.js.map +1 -1
  137. package/dist/onboarding-cli.e2e.test.js +7 -6
  138. package/dist/onboarding-cli.e2e.test.js.map +1 -1
  139. package/dist/onboarding.d.ts +3 -10
  140. package/dist/onboarding.e2e.test.js +8 -10
  141. package/dist/onboarding.e2e.test.js.map +1 -1
  142. package/dist/onboarding.js +14 -24
  143. package/dist/onboarding.js.map +1 -1
  144. package/dist/permissions.e2e.test.js +3 -3
  145. package/dist/plugin/coordinator.d.ts +4 -0
  146. package/dist/plugin/coordinator.js +389 -0
  147. package/dist/plugin/coordinator.js.map +1 -0
  148. package/dist/plugin/index.js +93 -61
  149. package/dist/plugin/index.js.map +1 -1
  150. package/dist/plugin/mainmast.d.ts +4 -0
  151. package/dist/plugin/mainmast.js +379 -0
  152. package/dist/plugin/mainmast.js.map +1 -0
  153. package/dist/plugin.e2e.test.js +1 -1
  154. package/dist/preference-commands.e2e.test.js +103 -24
  155. package/dist/preference-commands.e2e.test.js.map +1 -1
  156. package/dist/project.d.ts +1 -1
  157. package/dist/prompt.d.ts +22 -9
  158. package/dist/prompt.js +64 -48
  159. package/dist/prompt.js.map +1 -1
  160. package/dist/questions.e2e.test.js +13 -13
  161. package/dist/queue-sources.e2e.test.d.ts +1 -0
  162. package/dist/queue-sources.e2e.test.js +168 -0
  163. package/dist/queue-sources.e2e.test.js.map +1 -0
  164. package/dist/queue.d.ts +9 -1
  165. package/dist/queue.e2e.test.js +48 -30
  166. package/dist/queue.e2e.test.js.map +1 -1
  167. package/dist/queue.js +97 -24
  168. package/dist/queue.js.map +1 -1
  169. package/dist/queue.test.d.ts +1 -0
  170. package/dist/queue.test.js +57 -0
  171. package/dist/queue.test.js.map +1 -0
  172. package/dist/remote-send.e2e.test.js +6 -6
  173. package/dist/remote-send.js +2 -1
  174. package/dist/remote-send.js.map +1 -1
  175. package/dist/renderer.e2e.test.js +7 -7
  176. package/dist/restart.e2e.test.js +2 -2
  177. package/dist/routines.e2e.test.js +2 -2
  178. package/dist/scheduler.d.ts +3 -3
  179. package/dist/scheduler.e2e.test.js +43 -20
  180. package/dist/scheduler.e2e.test.js.map +1 -1
  181. package/dist/scheduler.js +4 -2
  182. package/dist/scheduler.js.map +1 -1
  183. package/dist/schema-sql.d.ts +1 -1
  184. package/dist/schema-sql.js +1 -1
  185. package/dist/schema-sql.js.map +1 -1
  186. package/dist/schema.d.ts +152 -6
  187. package/dist/schema.js +12 -0
  188. package/dist/schema.js.map +1 -1
  189. package/dist/session-commands.e2e.test.js +36 -11
  190. package/dist/session-commands.e2e.test.js.map +1 -1
  191. package/dist/session-events.d.ts +18 -6
  192. package/dist/session-events.js +175 -27
  193. package/dist/session-events.js.map +1 -1
  194. package/dist/session-ops.e2e.test.js +63 -12
  195. package/dist/session-ops.e2e.test.js.map +1 -1
  196. package/dist/sessions.d.ts +44 -21
  197. package/dist/sessions.js +177 -65
  198. package/dist/sessions.js.map +1 -1
  199. package/dist/shell.e2e.test.js +4 -4
  200. package/dist/slash-commands.js +6 -4
  201. package/dist/slash-commands.js.map +1 -1
  202. package/dist/slash-commands.test.js +2 -0
  203. package/dist/slash-commands.test.js.map +1 -1
  204. package/dist/sleep.e2e.test.js +29 -14
  205. package/dist/sleep.e2e.test.js.map +1 -1
  206. package/dist/sleeps.d.ts +2 -1
  207. package/dist/sleeps.js +21 -3
  208. package/dist/sleeps.js.map +1 -1
  209. package/dist/store.d.ts +1 -0
  210. package/dist/store.js +3 -2
  211. package/dist/store.js.map +1 -1
  212. package/dist/system-prompt.d.ts +5 -15
  213. package/dist/system-prompt.js +180 -131
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/test/generate-log-plugin/index.d.ts +3 -0
  216. package/dist/test/generate-log-plugin/index.js +15 -0
  217. package/dist/test/generate-log-plugin/index.js.map +1 -0
  218. package/dist/test/harness.d.ts +20 -2
  219. package/dist/test/harness.js +51 -11
  220. package/dist/test/harness.js.map +1 -1
  221. package/dist/test/replay.js +11 -8
  222. package/dist/test/replay.js.map +1 -1
  223. package/dist/thread-reducer.d.ts +29 -6
  224. package/dist/thread-reducer.fixtures.test.js +58 -51
  225. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  226. package/dist/thread-reducer.js +109 -50
  227. package/dist/thread-reducer.js.map +1 -1
  228. package/dist/thread-reducer.test.js +190 -48
  229. package/dist/thread-reducer.test.js.map +1 -1
  230. package/dist/tunnel.d.ts +9 -0
  231. package/dist/tunnel.js +117 -28
  232. package/dist/tunnel.js.map +1 -1
  233. package/dist/tunnel.test.js +81 -5
  234. package/dist/tunnel.test.js.map +1 -1
  235. package/dist/voice-attachments.e2e.test.js +108 -18
  236. package/dist/voice-attachments.e2e.test.js.map +1 -1
  237. package/dist/voice-call.e2e.test.js +9 -9
  238. package/dist/voice-call.js +6 -6
  239. package/dist/voice-call.js.map +1 -1
  240. package/dist/voice-openai.e2e.test.js +59 -23
  241. package/dist/voice-openai.e2e.test.js.map +1 -1
  242. package/dist/voice.d.ts +1 -1
  243. package/dist/voice.js +72 -24
  244. package/dist/voice.js.map +1 -1
  245. package/dist/working-directory.e2e.test.js +11 -11
  246. package/dist/working-directory.e2e.test.js.map +1 -1
  247. package/package.json +11 -5
  248. package/skills/kimaki-tunnel/SKILL.md +3 -2
  249. package/src/agent-ui.e2e.test.ts +27 -13
  250. package/src/agent-ui.ts +2 -1
  251. package/src/analytics.e2e.test.ts +1 -1
  252. package/src/autostart.ts +55 -10
  253. package/src/bot.ts +36 -19
  254. package/src/btw.e2e.test.ts +3 -3
  255. package/src/builtin-skills.test.ts +73 -1
  256. package/src/builtin-skills.ts +33 -0
  257. package/src/catalog-refresh.e2e.test.ts +2 -2
  258. package/src/cli/bot.ts +30 -19
  259. package/src/cli/discord.ts +30 -6
  260. package/src/cli/schedule.ts +23 -9
  261. package/src/cli/session.ts +35 -12
  262. package/src/cli/shared.ts +43 -8
  263. package/src/cli-discord.e2e.test.ts +13 -8
  264. package/src/cli-lock.e2e.test.ts +88 -20
  265. package/src/cli-read.e2e.test.ts +17 -8
  266. package/src/cli-sessions.e2e.test.ts +4 -3
  267. package/src/cli.ts +2 -0
  268. package/src/commands/preference-commands.ts +38 -4
  269. package/src/commands/session-commands.ts +12 -7
  270. package/src/commands/thread-commands.ts +92 -24
  271. package/src/commands/worktree-commands.ts +10 -9
  272. package/src/commands.e2e.test.ts +25 -21
  273. package/src/coordinator-agents.e2e.test.ts +145 -0
  274. package/src/coordinator-idle.e2e.test.ts +122 -0
  275. package/src/coordinator-queue.e2e.test.ts +279 -0
  276. package/src/coordinator-warmup.e2e.test.ts +158 -0
  277. package/src/coordinator.e2e.test.ts +545 -0
  278. package/src/coordinator.ts +168 -0
  279. package/src/db.test.ts +1 -0
  280. package/src/diagram.test.ts +56 -0
  281. package/src/diagram.ts +243 -0
  282. package/src/duration.ts +13 -5
  283. package/src/effects.ts +99 -27
  284. package/src/event-loop.ts +71 -45
  285. package/src/format-parts.ts +53 -22
  286. package/src/gateway-replay.e2e.test.ts +4 -4
  287. package/src/harness.e2e.test.ts +2 -2
  288. package/src/hello-thread.e2e.test.ts +59 -5
  289. package/src/ingress.ts +44 -11
  290. package/src/interaction-context.ts +12 -11
  291. package/src/interrupt.e2e.test.ts +8 -5
  292. package/src/legacy.e2e.test.ts +1 -1
  293. package/src/lock-routes.ts +62 -8
  294. package/src/lock-server.ts +23 -7
  295. package/src/main.ts +26 -10
  296. package/src/markdown/markdown.test.ts +15 -4
  297. package/src/markdown/render-markdown.ts +9 -6
  298. package/src/onboarding-cli.e2e.test.ts +7 -6
  299. package/src/onboarding.e2e.test.ts +8 -10
  300. package/src/onboarding.ts +13 -29
  301. package/src/permissions.e2e.test.ts +3 -3
  302. package/src/plugin/coordinator.ts +401 -0
  303. package/src/plugin/index.ts +86 -59
  304. package/src/plugin.e2e.test.ts +1 -1
  305. package/src/preference-commands.e2e.test.ts +103 -23
  306. package/src/prompt.ts +87 -43
  307. package/src/questions.e2e.test.ts +13 -13
  308. package/src/queue-sources.e2e.test.ts +197 -0
  309. package/src/queue.e2e.test.ts +48 -30
  310. package/src/queue.test.ts +59 -0
  311. package/src/queue.ts +104 -24
  312. package/src/remote-send.e2e.test.ts +6 -6
  313. package/src/remote-send.ts +2 -1
  314. package/src/renderer.e2e.test.ts +7 -7
  315. package/src/restart.e2e.test.ts +2 -2
  316. package/src/routines.e2e.test.ts +2 -2
  317. package/src/scheduler.e2e.test.ts +45 -19
  318. package/src/scheduler.ts +4 -2
  319. package/src/schema-sql.ts +1 -1
  320. package/src/schema.ts +13 -0
  321. package/src/session-commands.e2e.test.ts +38 -11
  322. package/src/session-events.ts +168 -25
  323. package/src/session-ops.e2e.test.ts +64 -11
  324. package/src/sessions.ts +217 -67
  325. package/src/shell.e2e.test.ts +4 -4
  326. package/src/slash-commands.test.ts +2 -0
  327. package/src/slash-commands.ts +6 -4
  328. package/src/sleep.e2e.test.ts +30 -13
  329. package/src/sleeps.ts +20 -3
  330. package/src/store.ts +5 -2
  331. package/src/system-prompt.ts +190 -134
  332. package/src/test/generate-log-plugin/index.ts +16 -0
  333. package/src/test/harness.ts +77 -17
  334. package/src/test/replay.ts +12 -7
  335. package/src/thread-reducer.fixtures.test.ts +58 -51
  336. package/src/thread-reducer.test.ts +199 -49
  337. package/src/thread-reducer.ts +135 -66
  338. package/src/tunnel.test.ts +81 -5
  339. package/src/tunnel.ts +124 -25
  340. package/src/voice-attachments.e2e.test.ts +110 -18
  341. package/src/voice-call.e2e.test.ts +9 -9
  342. package/src/voice-call.ts +6 -6
  343. package/src/voice-openai.e2e.test.ts +64 -24
  344. package/src/voice.ts +93 -26
  345. package/src/working-directory.e2e.test.ts +11 -11
  346. package/src/message-tool.e2e.test.ts +0 -45
@@ -47,6 +47,7 @@ test('catalog names are sanitized, keep their suffix, skip collisions and MCP pr
47
47
  "fork-subagent",
48
48
  "btw",
49
49
  "abort",
50
+ "reset-coordinator",
50
51
  "queue",
51
52
  "clear-queue",
52
53
  "queue-command",
@@ -60,6 +61,7 @@ test('catalog names are sanitized, keep their suffix, skip collisions and MCP pr
60
61
  "model",
61
62
  "model-variant",
62
63
  "verbosity",
64
+ "toggle-coordinator",
63
65
  "routines",
64
66
  "command",
65
67
  "skill",
@@ -322,25 +322,27 @@ async function sendInput(
322
322
  const target = await resolveTarget(bot, interaction.channelId)
323
323
  if (target instanceof Error) return replyError(interaction, target)
324
324
  const author = authorOf(interaction)
325
- if (target.thread && target.sessionId) {
325
+ // In a coordinator channel an agent pick starts a new thread: the coordinator keeps its own agent and model.
326
+ const newThread = route.kind === 'steer' && Boolean(route.agent) && target.conversation !== null && !target.conversation.isThread()
327
+ if (target.conversation && target.sessionId && !newThread) {
326
328
  const reply = await replyWithEcho(interaction, { author, text: echo })
327
329
  if (reply instanceof Error) return replyError(interaction, reply)
328
330
  if (model && !(route.kind === 'steer' && route.agent)) {
329
331
  const switched = await switchModel(bot, { sessionId: target.sessionId, model })
330
332
  if (switched instanceof Error) return replyError(interaction, switched)
331
333
  }
332
- const result = await dispatch(bot, { thread: target.thread, route, author, messageId: reply.id, model })
334
+ const result = await dispatch(bot, { conversation: target.conversation, route, author, messageId: reply.id, model })
333
335
  if (result instanceof Error) return replyError(interaction, result)
334
336
  return
335
337
  }
336
- if (target.thread) {
338
+ if (target.conversation && !newThread) {
337
339
  await interaction.reply({ content: 'Use this command in a thread with a Kimaki session', flags: MessageFlags.Ephemeral })
338
340
  return
339
341
  }
340
342
  await interaction.deferReply()
341
343
  const started = await startSession(bot, {
342
344
  channelId: target.channelId,
343
- directory: target.directory,
345
+ directory: target.projectDirectory,
344
346
  route,
345
347
  author,
346
348
  messageId: interaction.id,
@@ -1,8 +1,10 @@
1
1
  // `kimaki sleep` end to end (spec 10.5, 12.1, Phase 8): the agent runs the
2
2
  // command through the shim, the bot stores the wake with its clock, the
3
3
  // scheduler wakes the same thread, and a user message cancels the sleep.
4
+ // Sleeps of at most 45 minutes keep the prompt cache warm (plugin/coordinator.ts).
4
5
 
5
6
  import fs from 'node:fs'
7
+ import path from 'node:path'
6
8
  import { setTimeout as sleep } from 'node:timers/promises'
7
9
  import type { DeterministicMatcher } from 'opencode-deterministic-provider'
8
10
  import { afterAll, beforeAll, expect, test } from 'vitest'
@@ -18,12 +20,15 @@ import {
18
20
  tempDataDir,
19
21
  textParts,
20
22
  toolParts,
23
+ waitFor,
21
24
  waitForFooter,
22
25
  warmUp,
23
26
  type OpencodeTestServer,
24
27
  type TestTwin,
25
28
  } from './test/harness.ts'
26
29
 
30
+ const WARM_INTERVAL_MS = 200
31
+
27
32
  function sleepTurn({ marker, command }: { marker: string; command: string }): DeterministicMatcher[] {
28
33
  const callId = `${marker}-call`
29
34
  return [
@@ -44,16 +49,16 @@ let bot: BotHandle
44
49
 
45
50
  beforeAll(async () => {
46
51
  ;[server, twin] = await Promise.all([
47
- startOpencodeTestServer({ matchers: [
48
- ...sleepTurn({ marker: 'sleep-marker', command: "kimaki sleep --duration 2h --reason 'waiting for the deploy'" }),
49
- ...sleepTurn({ marker: 'cancel-marker', command: 'kimaki sleep --until 2030-01-01T12:00:00Z' }),
52
+ startOpencodeTestServer({ plugins: [path.resolve('src/test/generate-log-plugin')], matchers: [
53
+ ...sleepTurn({ marker: 'sleep-marker', command: "kimaki sleep --duration 30m --reason 'waiting for the deploy'" }),
54
+ ...sleepTurn({ marker: 'cancel-marker', command: 'kimaki sleep --until 2030-01-01T10:30:00Z' }),
50
55
  { id: 'wake', priority: 200, when: { latestUserTextIncludes: 'Woke after sleeping until' }, then: { parts: textParts('Awake, checking the deploy.') } },
51
56
  { id: 'never-mind', priority: 200, when: { latestUserTextIncludes: 'Never mind' }, then: { parts: textParts('Noted, no wake needed.') } },
52
57
  ] }),
53
58
  startTwin(),
54
59
  ])
55
60
  await seedProjectChannel({ dataDir, channelId: twin.channelId, guildId: twin.discord.guildId, directory: server.projectDirectory })
56
- bot = await startTestBot({ dataDir, twin, server, clock, schedulerIntervalMs: null })
61
+ bot = await startTestBot({ dataDir, twin, server, clock, schedulerIntervalMs: null, warmupTimers: { intervalMs: WARM_INTERVAL_MS, maxMs: 60_000 } })
57
62
  await warmUp({ server })
58
63
  }, 60_000)
59
64
 
@@ -72,17 +77,26 @@ async function start(prompt: string) {
72
77
  return { threadId: thread.id, sessionId: bot.store.getState().roots[thread.id]! }
73
78
  }
74
79
 
80
+ // Keep-alive requests of a session, from the test plugin.
81
+ function keepAlives(sessionId: string): number {
82
+ const file = path.join(server.root, 'generate.log')
83
+ if (!fs.existsSync(file)) return 0
84
+ return fs.readFileSync(file, 'utf8').split('\n').filter((line) => line === sessionId).length
85
+ }
86
+
75
87
  async function sleepRow(sessionId: string) {
76
88
  return bot.db.query.session_sleeps.findFirst({ where: { session_id: sessionId } })
77
89
  }
78
90
 
79
91
  test('kimaki sleep wakes the same thread at the clock time', async () => {
80
92
  const { threadId, sessionId } = await start('Deploy and then sleep-marker')
81
- expect((await sleepRow(sessionId))?.wake_at.toISOString()).toBe('2030-01-01T11:00:00.000Z')
93
+ expect((await sleepRow(sessionId))?.wake_at.toISOString()).toBe('2030-01-01T09:30:00.000Z')
94
+ // A 30 minute sleep stays warm until the wake.
95
+ await waitFor({ label: 'keep-alive while sleeping', check: async () => keepAlives(sessionId) > 0 || null })
82
96
 
83
- clock.set(Date.parse('2030-01-01T10:59:59Z'))
97
+ clock.set(Date.parse('2030-01-01T09:29:59Z'))
84
98
  await bot.scheduler.runDueTasks()
85
- clock.set(Date.parse('2030-01-01T11:00:00Z'))
99
+ clock.set(Date.parse('2030-01-01T09:30:00Z'))
86
100
  await bot.scheduler.runDueTasks()
87
101
  await waitForFooter({ discord: twin.discord, threadId, count: 2 })
88
102
  expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
@@ -95,10 +109,10 @@ test('kimaki sleep wakes the same thread at the clock time', async () => {
95
109
  -# ┣ shell _Sleep until the deploy is done_
96
110
 
97
111
  Sleeping until the deploy is done.
98
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
99
- -# Woke after sleeping until 2030-01-01 11:00 UTC. Reason: waiting for the deploy
112
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
113
+ -# Woke after sleeping until 2030-01-01 09:30 UTC. Reason: waiting for the deploy
100
114
  Awake, checking the deploy.
101
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
115
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
102
116
  `)
103
117
  const row = await sleepRow(sessionId)
104
118
  expect(row?.status).toBe('consumed')
@@ -116,11 +130,14 @@ test('kimaki sleep wakes the same thread at the clock time', async () => {
116
130
  test('a user message cancels the sleep', async () => {
117
131
  const { threadId, sessionId } = await start('Deploy and then cancel-marker')
118
132
  expect((await sleepRow(sessionId))?.status).toBe('planned')
133
+ // The clock is at 09:30 after the first test: a 1 hour sleep is too long to keep warm, no keep-alive after a few intervals.
134
+ await sleep(WARM_INTERVAL_MS * 3)
135
+ expect(keepAlives(sessionId)).toBe(0)
119
136
  await twin.discord.thread(threadId).user(TEST_USER_ID).sendMessage({ content: 'Never mind, it is deployed' })
120
137
  await waitForFooter({ discord: twin.discord, threadId, count: 2 })
121
138
  expect((await sleepRow(sessionId))?.status).toBe('cancelled')
122
139
 
123
- clock.set(Date.parse('2030-01-01T12:00:00Z'))
140
+ clock.set(Date.parse('2030-01-01T10:30:00Z'))
124
141
  await bot.scheduler.runDueTasks()
125
142
  // Nothing may appear: poll briefly instead of a wait helper.
126
143
  for (let attempt = 0; attempt < 10; attempt++) {
@@ -138,11 +155,11 @@ test('a user message cancels the sleep', async () => {
138
155
  -# ┣ shell _Sleep until the deploy is done_
139
156
 
140
157
  Sleeping until the deploy is done.
141
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
158
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
142
159
  --- from: user (tommy)
143
160
  Never mind, it is deployed
144
161
  --- from: assistant (TestBot)
145
162
  Noted, no wake needed.
146
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
163
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
147
164
  `)
148
165
  })
package/src/sleeps.ts CHANGED
@@ -9,7 +9,7 @@ import crypto from 'node:crypto'
9
9
  import * as orm from 'drizzle-orm'
10
10
  import dedent from 'string-dedent'
11
11
 
12
- import { fetchThread, threadOfSession, type Bot } from './bot.ts'
12
+ import { fetchConversation, threadOfSession, type Bot } from './bot.ts'
13
13
  import { parseDuration } from './duration.ts'
14
14
  import { ConfigError, DbError } from './errors.ts'
15
15
  import { asSubtext } from './format-parts.ts'
@@ -21,6 +21,8 @@ const logger = createLogger('TASK')
21
21
 
22
22
  const WAKE_RETRY_MS = 30_000
23
23
  const WAKE_MAX_ATTEMPTS = 5
24
+ // Longer sleeps let the prompt cache expire: past ~45 minutes, keep-alives (0.1x input each) cost more than one cache write at the wake (1.25x).
25
+ const WARM_SLEEP_MAX_MS = 45 * 60_000
24
26
 
25
27
  type SleepRow = typeof schema.session_sleeps.$inferSelect
26
28
 
@@ -58,7 +60,7 @@ export function sleepOutput({ wakeAt, reason }: { wakeAt: number; reason: string
58
60
  return [
59
61
  `Sleeping until ${utcLabel(wakeAt)}.${reason ? ` Reason: ${reason}.` : ''}`,
60
62
  'This result is not a wake. Do not continue the waited work. Do not run more commands.',
61
- 'Reply with one short line that you are waiting until that time, then stop.',
63
+ 'If this turn has no text yet that answers the user, write your full answer now: text you only planned in your reasoning never reaches Discord. Then write one short line that you are waiting until that time, and stop.',
62
64
  'The real wake is a later message that starts with "Woke after sleeping until". Only then continue the wait reason.',
63
65
  'A new user message in this thread cancels the sleep. If you still need that later wake after answering, run kimaki sleep again with --until set to the same UTC time.',
64
66
  'When monitoring something slow (email replies, PR reviews), wait 2h or more and never post a status report on a wake where nothing changed.',
@@ -144,6 +146,19 @@ export async function createSleep(
144
146
  return { sessionId: root, threadId, wakeAt: new Date(wakeAt).toISOString(), output: sleepOutput({ wakeAt, reason: why }) }
145
147
  }
146
148
 
149
+ // The session waits for a wake at most 45 minutes after its sleep started: the plugin
150
+ // keeps its prompt cache warm until then (plugin/coordinator.ts, `session.warmup` route).
151
+ export async function sleepsWarm(bot: Bot, sessionId: string): Promise<DbError | boolean> {
152
+ const row = await bot.db.query.session_sleeps
153
+ .findFirst({ where: { session_id: sessionId, status: 'planned' } })
154
+ .catch((cause) => new DbError({ operation: 'read session_sleeps', cause }))
155
+ if (row instanceof Error) return row
156
+ // Sleeps imported from V1 may have no start time.
157
+ if (!row?.created_at) return false
158
+ const wakeAt = row.wake_at.getTime()
159
+ return wakeAt > bot.clock.now() && wakeAt - row.created_at.getTime() <= WARM_SLEEP_MAX_MS
160
+ }
161
+
147
162
  // Delivers a due wake if it is still the planned sleep and its retry delay
148
163
  // passed. The prompt ID comes from the delivery ID, so a retried wake cannot
149
164
  // deliver twice: OpenCode returns the existing inbox item for a repeated ID
@@ -184,7 +199,7 @@ async function wake(bot: Bot, row: SleepRow): Promise<Error | void> {
184
199
  return settle('failed')
185
200
  }
186
201
  const sent = await (async () => {
187
- const thread = await fetchThread(bot, threadId)
202
+ const thread = await fetchConversation(bot, threadId)
188
203
  if (thread instanceof Error) return thread
189
204
  return prompt(bot, {
190
205
  sessionId,
@@ -196,6 +211,8 @@ async function wake(bot: Bot, row: SleepRow): Promise<Error | void> {
196
211
  author: { id: bot.discord.user!.id, username: 'kimaki' },
197
212
  messageId: deliveryId,
198
213
  id: `msg_sleep_${deliveryId}`,
214
+ // The session asked for this wake itself.
215
+ sender: { sessionId },
199
216
  })
200
217
  })()
201
218
  if (!(sent instanceof Error)) return settle('consumed')
package/src/store.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // The only shared mutable in-memory state of the bot (spec 27.4). One zustand
2
- // atom. The event loop writes bindings and views; /verbosity writes verbosity.
2
+ // atom. The event loop writes bindings and views; /verbosity writes verbosity,
3
+ // /toggle-coordinator writes coordinator.
3
4
  // State private to one module (held events, wizard picks, upload waits) stays
4
5
  // in that module's closure.
5
6
 
@@ -17,10 +18,12 @@ export type State = {
17
18
  threads: Readonly<Record<string, ThreadView>>
18
19
  // channelId -> verbosity, from channel_verbosity (SQLite). Missing: the default.
19
20
  verbosity: Readonly<Record<string, Verbosity>>
21
+ // channelId -> coordinator mode, from channel_coordinator (SQLite). Missing: bot.coordinatorDefault.
22
+ coordinator: Readonly<Record<string, boolean>>
20
23
  }
21
24
 
22
25
  export function createBotStore() {
23
- return createStore<State>(() => ({ roots: {}, sessionThreads: {}, threads: {}, verbosity: {} }))
26
+ return createStore<State>(() => ({ roots: {}, sessionThreads: {}, threads: {}, verbosity: {}, coordinator: {} }))
24
27
  }
25
28
 
26
29
  export type BotStore = ReturnType<typeof createBotStore>