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
@@ -21,7 +21,7 @@ import {
21
21
  test,
22
22
  expect,
23
23
  } from 'vitest'
24
- import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder } from 'discord.js'
24
+ import { ChannelType, Client, GatewayIntentBits, Partials, REST, Routes } from 'discord.js'
25
25
  import { DigitalDiscord } from 'discord-digital-twin/src'
26
26
  import {
27
27
  buildDeterministicOpencodeConfig,
@@ -42,6 +42,7 @@ import {
42
42
  getSessionModel,
43
43
  getSessionAgent,
44
44
  getChannelAgent,
45
+ getChannelModel,
45
46
  setSessionModel,
46
47
  type VerbosityLevel,
47
48
  } from './database.js'
@@ -54,10 +55,12 @@ import {
54
55
  chooseLockPort,
55
56
  cleanupTestSessions,
56
57
  initTestGitRepo,
58
+ isFooterMessage,
57
59
  waitForBotMessageContaining,
58
60
  waitForFooterMessage,
59
61
  } from './test-utils.js'
60
62
  import { buildQuickAgentCommandDescription } from './commands/agent.js'
63
+ import { buildQuickAgentSlashCommand } from './discord-command-registration.js'
61
64
 
62
65
 
63
66
  const TEST_USER_ID = '200000000000000920'
@@ -339,6 +342,9 @@ describe('agent model resolution', () => {
339
342
  }
340
343
  providerConfig.models[AGENT_MODEL] = { name: AGENT_MODEL }
341
344
  providerConfig.models[PLAN_AGENT_MODEL] = { name: PLAN_AGENT_MODEL }
345
+ Object.assign(providerConfig.models[PLAN_AGENT_MODEL], {
346
+ variants: { high: {}, max: {} },
347
+ })
342
348
  providerConfig.models[CHANNEL_MODEL] = { name: CHANNEL_MODEL }
343
349
 
344
350
  fs.writeFileSync(
@@ -400,22 +406,13 @@ describe('agent model resolution', () => {
400
406
  // Register quick agent slash commands so /plan-agent and /test-agent-agent
401
407
  // are resolvable by handleQuickAgentCommand via guild.commands.fetch().
402
408
  const agentCommands = ['test-agent', 'plan', 'plain'].map((agentName) => {
403
- return new SlashCommandBuilder()
404
- .setName(`${agentName}-agent`)
405
- .setDescription(
406
- buildQuickAgentCommandDescription({
407
- agentName,
408
- description: `Switch to ${agentName} agent`,
409
- }),
410
- )
411
- .setDMPermission(false)
412
- .addStringOption((opt) =>
413
- opt
414
- .setName('prompt')
415
- .setDescription('Send a prompt with this agent')
416
- .setRequired(false),
417
- )
418
- .toJSON()
409
+ return buildQuickAgentSlashCommand({
410
+ commandName: `${agentName}-agent`,
411
+ description: buildQuickAgentCommandDescription({
412
+ agentName,
413
+ description: `Switch to ${agentName} agent`,
414
+ }),
415
+ }).toJSON()
419
416
  })
420
417
  const rest = new REST({ version: '10', api: discord.restUrl }).setToken(
421
418
  discord.botToken,
@@ -500,7 +497,7 @@ describe('agent model resolution', () => {
500
497
  const footerMessage = messages.find((message) => {
501
498
  return (
502
499
  message.author.id === discord.botUserId &&
503
- message.content.startsWith('*')
500
+ isFooterMessage({ message, botUserId: discord.botUserId })
504
501
  )
505
502
  })
506
503
 
@@ -508,9 +505,9 @@ describe('agent model resolution', () => {
508
505
  "--- from: user (agent-model-tester)
509
506
  Reply with exactly: agent-model-check
510
507
  --- from: assistant (TestBot)
511
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
512
- ⬥ ok
513
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
508
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
509
+ > ok
510
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
514
511
  `)
515
512
  expect(footerMessage).toBeDefined()
516
513
  if (!footerMessage) {
@@ -565,9 +562,9 @@ describe('agent model resolution', () => {
565
562
  "--- from: user (agent-model-tester)
566
563
  Reply with exactly: system-context-check
567
564
  --- from: assistant (TestBot)
568
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
569
- ⬥ system-context-ok
570
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
565
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
566
+ > system-context-ok
567
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
571
568
  `)
572
569
  },
573
570
  15_000,
@@ -659,7 +656,7 @@ describe('agent model resolution', () => {
659
656
  const threadText = await discord.thread(thread.id).text()
660
657
  expect(threadText).toContain('first message in thread')
661
658
  expect(threadText).toContain('Reply with exactly: reply-context-check')
662
- expect(threadText).toContain('⬥ ok')
659
+ expect(threadText).toContain('ok')
663
660
  },
664
661
  15_000,
665
662
  )
@@ -700,7 +697,7 @@ describe('agent model resolution', () => {
700
697
  const footerMessage = messages.find((message) => {
701
698
  return (
702
699
  message.author.id === discord.botUserId &&
703
- message.content.startsWith('*')
700
+ isFooterMessage({ message, botUserId: discord.botUserId })
704
701
  )
705
702
  })
706
703
 
@@ -708,9 +705,9 @@ describe('agent model resolution', () => {
708
705
  "--- from: user (agent-model-tester)
709
706
  Reply with exactly: channel-model-check
710
707
  --- from: assistant (TestBot)
711
- *using deterministic-provider/channel-model-v2*
712
- ⬥ ok
713
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
708
+ > *using deterministic-provider/channel-model-v2*
709
+ > ok
710
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
714
711
  `)
715
712
  expect(footerMessage).toBeDefined()
716
713
  if (!footerMessage) {
@@ -730,71 +727,44 @@ describe('agent model resolution', () => {
730
727
  )
731
728
 
732
729
  test(
733
- 'channel model with variant preference completes without error',
730
+ 'unsupported channel model variant falls back to the selected model',
734
731
  async () => {
735
- // Clear channel agent so model resolution falls through to channel model
736
732
  const db = await getDb()
737
733
  await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID))
738
-
739
- // Set channel model with a variant (thinking level)
740
- // The deterministic provider doesn't support thinking, so the variant
741
- // is resolved but silently dropped (no matching thinking values).
742
- // This test verifies the variant cascade code path runs without crashing
743
- // and the correct model still appears in the footer.
744
734
  await setChannelModel({
745
735
  channelId: TEXT_CHANNEL_ID,
746
736
  modelId: `${PROVIDER_NAME}/${CHANNEL_MODEL}`,
747
737
  variant: 'high',
748
738
  })
749
739
 
740
+ const prompt = 'Reply with exactly: variant-check'
750
741
  await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
751
- content: 'Reply with exactly: variant-check',
742
+ content: prompt,
752
743
  })
753
-
754
744
  const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
755
745
  timeout: 4_000,
756
- predicate: (t) => {
757
- return t.name === 'Reply with exactly: variant-check'
758
- },
746
+ predicate: (candidate) => candidate.name === prompt,
759
747
  })
760
-
761
- await waitForBotMessageContaining({
748
+ const messages = await waitForFooterMessage({
762
749
  discord,
763
750
  threadId: thread.id,
764
- userId: TEST_USER_ID,
765
- text: '*project',
766
751
  timeout: 4_000,
767
752
  })
768
-
769
- const messages = await discord.thread(thread.id).getMessages()
770
- const footerMessage = messages.find((message) => {
771
- return (
772
- message.author.id === discord.botUserId &&
773
- message.content.startsWith('*')
774
- )
753
+ const footer = messages.find((message) => {
754
+ return message.author.id === discord.botUserId
755
+ && isFooterMessage({ message, botUserId: discord.botUserId })
775
756
  })
776
757
 
777
758
  expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
778
759
  "--- from: user (agent-model-tester)
779
760
  Reply with exactly: variant-check
780
761
  --- from: assistant (TestBot)
781
- *using deterministic-provider/channel-model-v2*
782
- ⬥ ok
783
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
762
+ > *using deterministic-provider/channel-model-v2*
763
+ > ok
764
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>"
784
765
  `)
785
- expect(footerMessage).toBeDefined()
786
- if (!footerMessage) {
787
- throw new Error(
788
- `Expected footer message but none found. Bot messages: ${messages
789
- .filter((m) => m.author.id === discord.botUserId)
790
- .map((m) => m.content.slice(0, 150))
791
- .join(' | ')}`,
792
- )
793
- }
794
-
795
- // Footer should still contain the channel model (variant doesn't crash)
796
- expect(footerMessage.content).toContain(CHANNEL_MODEL)
797
- expect(footerMessage.content).not.toContain(DEFAULT_MODEL)
766
+ expect(footer?.content).toContain(CHANNEL_MODEL)
767
+ expect(footer?.content).not.toContain(DEFAULT_MODEL)
798
768
  },
799
769
  15_000,
800
770
  )
@@ -876,8 +846,8 @@ describe('agent model resolution', () => {
876
846
  "--- from: assistant (TestBot)
877
847
  Reusing context from <#SOURCE_THREAD> to answer prompt...
878
848
  Reply with exactly: btw-model-check
879
- ⬥ ok
880
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2*"
849
+ > ok
850
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2* <@200000000000000920>"
881
851
  `)
882
852
  expect(forkedSessionModel).toMatchInlineSnapshot(`
883
853
  {
@@ -920,7 +890,7 @@ describe('agent model resolution', () => {
920
890
  const firstMessages = await discord.thread(thread.id).getMessages()
921
891
  const firstFooter = firstMessages.find((m) => {
922
892
  return (
923
- m.author.id === discord.botUserId && m.content.startsWith('*')
893
+ isFooterMessage({ message: m, botUserId: discord.botUserId })
924
894
  )
925
895
  })
926
896
  expect(firstFooter).toBeDefined()
@@ -956,14 +926,14 @@ describe('agent model resolution', () => {
956
926
  "--- from: user (agent-model-tester)
957
927
  Reply with exactly: first-thread-msg
958
928
  --- from: assistant (TestBot)
959
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
960
- ⬥ ok
961
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
929
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
930
+ > ok
931
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
962
932
  --- from: user (agent-model-tester)
963
933
  Reply with exactly: second-thread-msg
964
934
  --- from: assistant (TestBot)
965
- ⬥ ok
966
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
935
+ > ok
936
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
967
937
  `)
968
938
 
969
939
  const secondMessages = await discord.thread(thread.id).getMessages()
@@ -971,7 +941,7 @@ describe('agent model resolution', () => {
971
941
  .reverse()
972
942
  .find((m) => {
973
943
  return (
974
- m.author.id === discord.botUserId && m.content.startsWith('*')
944
+ isFooterMessage({ message: m, botUserId: discord.botUserId })
975
945
  )
976
946
  })
977
947
  expect(secondFooter).toBeDefined()
@@ -985,27 +955,30 @@ describe('agent model resolution', () => {
985
955
  )
986
956
 
987
957
  test(
988
- 'thread created with no agent keeps default model after channel agent is set',
958
+ '/plan-agent with prompt starts a session with the plan agent',
989
959
  async () => {
990
- // Clear any channel agent — thread starts with default (no agent)
991
- const db = await getDb()
992
- await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID))
993
- // Also clear channel model so we get the pure default
994
- await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID))
960
+ await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent')
995
961
 
996
- // 1. Send a message to create a thread (no channel agent set)
997
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
998
- content: 'Reply with exactly: default-thread-msg',
999
- })
962
+ const prompt = 'Reply with exactly: inline-plan-agent-msg'
963
+ const { id: interactionId } = await discord
964
+ .channel(TEXT_CHANNEL_ID)
965
+ .user(TEST_USER_ID)
966
+ .runSlashCommand({
967
+ name: 'plan-agent',
968
+ options: [{ name: 'prompt', type: 3, value: prompt }],
969
+ })
970
+
971
+ await discord
972
+ .channel(TEXT_CHANNEL_ID)
973
+ .waitForInteractionAck({ interactionId, timeout: 4_000 })
1000
974
 
1001
975
  const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1002
976
  timeout: 4_000,
1003
977
  predicate: (t) => {
1004
- return t.name === 'Reply with exactly: default-thread-msg'
978
+ return t.name === prompt
1005
979
  },
1006
980
  })
1007
981
 
1008
- // Wait for footer — should show the default model
1009
982
  await waitForFooterMessage({
1010
983
  discord,
1011
984
  threadId: thread.id,
@@ -1014,119 +987,122 @@ describe('agent model resolution', () => {
1014
987
  afterAuthorId: discord.botUserId,
1015
988
  })
1016
989
 
1017
- const firstMessages = await discord.thread(thread.id).getMessages()
1018
- const firstFooter = firstMessages.find((m) => {
1019
- return (
1020
- m.author.id === discord.botUserId && m.content.startsWith('*')
1021
- )
1022
- })
1023
- expect(firstFooter).toBeDefined()
1024
- // First run uses the default model (no agent set)
1025
- expect(firstFooter!.content).toContain(DEFAULT_MODEL)
1026
- expect(firstFooter!.content).not.toContain(AGENT_MODEL)
990
+ const sessionId = await getThreadSession(thread.id)
991
+ expect(sessionId).toBeDefined()
992
+ expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan')
993
+ expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent')
994
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
995
+ "--- from: assistant (TestBot)
996
+ » **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
997
+ > *using deterministic-provider/plan-model-v2 ⋅ plan*
998
+ > ok
999
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1000
+ `)
1001
+ },
1002
+ 20_000,
1003
+ )
1027
1004
 
1028
- // 2. Set channel agent to test-agent via /test-agent-agent in the CHANNEL
1005
+ test(
1006
+ '/plan-agent with prompt persists a supported thinking variant',
1007
+ async () => {
1008
+ const prompt = 'Reply with exactly: inline-plan-agent-variant-msg'
1029
1009
  const { id: interactionId } = await discord
1030
1010
  .channel(TEXT_CHANNEL_ID)
1031
1011
  .user(TEST_USER_ID)
1032
- .runSlashCommand({ name: 'test-agent-agent' })
1012
+ .runSlashCommand({
1013
+ name: 'plan-agent',
1014
+ options: [
1015
+ { name: 'prompt', type: 3, value: prompt },
1016
+ { name: 'variant', type: 3, value: 'high' },
1017
+ ],
1018
+ })
1033
1019
 
1034
1020
  await discord
1035
1021
  .channel(TEXT_CHANNEL_ID)
1036
1022
  .waitForInteractionAck({ interactionId, timeout: 4_000 })
1037
1023
 
1038
- // 3. Send a second message in the EXISTING thread
1039
- await discord
1040
- .thread(thread.id)
1041
- .user(TEST_USER_ID)
1042
- .sendMessage({
1043
- content: 'Reply with exactly: default-second-msg',
1044
- })
1024
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1025
+ timeout: 4_000,
1026
+ predicate: (t) => t.name === prompt,
1027
+ })
1045
1028
 
1046
1029
  await waitForFooterMessage({
1047
1030
  discord,
1048
1031
  threadId: thread.id,
1049
1032
  timeout: 4_000,
1050
- afterMessageIncludes: 'default-second-msg',
1051
- afterAuthorId: TEST_USER_ID,
1033
+ afterMessageIncludes: 'ok',
1034
+ afterAuthorId: discord.botUserId,
1052
1035
  })
1053
1036
 
1037
+ const sessionId = await getThreadSession(thread.id)
1038
+ expect(sessionId).toBeDefined()
1054
1039
  expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
1055
- "--- from: user (agent-model-tester)
1056
- Reply with exactly: default-thread-msg
1057
- --- from: assistant (TestBot)
1058
- *using deterministic-provider/deterministic-v2*
1059
- ⬥ ok
1060
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1061
- --- from: user (agent-model-tester)
1062
- Reply with exactly: default-second-msg
1063
- --- from: assistant (TestBot)
1064
- ⬥ ok
1065
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1040
+ "--- from: assistant (TestBot)
1041
+ » **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
1042
+ > *using deterministic-provider/plan-model-v2 ⋅ plan*
1043
+ > ok
1044
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1045
+ `)
1046
+ expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
1047
+ {
1048
+ "modelId": "deterministic-provider/plan-model-v2",
1049
+ "variant": "high",
1050
+ }
1066
1051
  `)
1067
-
1068
- const secondMessages = await discord.thread(thread.id).getMessages()
1069
- const secondFooter = [...secondMessages]
1070
- .reverse()
1071
- .find((m) => {
1072
- return (
1073
- m.author.id === discord.botUserId && m.content.startsWith('*')
1074
- )
1075
- })
1076
- expect(secondFooter).toBeDefined()
1077
-
1078
- // The existing thread should still use the DEFAULT model,
1079
- // NOT the test-agent's model (AGENT_MODEL)
1080
- expect(secondFooter!.content).toContain(DEFAULT_MODEL)
1081
- expect(secondFooter!.content).not.toContain(AGENT_MODEL)
1082
1052
  },
1083
1053
  20_000,
1084
1054
  )
1085
1055
 
1086
1056
  test(
1087
- '/plan-agent with prompt starts a session with the plan agent',
1057
+ '/plan-agent variant persists on the channel without a prompt',
1088
1058
  async () => {
1089
- await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent')
1059
+ const db = await getDb()
1060
+ await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID))
1090
1061
 
1091
- const prompt = 'Reply with exactly: inline-plan-agent-msg'
1092
1062
  const { id: interactionId } = await discord
1093
1063
  .channel(TEXT_CHANNEL_ID)
1094
1064
  .user(TEST_USER_ID)
1095
1065
  .runSlashCommand({
1096
1066
  name: 'plan-agent',
1097
- options: [{ name: 'prompt', type: 3, value: prompt }],
1067
+ options: [{ name: 'variant', type: 3, value: 'max' }],
1098
1068
  })
1099
1069
 
1100
1070
  await discord
1101
1071
  .channel(TEXT_CHANNEL_ID)
1102
1072
  .waitForInteractionAck({ interactionId, timeout: 4_000 })
1103
1073
 
1104
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1105
- timeout: 4_000,
1106
- predicate: (t) => {
1107
- return t.name === prompt
1108
- },
1109
- })
1110
-
1111
- await waitForFooterMessage({
1112
- discord,
1113
- threadId: thread.id,
1114
- timeout: 4_000,
1115
- afterMessageIncludes: 'ok',
1116
- afterAuthorId: discord.botUserId,
1117
- })
1074
+ const start = Date.now()
1075
+ let confirmation = ''
1076
+ while (Date.now() - start < 4_000) {
1077
+ const messages = await discord.channel(TEXT_CHANNEL_ID).getMessages()
1078
+ const match = [...messages].reverse().find((message) => {
1079
+ return (
1080
+ message.author.id === discord.botUserId &&
1081
+ message.content.includes('Variant: **max**')
1082
+ )
1083
+ })
1084
+ if (match) {
1085
+ confirmation = match.content
1086
+ break
1087
+ }
1088
+ await new Promise((resolve) => setTimeout(resolve, 100))
1089
+ }
1118
1090
 
1119
- const sessionId = await getThreadSession(thread.id)
1120
- expect(sessionId).toBeDefined()
1121
- expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan')
1122
- expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent')
1123
- expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
1124
- "--- from: assistant (TestBot)
1125
- » **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
1126
- *using deterministic-provider/plan-model-v2 ⋅ plan*
1127
- ⬥ ok
1128
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
1091
+ expect(confirmation).toMatchInlineSnapshot(`
1092
+ "Switched to **plan** agent for this channel (was **test-agent**)
1093
+ Model: *deterministic-provider/plan-model-v2* (agent "plan")
1094
+ Variant: **max**
1095
+ All new sessions will use this agent."
1129
1096
  `)
1097
+ expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('plan')
1098
+ expect(await getChannelModel(TEXT_CHANNEL_ID)).toMatchInlineSnapshot(`
1099
+ {
1100
+ "modelId": "deterministic-provider/plan-model-v2",
1101
+ "variant": "max",
1102
+ }
1103
+ `)
1104
+
1105
+ await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID))
1130
1106
  },
1131
1107
  20_000,
1132
1108
  )
@@ -1180,12 +1156,12 @@ describe('agent model resolution', () => {
1180
1156
  "--- from: user (agent-model-tester)
1181
1157
  Reply with exactly: inline-existing-first-msg
1182
1158
  --- from: assistant (TestBot)
1183
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1184
- ⬥ ok
1185
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
1159
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1160
+ > ok
1161
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
1186
1162
  » **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
1187
- ⬥ ok
1188
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
1163
+ > ok
1164
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1189
1165
  `)
1190
1166
  },
1191
1167
  20_000,
@@ -1220,7 +1196,7 @@ describe('agent model resolution', () => {
1220
1196
  const firstFooter = (await discord.thread(thread.id).getMessages()).find(
1221
1197
  (m) => {
1222
1198
  return (
1223
- m.author.id === discord.botUserId && m.content.startsWith('*')
1199
+ isFooterMessage({ message: m, botUserId: discord.botUserId })
1224
1200
  )
1225
1201
  },
1226
1202
  )
@@ -1252,24 +1228,24 @@ describe('agent model resolution', () => {
1252
1228
  "--- from: user (agent-model-tester)
1253
1229
  Reply with exactly: switch-in-thread-msg
1254
1230
  --- from: assistant (TestBot)
1255
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1256
- ⬥ ok
1257
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
1231
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1232
+ > ok
1233
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
1258
1234
  Switched to **plan** agent for this session (was **test-agent**)
1259
1235
  Model: *deterministic-provider/plan-model-v2* (agent "plan")
1260
1236
  The agent will change on the next message.
1261
1237
  --- from: user (agent-model-tester)
1262
1238
  Reply with exactly: after-switch-msg
1263
1239
  --- from: assistant (TestBot)
1264
- ⬥ ok
1265
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
1240
+ > ok
1241
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1266
1242
  `)
1267
1243
 
1268
1244
  const secondFooter = [...(await discord.thread(thread.id).getMessages())]
1269
1245
  .reverse()
1270
1246
  .find((m) => {
1271
1247
  return (
1272
- m.author.id === discord.botUserId && m.content.startsWith('*')
1248
+ isFooterMessage({ message: m, botUserId: discord.botUserId })
1273
1249
  )
1274
1250
  })
1275
1251
  expect(secondFooter).toBeDefined()
@@ -1281,6 +1257,131 @@ describe('agent model resolution', () => {
1281
1257
  20_000,
1282
1258
  )
1283
1259
 
1260
+ test(
1261
+ '/plan-agent then an immediate follow-up uses the new agent',
1262
+ async () => {
1263
+ await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent')
1264
+
1265
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1266
+ content: 'Reply with exactly: race-switch-first-msg',
1267
+ })
1268
+
1269
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1270
+ timeout: 4_000,
1271
+ predicate: (t) => {
1272
+ return t.name === 'Reply with exactly: race-switch-first-msg'
1273
+ },
1274
+ })
1275
+
1276
+ await waitForFooterMessage({
1277
+ discord,
1278
+ threadId: thread.id,
1279
+ timeout: 4_000,
1280
+ afterMessageIncludes: 'ok',
1281
+ afterAuthorId: discord.botUserId,
1282
+ })
1283
+
1284
+ const th = discord.thread(thread.id)
1285
+ const { id: interactionId } = await th
1286
+ .user(TEST_USER_ID)
1287
+ .runSlashCommand({ name: 'plan-agent' })
1288
+ await th.user(TEST_USER_ID).sendMessage({
1289
+ content: 'Reply with exactly: race-switch-follow-up',
1290
+ })
1291
+
1292
+ await th.waitForInteractionAck({ interactionId, timeout: 4_000 })
1293
+ await waitForFooterMessage({
1294
+ discord,
1295
+ threadId: thread.id,
1296
+ timeout: 4_000,
1297
+ afterMessageIncludes: 'race-switch-follow-up',
1298
+ afterAuthorId: TEST_USER_ID,
1299
+ })
1300
+
1301
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
1302
+ "--- from: user (agent-model-tester)
1303
+ Reply with exactly: race-switch-first-msg
1304
+ --- from: assistant (TestBot)
1305
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1306
+ > ok
1307
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
1308
+ --- from: user (agent-model-tester)
1309
+ Reply with exactly: race-switch-follow-up
1310
+ --- from: assistant (TestBot)
1311
+ Switched to **plan** agent for this session (was **test-agent**)
1312
+ Model: *deterministic-provider/plan-model-v2* (agent "plan")
1313
+ The agent will change on the next message.
1314
+ > ok
1315
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1316
+ `)
1317
+
1318
+ const secondFooter = [...(await discord.thread(thread.id).getMessages())]
1319
+ .reverse()
1320
+ .find((m) => {
1321
+ return (
1322
+ isFooterMessage({ message: m, botUserId: discord.botUserId })
1323
+ )
1324
+ })
1325
+ expect(secondFooter).toBeDefined()
1326
+ expect(secondFooter!.content).toContain(PLAN_AGENT_MODEL)
1327
+ expect(secondFooter!.content).not.toContain(AGENT_MODEL)
1328
+ },
1329
+ 20_000,
1330
+ )
1331
+
1332
+ test(
1333
+ '/plan-agent in a channel then an immediate message uses the new agent',
1334
+ async () => {
1335
+ await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent')
1336
+
1337
+ const { id: interactionId } = await discord
1338
+ .channel(TEXT_CHANNEL_ID)
1339
+ .user(TEST_USER_ID)
1340
+ .runSlashCommand({ name: 'plan-agent' })
1341
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1342
+ content: 'Reply with exactly: race-channel-follow-up',
1343
+ })
1344
+
1345
+ await discord
1346
+ .channel(TEXT_CHANNEL_ID)
1347
+ .waitForInteractionAck({ interactionId, timeout: 4_000 })
1348
+
1349
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1350
+ timeout: 4_000,
1351
+ predicate: (t) => {
1352
+ return t.name === 'Reply with exactly: race-channel-follow-up'
1353
+ },
1354
+ })
1355
+
1356
+ await waitForFooterMessage({
1357
+ discord,
1358
+ threadId: thread.id,
1359
+ timeout: 4_000,
1360
+ afterMessageIncludes: 'ok',
1361
+ afterAuthorId: discord.botUserId,
1362
+ })
1363
+
1364
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
1365
+ "--- from: user (agent-model-tester)
1366
+ Reply with exactly: race-channel-follow-up
1367
+ --- from: assistant (TestBot)
1368
+ > *using deterministic-provider/plan-model-v2 ⋅ plan*
1369
+ > ok
1370
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
1371
+ `)
1372
+
1373
+ const footer = [...(await discord.thread(thread.id).getMessages())]
1374
+ .reverse()
1375
+ .find((m) => {
1376
+ return isFooterMessage({ message: m, botUserId: discord.botUserId })
1377
+ })
1378
+ expect(footer).toBeDefined()
1379
+ expect(footer!.content).toContain(PLAN_AGENT_MODEL)
1380
+ expect(footer!.content).not.toContain(AGENT_MODEL)
1381
+ },
1382
+ 20_000,
1383
+ )
1384
+
1284
1385
  test(
1285
1386
  '/plan-agent on the same agent refreshes a stale session model',
1286
1387
  async () => {
@@ -1325,9 +1426,9 @@ describe('agent model resolution', () => {
1325
1426
  "--- from: user (agent-model-tester)
1326
1427
  Reply with exactly: refresh-agent-model-msg
1327
1428
  --- from: assistant (TestBot)
1328
- *using deterministic-provider/plan-model-v2 ⋅ plan*
1329
- ⬥ ok
1330
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***
1429
+ > *using deterministic-provider/plan-model-v2 ⋅ plan*
1430
+ > ok
1431
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>
1331
1432
  Using **plan** agent for this session
1332
1433
  Model: *deterministic-provider/plan-model-v2* (agent "plan")
1333
1434
  The agent will change on the next message."
@@ -1378,9 +1479,9 @@ describe('agent model resolution', () => {
1378
1479
  "--- from: user (agent-model-tester)
1379
1480
  Reply with exactly: channel-vs-agent-msg
1380
1481
  --- from: assistant (TestBot)
1381
- *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1382
- ⬥ ok
1383
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
1482
+ > *using deterministic-provider/agent-model-v2 ⋅ test-agent*
1483
+ > ok
1484
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
1384
1485
  Switched to **plan** agent for this session (was **test-agent**)
1385
1486
  Model: *deterministic-provider/plan-model-v2* (agent "plan")
1386
1487
  The agent will change on the next message."
@@ -1433,9 +1534,9 @@ describe('agent model resolution', () => {
1433
1534
  "--- from: user (agent-model-tester)
1434
1535
  Reply with exactly: plain-agent-override-msg
1435
1536
  --- from: assistant (TestBot)
1436
- *using deterministic-provider/channel-model-v2*
1437
- ⬥ ok
1438
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*
1537
+ > *using deterministic-provider/channel-model-v2*
1538
+ > ok
1539
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ channel-model-v2* <@200000000000000920>
1439
1540
  Switched to **plain** agent for this session
1440
1541
  Model: *deterministic-provider/channel-model-v2* (channel override)
1441
1542
  This model comes from a channel override. Use /model and press Clear override if you want this agent's model.