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
@@ -13,6 +13,7 @@ import fs from 'node:fs'
13
13
  import path from 'node:path'
14
14
  import { getDataDir } from './config.js'
15
15
  import { store } from './store.js'
16
+ import { SESSION_SEARCH_DEFAULT_DAYS } from './session-search.js'
16
17
 
17
18
  /** Subfolder under the kimaki data dir for session system prompt side-channel files. */
18
19
  export const SESSION_SYSTEM_PROMPT_DIR = 'session-system'
@@ -175,7 +176,7 @@ this feature by restarting kimaki with the \`--no-critique\` flag.
175
176
  const KIMAKI_TUNNEL_INSTRUCTIONS = `
176
177
  ## running dev servers with tunnel access
177
178
 
178
- ALWAYS use \`kimaki tunnel\` when starting any dev server. NEVER run \`pnpm dev\`, \`npm run dev\`, or any dev server command without wrapping it in \`kimaki tunnel\`. Always invoke Kimaki directly as \`kimaki\`, never via \`npx\` or \`bunx\`. The user is on Discord, not at the terminal — localhost URLs are useless to them. They need a tunnel URL to access the site.
179
+ When starting a local dev server that the Discord user should open in a browser, prefer wrapping it in \`kimaki tunnel\` so they get a public URL. Localhost URLs are useless from Discord. Invoke Kimaki directly as \`kimaki\`, not via \`npx\` or \`bunx\`.
179
180
 
180
181
  Use \`bunx tuistory\` to run the tunnel + dev server combo in the background so it persists across commands. This is preferable to raw shell backgrounding because you can wait for real output, read logs, and interact with the running process.
181
182
 
@@ -316,9 +317,13 @@ export type ThreadStartMarker = {
316
317
  /**
317
318
  * OpenCode session ID of the parent session that spawned this thread via
318
319
  * `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
- */
320
+ * child can message the parent only when the user explicitly asks.
321
+ */
321
322
  parentSessionId?: string
323
+ /** Wake prompt posted after kimaki_sleep. Ingress must not cancel this sleep. */
324
+ sleepWake?: boolean
325
+ /** delivery_id of the session_sleeps row this wake delivers. */
326
+ sleepId?: string
322
327
  }
323
328
 
324
329
  export function isInjectedPromptMarker({
@@ -337,6 +342,40 @@ export type AgentInfo = {
337
342
  description?: string
338
343
  }
339
344
 
345
+ /**
346
+ * Info about the scheduled task that started this session, resolved once per
347
+ * session and kept stable across turns so the system prompt prefix stays
348
+ * cacheable.
349
+ */
350
+ export type ScheduledTaskSystemContext = {
351
+ /** Scheduled task ID. Missing for one-shot 'at' tasks (deleted after run). */
352
+ taskId?: number
353
+ scheduleKind: 'at' | 'cron'
354
+ /** Cron expression in the task's timezone. Only set for 'cron' tasks. */
355
+ cronExpr?: string | null
356
+ /** IANA timezone the cron fires in. Defaults to UTC when unset. */
357
+ timezone?: string | null
358
+ }
359
+
360
+ function getScheduledTaskSection(context: ScheduledTaskSystemContext): string {
361
+ const origin = context.taskId
362
+ ? `kimaki scheduled task #${context.taskId}`
363
+ : 'a one-time kimaki scheduled task'
364
+ const schedule = context.scheduleKind === 'cron' && context.cronExpr
365
+ ? `Schedule: cron \`${context.cronExpr}\`${context.timezone ? ` in ${context.timezone}` : ' in UTC'}.`
366
+ : context.scheduleKind === 'at'
367
+ ? 'This task runs once and does not repeat.'
368
+ : ''
369
+ return `
370
+ ## scheduled task session
371
+
372
+ This session was started automatically by ${origin}.
373
+ ${schedule}
374
+ When your run is done, just stop: the task fires again on its schedule and starts a fresh session automatically.
375
+ Do NOT use \`kimaki_sleep\` to wait for the next run. Sleeping pins this session and never triggers the next one; each firing of the task starts a new session on its own.
376
+ `
377
+ }
378
+
340
379
  function escapePromptAttribute(value: string): string {
341
380
  return value
342
381
  .replaceAll('&', '&')
@@ -357,6 +396,7 @@ export function getOpencodePromptContext({
357
396
  userId,
358
397
  sourceMessageId,
359
398
  sourceThreadId,
399
+ threadName,
360
400
  repliedMessage,
361
401
  worktree,
362
402
  currentAgent,
@@ -366,6 +406,7 @@ export function getOpencodePromptContext({
366
406
  userId?: string
367
407
  sourceMessageId?: string
368
408
  sourceThreadId?: string
409
+ threadName?: string
369
410
  repliedMessage?: RepliedMessageContext
370
411
  worktree?: WorktreeInfo
371
412
  currentAgent?: string
@@ -384,6 +425,9 @@ export function getOpencodePromptContext({
384
425
  ...(sourceThreadId
385
426
  ? [` thread-id="${escapePromptAttribute(sourceThreadId)}"`]
386
427
  : []),
428
+ ...(threadName
429
+ ? [` thread-name="${escapePromptAttribute(threadName)}"`]
430
+ : []),
387
431
  ].join('')
388
432
  const repliedMessageXml = repliedMessage
389
433
  ? `This message was a reply to message
@@ -422,6 +466,7 @@ export function getOpencodeSystemMessage({
422
466
  agents,
423
467
  userId,
424
468
  parentSessionId,
469
+ scheduledTask,
425
470
  }: {
426
471
  sessionId: string
427
472
  channelId?: string
@@ -440,6 +485,11 @@ export function getOpencodeSystemMessage({
440
485
  * per-parent block. Never auto-derive this from OpenCode parent session IDs.
441
486
  */
442
487
  parentSessionId?: string
488
+ /**
489
+ * Set only when the session was started by a scheduled task. Resolved from
490
+ * the session_start_sources row, so it stays identical across turns.
491
+ */
492
+ scheduledTask?: ScheduledTaskSystemContext
443
493
  }) {
444
494
  const userArg = ` --user '${userId || '<discord-user-id>'}'`
445
495
  const parentSessionArg = ` --parent-session ${sessionId}`
@@ -465,6 +515,12 @@ export function getOpencodeSystemMessage({
465
515
  return `
466
516
  The user is reading your messages from inside Discord, via kimaki.dev
467
517
 
518
+ ## Discord output
519
+
520
+ Be concise. Do not narrate between tool calls. Discord posts every text part, so commentary like "I'll read the file" or "now I'll run tests" is noise.
521
+ Do not output text until you are ready to give the user the final answer for this turn. Tool calls can run with no preceding text.
522
+ Exceptions: when a tool requires user-visible text first (\`question\`, \`kimaki_action_buttons\`, \`kimaki_file_upload\`, \`kimaki_sleep\`), write that required text, then call the tool.
523
+
468
524
  ## bash tool
469
525
 
470
526
  When calling the bash tool, always include these extra fields alongside \`command\`:
@@ -481,12 +537,12 @@ interface BashToolInput {
481
537
  }
482
538
  \`\`\`
483
539
 
484
- \`description\` is shown to the user in Discord as a summary of the bash call.
540
+ \`description\` is shown in Discord when the bash command is longer than 50 characters.
485
541
  \`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
486
542
 
487
543
  Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}${parentSessionContext}
488
544
 
489
- Per-turn Discord metadata like the current user and current agent is delivered in synthetic user message parts.
545
+ Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
490
546
 
491
547
  ## permissions
492
548
 
@@ -509,6 +565,7 @@ Do not restart the bot unless the user explicitly asks for it.
509
565
 
510
566
  ## debugging kimaki issues
511
567
 
568
+ 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
569
  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
570
 
514
571
  ## uploading files to discord
@@ -517,6 +574,8 @@ To upload files to the Discord thread (images, screenshots, long files that woul
517
574
 
518
575
  kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
519
576
 
577
+ NEVER show images with markdown like \`![alt](/tmp/file.png)\` or \`![alt](file://...)\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
578
+
520
579
  ## generating audio from text
521
580
 
522
581
  When the user asks you to generate audio of some text so they can listen instead of reading, use \`kimaki tts\` to create a speech file and \`kimaki upload-to-discord\` to send it to the thread. Only use this when the user explicitly asks for audio.
@@ -538,9 +597,18 @@ see --help for options like voice, speed, etc.
538
597
  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
598
  You MUST call \`kimaki_file_upload\` LAST, after ALL text.
540
599
 
600
+ ## sleeping the session
601
+
602
+ Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
603
+ Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
604
+ You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
605
+ A new user message cancels the sleep. If you still need to wake later after answering, call \`kimaki_sleep\` again with \`until\` set to the original UTC time.
606
+ The tool result is not a wake. After it succeeds, write one short line that you are waiting, then stop. Do not continue the wait reason and do not pretend time has passed.
607
+ Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
608
+ ${scheduledTask ? getScheduledTaskSection(scheduledTask) : ''}
541
609
  ## archiving the current thread
542
610
 
543
- To archive the current Discord thread (hide it from sidebar) and stop the session, run:
611
+ To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
544
612
 
545
613
  kimaki session archive ${archiveTarget}
546
614
 
@@ -555,6 +623,17 @@ kimaki session abort <session_id>
555
623
  This stops the AI from processing but keeps the thread visible in Discord.
556
624
  Different from \`kimaki session archive\` which hides the thread.
557
625
 
626
+ ## updating the session title
627
+
628
+ When the session scope or goal changed from the first message, update the title:
629
+
630
+ kimaki session title 'Short title' --session ${sessionId}
631
+
632
+ The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
633
+ This updates the OpenCode title. Discord follows automatically.
634
+ Do not retitle every turn. Discord rate-limits thread renames.
635
+ Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
636
+
558
637
  ## discord user mentions
559
638
 
560
639
  Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
@@ -653,13 +732,13 @@ kimaki send --thread <thread_id> --prompt '/<agentname>-agent' --agent <current_
653
732
  Use \`--send-at\` to schedule a one-time or recurring task:
654
733
 
655
734
  kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${parentSessionArg}${userArg}
656
- kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
735
+ kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
657
736
 
658
737
  Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
659
738
 
660
739
  Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
661
740
 
662
- **ALWAYS pass \`--user\` when scheduling a task.** Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member, so the task can fire completely unnoticed if the user never joined the thread or already left it. This applies to \`--channel\` and \`--thread\` scheduling alike.
741
+ Pass \`--user\` when the user should see or act on the thread. Omit \`--user\` for routine autonomous work (digests, monitors, housekeeping) so the thread does not appear in the user's Discord sidebar. Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member. This applies to \`--channel\` and \`--thread\` scheduling alike.
663
742
 
664
743
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
665
744
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -671,14 +750,14 @@ When the user specifies a time without a timezone, ask them to confirm their tim
671
750
  - \`--pre-run\` to start only when a project command exits with code 0
672
751
  - \`--allow-concurrency\` to permit overlapping runs from the same task
673
752
  - \`--parent-session\` to pass this session as parent of the scheduled child
674
- - \`--user\` to add a specific user to the scheduled thread (always pass this)
753
+ - \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
675
754
 
676
755
  \`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
677
756
 
678
757
  Keep scheduled task prompts **short**. The prompt text becomes the first message in the Discord thread, so long prompts clutter the channel. Instead of inlining the full task description in \`--prompt\`, write a markdown file in the project's \`tasks/\` folder and reference it:
679
758
 
680
759
  \`\`\`bash
681
- kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
760
+ kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
682
761
  \`\`\`
683
762
 
684
763
  The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
@@ -697,7 +776,7 @@ For simple reminders and notifications (\`--notify-only\`), inline the prompt di
697
776
  Notification strategy:
698
777
  - NEVER use \`@username\` (e.g. \`@Tommy\`) directly in task prompts. The prompt text becomes the first message in the thread, so a raw \`@\` mention triggers an actual Discord ping every time the task fires. Instead, wrap it in inline code like \`\\\`@Tommy\\\`\`, or use Discord user ID mentions like \`<@USER_ID>\` only in the body of the prompt where the agent will process it, not in the opening line.
699
778
  - If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
700
- - With \`--user\`, the user is added to the thread and receives thread-level notifications.
779
+ - With \`--user\`, the user is added to the thread and receives thread-level notifications. Omit \`--user\` when the work should stay out of the sidebar.
701
780
  - If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive ${archiveTarget}\`
702
781
 
703
782
  Manage scheduled tasks with:
@@ -706,7 +785,7 @@ kimaki task list
706
785
  kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
707
786
  kimaki task delete <id>
708
787
 
709
- \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Fix it with \`kimaki task edit <id> --user '<discord-user-id>'\` instead of deleting and recreating the task. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
788
+ \`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Add a user with \`kimaki task edit <id> --user '<discord-user-id>'\`. Clear a stored user with \`kimaki task edit <id> --user ''\`. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
710
789
 
711
790
  \`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
712
791
 
@@ -784,15 +863,22 @@ To list all sessions in this project (shows which were started via kimaki):
784
863
  kimaki session list
785
864
  kimaki session list --json # machine-readable output
786
865
  kimaki session list --project /path/to/project # specific project
866
+
867
+ # List only in-progress sessions. Exit status is 1 when none remain.
868
+ kimaki session list --active
787
869
  \`\`\`
788
870
 
789
- To search past sessions for this project (supports plain text or /regex/flags):
871
+ Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
872
+
873
+ To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last ${SESSION_SEARCH_DEFAULT_DAYS} days. Use \`--days 0\` for all time. Use \`--all\` to search every locally registered project:
790
874
 
791
875
  \`\`\`bash
792
876
  kimaki session search "auth timeout"
877
+ kimaki session search "auth timeout" --days 0
793
878
  kimaki session search "/error\\s+42/i"
794
879
  kimaki session search "rate limit" --project /path/to/project
795
880
  kimaki session search "/panic|crash/i" --channel <channel_id>
881
+ kimaki session search "auth timeout" --all
796
882
  \`\`\`
797
883
 
798
884
  To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
@@ -804,6 +890,29 @@ kimaki session read <sessionId> > ./tmp/session.md 2>/dev/null
804
890
 
805
891
  Then use grep/read tools on the file to find what you need.
806
892
 
893
+ ### who edited a file
894
+
895
+ To find the session that last edited a file, run:
896
+
897
+ \`\`\`bash
898
+ kimaki session editors src/foo.ts
899
+ kimaki session editors src/foo.ts --json
900
+ \`\`\`
901
+
902
+ Output is newest first. Each row has:
903
+
904
+ - **session ID** (\`ses_xxx\`)
905
+ - **title** (Discord thread name, so you can tell what that session was doing)
906
+ - **time ago** (when that session last edited the file)
907
+
908
+ 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:
909
+
910
+ \`\`\`
911
+ Session: ses_xxx
912
+ \`\`\`
913
+
914
+ 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.
915
+
807
916
  ## cross-project commands
808
917
 
809
918
  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 +979,7 @@ Use \`--wait\` to block until a session completes and print its full conversatio
870
979
 
871
980
  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
981
 
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.
982
+ 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
983
 
875
984
  If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
876
985
 
@@ -885,9 +994,14 @@ kimaki send --thread <thread_id> --prompt 'Run the tests' --wait --agent <curren
885
994
 
886
995
  # Wait for a session that was already started elsewhere
887
996
  kimaki session wait <session_id>
997
+
998
+ # Wait until every other in-progress session in this project finishes
999
+ while kimaki session list --active --exclude ${sessionId}; do sleep 5; done
888
1000
  \`\`\`
889
1001
 
890
- The command exits with the session markdown on stdout once the model finishes responding.
1002
+ \`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.
1003
+
1004
+ \`session wait\` exits with the session markdown on stdout once the model finishes responding.
891
1005
 
892
1006
  Use \`--wait\` when you need to:
893
1007
  - **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
@@ -1001,7 +1115,7 @@ You MUST write ALL user-visible text FIRST.
1001
1115
  You MUST call \`question\` LAST, after ALL text parts.
1002
1116
  NEVER call \`question\` before your text. Discord will hide the message.
1003
1117
 
1004
- The same rule applies to \`kimaki_action_buttons\` and \`kimaki_file_upload\`.
1118
+ The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
1005
1119
  You MUST call them LAST, after ALL text.
1006
1120
 
1007
1121
  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,8 +20,14 @@ 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'
30
+ import { QUEUE_PREFIX } from './message-formatting.js'
25
31
  import { initializeOpencodeForDirectory } from './opencode.js'
26
32
  import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
27
33
  import { notifyError } from './sentry.js'
@@ -29,6 +35,7 @@ import type { ThreadStartMarker } from './system-message.js'
29
35
  import {
30
36
  type ScheduledTaskPayload,
31
37
  appendTaskCommandOutput,
38
+ formatSessionSleepWakePrompt,
32
39
  getNextCronRun,
33
40
  getPromptPreview,
34
41
  parseScheduledTaskPayload,
@@ -93,7 +100,7 @@ async function executeThreadScheduledTask({
93
100
  const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
94
101
  // Newline between prefix and prompt so leading /command detection can
95
102
  // find the command on its own line.
96
- const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`
103
+ const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`
97
104
 
98
105
  // Re-join the user before posting, so the message they get notified about is
99
106
  // in a thread they are already a member of. Works on archived threads too;
@@ -129,6 +136,132 @@ async function executeThreadScheduledTask({
129
136
  return payload.threadId
130
137
  }
131
138
 
139
+ /** Wait before retrying a wake that ingress has not consumed yet. */
140
+ const SLEEP_WAKE_RETRY_AFTER_MS = 30_000
141
+ const SLEEP_WAKE_MAX_ATTEMPTS = 5
142
+
143
+ type SleepWakeFailure = { error: Error; permanent: boolean }
144
+
145
+ export function buildSessionSleepWakeBody({
146
+ deliveryId,
147
+ wakeAt,
148
+ reason,
149
+ }: {
150
+ deliveryId: string
151
+ wakeAt: Date
152
+ reason: string | null
153
+ }) {
154
+ const marker: ThreadStartMarker = {
155
+ start: true,
156
+ sleepWake: true,
157
+ sleepId: deliveryId,
158
+ }
159
+ return {
160
+ content: formatSessionSleepWakePrompt({ wakeAt, reason }),
161
+ embeds: [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }],
162
+ // Discord limits nonces to 25 characters. The full ID stays in the marker.
163
+ // A stable nonce deduplicates retries when a successful response is lost.
164
+ nonce: deliveryId.replaceAll('-', '').slice(0, 25),
165
+ enforce_nonce: true,
166
+ }
167
+ }
168
+
169
+ async function postSessionSleepWake({
170
+ rest,
171
+ sleep,
172
+ threadId,
173
+ }: {
174
+ rest: REST
175
+ sleep: SessionSleep
176
+ threadId: string
177
+ }): Promise<SleepWakeFailure | null> {
178
+ // `.then(() => null)` keeps the success type concrete. Returning the raw post
179
+ // value would widen the union to `unknown` and erase the failure shape.
180
+ return await rest
181
+ .post(Routes.channelMessages(threadId), {
182
+ body: buildSessionSleepWakeBody({
183
+ deliveryId: sleep.delivery_id,
184
+ wakeAt: sleep.wake_at,
185
+ reason: sleep.reason,
186
+ }),
187
+ })
188
+ .then((): SleepWakeFailure | null => {
189
+ return null
190
+ })
191
+ .catch((error): SleepWakeFailure => {
192
+ // Only an unknown channel/message is genuinely unrecoverable. A 403 is
193
+ // often temporary (permissions revoked, thread locked) so it keeps
194
+ // retrying until the attempt budget runs out.
195
+ const permanent =
196
+ error instanceof DiscordAPIError &&
197
+ error.status === 404
198
+ return {
199
+ error: new Error(
200
+ `Failed to post sleep wake for session ${sleep.session_id}`,
201
+ { cause: error },
202
+ ),
203
+ permanent,
204
+ }
205
+ })
206
+ }
207
+
208
+ export async function wakeDueSessionSleeps({
209
+ rest,
210
+ now = new Date(),
211
+ limit = 20,
212
+ }: {
213
+ rest: REST
214
+ now?: Date
215
+ limit?: number
216
+ }): Promise<void> {
217
+ const dueSleeps = await getDueSessionSleeps({
218
+ now,
219
+ retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
220
+ limit,
221
+ })
222
+ for (const sleep of dueSleeps) {
223
+ // Reserves an attempt but stays `planned`. The row only leaves `planned`
224
+ // when ingress consumes the wake, so dying anywhere below costs one retry
225
+ // delay instead of losing the wake.
226
+ const claimed = await claimSessionSleepAttempt({
227
+ sessionId: sleep.session_id,
228
+ deliveryId: sleep.delivery_id,
229
+ now,
230
+ retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
231
+ })
232
+ if (!claimed) continue
233
+
234
+ // Resolved now, not when the tool ran, so a session rebound by /resume
235
+ // wakes in the thread that currently owns it.
236
+ const threadId = await getThreadIdBySessionId(claimed.session_id)
237
+ if (!threadId) {
238
+ await markSessionSleepFailed({
239
+ sessionId: claimed.session_id,
240
+ deliveryId: claimed.delivery_id,
241
+ })
242
+ taskLogger.warn(
243
+ `[task-runner] no thread owns session ${claimed.session_id}, dropping sleep wake`,
244
+ )
245
+ continue
246
+ }
247
+
248
+ // A successful post changes nothing here on purpose: ingress owns the
249
+ // transition out of `planned` when the wake actually becomes a turn.
250
+ const postResult = await postSessionSleepWake({ rest, sleep: claimed, threadId })
251
+ if (!postResult) continue
252
+
253
+ const exhausted = claimed.attempts >= SLEEP_WAKE_MAX_ATTEMPTS
254
+ if (postResult.permanent || exhausted) {
255
+ await markSessionSleepFailed({
256
+ sessionId: claimed.session_id,
257
+ deliveryId: claimed.delivery_id,
258
+ })
259
+ }
260
+ taskLogger.error(`[task-runner] ${formatErrorWithStack(postResult.error)}`)
261
+ void notifyError(postResult.error, 'Session sleep wake failed')
262
+ }
263
+ }
264
+
132
265
  async function executeChannelScheduledTask({
133
266
  rest,
134
267
  task,
@@ -564,6 +697,8 @@ async function runTaskRunnerTick({
564
697
  await previous
565
698
  await processDueTask({ rest, task })
566
699
  }, Promise.resolve())
700
+
701
+ await wakeDueSessionSleeps({ rest })
567
702
  }
568
703
 
569
704
  export function startTaskRunner({