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
@@ -6,7 +6,10 @@ import {
6
6
  setupQueueAdvancedSuite,
7
7
  TEST_USER_ID,
8
8
  } from './queue-advanced-e2e-setup.js'
9
+ import { store } from './store.js'
9
10
  import {
11
+ getMessageVisibleText,
12
+ isFooterMessage,
10
13
  waitForFooterMessage,
11
14
  waitForBotMessageContaining,
12
15
  waitForBotReplyAfterUserMessage,
@@ -50,20 +53,58 @@ e2eTest('queue advanced: footer emission', () => {
50
53
  "--- from: user (queue-advanced-tester)
51
54
  Reply with exactly: footer-check
52
55
  --- from: assistant (TestBot)
53
- *using deterministic-provider/deterministic-v2*
54
- ⬥ ok
55
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
56
+ > *using deterministic-provider/deterministic-v2*
57
+ > ok
58
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
56
59
  `)
57
60
  const foundFooter = footerMessages.some((m) => {
58
61
  return m.author.id === ctx.discord.botUserId
59
- && m.content.startsWith('*')
60
- && m.content.includes('⋅')
62
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId })
61
63
  })
62
64
  expect(foundFooter).toBe(true)
63
65
  },
64
66
  8_000,
65
67
  )
66
68
 
69
+ test(
70
+ 'skip-footer-mentions omits the thread creator mention',
71
+ async () => {
72
+ store.setState({ footerMentionsEnabled: false })
73
+ try {
74
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
75
+ content: 'Reply with exactly: footer-without-mention',
76
+ })
77
+
78
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
79
+ timeout: 4_000,
80
+ predicate: (t) => {
81
+ return t.name === 'Reply with exactly: footer-without-mention'
82
+ },
83
+ })
84
+
85
+ const th = ctx.discord.thread(thread.id)
86
+ await th.waitForBotReply({ timeout: 4_000 })
87
+ await waitForFooterMessage({
88
+ discord: ctx.discord,
89
+ threadId: thread.id,
90
+ timeout: 4_000,
91
+ })
92
+
93
+ expect(await th.text()).toMatchInlineSnapshot(`
94
+ "--- from: user (queue-advanced-tester)
95
+ Reply with exactly: footer-without-mention
96
+ --- from: assistant (TestBot)
97
+ > *using deterministic-provider/deterministic-v2*
98
+ > ok
99
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*"
100
+ `)
101
+ } finally {
102
+ store.setState({ footerMentionsEnabled: true })
103
+ }
104
+ },
105
+ 8_000,
106
+ )
107
+
67
108
  test(
68
109
  'footer appears after second message in same session',
69
110
  async () => {
@@ -112,21 +153,20 @@ e2eTest('queue advanced: footer emission', () => {
112
153
  const msgs = await th.getMessages()
113
154
  const footerCount = msgs.filter((m) => {
114
155
  return m.author.id === ctx.discord.botUserId
115
- && m.content.startsWith('*')
116
- && m.content.includes('⋅')
156
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId })
117
157
  }).length
118
158
  expect(await th.text()).toMatchInlineSnapshot(`
119
159
  "--- from: user (queue-advanced-tester)
120
160
  Reply with exactly: footer-multi-setup
121
161
  --- from: assistant (TestBot)
122
- *using deterministic-provider/deterministic-v2*
123
- ⬥ ok
124
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
162
+ > *using deterministic-provider/deterministic-v2*
163
+ > ok
164
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
125
165
  --- from: user (queue-advanced-tester)
126
166
  Reply with exactly: footer-multi-second
127
167
  --- from: assistant (TestBot)
128
- ⬥ ok
129
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
168
+ > ok
169
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
130
170
  `)
131
171
  if (footerCount >= 2) {
132
172
  expect(footerCount).toBeGreaterThanOrEqual(2)
@@ -141,9 +181,7 @@ e2eTest('queue advanced: footer emission', () => {
141
181
  })
142
182
  const latestMsgs = await th.getMessages()
143
183
  const count = latestMsgs.filter((m) => {
144
- return m.author.id === ctx.discord.botUserId
145
- && m.content.startsWith('*')
146
- && m.content.includes('⋅')
184
+ return isFooterMessage({ message: m, botUserId: ctx.discord.botUserId })
147
185
  }).length
148
186
  if (count >= 2) {
149
187
  found = true
@@ -218,7 +256,7 @@ e2eTest('queue advanced: footer emission', () => {
218
256
  if (idx <= followupUserIdx) {
219
257
  return false
220
258
  }
221
- return m.author.id === ctx.discord.botUserId && m.content.includes('ok')
259
+ return m.author.id === ctx.discord.botUserId && getMessageVisibleText(m).includes('ok')
222
260
  })
223
261
 
224
262
  await waitForFooterMessage({
@@ -233,18 +271,18 @@ e2eTest('queue advanced: footer emission', () => {
233
271
  "--- from: user (queue-advanced-tester)
234
272
  Reply with exactly: interrupt-footer-setup
235
273
  --- from: assistant (TestBot)
236
- *using deterministic-provider/deterministic-v2*
237
- ⬥ ok
238
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
274
+ > *using deterministic-provider/deterministic-v2*
275
+ > ok
276
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
239
277
  --- from: user (queue-advanced-tester)
240
278
  PLUGIN_TIMEOUT_SLEEP_MARKER
241
279
  --- from: assistant (TestBot)
242
- ⬥ starting sleep 100
280
+ > starting sleep 100
243
281
  --- from: user (queue-advanced-tester)
244
282
  Reply with exactly: interrupt-footer-followup
245
283
  --- from: assistant (TestBot)
246
- ⬥ ok
247
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
284
+ > ok
285
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
248
286
  `)
249
287
  expect(followupUserIdx).toBeGreaterThanOrEqual(0)
250
288
  expect(okReplyIdx).toBeGreaterThan(followupUserIdx)
@@ -254,8 +292,7 @@ e2eTest('queue advanced: footer emission', () => {
254
292
  return false
255
293
  }
256
294
  return m.author.id === ctx.discord.botUserId
257
- && m.content.startsWith('*')
258
- && m.content.includes('⋅')
295
+ && isFooterMessage({ message: m, botUserId: ctx.discord.botUserId })
259
296
  })
260
297
  expect(footerBetween).toBe(false)
261
298
  },
@@ -263,120 +300,8 @@ e2eTest('queue advanced: footer emission', () => {
263
300
  )
264
301
 
265
302
  test(
266
- 'plugin timeout interrupt aborts slow sleep and avoids intermediate footer',
303
+ 'tool-call turn emits a footer after the final follow-up',
267
304
  async () => {
268
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
269
- content: 'Reply with exactly: plugin-timeout-setup',
270
- })
271
-
272
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
273
- timeout: 4_000,
274
- predicate: (t) => {
275
- return t.name === 'Reply with exactly: plugin-timeout-setup'
276
- },
277
- })
278
-
279
- const th = ctx.discord.thread(thread.id)
280
- await th.waitForBotReply({ timeout: 4_000 })
281
- await waitForBotMessageContaining({
282
- discord: ctx.discord,
283
- threadId: thread.id,
284
- userId: TEST_USER_ID,
285
- text: '*project',
286
- timeout: 4_000,
287
- })
288
-
289
- await th.user(TEST_USER_ID).sendMessage({
290
- content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
291
- })
292
-
293
- await waitForBotMessageContaining({
294
- discord: ctx.discord,
295
- threadId: thread.id,
296
- userId: TEST_USER_ID,
297
- text: 'starting sleep 100',
298
- afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
299
- timeout: 4_000,
300
- })
301
-
302
- await th.user(TEST_USER_ID).sendMessage({
303
- content: 'Reply with exactly: plugin-timeout-after',
304
- })
305
-
306
- const messages = await waitForBotMessageContaining({
307
- discord: ctx.discord,
308
- threadId: thread.id,
309
- userId: TEST_USER_ID,
310
- text: 'ok',
311
- afterUserMessageIncludes: 'plugin-timeout-after',
312
- timeout: 12_000,
313
- })
314
-
315
- const messagesWithFooter = await waitForFooterMessage({
316
- discord: ctx.discord,
317
- threadId: thread.id,
318
- timeout: 12_000,
319
- afterMessageIncludes: 'plugin-timeout-after',
320
- afterAuthorId: TEST_USER_ID,
321
- })
322
-
323
- const afterIndex = messagesWithFooter.findIndex((message) => {
324
- return (
325
- message.author.id === TEST_USER_ID
326
- && message.content.includes('plugin-timeout-after')
327
- )
328
- })
329
- expect(await th.text()).toMatchInlineSnapshot(`
330
- "--- from: user (queue-advanced-tester)
331
- Reply with exactly: plugin-timeout-setup
332
- --- from: assistant (TestBot)
333
- *using deterministic-provider/deterministic-v2*
334
- ⬥ ok
335
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
336
- --- from: user (queue-advanced-tester)
337
- PLUGIN_TIMEOUT_SLEEP_MARKER
338
- --- from: assistant (TestBot)
339
- ⬥ starting sleep 100
340
- --- from: user (queue-advanced-tester)
341
- Reply with exactly: plugin-timeout-after
342
- --- from: assistant (TestBot)
343
- ⬥ ok
344
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
345
- `)
346
- expect(afterIndex).toBeGreaterThanOrEqual(0)
347
-
348
- const okReplyIndex = messagesWithFooter.findIndex((message, index) => {
349
- if (index <= afterIndex) {
350
- return false
351
- }
352
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok')
353
- })
354
- expect(okReplyIndex).toBeGreaterThan(afterIndex)
355
-
356
- const footerBeforeReply = messagesWithFooter.some((message, index) => {
357
- if (index <= afterIndex || index >= okReplyIndex) {
358
- return false
359
- }
360
- if (message.author.id !== ctx.discord.botUserId) {
361
- return false
362
- }
363
- return message.content.startsWith('*') && message.content.includes('⋅')
364
- })
365
- expect(footerBeforeReply).toBe(false)
366
- },
367
- 15_000,
368
- )
369
-
370
- test(
371
- 'tool-call assistant message gets footer when it completes normally',
372
- async () => {
373
- // Reproduces the bug: model responds with text + tool call,
374
- // finish="tool-calls", message gets completed timestamp. Then the tool
375
- // result triggers a follow-up text response in a second assistant message.
376
- // The second message gets a footer, but the first (tool-call) message
377
- // should ALSO get a footer since it completed normally.
378
- // This matches the real-world scenario where an agent calls a bash tool
379
- // (e.g. `kimaki send`) and then follows up with a summary text.
380
305
  const existingThreadIds = new Set(
381
306
  (await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
382
307
  return thread.id
@@ -413,193 +338,20 @@ e2eTest('queue advanced: footer emission', () => {
413
338
  timeout: 4_000,
414
339
  })
415
340
 
416
- // Poll until both footers have arrived — the first footer (after the
417
- // tool-call step) and the second (after the text follow-up) are emitted
418
- // by sequential handleNaturalAssistantCompletion calls but the second
419
- // may not have hit the Discord thread by the time we first check.
420
- const deadline = Date.now() + 4_000
421
- let footerCount = 0
422
- while (Date.now() < deadline) {
423
- const msgs = await th.getMessages()
424
- footerCount = msgs.filter((m) => {
425
- return m.author.id === ctx.discord.botUserId
426
- && m.content.startsWith('*')
427
- && m.content.includes('⋅')
428
- }).length
429
- if (footerCount >= 2) {
430
- break
431
- }
432
- await new Promise((resolve) => {
433
- setTimeout(resolve, 100)
434
- })
435
- }
436
-
437
341
  expect(await th.text()).toMatchInlineSnapshot(`
438
342
  "--- from: user (queue-advanced-tester)
439
343
  TOOL_CALL_FOOTER_MARKER
440
344
  --- from: assistant (TestBot)
441
- *using deterministic-provider/deterministic-v2*
442
- ⬥ running tool
443
- ┣ bash _echo tool-call-footer-test_
444
- ⬥ tool call completed
445
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
446
- `)
447
-
448
- // Only ONE footer at the end — the tool-call step's footer is NOT
449
- // emitted mid-turn. The final text follow-up gets the footer.
450
- expect(footerCount).toBe(1)
451
- },
452
- 10_000,
453
- )
454
-
455
- test(
456
- 'multi-step tool chain should only have one footer at the end',
457
- async () => {
458
- // Model does 3 sequential tool calls (each a separate assistant message
459
- // with finish="tool-calls") then a final text response. Only the final
460
- // text response should get a footer — intermediate tool-call steps
461
- // should NOT get footers since they're mid-turn work.
462
- const existingThreadIds = new Set(
463
- (await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
464
- return thread.id
465
- }),
466
- )
467
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
468
- content: 'MULTI_TOOL_FOOTER_MARKER',
469
- })
345
+ > *using deterministic-provider/deterministic-v2*
346
+ > running tool
470
347
 
471
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
472
- timeout: 6_000,
473
- predicate: (t) => {
474
- return !existingThreadIds.has(t.id)
475
- },
476
- })
477
-
478
- const th = ctx.discord.thread(thread.id)
479
-
480
- // Wait for the final text response after all 3 tool steps
481
- await waitForBotMessageContaining({
482
- discord: ctx.discord,
483
- threadId: thread.id,
484
- userId: TEST_USER_ID,
485
- text: 'all done, fixed 3 files',
486
- timeout: 6_000,
487
- })
488
-
489
- // Wait for the footer after the final response
490
- await waitForFooterMessage({
491
- discord: ctx.discord,
492
- threadId: thread.id,
493
- timeout: 6_000,
494
- })
495
-
496
- // Give any spurious extra footers time to arrive
497
- await new Promise((resolve) => {
498
- setTimeout(resolve, 500)
499
- })
500
-
501
- const messages = await th.getMessages()
502
- const footerCount = messages.filter((m) => {
503
- return m.author.id === ctx.discord.botUserId
504
- && m.content.startsWith('*')
505
- && m.content.includes('⋅')
506
- }).length
348
+ ┣ bash _echo tool-call-footer-test_
507
349
 
508
- expect(await th.text()).toMatchInlineSnapshot(`
509
- "--- from: user (queue-advanced-tester)
510
- MULTI_TOOL_FOOTER_MARKER
511
- --- from: assistant (TestBot)
512
- *using deterministic-provider/deterministic-v2*
513
- ⬥ investigating the issue
514
- ┣ bash _echo search-done_
515
- ┣ bash _echo read-done_
516
- ┣ bash _echo fix-done_
517
- ⬥ all done, fixed 3 files
518
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
350
+ > tool call completed
351
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
519
352
  `)
520
-
521
- // Only ONE footer should appear — after the final text response.
522
- // Intermediate tool-call steps should NOT get footers.
523
- expect(footerCount).toBe(1)
524
353
  },
525
354
  10_000,
526
355
  )
527
356
 
528
- test(
529
- '3 sequential tool-call steps produce exactly 1 footer, not 3',
530
- async () => {
531
- // This is the most obvious reproduction of the multi-footer bug:
532
- // the model runs 3 sequential tool-call steps (each a SEPARATE
533
- // assistant message with finish="tool-calls"), then a final text.
534
- // With a naive fix that treats tool-calls as natural completions,
535
- // you'd see 4 footers (one per assistant message). Only the final
536
- // text response should produce a footer.
537
- const existingThreadIds = new Set(
538
- (await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
539
- return thread.id
540
- }),
541
- )
542
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
543
- content: 'MULTI_STEP_CHAIN_MARKER',
544
- })
545
-
546
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
547
- timeout: 6_000,
548
- predicate: (t) => {
549
- return !existingThreadIds.has(t.id)
550
- },
551
- })
552
-
553
- const th = ctx.discord.thread(thread.id)
554
-
555
- // Wait for the final text after all 3 sequential tool steps
556
- await waitForBotMessageContaining({
557
- discord: ctx.discord,
558
- threadId: thread.id,
559
- userId: TEST_USER_ID,
560
- text: 'chain complete: all 3 steps done',
561
- timeout: 10_000,
562
- })
563
-
564
- // Wait for footer
565
- await waitForFooterMessage({
566
- discord: ctx.discord,
567
- threadId: thread.id,
568
- timeout: 6_000,
569
- })
570
-
571
- // Give any spurious extra footers time to arrive
572
- await new Promise((resolve) => {
573
- setTimeout(resolve, 500)
574
- })
575
-
576
- const messages = await th.getMessages()
577
- const footerCount = messages.filter((m) => {
578
- return m.author.id === ctx.discord.botUserId
579
- && m.content.startsWith('*')
580
- && m.content.includes('⋅')
581
- }).length
582
-
583
- expect(await th.text()).toMatchInlineSnapshot(`
584
- "--- from: user (queue-advanced-tester)
585
- MULTI_STEP_CHAIN_MARKER
586
- --- from: assistant (TestBot)
587
- *using deterministic-provider/deterministic-v2*
588
- ⬥ chain step 1: reading config
589
- ┣ bash _echo chain-step-1-output_
590
- ⬥ chain step 2: analyzing results
591
- ┣ bash _echo chain-step-2-output_
592
- ⬥ chain step 3: applying fix
593
- ┣ bash _echo chain-step-3-output_
594
- ⬥ chain complete: all 3 steps done
595
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
596
- `)
597
-
598
- // The critical assertion: only 1 footer at the very end.
599
- // With the naive "allow tool-calls as natural completion" fix,
600
- // this would be 4 (one per assistant message). We want 1.
601
- expect(footerCount).toBe(1)
602
- },
603
- 15_000,
604
- )
605
357
  })
@@ -11,6 +11,7 @@ import {
11
11
  TEST_USER_ID,
12
12
  } from './queue-advanced-e2e-setup.js'
13
13
  import {
14
+ isFooterMessage,
14
15
  waitForBotMessageContaining,
15
16
  waitForBotReplyAfterUserMessage,
16
17
  waitForFooterMessage,
@@ -320,17 +321,15 @@ describe('queue advanced: /model with interrupt recovery', () => {
320
321
  })
321
322
 
322
323
  const footer = [...finalMessages].reverse().find((message) => {
323
- return message.author.id === ctx.discord.botUserId
324
- && message.content.startsWith('*')
325
- && message.content.includes('⋅')
324
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId })
326
325
  })
327
326
  expect(await th.text()).toMatchInlineSnapshot(`
328
327
  "--- from: user (queue-model-switch-tester)
329
328
  Reply with exactly: model-switcher-setup
330
329
  --- from: assistant (TestBot)
331
- *using deterministic-provider/deterministic-v2*
332
- ⬥ ok
333
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
330
+ > *using deterministic-provider/deterministic-v2*
331
+ > ok
332
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
334
333
  Model set for this session:
335
334
  **Deterministic Provider** / **deterministic-v3**
336
335
  \`deterministic-provider/deterministic-v3\`
@@ -339,13 +338,13 @@ describe('queue advanced: /model with interrupt recovery', () => {
339
338
  --- from: user (queue-model-switch-tester)
340
339
  PLUGIN_TIMEOUT_SLEEP_MARKER
341
340
  --- from: assistant (TestBot)
342
- ⬥ ok
343
- ⬥ starting sleep 100
341
+ > ok
342
+ > starting sleep 100
344
343
  --- from: user (queue-model-switch-tester)
345
344
  Reply with exactly: model-switcher-followup
346
345
  --- from: assistant (TestBot)
347
- ⬥ ok
348
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v3*"
346
+ > ok
347
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v3* <@200000000000000991>"
349
348
  `)
350
349
 
351
350
  expect(footer).toBeDefined()
@@ -51,7 +51,7 @@ describe('queue advanced: typing around permissions', () => {
51
51
  })
52
52
 
53
53
  test(
54
- 'permission prompt pauses typing until user click, then typing resumes for long follow-up step',
54
+ 'permission prompt pauses typing until user click, then typing resumes',
55
55
  async () => {
56
56
  await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
57
57
  content: 'PERMISSION_TYPING_MARKER',
@@ -83,7 +83,7 @@ describe('queue advanced: typing around permissions', () => {
83
83
 
84
84
  th.clearTypingEvents()
85
85
 
86
- await th.waitForTypingEvent({ timeout: 2_000 }).then(
86
+ await th.waitForTypingEvent({ timeout: 700 }).then(
87
87
  () => {
88
88
  throw new Error('Typing should stay paused while permission UI is pending')
89
89
  },
@@ -102,7 +102,7 @@ describe('queue advanced: typing around permissions', () => {
102
102
  timeout: 4_000,
103
103
  })
104
104
 
105
- const resumedTyping = await th.waitForTypingEvent({ timeout: 9_000 })
105
+ const resumedTyping = await th.waitForTypingEvent({ timeout: 2_000 })
106
106
  expect(resumedTyping).toBeDefined()
107
107
 
108
108
  await waitForBotMessageContaining({
@@ -110,13 +110,13 @@ describe('queue advanced: typing around permissions', () => {
110
110
  threadId: thread.id,
111
111
  userId: TEST_USER_ID,
112
112
  text: 'permission-flow-done',
113
- timeout: 12_000,
113
+ timeout: 4_000,
114
114
  })
115
115
 
116
116
  await waitForFooterMessage({
117
117
  discord: ctx.discord,
118
118
  threadId: thread.id,
119
- timeout: 12_000,
119
+ timeout: 4_000,
120
120
  afterMessageIncludes: 'permission-flow-done',
121
121
  afterAuthorId: ctx.discord.botUserId,
122
122
  })
@@ -125,8 +125,9 @@ describe('queue advanced: typing around permissions', () => {
125
125
  "--- from: user (queue-permission-tester)
126
126
  PERMISSION_TYPING_MARKER
127
127
  --- from: assistant (TestBot)
128
- *using deterministic-provider/deterministic-v2*
129
- ⬥ requesting external read permission
128
+ > *using deterministic-provider/deterministic-v2*
129
+ > requesting external read permission
130
+
130
131
  ┣ read *.zprofile*
131
132
  ⚠️ **Permission Required**
132
133
  **Type:** \`external_directory\`
@@ -134,8 +135,9 @@ describe('queue advanced: typing around permissions', () => {
134
135
  **Pattern:** \`/Users/morse/*\`
135
136
  ✅ Permission **accepted**
136
137
  [user clicks button]
137
- ⬥ permission-flow-done
138
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
138
+
139
+ > permission-flow-done
140
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
139
141
  `)
140
142
 
141
143
  const timeline = await th.text({
@@ -143,19 +145,17 @@ describe('queue advanced: typing around permissions', () => {
143
145
  showInteractions: true,
144
146
  })
145
147
  const clickPosition = timeline.indexOf('[user clicks button]')
146
- const donePosition = timeline.indexOf('⬥ permission-flow-done')
148
+ const donePosition = timeline.indexOf('permission-flow-done')
147
149
  const footerPosition = timeline.lastIndexOf('*project ⋅')
148
150
  expect(clickPosition).toBeGreaterThanOrEqual(0)
149
151
  expect(donePosition).toBeGreaterThan(clickPosition)
150
152
  expect(footerPosition).toBeGreaterThan(donePosition)
151
153
 
152
154
  const afterClick = timeline.slice(clickPosition, donePosition)
153
- const afterDone = timeline.slice(donePosition, footerPosition)
154
155
  expect(afterClick).toContain('[bot typing]')
155
- expect(afterDone).toContain('[bot typing]')
156
156
  expect(timeline.slice(footerPosition)).not.toContain('[bot typing]')
157
157
  },
158
- 20_000,
158
+ 12_000,
159
159
  )
160
160
 
161
161
  test(
@@ -232,7 +232,7 @@ describe('queue advanced: typing around permissions', () => {
232
232
 
233
233
  const timeline = await th.text({ showInteractions: true })
234
234
  const normalizedTimeline = timeline.replace(
235
- '⬥ requesting external read permission\n',
235
+ 'requesting external read permission\n',
236
236
  '',
237
237
  )
238
238
  expect(normalizedTimeline).toContain('PERMISSION_TYPING_MARKER dismiss-flow')
@@ -242,7 +242,7 @@ describe('queue advanced: typing around permissions', () => {
242
242
  const followupUserPosition = normalizedTimeline.indexOf(
243
243
  'Reply with exactly: post-permission-user-message',
244
244
  )
245
- const followupReplyPosition = normalizedTimeline.indexOf('⬥ ok', followupUserPosition)
245
+ const followupReplyPosition = normalizedTimeline.indexOf('ok', followupUserPosition)
246
246
  const followupFooterPosition = normalizedTimeline.indexOf(
247
247
  '*project ⋅',
248
248
  followupReplyPosition,