kimaki 1.4.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 (335) 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 +10 -1
  15. package/dist/bot.js +14 -6
  16. package/dist/bot.js.map +1 -1
  17. package/dist/btw.e2e.test.js +3 -3
  18. package/dist/builtin-skills.d.ts +1 -1
  19. package/dist/builtin-skills.js +1 -1
  20. package/dist/builtin-skills.js.map +1 -1
  21. package/dist/builtin-skills.test.js +3 -4
  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.js +25 -14
  25. package/dist/cli/bot.js.map +1 -1
  26. package/dist/cli/discord.js +32 -6
  27. package/dist/cli/discord.js.map +1 -1
  28. package/dist/cli/schedule.js +23 -9
  29. package/dist/cli/schedule.js.map +1 -1
  30. package/dist/cli/session.js +37 -12
  31. package/dist/cli/session.js.map +1 -1
  32. package/dist/cli/shared.d.ts +5 -3
  33. package/dist/cli/shared.js +45 -10
  34. package/dist/cli/shared.js.map +1 -1
  35. package/dist/cli-discord.e2e.test.js +13 -8
  36. package/dist/cli-discord.e2e.test.js.map +1 -1
  37. package/dist/cli-lock.e2e.test.js +87 -20
  38. package/dist/cli-lock.e2e.test.js.map +1 -1
  39. package/dist/cli-read.e2e.test.js +18 -9
  40. package/dist/cli-read.e2e.test.js.map +1 -1
  41. package/dist/cli-sessions.e2e.test.js +4 -3
  42. package/dist/cli-sessions.e2e.test.js.map +1 -1
  43. package/dist/cli.js +2 -0
  44. package/dist/cli.js.map +1 -1
  45. package/dist/commands/preference-commands.d.ts +4 -0
  46. package/dist/commands/preference-commands.js +38 -4
  47. package/dist/commands/preference-commands.js.map +1 -1
  48. package/dist/commands/session-commands.js +11 -8
  49. package/dist/commands/session-commands.js.map +1 -1
  50. package/dist/commands/thread-commands.js +94 -24
  51. package/dist/commands/thread-commands.js.map +1 -1
  52. package/dist/commands/worktree-commands.d.ts +5 -6
  53. package/dist/commands/worktree-commands.js +8 -7
  54. package/dist/commands/worktree-commands.js.map +1 -1
  55. package/dist/commands.e2e.test.js +25 -21
  56. package/dist/commands.e2e.test.js.map +1 -1
  57. package/dist/coordinator-agents.e2e.test.d.ts +1 -0
  58. package/dist/coordinator-agents.e2e.test.js +113 -0
  59. package/dist/coordinator-agents.e2e.test.js.map +1 -0
  60. package/dist/coordinator-idle.e2e.test.d.ts +1 -0
  61. package/dist/coordinator-idle.e2e.test.js +100 -0
  62. package/dist/coordinator-idle.e2e.test.js.map +1 -0
  63. package/dist/coordinator-queue.e2e.test.d.ts +1 -0
  64. package/dist/coordinator-queue.e2e.test.js +244 -0
  65. package/dist/coordinator-queue.e2e.test.js.map +1 -0
  66. package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
  67. package/dist/coordinator-warmup.e2e.test.js +131 -0
  68. package/dist/coordinator-warmup.e2e.test.js.map +1 -0
  69. package/dist/coordinator.d.ts +6 -0
  70. package/dist/coordinator.e2e.test.d.ts +1 -0
  71. package/dist/coordinator.e2e.test.js +505 -0
  72. package/dist/coordinator.e2e.test.js.map +1 -0
  73. package/dist/coordinator.js +164 -0
  74. package/dist/coordinator.js.map +1 -0
  75. package/dist/db.d.ts +78 -3
  76. package/dist/db.test.js +1 -0
  77. package/dist/db.test.js.map +1 -1
  78. package/dist/diagram.d.ts +11 -0
  79. package/dist/diagram.js +232 -0
  80. package/dist/diagram.js.map +1 -0
  81. package/dist/diagram.test.d.ts +1 -0
  82. package/dist/diagram.test.js +52 -0
  83. package/dist/diagram.test.js.map +1 -0
  84. package/dist/duration.js +13 -5
  85. package/dist/duration.js.map +1 -1
  86. package/dist/effects.d.ts +6 -1
  87. package/dist/effects.js +97 -26
  88. package/dist/effects.js.map +1 -1
  89. package/dist/event-loop.d.ts +1 -1
  90. package/dist/event-loop.js +73 -43
  91. package/dist/event-loop.js.map +1 -1
  92. package/dist/format-parts.d.ts +21 -4
  93. package/dist/format-parts.js +40 -23
  94. package/dist/format-parts.js.map +1 -1
  95. package/dist/gateway-replay.e2e.test.js +4 -4
  96. package/dist/harness.e2e.test.js +2 -2
  97. package/dist/harness.e2e.test.js.map +1 -1
  98. package/dist/hello-thread.e2e.test.js +56 -5
  99. package/dist/hello-thread.e2e.test.js.map +1 -1
  100. package/dist/ingress.js +47 -12
  101. package/dist/ingress.js.map +1 -1
  102. package/dist/interaction-context.d.ts +4 -4
  103. package/dist/interaction-context.js +9 -7
  104. package/dist/interaction-context.js.map +1 -1
  105. package/dist/interrupt.e2e.test.js +8 -5
  106. package/dist/interrupt.e2e.test.js.map +1 -1
  107. package/dist/legacy.e2e.test.js +1 -1
  108. package/dist/lock-routes.d.ts +83 -31
  109. package/dist/lock-routes.js +68 -8
  110. package/dist/lock-routes.js.map +1 -1
  111. package/dist/lock-server.d.ts +3 -2
  112. package/dist/lock-server.js +23 -7
  113. package/dist/lock-server.js.map +1 -1
  114. package/dist/main.d.ts +4 -2
  115. package/dist/main.js +13 -3
  116. package/dist/main.js.map +1 -1
  117. package/dist/mainmast-idle.e2e.test.d.ts +1 -0
  118. package/dist/mainmast-idle.e2e.test.js +99 -0
  119. package/dist/mainmast-idle.e2e.test.js.map +1 -0
  120. package/dist/mainmast-queue.e2e.test.d.ts +1 -0
  121. package/dist/mainmast-queue.e2e.test.js +244 -0
  122. package/dist/mainmast-queue.e2e.test.js.map +1 -0
  123. package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
  124. package/dist/mainmast-warmup.e2e.test.js +130 -0
  125. package/dist/mainmast-warmup.e2e.test.js.map +1 -0
  126. package/dist/mainmast.d.ts +6 -0
  127. package/dist/mainmast.e2e.test.d.ts +1 -0
  128. package/dist/mainmast.e2e.test.js +428 -0
  129. package/dist/mainmast.e2e.test.js.map +1 -0
  130. package/dist/mainmast.js +156 -0
  131. package/dist/mainmast.js.map +1 -0
  132. package/dist/markdown/markdown.test.js +4 -4
  133. package/dist/markdown/render-markdown.js +6 -5
  134. package/dist/markdown/render-markdown.js.map +1 -1
  135. package/dist/onboarding-cli.e2e.test.js +7 -6
  136. package/dist/onboarding-cli.e2e.test.js.map +1 -1
  137. package/dist/onboarding.d.ts +3 -10
  138. package/dist/onboarding.e2e.test.js +8 -10
  139. package/dist/onboarding.e2e.test.js.map +1 -1
  140. package/dist/onboarding.js +14 -24
  141. package/dist/onboarding.js.map +1 -1
  142. package/dist/permissions.e2e.test.js +3 -3
  143. package/dist/plugin/coordinator.d.ts +4 -0
  144. package/dist/plugin/coordinator.js +389 -0
  145. package/dist/plugin/coordinator.js.map +1 -0
  146. package/dist/plugin/index.js +76 -17
  147. package/dist/plugin/index.js.map +1 -1
  148. package/dist/plugin/mainmast.d.ts +4 -0
  149. package/dist/plugin/mainmast.js +379 -0
  150. package/dist/plugin/mainmast.js.map +1 -0
  151. package/dist/plugin.e2e.test.js +1 -1
  152. package/dist/preference-commands.e2e.test.js +103 -24
  153. package/dist/preference-commands.e2e.test.js.map +1 -1
  154. package/dist/project.d.ts +1 -1
  155. package/dist/prompt.d.ts +22 -9
  156. package/dist/prompt.js +64 -48
  157. package/dist/prompt.js.map +1 -1
  158. package/dist/questions.e2e.test.js +13 -13
  159. package/dist/queue-sources.e2e.test.d.ts +1 -0
  160. package/dist/queue-sources.e2e.test.js +168 -0
  161. package/dist/queue-sources.e2e.test.js.map +1 -0
  162. package/dist/queue.d.ts +9 -1
  163. package/dist/queue.e2e.test.js +48 -30
  164. package/dist/queue.e2e.test.js.map +1 -1
  165. package/dist/queue.js +97 -24
  166. package/dist/queue.js.map +1 -1
  167. package/dist/queue.test.d.ts +1 -0
  168. package/dist/queue.test.js +57 -0
  169. package/dist/queue.test.js.map +1 -0
  170. package/dist/remote-send.e2e.test.js +6 -6
  171. package/dist/remote-send.js +2 -1
  172. package/dist/remote-send.js.map +1 -1
  173. package/dist/renderer.e2e.test.js +7 -7
  174. package/dist/restart.e2e.test.js +2 -2
  175. package/dist/routines.e2e.test.js +2 -2
  176. package/dist/scheduler.d.ts +3 -3
  177. package/dist/scheduler.e2e.test.js +43 -20
  178. package/dist/scheduler.e2e.test.js.map +1 -1
  179. package/dist/scheduler.js +4 -2
  180. package/dist/scheduler.js.map +1 -1
  181. package/dist/schema-sql.d.ts +1 -1
  182. package/dist/schema-sql.js +1 -1
  183. package/dist/schema-sql.js.map +1 -1
  184. package/dist/schema.d.ts +152 -6
  185. package/dist/schema.js +12 -0
  186. package/dist/schema.js.map +1 -1
  187. package/dist/session-commands.e2e.test.js +36 -11
  188. package/dist/session-commands.e2e.test.js.map +1 -1
  189. package/dist/session-events.d.ts +18 -6
  190. package/dist/session-events.js +166 -22
  191. package/dist/session-events.js.map +1 -1
  192. package/dist/session-ops.e2e.test.js +63 -12
  193. package/dist/session-ops.e2e.test.js.map +1 -1
  194. package/dist/sessions.d.ts +44 -21
  195. package/dist/sessions.js +174 -63
  196. package/dist/sessions.js.map +1 -1
  197. package/dist/shell.e2e.test.js +4 -4
  198. package/dist/slash-commands.js +6 -4
  199. package/dist/slash-commands.js.map +1 -1
  200. package/dist/slash-commands.test.js +2 -0
  201. package/dist/slash-commands.test.js.map +1 -1
  202. package/dist/sleep.e2e.test.js +29 -14
  203. package/dist/sleep.e2e.test.js.map +1 -1
  204. package/dist/sleeps.d.ts +2 -1
  205. package/dist/sleeps.js +21 -3
  206. package/dist/sleeps.js.map +1 -1
  207. package/dist/store.d.ts +1 -0
  208. package/dist/store.js +3 -2
  209. package/dist/store.js.map +1 -1
  210. package/dist/system-prompt.d.ts +3 -1
  211. package/dist/system-prompt.js +138 -64
  212. package/dist/system-prompt.js.map +1 -1
  213. package/dist/test/generate-log-plugin/index.d.ts +3 -0
  214. package/dist/test/generate-log-plugin/index.js +15 -0
  215. package/dist/test/generate-log-plugin/index.js.map +1 -0
  216. package/dist/test/harness.d.ts +20 -1
  217. package/dist/test/harness.js +51 -10
  218. package/dist/test/harness.js.map +1 -1
  219. package/dist/test/replay.js +11 -7
  220. package/dist/test/replay.js.map +1 -1
  221. package/dist/thread-reducer.d.ts +29 -3
  222. package/dist/thread-reducer.fixtures.test.js +58 -51
  223. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  224. package/dist/thread-reducer.js +112 -25
  225. package/dist/thread-reducer.js.map +1 -1
  226. package/dist/thread-reducer.test.js +197 -24
  227. package/dist/thread-reducer.test.js.map +1 -1
  228. package/dist/voice-attachments.e2e.test.js +108 -18
  229. package/dist/voice-attachments.e2e.test.js.map +1 -1
  230. package/dist/voice-call.e2e.test.js +9 -9
  231. package/dist/voice-call.js +6 -6
  232. package/dist/voice-call.js.map +1 -1
  233. package/dist/voice-openai.e2e.test.js +59 -23
  234. package/dist/voice-openai.e2e.test.js.map +1 -1
  235. package/dist/voice.d.ts +1 -1
  236. package/dist/voice.js +72 -24
  237. package/dist/voice.js.map +1 -1
  238. package/dist/working-directory.e2e.test.js +11 -11
  239. package/dist/working-directory.e2e.test.js.map +1 -1
  240. package/package.json +12 -6
  241. package/src/agent-ui.e2e.test.ts +27 -13
  242. package/src/agent-ui.ts +2 -1
  243. package/src/analytics.e2e.test.ts +1 -1
  244. package/src/autostart.ts +55 -10
  245. package/src/bot.ts +29 -5
  246. package/src/btw.e2e.test.ts +3 -3
  247. package/src/builtin-skills.test.ts +3 -4
  248. package/src/builtin-skills.ts +1 -1
  249. package/src/catalog-refresh.e2e.test.ts +2 -2
  250. package/src/cli/bot.ts +23 -14
  251. package/src/cli/discord.ts +30 -6
  252. package/src/cli/schedule.ts +23 -9
  253. package/src/cli/session.ts +35 -12
  254. package/src/cli/shared.ts +43 -8
  255. package/src/cli-discord.e2e.test.ts +13 -8
  256. package/src/cli-lock.e2e.test.ts +88 -20
  257. package/src/cli-read.e2e.test.ts +17 -8
  258. package/src/cli-sessions.e2e.test.ts +4 -3
  259. package/src/cli.ts +2 -0
  260. package/src/commands/preference-commands.ts +38 -4
  261. package/src/commands/session-commands.ts +12 -7
  262. package/src/commands/thread-commands.ts +92 -24
  263. package/src/commands/worktree-commands.ts +10 -9
  264. package/src/commands.e2e.test.ts +25 -21
  265. package/src/coordinator-agents.e2e.test.ts +145 -0
  266. package/src/coordinator-idle.e2e.test.ts +122 -0
  267. package/src/coordinator-queue.e2e.test.ts +279 -0
  268. package/src/coordinator-warmup.e2e.test.ts +158 -0
  269. package/src/coordinator.e2e.test.ts +545 -0
  270. package/src/coordinator.ts +168 -0
  271. package/src/db.test.ts +1 -0
  272. package/src/diagram.test.ts +56 -0
  273. package/src/diagram.ts +243 -0
  274. package/src/duration.ts +13 -5
  275. package/src/effects.ts +99 -27
  276. package/src/event-loop.ts +71 -42
  277. package/src/format-parts.ts +53 -22
  278. package/src/gateway-replay.e2e.test.ts +4 -4
  279. package/src/harness.e2e.test.ts +2 -2
  280. package/src/hello-thread.e2e.test.ts +59 -5
  281. package/src/ingress.ts +44 -11
  282. package/src/interaction-context.ts +12 -11
  283. package/src/interrupt.e2e.test.ts +8 -5
  284. package/src/legacy.e2e.test.ts +1 -1
  285. package/src/lock-routes.ts +62 -8
  286. package/src/lock-server.ts +23 -7
  287. package/src/main.ts +22 -7
  288. package/src/markdown/markdown.test.ts +4 -4
  289. package/src/markdown/render-markdown.ts +6 -5
  290. package/src/onboarding-cli.e2e.test.ts +7 -6
  291. package/src/onboarding.e2e.test.ts +8 -10
  292. package/src/onboarding.ts +13 -29
  293. package/src/permissions.e2e.test.ts +3 -3
  294. package/src/plugin/coordinator.ts +401 -0
  295. package/src/plugin/index.ts +68 -15
  296. package/src/plugin.e2e.test.ts +1 -1
  297. package/src/preference-commands.e2e.test.ts +103 -23
  298. package/src/prompt.ts +87 -43
  299. package/src/questions.e2e.test.ts +13 -13
  300. package/src/queue-sources.e2e.test.ts +197 -0
  301. package/src/queue.e2e.test.ts +48 -30
  302. package/src/queue.test.ts +59 -0
  303. package/src/queue.ts +104 -24
  304. package/src/remote-send.e2e.test.ts +6 -6
  305. package/src/remote-send.ts +2 -1
  306. package/src/renderer.e2e.test.ts +7 -7
  307. package/src/restart.e2e.test.ts +2 -2
  308. package/src/routines.e2e.test.ts +2 -2
  309. package/src/scheduler.e2e.test.ts +45 -19
  310. package/src/scheduler.ts +4 -2
  311. package/src/schema-sql.ts +1 -1
  312. package/src/schema.ts +13 -0
  313. package/src/session-commands.e2e.test.ts +38 -11
  314. package/src/session-events.ts +160 -21
  315. package/src/session-ops.e2e.test.ts +64 -11
  316. package/src/sessions.ts +214 -64
  317. package/src/shell.e2e.test.ts +4 -4
  318. package/src/slash-commands.test.ts +2 -0
  319. package/src/slash-commands.ts +6 -4
  320. package/src/sleep.e2e.test.ts +30 -13
  321. package/src/sleeps.ts +20 -3
  322. package/src/store.ts +5 -2
  323. package/src/system-prompt.ts +143 -63
  324. package/src/test/generate-log-plugin/index.ts +16 -0
  325. package/src/test/harness.ts +77 -14
  326. package/src/test/replay.ts +12 -6
  327. package/src/thread-reducer.fixtures.test.ts +58 -51
  328. package/src/thread-reducer.test.ts +207 -26
  329. package/src/thread-reducer.ts +138 -37
  330. package/src/voice-attachments.e2e.test.ts +110 -18
  331. package/src/voice-call.e2e.test.ts +9 -9
  332. package/src/voice-call.ts +6 -6
  333. package/src/voice-openai.e2e.test.ts +64 -24
  334. package/src/voice.ts +93 -26
  335. package/src/working-directory.e2e.test.ts +11 -11
@@ -9,6 +9,7 @@ import { ComponentType } from 'discord-api-types/v10'
9
9
  import { afterAll, beforeAll, expect, onTestFinished, test } from 'vitest'
10
10
 
11
11
  import type { BotHandle } from './main.ts'
12
+ import { send } from './prompt.ts'
12
13
  import * as schema from './schema.ts'
13
14
  import {
14
15
  TEST_USER_ID,
@@ -19,6 +20,7 @@ import {
19
20
  startTwin,
20
21
  tempDataDir,
21
22
  waitFor,
23
+ waitForBotMessageContaining,
22
24
  waitForFooter,
23
25
  waitForSelectMenu,
24
26
  warmUp,
@@ -144,14 +146,15 @@ test('/agent switches the session agent in a thread and the default in a channel
144
146
  --- from: assistant (TestBot)
145
147
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
146
148
  ok
147
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
149
+ -# *main ⋅ Ns ⋅ deterministic-v2*
148
150
  Switched to **plan** agent for this session
149
151
  The agent changes from the next step.
152
+ -# Switched agent to Plan
150
153
  --- from: user (tommy)
151
154
  Next message
152
155
  --- from: assistant (TestBot)
153
156
  ok
154
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2 ⋅ plan*"
157
+ -# *main ⋅ Ns ⋅ deterministic-v2 ⋅ plan*"
155
158
  `)
156
159
  expect((await sessionModel(thread.id)).agent).toBe('plan')
157
160
 
@@ -228,26 +231,29 @@ test('/model switches the session model from the next step, /model-variant its t
228
231
  "variant": "low",
229
232
  }
230
233
  `)
234
+ await waitForBotMessageContaining({ discord, threadId: thread.id, text: 'Switched model to Thinker (low)' })
231
235
  expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
232
236
  "--- from: user (tommy)
233
237
  Model thread
234
238
  --- from: assistant (TestBot)
235
239
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
236
240
  ok
237
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
241
+ -# *main ⋅ Ns ⋅ deterministic-v2*
238
242
  Model set for this session:
239
243
  **Deterministic Provider** / **deterministic-thinker** (high)
240
244
  \`deterministic-provider/deterministic-thinker (high)\`
241
245
  Applies from the next step.
246
+ -# Switched model to Thinker (high)
242
247
  --- from: user (tommy)
243
248
  With the new model
244
249
  --- from: assistant (TestBot)
245
250
  ok
246
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker*
251
+ -# *main ⋅ Ns ⋅ deterministic-thinker*
247
252
  Model set for this session:
248
253
  **Deterministic Provider** / **deterministic-thinker** (low)
249
254
  \`deterministic-provider/deterministic-thinker (low)\`
250
- Applies from the next step."
255
+ Applies from the next step.
256
+ -# Switched model to Thinker (low)"
251
257
  `)
252
258
 
253
259
  await user.runSlashCommand({ name: 'model' })
@@ -256,7 +262,8 @@ test('/model switches the session model from the next step, /model-variant its t
256
262
  "Reset session model",
257
263
  ]
258
264
  `)
259
- expect((await discord.thread(thread.id).getMessages()).at(-1)?.content).toMatchInlineSnapshot(`
265
+ await waitForBotMessageContaining({ discord, threadId: thread.id, text: 'Switched model to deterministic-v2' })
266
+ expect((await discord.thread(thread.id).getMessages()).at(-2)?.content).toMatchInlineSnapshot(`
260
267
  "Session model reset to the channel default:
261
268
  \`deterministic-provider/deterministic-v2\`
262
269
  Applies from the next step."
@@ -294,7 +301,7 @@ test('/model with channel scope sets the model of new sessions', async () => {
294
301
  --- from: assistant (TestBot)
295
302
  -# *using deterministic-provider/deterministic-thinker ⋅ plan*
296
303
  ok
297
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker ⋅ plan*"
304
+ -# *main ⋅ Ns ⋅ deterministic-thinker*"
298
305
  `)
299
306
  const row = await bot.db.query.channel_models.findFirst({ where: { channel_id: channelId } })
300
307
  expect({ model: row?.model_id, variant: row?.variant }).toMatchInlineSnapshot(`
@@ -348,12 +355,12 @@ test('/verbosity applies to running sessions of the channel at once', async () =
348
355
  -# ┣ shell _Say hi_
349
356
 
350
357
  tools done
351
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
358
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
352
359
  --- from: user (tommy)
353
360
  Again tools-marker
354
361
  --- from: assistant (TestBot)
355
362
  tools done
356
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
363
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
357
364
  `)
358
365
  expect(await discord.channel(channelId).text()).toMatchInlineSnapshot(`
359
366
  "--- from: user (tommy)
@@ -411,10 +418,12 @@ test('/<agent>-agent variant: sets the thinking level with the agent, per sessio
411
418
  --- from: assistant (TestBot)
412
419
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
413
420
  ok
414
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
415
- » **tommy:** (plan, deep) Think hard
421
+ -# *main ⋅ Ns ⋅ deterministic-v2*
422
+ -# » **tommy:** (plan, deep) Think hard
423
+ -# Switched model to deterministic-v2 (deep)
424
+ -# Switched agent to Plan
416
425
  ok
417
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2 ⋅ plan*
426
+ -# *main ⋅ Ns ⋅ deterministic-v2 ⋅ plan*
418
427
  \`deterministic-provider/deterministic-v2\` has no thinking level \`nope\`. Available: \`fast\`, \`deep\`"
419
428
  `)
420
429
 
@@ -454,16 +463,20 @@ test('agent selection replaces a stale model and uses the agent model for varian
454
463
  await client.session.switchModel({ sessionID, model: { providerID: 'missing', id: 'stale-model' } })
455
464
  await user.runSlashCommand({ name: 'agent' })
456
465
  await pick({ channelId: thread.id, prefix: 'agent:', value: 'thinker' })
466
+ await waitForBotMessageContaining({ discord, threadId: thread.id, text: 'Switched agent from Build to Thinker' })
457
467
  expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
458
468
  "--- from: user (tommy)
459
469
  Agent model thread
460
470
  --- from: assistant (TestBot)
461
471
  -# *using deterministic-provider/deterministic-v2 fast ⋅ build*
462
472
  ok
463
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
473
+ -# *main ⋅ Ns ⋅ deterministic-v2*
474
+ -# Switched model to missing/stale-model
464
475
  Switched to **thinker** agent for this session (was **build**)
465
476
  Model: \`deterministic-provider/deterministic-thinker (high)\`
466
- The agent changes from the next step."
477
+ The agent changes from the next step.
478
+ -# Switched model to Thinker (high)
479
+ -# Switched agent from Build to Thinker"
467
480
  `)
468
481
  expect((await sessionModel(thread.id)).model).toEqual({ providerID: 'deterministic-provider', id: 'deterministic-thinker', variant: 'high' })
469
482
 
@@ -482,15 +495,21 @@ test('agent selection replaces a stale model and uses the agent model for varian
482
495
  --- from: assistant (TestBot)
483
496
  -# *using deterministic-provider/deterministic-v2 fast ⋅ build*
484
497
  ok
485
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
498
+ -# *main ⋅ Ns ⋅ deterministic-v2*
499
+ -# Switched model to missing/stale-model
486
500
  Switched to **thinker** agent for this session (was **build**)
487
501
  Model: \`deterministic-provider/deterministic-thinker (high)\`
488
502
  The agent changes from the next step.
503
+ -# Switched model to Thinker (high)
504
+ -# Switched agent from Build to Thinker
489
505
  Switched to **build** agent for this session (was **thinker**)
490
506
  The agent changes from the next step.
491
- » **tommy:** (thinker, low) Use the agent model
507
+ -# Switched agent from Thinker to Build
508
+ -# » **tommy:** (thinker, low) Use the agent model
509
+ -# Switched model to Thinker (low)
510
+ -# Switched agent from Build to Thinker
492
511
  ok
493
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*"
512
+ -# *main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*"
494
513
  `)
495
514
  expect((await sessionModel(thread.id)).model).toEqual({ providerID: 'deterministic-provider', id: 'deterministic-thinker', variant: 'low' })
496
515
 
@@ -502,15 +521,22 @@ test('agent selection replaces a stale model and uses the agent model for varian
502
521
  --- from: assistant (TestBot)
503
522
  -# *using deterministic-provider/deterministic-v2 fast ⋅ build*
504
523
  ok
505
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-v2*
524
+ -# *main ⋅ Ns ⋅ deterministic-v2*
525
+ -# Switched model to missing/stale-model
506
526
  Switched to **thinker** agent for this session (was **build**)
507
527
  Model: \`deterministic-provider/deterministic-thinker (high)\`
508
528
  The agent changes from the next step.
529
+ -# Switched model to Thinker (high)
530
+ -# Switched agent from Build to Thinker
509
531
  Switched to **build** agent for this session (was **thinker**)
510
532
  The agent changes from the next step.
511
- » **tommy:** (thinker, low) Use the agent model
533
+ -# Switched agent from Thinker to Build
534
+ -# » **tommy:** (thinker, low) Use the agent model
535
+ -# Switched model to Thinker (low)
536
+ -# Switched agent from Build to Thinker
512
537
  ok
513
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*
538
+ -# *main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*
539
+ -# Switched model to Thinker (high)
514
540
  Using **thinker** agent for this session
515
541
  Model: \`deterministic-provider/deterministic-thinker (high)\`
516
542
  The agent changes from the next step."
@@ -525,7 +551,7 @@ test('agent selection replaces a stale model and uses the agent model for varian
525
551
  --- from: assistant (TestBot)
526
552
  -# *using deterministic-provider/deterministic-thinker high ⋅ thinker*
527
553
  ok
528
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*"
554
+ -# *main ⋅ Ns ⋅ deterministic-thinker*"
529
555
  `)
530
556
  expect((await sessionModel(fresh.id)).model).toEqual({ providerID: 'deterministic-provider', id: 'deterministic-thinker', variant: 'high' })
531
557
 
@@ -538,7 +564,7 @@ test('agent selection replaces a stale model and uses the agent model for varian
538
564
  --- from: assistant (TestBot)
539
565
  -# *using deterministic-provider/deterministic-thinker low ⋅ thinker*
540
566
  ok
541
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*"
567
+ -# *main ⋅ Ns ⋅ deterministic-thinker*"
542
568
  `)
543
569
  expect((await sessionModel(low.id)).model).toEqual({ providerID: 'deterministic-provider', id: 'deterministic-thinker', variant: 'low' })
544
570
  })
@@ -576,7 +602,7 @@ test('/model with all channels scope sets the model of channels without their ow
576
602
  --- from: assistant (TestBot)
577
603
  -# *using deterministic-provider/deterministic-thinker low ⋅ build*
578
604
  ok
579
- -# *project ⋅ main ⋅ Ns ⋅ deterministic-thinker*"
605
+ -# *main ⋅ Ns ⋅ deterministic-thinker*"
580
606
  `)
581
607
  expect((await sessionModel(thread.id)).model).toMatchInlineSnapshot(`
582
608
  {
@@ -595,3 +621,57 @@ test('/model with all channels scope sets the model of channels without their ow
595
621
  Select a provider:"
596
622
  `)
597
623
  })
624
+
625
+ // https://github.com/remorses/kimaki/issues/245: agents pass --agent <current_agent> on every send.
626
+ test('naming the current agent again keeps the session model, and --model wins over the agent model', async () => {
627
+ const { discord, channelId } = twin
628
+ const thread = await newThread(channelId, 'Model kept thread')
629
+ const sessionId = bot.store.getState().roots[thread.id]!
630
+ const sendAndWait = async (input: { prompt: string; agent: string; model?: string }, count: number) => {
631
+ const sent = await send(bot, { sessionId, ...input })
632
+ if (sent instanceof Error) throw sent
633
+ await waitForFooter({ discord, threadId: thread.id, count })
634
+ return sessionModel(thread.id)
635
+ }
636
+ const explicit = await sendAndWait({ prompt: 'Explicit model', agent: 'thinker', model: 'deterministic-provider/deterministic-v2' }, 2)
637
+ // Like /model in the thread.
638
+ await (await server.client()).session.switchModel({ sessionID: sessionId, model: { providerID: 'deterministic-provider', id: 'deterministic-thinker', variant: 'low' } })
639
+ const kept = await sendAndWait({ prompt: 'Same agent again', agent: 'thinker' }, 3)
640
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
641
+ "--- from: user (tommy)
642
+ Model kept thread
643
+ --- from: assistant (TestBot)
644
+ -# *using deterministic-provider/deterministic-v2 ⋅ build*
645
+ ok
646
+ -# *main ⋅ Ns ⋅ deterministic-v2*
647
+ -# Switched model to deterministic-v2
648
+ -# Switched agent to Thinker
649
+ -# » **CLI:** Explicit model
650
+ ok
651
+ -# *main ⋅ Ns ⋅ deterministic-v2 ⋅ thinker*
652
+ -# Switched model to Thinker (low)
653
+ -# » **CLI:** Same agent again
654
+ ok
655
+ -# *main ⋅ Ns ⋅ deterministic-thinker ⋅ thinker*"
656
+ `)
657
+ expect({ explicit, kept }).toMatchInlineSnapshot(`
658
+ {
659
+ "explicit": {
660
+ "agent": "thinker",
661
+ "model": {
662
+ "id": "deterministic-v2",
663
+ "providerID": "deterministic-provider",
664
+ "variant": "default",
665
+ },
666
+ },
667
+ "kept": {
668
+ "agent": "thinker",
669
+ "model": {
670
+ "id": "deterministic-thinker",
671
+ "providerID": "deterministic-provider",
672
+ "variant": "low",
673
+ },
674
+ },
675
+ }
676
+ `)
677
+ })
package/src/prompt.ts CHANGED
@@ -15,13 +15,14 @@
15
15
  import fs from 'node:fs'
16
16
  import path from 'node:path'
17
17
  import crypto from 'node:crypto'
18
- import { Events, MessageFlags, type Message, type ThreadChannel } from 'discord.js'
18
+ import { Events, MessageFlags, type Message } from 'discord.js'
19
19
  import * as errore from 'errore'
20
20
 
21
21
  import {
22
- fetchThread,
22
+ fetchConversation,
23
23
  oc,
24
24
  parseModel,
25
+ projectChannelOf,
25
26
  projectOf,
26
27
  rootSession,
27
28
  routeText,
@@ -29,6 +30,7 @@ import {
29
30
  threadOfSession,
30
31
  type Author,
31
32
  type Bot,
33
+ type Conversation,
32
34
  type ModelChoice,
33
35
  type PromptFile,
34
36
  } from './bot.ts'
@@ -37,11 +39,11 @@ import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableE
37
39
  import { formatError, NOTIFY_MESSAGE_FLAGS } from './format-parts.ts'
38
40
  import { createLogger } from './logger.ts'
39
41
  import type { SendInput } from './lock-routes.ts'
40
- import { formatEcho } from './queue.ts'
42
+ import { discordMessageId, formatEcho } from './queue.ts'
41
43
  import { noAnswerError, parseRemoteResult, remoteEnvelope, promptFilePath, REMOTE_TIMEOUT_MS, type RemoteResult } from './remote-send.ts'
42
44
  import { parseTextMessage, type Route } from './routes.ts'
43
45
  import { allMessages } from './session-events.ts'
44
- import { ensureSessionMarker, forkBtw, startSession, switchAgent, threadProject } from './sessions.ts'
46
+ import { ensureSessionMarker, forkBtw, startSession, switchAgent, conversationProject } from './sessions.ts'
45
47
  import { cancelSleep } from './sleeps.ts'
46
48
  import type { ScheduledRun } from './system-prompt.ts'
47
49
  import { attachmentsBlock, type PromptAttachment, type TurnContext } from './system-prompt.ts'
@@ -51,6 +53,16 @@ import { resolveWorkingDirectory } from './worktrees.ts'
51
53
  const logger = createLogger('PROMPT')
52
54
 
53
55
 
56
+ // Who sent a prompt that no person typed in Discord: `kimaki send`, routines,
57
+ // remote sends, sleep wakes. Stored in the prompt metadata; at the run end
58
+ // coordinator.ts reads it: only a run a coordinator prompted wakes that coordinator.
59
+ export type Sender = {
60
+ // The session whose shell ran `kimaki send` (OPENCODE_SESSION_ID).
61
+ sessionId?: string
62
+ // `kimaki send --wait`: until this time (epoch ms) the sender waits for the result itself, so a run that ends before it sends no note.
63
+ waitUntil?: number
64
+ }
65
+
54
66
  // Prompt IDs map a Discord message to its inbox item without stored state (spec 9.2.2).
55
67
  export function promptIdForMessage(messageId: string): string {
56
68
  return `msg_discord_${messageId}`
@@ -70,6 +82,7 @@ export async function prompt(
70
82
  skills = [],
71
83
  id = promptIdForMessage(messageId),
72
84
  echo,
85
+ sender,
73
86
  }: {
74
87
  sessionId: string
75
88
  threadId: string
@@ -86,6 +99,8 @@ export async function prompt(
86
99
  // Line the thread shows when OpenCode takes the prompt (queue.ts), for
87
100
  // inputs that have no Discord message: wakes, scheduled runs.
88
101
  echo?: string
102
+ // Unset: a person typed it in Discord.
103
+ sender?: Sender
89
104
  },
90
105
  ): Promise<OpenCodeUnavailableError | OpenCodeError | void> {
91
106
  const { attached, attachments } = await splitPromptFiles(files)
@@ -100,7 +115,7 @@ export async function prompt(
100
115
  files: attached,
101
116
  ...(skills.length > 0 && { skills: skills.map((skill) => ({ id: skill })) }),
102
117
  delivery,
103
- metadata: { discord: { ...discord, ...(echo && { echo }) } },
118
+ metadata: { discord: { ...discord, ...(echo && { echo }) }, ...(sender && { sender }) },
104
119
  }),
105
120
  )
106
121
  if (result instanceof Error) return result
@@ -163,6 +178,7 @@ export async function steer(
163
178
  files?: readonly PromptFile[]
164
179
  skills?: readonly string[]
165
180
  echo?: string
181
+ sender?: Sender
166
182
  },
167
183
  ): Promise<OpenCodeUnavailableError | OpenCodeError | void> {
168
184
  const cancelled = await cancelPendingUi(bot, input.threadId)
@@ -283,6 +299,7 @@ async function command(
283
299
  author: Author
284
300
  messageId: string
285
301
  files: readonly PromptFile[]
302
+ echo?: string
286
303
  },
287
304
  ): Promise<OpenCodeUnavailableError | OpenCodeError | void> {
288
305
  const { route } = input
@@ -301,7 +318,7 @@ async function command(
301
318
  }
302
319
  const { attached, attachments } = await splitPromptFiles(input.files)
303
320
  const block = attachmentsBlock(attachments)
304
- bot.eventLoop.dispatch(input.threadId, { type: 'kimaki.command', text: typed, username: input.author.username })
321
+ bot.eventLoop.dispatch(input.threadId, { type: 'kimaki.command', text: typed, username: input.author.username, messageId: discordMessageId(input.messageId), echo: input.echo ?? null })
305
322
  const result = await oc(bot, 'session.command', (client) =>
306
323
  client.session.command({
307
324
  sessionID: input.sessionId,
@@ -330,6 +347,7 @@ export async function runInSession(
330
347
  messageId,
331
348
  files,
332
349
  echo,
350
+ sender,
333
351
  model,
334
352
  }: {
335
353
  sessionId: string
@@ -341,10 +359,11 @@ export async function runInSession(
341
359
  messageId: string
342
360
  files: readonly PromptFile[]
343
361
  echo?: string
362
+ sender?: Sender
344
363
  model?: ModelChoice | null
345
364
  },
346
365
  ) {
347
- const base = { sessionId, threadId, threadName, author, messageId, files, echo }
366
+ const base = { sessionId, threadId, threadName, author, messageId, files, echo, sender }
348
367
  switch (route.kind) {
349
368
  case 'shell':
350
369
  return shell(bot, { threadId, sessionId, command: route.command })
@@ -371,32 +390,37 @@ export async function runInSession(
371
390
  export async function dispatch(
372
391
  bot: Bot,
373
392
  {
374
- thread,
393
+ conversation,
375
394
  route,
376
395
  author,
377
396
  messageId,
378
397
  files = [],
379
398
  echo,
399
+ sender,
380
400
  model,
401
+ parentSessionId,
381
402
  }: {
382
- thread: ThreadChannel
403
+ conversation: Conversation
383
404
  route: Route
384
405
  author: Author
385
406
  messageId: string
386
407
  files?: readonly PromptFile[]
387
408
  echo?: string
409
+ sender?: Sender
388
410
  model?: ModelChoice | null
411
+ // `kimaki send --parent-session`: only a btw fork records it, the session itself exists.
412
+ parentSessionId?: string
389
413
  },
390
414
  ) {
391
- const sessionId = rootSession(bot, thread.id)
415
+ const sessionId = rootSession(bot, conversation.id)
392
416
  if (sessionId instanceof Error) return sessionId
393
417
  // New input supersedes a pending `kimaki sleep` of this session.
394
418
  await cancelSleep(bot, sessionId)
395
- const project = await threadProject(bot, thread)
419
+ const project = await conversationProject(bot, conversation)
396
420
  if (project instanceof Error) return project
397
421
  const marked = await ensureSessionMarker(bot, {
398
422
  sessionId,
399
- thread,
423
+ conversation,
400
424
  channelId: project.channelId,
401
425
  directory: project.directory,
402
426
  userId: author.id,
@@ -404,9 +428,9 @@ export async function dispatch(
404
428
  if (marked instanceof Error) return marked
405
429
  switch (route.kind) {
406
430
  case 'btw':
407
- return forkBtw(bot, { sourceThread: thread, text: route.text, author, messageId, files, agent: route.agent })
431
+ return forkBtw(bot, { source: conversation, text: route.text, author, messageId, files, agent: route.agent, parentSessionId, sender })
408
432
  case 'new-session':
409
- // A fresh thread in the same channel, with no history.
433
+ // A fresh conversation in the same channel, with no history.
410
434
  return startSession(bot, {
411
435
  channelId: project.channelId,
412
436
  directory: project.directory,
@@ -415,20 +439,26 @@ export async function dispatch(
415
439
  messageId,
416
440
  startMessageId: null,
417
441
  files,
442
+ sender,
418
443
  })
419
- default:
444
+ default: {
445
+ // The coordinator keeps its agent and model: a worker's `kimaki send --agent <current_agent>` names the worker's agent.
446
+ const coordinator = !conversation.isThread()
447
+ if (coordinator && route.kind === 'steer' && route.agent) logger.log(`ignoring agent ${route.agent} of a prompt to coordinator ${conversation.id}`)
420
448
  return runInSession(bot, {
421
449
  sessionId,
422
- threadId: thread.id,
423
- threadName: thread.name,
450
+ threadId: conversation.id,
451
+ threadName: conversation.name,
424
452
  directory: project.directory,
425
- route,
453
+ route: coordinator && route.kind === 'steer' ? { kind: 'steer', text: route.text } : route,
426
454
  author,
427
455
  messageId,
428
456
  files,
429
457
  echo,
430
- model,
458
+ sender,
459
+ model: coordinator ? null : model,
431
460
  })
461
+ }
432
462
  }
433
463
  }
434
464
 
@@ -528,7 +558,13 @@ async function remoteSend(bot: Bot, input: SendInput) {
528
558
 
529
559
  // `localOnly`: never forward to another machine (remote envelopes, scheduled runs).
530
560
  // `task`: a scheduled run; its input shows as "» routine #N: prompt".
531
- export async function send(bot: Bot, input: SendInput, { localOnly = false, task }: { localOnly?: boolean; task?: ScheduledRun } = {}) {
561
+ // `shownIn`: a Discord message that already shows the prompt (remote envelope, clicked buttons).
562
+ // Without it the thread shows the prompt as an echo.
563
+ export async function send(
564
+ bot: Bot,
565
+ input: SendInput,
566
+ { localOnly = false, task, shownIn }: { localOnly?: boolean; task?: ScheduledRun; shownIn?: string } = {},
567
+ ) {
532
568
  if (input.worktree !== undefined && (input.cwd || input.threadId || input.sessionId || input.notifyOnly)) {
533
569
  return new ConfigError({ reason: '--worktree requires a new session without --cwd or --notify-only.' })
534
570
  }
@@ -538,11 +574,16 @@ export async function send(bot: Bot, input: SendInput, { localOnly = false, task
538
574
  }
539
575
  const userIds = (input.user ?? []).map((user) => user.replace(/[<@!>]/g, ''))
540
576
  const author = { id: userIds[0] ?? bot.discord.user!.id, username: task ? `routine #${task.id}` : 'CLI' }
541
- // Every --user joins the thread; startSession adds the author of a new session itself.
542
- const addMembers = async (thread: ThreadChannel, ids: readonly string[]) => {
543
- for (const id of ids) await thread.members.add(id).catch((error: Error) => logger.warn(`add thread member ${id}`, error))
577
+ // Every --user joins the thread; startSession adds them to a new session itself.
578
+ // Not awaited: the prompt does not wait for Discord round trips.
579
+ const addMembers = (thread: Conversation, ids: readonly string[]) => {
580
+ // A coordinator channel has no thread members to add.
581
+ if (!thread.isThread()) return
582
+ for (const id of ids) void thread.members.add(id).catch((error: Error) => logger.warn(`add thread member ${id}`, error))
544
583
  }
545
- const messageId = crypto.randomUUID()
584
+ const messageId = shownIn ?? crypto.randomUUID()
585
+ // Also set without a session (routines, a terminal): it marks the prompt as not typed by a person.
586
+ const sender: Sender = { ...(input.senderSessionId && { sessionId: input.senderSessionId }), ...(input.wait !== undefined && { waitUntil: Date.now() + input.wait * 1000 }) }
546
587
  const route = parseTextMessage({ content: input.prompt })
547
588
  if (!route) return new ConfigError({ reason: 'Prompt is empty' })
548
589
  if (input.agent && (route.kind === 'steer' || route.kind === 'btw')) route.agent = input.agent
@@ -550,23 +591,28 @@ export async function send(bot: Bot, input: SendInput, { localOnly = false, task
550
591
  if (input.sessionId || input.threadId) {
551
592
  const threadId = input.threadId ?? threadOfSession(bot, input.sessionId!)
552
593
  if (!threadId) return new ConfigError({ reason: 'No local thread for this session. Use --thread on its owning machine.' })
553
- const thread = await fetchThread(bot, threadId)
594
+ const thread = await fetchConversation(bot, threadId)
554
595
  if (thread instanceof Error) return thread
555
596
  const sessionId = bot.store.getState().roots[thread.id]
556
597
  if (!sessionId) {
557
- const project = thread.parentId ? await projectOf(bot, thread.parentId) : null
598
+ const channelId = projectChannelOf(thread)
599
+ const project = channelId ? await projectOf(bot, channelId) : null
558
600
  if (project instanceof Error) return project
559
601
  return !project && !localOnly ? remoteSend(bot, input) : new ConfigError({ reason: 'No local session for this thread' })
560
602
  }
561
- await addMembers(thread, userIds)
562
- if (input.model) {
563
- const model = parseModel(input.model, null)
564
- if (!model) return new ConfigError({ reason: 'Use --model provider/model' })
565
- const switched = await switchModel(bot, { sessionId, model: { ...model, variant: null } })
603
+ addMembers(thread, userIds)
604
+ const explicit = input.model ? parseModel(input.model, null) : null
605
+ if (input.model && !explicit) return new ConfigError({ reason: 'Use --model provider/model' })
606
+ // The coordinator keeps its model; /model in its channel changes it.
607
+ const model = explicit && thread.isThread() ? { ...explicit, variant: null } : null
608
+ if (explicit && !model) logger.log(`ignoring --model ${input.model} of a prompt to coordinator ${thread.id}`)
609
+ if (model) {
610
+ const switched = await switchModel(bot, { sessionId, model })
566
611
  if (switched instanceof Error) return switched
567
612
  }
568
- const echo = task ? formatEcho({ username: author.username, text: routeText(route) }) : undefined
569
- const result = await dispatch(bot, { thread, route, author, messageId, files: input.files, echo })
613
+ const echo = shownIn ? undefined : formatEcho({ username: author.username, text: routeText(route) })
614
+ // --model reaches the agent switch too, so an agent with its own model cannot replace it.
615
+ const result = await dispatch(bot, { conversation: thread, route, author, messageId, files: input.files, echo, sender, parentSessionId: input.parentSessionId, model })
570
616
  if (result instanceof Error) return result
571
617
  return result ?? { threadId, sessionId: bot.store.getState().roots[threadId]! }
572
618
  }
@@ -593,7 +639,7 @@ export async function send(bot: Bot, input: SendInput, { localOnly = false, task
593
639
  })
594
640
  .catch((cause) => new DiscordError({ operation: 'post notification', cause }))
595
641
  if (shown instanceof Error) return shown
596
- await addMembers(thread, userIds)
642
+ addMembers(thread, userIds)
597
643
  return { threadId: thread.id, sessionId: null }
598
644
  }
599
645
 
@@ -618,27 +664,24 @@ export async function send(bot: Bot, input: SendInput, { localOnly = false, task
618
664
  : task && input.worktree
619
665
  ? `${input.worktree.slice(0, 40)}-${crypto.randomBytes(4).toString('hex')}`
620
666
  : input.worktree
621
- const started = await startSession(bot, {
667
+ return startSession(bot, {
622
668
  channelId: project.channel_id,
623
669
  directory,
624
670
  route: first,
625
671
  author,
672
+ members: userIds.slice(1),
626
673
  messageId,
627
674
  startMessageId: null,
628
675
  threadName: input.name,
629
676
  files: input.files,
630
677
  permissions,
631
678
  parentSessionId: input.parentSessionId,
679
+ sender,
632
680
  task,
633
681
  worktree,
634
682
  baseBranch: input.baseBranch,
635
683
  ...(model && { model: { ...model, variant: null } }),
636
684
  })
637
- if (started instanceof Error || userIds.length < 2) return started
638
- const thread = await fetchThread(bot, started.threadId)
639
- if (thread instanceof Error) logger.warn(`add thread members`, thread)
640
- else await addMembers(thread, userIds.slice(1))
641
- return started
642
685
  }
643
686
 
644
687
  // `kimaki upload-to-discord`: posts local files into the session's thread (or the voice channel chat),
@@ -658,7 +701,8 @@ export async function upload(bot: Bot, { id, files }: { id: string; files: Array
658
701
  if (!stat?.isFile()) return new ConfigError({ reason: `File not found: ${file.path}` })
659
702
  sized.push({ ...file, size: stat.size })
660
703
  }
661
- const error = await new Promise<Error | null>((settle) => bot.effects.run(threadId, [{ type: 'attachments', files: sized, settle }]))
662
- if (error) return error
663
- return { uploaded: files.map((file) => file.name) }
704
+ const urls = await new Promise<Error | string[]>((settle) => bot.effects.run(threadId, [{ type: 'attachments', files: sized, settle }]))
705
+ if (urls instanceof Error) return urls
706
+ // Discord keeps attachments in upload order. The URLs let a coordinator repost the files.
707
+ return { uploaded: files.map((file, index) => ({ name: file.name, url: urls[index] ?? null })) }
664
708
  }