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
@@ -2,7 +2,8 @@
2
2
  // temp root with private XDG dirs and SQLite DB, started with the same
3
3
  // `serve --service` command the user's service uses, so the bot connects
4
4
  // through Service.discover() exactly like production. The bot runs in-process
5
- // against discord-digital-twin.
5
+ // against discord-digital-twin. The model is opencode-deterministic-provider,
6
+ // answered by startTestModel() in this process.
6
7
 
7
8
  import { execFile, execFileSync, spawn, type ChildProcess } from 'node:child_process'
8
9
  import { promisify } from 'node:util'
@@ -22,10 +23,13 @@ import { ChannelType, ComponentType, type APIMessage } from 'discord.js'
22
23
  import { DigitalDiscord } from 'discord-digital-twin/src'
23
24
  import {
24
25
  buildDeterministicOpencode2Config,
26
+ getLatestUserText,
27
+ resolveResponse,
25
28
  type DeterministicMatcher,
26
29
  } from 'opencode-deterministic-provider'
27
30
 
28
31
  import { disabledAnalytics, type Analytics } from '../analytics.ts'
32
+ import type { WarmupTimers } from '../bot.ts'
29
33
  import { GATEWAY_APP_ID, saveCredentials, type Credentials } from '../credentials.ts'
30
34
  import { openDb, verbosityToV1, type Verbosity } from '../db.ts'
31
35
  import type { ToolInput } from '../format-parts.ts'
@@ -60,8 +64,50 @@ export function opencodeBinary(): string {
60
64
  return path.join(path.dirname(packageJsonPath), 'bin/opencode.exe')
61
65
  }
62
66
 
67
+ export type TestModelRequest = {
68
+ latestUserText: string
69
+ // The whole prompt as JSON: system, history and tool results.
70
+ raw: string
71
+ }
72
+
73
+ // The deterministic model, answered in this process: OpenCode's provider POSTs each
74
+ // prompt here. Tests read `requests` to check what the model saw and may push matchers.
75
+ export type TestModel = {
76
+ matchers: DeterministicMatcher[]
77
+ requests: TestModelRequest[]
78
+ }
79
+
80
+ async function startTestModel({ matchers }: { matchers: DeterministicMatcher[] }) {
81
+ const model: TestModel = { matchers, requests: [] }
82
+ const server = http.createServer((request, response) => {
83
+ const chunks: Buffer[] = []
84
+ request.on('data', (chunk: Buffer) => chunks.push(chunk))
85
+ request.on('end', () => {
86
+ try {
87
+ const { prompt } = JSON.parse(Buffer.concat(chunks).toString('utf8')) as Parameters<typeof resolveResponse>[0]
88
+ model.requests.push({ latestUserText: getLatestUserText({ prompt }), raw: JSON.stringify(prompt) })
89
+ response.setHeader('content-type', 'application/json')
90
+ response.end(JSON.stringify(resolveResponse({ prompt, settings: { strict: false, matchers: model.matchers } })))
91
+ } catch (error) {
92
+ // The provider turns this into a model error, so the run fails visibly instead of hanging.
93
+ response.statusCode = 500
94
+ response.end(error instanceof Error ? error.stack : String(error))
95
+ }
96
+ })
97
+ })
98
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
99
+ const address = server.address()
100
+ if (!address || typeof address !== 'object') throw new Error('test model has no port')
101
+ return {
102
+ model,
103
+ url: `http://127.0.0.1:${address.port}`,
104
+ stop: () => new Promise<void>((resolve) => server.close(() => resolve())),
105
+ }
106
+ }
107
+
63
108
  export type OpencodeTestServer = {
64
109
  root: string
110
+ model: TestModel
65
111
  serviceFile: string
66
112
  // Global OpenCode config dir of this server (XDG_CONFIG_HOME/opencode).
67
113
  configDir: string
@@ -91,7 +137,7 @@ export async function startOpencodeTestServer({
91
137
  // Appended after the allow-all rules: the last matching rule wins.
92
138
  permissions?: Array<{ action: string; resource: string; effect: 'allow' | 'deny' | 'ask' }>
93
139
  // More deterministic models next to TEST_MODEL, e.g. with thinking variants.
94
- models?: Record<string, { name: string; variants?: Array<{ id: string }> }>
140
+ models?: Record<string, { name: string; variants?: Array<{ id: string }>; limit?: { context?: number; output?: number } }>
95
141
  // Agent overrides and custom agents (config `agents`).
96
142
  agents?: Record<string, { description?: string; mode?: 'primary' | 'subagent' | 'all'; hidden?: boolean; model?: string }>
97
143
  // MCP servers (config `mcp.servers`).
@@ -104,9 +150,10 @@ export async function startOpencodeTestServer({
104
150
  fs.mkdirSync(projectDirectory, { recursive: true })
105
151
  execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: projectDirectory })
106
152
  const serviceFile = path.join(root, 'state', 'opencode', 'service.json')
153
+ const testModel = await startTestModel({ matchers })
107
154
  const config = buildDeterministicOpencode2Config({
108
155
  model: TEST_MODEL,
109
- settings: { strict: false, matchers },
156
+ settings: { responderUrl: testModel.url },
110
157
  permissions: [
111
158
  { action: 'shell', resource: '*', effect: 'allow' },
112
159
  { action: 'edit', resource: '*', effect: 'allow' },
@@ -183,6 +230,7 @@ export async function startOpencodeTestServer({
183
230
  await start()
184
231
  return {
185
232
  root,
233
+ model: testModel.model,
186
234
  serviceFile,
187
235
  configDir: path.join(env.XDG_CONFIG_HOME, 'opencode'),
188
236
  dbFile: path.join(env.XDG_DATA_HOME, 'opencode', 'opencode.db'),
@@ -193,6 +241,7 @@ export async function startOpencodeTestServer({
193
241
  start,
194
242
  stop: async () => {
195
243
  await kill()
244
+ await testModel.stop()
196
245
  fs.rmSync(root, { recursive: true, force: true })
197
246
  },
198
247
  }
@@ -499,6 +548,8 @@ export async function startTestBot({
499
548
  analytics,
500
549
  clock,
501
550
  schedulerIntervalMs,
551
+ coordinator,
552
+ warmupTimers,
502
553
  }: {
503
554
  dataDir: string
504
555
  twin: TestTwin
@@ -519,6 +570,9 @@ export async function startTestBot({
519
570
  // Scheduling tests pass manualClock() and null, then call bot.scheduler.runDueTasks().
520
571
  clock?: Clock
521
572
  schedulerIntervalMs?: number | null
573
+ // Default off (`--coordinator false`) so most tests get threads; coordinator tests pass true.
574
+ coordinator?: boolean
575
+ warmupTimers?: WarmupTimers
522
576
  }): Promise<BotHandle> {
523
577
  // What credential resolution saves in production; `kimaki project add` reads it.
524
578
  const credentials: Credentials = twin.discord.botToken.includes(':')
@@ -535,7 +589,7 @@ export async function startTestBot({
535
589
  if (lock instanceof Error) throw lock
536
590
  const bot = await startBot({
537
591
  dataDir,
538
- kimakiCommand: `'${process.execPath}' --import '${createRequire(import.meta.url).resolve('tsx')}' '${path.resolve('src/cli.ts')}'`,
592
+ kimakiArgv: [process.execPath, '--import', createRequire(import.meta.url).resolve('tsx'), path.resolve('src/cli.ts')],
539
593
  token: credentials.token,
540
594
  appId: credentials.appId,
541
595
  lock,
@@ -548,6 +602,8 @@ export async function startTestBot({
548
602
  ...(clock && { clock }),
549
603
  ...(schedulerIntervalMs !== undefined && { schedulerIntervalMs }),
550
604
  ...(realtimeBaseUrls && { realtimeBaseUrls }),
605
+ coordinator: coordinator ?? false,
606
+ ...(warmupTimers && { warmupTimers }),
551
607
  transcriptionBaseUrls: {
552
608
  ...(geminiBaseUrl && { gemini: geminiBaseUrl }),
553
609
  ...(openaiBaseUrl && { openai: openaiBaseUrl }),
@@ -620,7 +676,8 @@ export function schedulingKit({ suite, dataDir, clock }: {
620
676
  }
621
677
  }
622
678
 
623
- export type FakeTranscription = { transcription: string; route?: string; agent?: string }
679
+ // delayMs: how long the fake Gemini waits before it answers (not sent back as a tool argument).
680
+ export type FakeTranscription = { transcription: string; route?: string; agent?: string; delayMs?: number }
624
681
 
625
682
  // A voice message whose audio bytes are the transcription the fake returns.
626
683
  export function voiceUrl(result: FakeTranscription): string {
@@ -650,13 +707,15 @@ export async function startFakeGemini(): Promise<FakeGemini> {
650
707
  const declaration = body.tools[0]!.functionDeclarations[0]!
651
708
  const audio = body.contents[0]!.parts.find((part) => part.inlineData)!.inlineData!
652
709
  requests.push({ routes: declaration.parameters.properties.route?.enum ?? null, mimeType: audio.mimeType })
653
- const args = JSON.parse(Buffer.from(audio.data, 'base64').toString('utf8')) as FakeTranscription
654
- response.setHeader('content-type', 'application/json')
655
- response.end(
656
- JSON.stringify({
657
- candidates: [{ finishReason: 'STOP', content: { parts: [{ functionCall: { name: declaration.name, args } }] } }],
658
- }),
659
- )
710
+ const { delayMs = 0, ...args } = JSON.parse(Buffer.from(audio.data, 'base64').toString('utf8')) as FakeTranscription
711
+ setTimeout(() => {
712
+ response.setHeader('content-type', 'application/json')
713
+ response.end(
714
+ JSON.stringify({
715
+ candidates: [{ finishReason: 'STOP', content: { parts: [{ functionCall: { name: declaration.name, args } }] } }],
716
+ }),
717
+ )
718
+ }, delayMs)
660
719
  })
661
720
  })
662
721
  const port = await freePort()
@@ -727,14 +786,15 @@ export async function startFakeOpenAI({ result }: { result: FakeTranscription })
727
786
 
728
787
  export type FakeChatGPT = {
729
788
  baseUrl: string
730
- // Each POST /transcribe, as ChatGPT received it.
731
- requests: Array<{ authorization: string | null; accountId: string | null; filename: string; type: string; audio: Buffer }>
789
+ // Each POST, as the server received it.
790
+ requests: Array<{ path: string; authorization: string | null; accountId: string | null; model: string | null; filename: string; type: string; audio: Buffer }>
732
791
  // Reply status; tests set 401 to check the API key fallback.
733
792
  status: number
734
793
  stop: () => Promise<void>
735
794
  }
736
795
 
737
796
  // chatgpt.com/backend-api/transcribe: multipart `file` in, `{ text }` out.
797
+ // Also a local Whisper server (/audio/transcriptions), which takes the same shape plus `model`.
738
798
  export async function startFakeChatGPT({ text }: { text: string }): Promise<FakeChatGPT> {
739
799
  const fake: Omit<FakeChatGPT, 'baseUrl' | 'stop'> = { requests: [], status: 200 }
740
800
  const server = http.createServer((request, response) => {
@@ -745,9 +805,12 @@ export async function startFakeChatGPT({ text }: { text: string }): Promise<Fake
745
805
  const form = await new Request('http://fake/transcribe', { method: 'POST', headers, body: Buffer.concat(chunks) }).formData()
746
806
  const file = form.get('file')
747
807
  if (!(file instanceof File)) throw new Error('fake ChatGPT: no file field')
808
+ const model = form.get('model')
748
809
  fake.requests.push({
810
+ path: request.url ?? '',
749
811
  authorization: headers.get('authorization'),
750
812
  accountId: headers.get('chatgpt-account-id'),
813
+ model: typeof model === 'string' ? model : null,
751
814
  filename: file.name,
752
815
  type: file.type,
753
816
  audio: Buffer.from(await file.arrayBuffer()),
@@ -25,9 +25,11 @@ export function rootSessionId(events: V2Event[]): string {
25
25
  return created.data.sessionID
26
26
  }
27
27
 
28
+ const REPLAY_PROJECT = '/project'
29
+
28
30
  export const DEFAULT_PREFS: Prefs = {
29
31
  verbosity: 'tools',
30
- contextLimits: { 'openai/gpt-6-luna': 400_000 },
32
+ models: { 'openai/gpt-6-luna': { name: 'GPT 6 Luna', context: 400_000 } },
31
33
  }
32
34
 
33
35
  export function replay({
@@ -44,8 +46,9 @@ export function replay({
44
46
  emptyView({
45
47
  sessionId: rootSessionId(events.filter((event): event is V2Event => !event.type.startsWith('kimaki.'))),
46
48
  channelId: 'channel',
47
- directory: '/project',
49
+ directory: REPLAY_PROJECT,
48
50
  isNew: true,
51
+ coordinator: false,
49
52
  })
50
53
  return events.reduce<{ view: ThreadView; effects: Effect[] }>(
51
54
  (acc, event) => {
@@ -74,15 +77,18 @@ export function effectLines(effects: Effect[]): string[] {
74
77
  if (effect.type === 'viewed') return []
75
78
  if (effect.type === 'typing') return `[typing ${effect.on ? 'on' : 'off'}]`
76
79
  if (effect.type === 'attachments') return `[attachments ${effect.files.map((file) => file.name).join(', ')}]`
77
- if (effect.type === 'markdown') return `${effect.blankLineBefore ? '\\n' : ''}${effect.text}`
80
+ if (effect.type === 'markdown') return `${effect.replyTo ? `[reply to ${effect.replyTo}] ` : ''}${effect.blankLineBefore ? '\\n' : ''}${effect.text}`
78
81
  if (effect.type === 'show') {
79
82
  const reply = effect.replyTo ? ` reply to ${effect.replyTo}` : ''
80
83
  return `[show ${effect.key}${reply}] ${effect.messages.map(uiLine).join(' | ')}`
81
84
  }
82
- if (effect.type === 'edit') return `[edit ${effect.key}] ${effect.messages.map(uiLine).join(' | ')}`
83
- // Tests have no git checkout: the branch is always main.
85
+ if (effect.type === 'edit') {
86
+ const reply = effect.reply === undefined ? [] : [`[reply to ${effect.replyTo ?? effect.key}] ${effect.reply}`]
87
+ return [`[edit ${effect.key}] ${effect.messages.map(uiLine).join(' | ')}`, ...reply]
88
+ }
89
+ // Tests have no git checkout: the branch is always main. The channel runs build in /project.
84
90
  if (effect.type === 'footer') {
85
- const footer = formatFooter({ ...effect, folder: path.basename(effect.directory), branch: 'main' })
91
+ const footer = formatFooter({ ...effect, branch: 'main', defaultBranch: null, projectDirectory: REPLAY_PROJECT, defaultAgent: 'build' })
86
92
  return effect.notify ? footer : `[silent] ${footer}`
87
93
  }
88
94
  return effect.text.replace(/^\n/, '\\n')
@@ -11,7 +11,7 @@ test('tools: names from tool.input.started, failed tool lines, read-only tools h
11
11
  const { effects } = replay({ events: loadFixture('tools.events.jsonl') })
12
12
  expect(effectLines(effects)).toMatchInlineSnapshot(`
13
13
  [
14
- "» **user:** Do these steps with tools, one by one: 1) run \`ls cli/src | head -5\` with bash, 2) write a file tmp-events/hello.txt containing "hello", 3) edit that file to replace hello with "hello world", 4) glob for **/*.txt under tmp-events, 5) read the file. Then reply with one short line.",
14
+ "-# » **user:** Do these steps with tools, one by one: 1) run \`ls cli/src | head -5\` with bash, 2) write a file tmp-events/hello.txt containing "hello", 3) edit that file to replace hello with "hello world", 4) glob for **/*.txt under tmp-events, 5) read the file. Then reply with one short line.",
15
15
  "[typing on]",
16
16
  "-# *using openai/gpt-6-luna low ⋅ build*",
17
17
  "-# ┣ shell _ls cli/src \\| head -5_",
@@ -23,7 +23,7 @@ test('tools: names from tool.input.started, failed tool lines, read-only tools h
23
23
  "-# ┣ shell _python3 -c "from pathlib import Path; p=Path('tmp-events/hello.txt'); p.write\\_t…_",
24
24
  "\\nDone. \`tmp-events/hello.txt\` contains \`hello world\`.",
25
25
  "[typing off]",
26
- "-# *project ⋅ main ⋅ 17s ⋅ 6% ⋅ gpt-6-luna*",
26
+ "-# *main ⋅ 17s ⋅ 6% ⋅ gpt-6-luna*",
27
27
  ]
28
28
  `)
29
29
  })
@@ -31,18 +31,18 @@ test('tools: names from tool.input.started, failed tool lines, read-only tools h
31
31
  test('tools at text verbosity: text, edits and errors only', () => {
32
32
  const { effects } = replay({
33
33
  events: loadFixture('tools.events.jsonl'),
34
- prefs: { verbosity: 'text', contextLimits: {} },
34
+ prefs: { verbosity: 'text', models: {} },
35
35
  })
36
36
  expect(effectLines(effects)).toMatchInlineSnapshot(`
37
37
  [
38
- "» **user:** Do these steps with tools, one by one: 1) run \`ls cli/src | head -5\` with bash, 2) write a file tmp-events/hello.txt containing "hello", 3) edit that file to replace hello with "hello world", 4) glob for **/*.txt under tmp-events, 5) read the file. Then reply with one short line.",
38
+ "-# » **user:** Do these steps with tools, one by one: 1) run \`ls cli/src | head -5\` with bash, 2) write a file tmp-events/hello.txt containing "hello", 3) edit that file to replace hello with "hello world", 4) glob for **/*.txt under tmp-events, 5) read the file. Then reply with one short line.",
39
39
  "[typing on]",
40
40
  "-# *using openai/gpt-6-luna low ⋅ build*",
41
41
  "-# ⨯ shell _Invalid arguments for tool "shell":_",
42
42
  "-# ⨯ shell _Invalid arguments for tool "shell":_",
43
43
  "\\nDone. \`tmp-events/hello.txt\` contains \`hello world\`.",
44
44
  "[typing off]",
45
- "-# *project ⋅ main ⋅ 17s ⋅ gpt-6-luna*",
45
+ "-# *main ⋅ 17s ⋅ gpt-6-luna*",
46
46
  ]
47
47
  `)
48
48
  })
@@ -51,13 +51,13 @@ test('task-subagent: child tool lines labelled with the agent, no child text, fo
51
51
  const { effects, view } = replay({ events: loadFixture('task-subagent.events.jsonl') })
52
52
  expect(effectLines(effects)).toMatchInlineSnapshot(`
53
53
  [
54
- "» **user:** Use the subagent tool with agent "general" to find which file defines the Drizzle SQLite schema in cli/src (the subagent should use glob and read). Then read the first 10 lines of that file yourself with the read tool and answer in 2 short sentences.",
54
+ "-# » **user:** Use the subagent tool with agent "general" to find which file defines the Drizzle SQLite schema in cli/src (the subagent should use glob and read). Then read the first 10 lines of that file yourself with the read tool and answer in 2 short sentences.",
55
55
  "[typing on]",
56
56
  "-# *using openai/gpt-6-luna low ⋅ build*",
57
57
  "-# ┣ general **Find SQLite schema file**",
58
58
  "\\nThe Drizzle SQLite schema is defined in \`cli/src/schema.ts\`. Its first lines confirm it defines tables for Kimaki’s local SQLite database.",
59
59
  "[typing off]",
60
- "-# *project ⋅ main ⋅ 12s ⋅ 6% ⋅ gpt-6-luna*",
60
+ "-# *main ⋅ 12s ⋅ 6% ⋅ gpt-6-luna*",
61
61
  ]
62
62
  `)
63
63
  expect(view.children).toMatchInlineSnapshot(`
@@ -76,12 +76,12 @@ test('task-parallel: background children show start and end lines only, no foote
76
76
  const { effects, view } = replay({ events: loadFixture('task-parallel.events.jsonl') })
77
77
  expect(effectLines(effects)).toMatchInlineSnapshot(`
78
78
  [
79
- "» **user:** In a single step, call the subagent tool twice in parallel, both with agent "general": one to count the .ts files in cli/src/commands using a shell command, one to list the top-level folders of the repo with a shell command. Then reply with both results in 2 lines.",
79
+ "-# » **user:** In a single step, call the subagent tool twice in parallel, both with agent "general": one to count the .ts files in cli/src/commands using a shell command, one to list the top-level folders of the repo with a shell command. Then reply with both results in 2 lines.",
80
80
  "[typing on]",
81
81
  "-# *using openai/gpt-6-luna low ⋅ build*",
82
82
  "-# ┣ general **Count command TypeScript** (background)",
83
83
  "-# ┣ general **List repo folders** (background)",
84
- "-# ⬦ general finished: Count command TypeScript",
84
+ "-# general finished: Count command TypeScript",
85
85
  "\\nBoth tasks are running. I’ll report the two results when they finish.",
86
86
  "\\n-# ┣ general **List repo folders**",
87
87
  "-# ┣ general ⋅ shell _find . -maxdepth 1 -type d -not -name . -print \\| sed 's#^./##' \\| sort_",
@@ -115,16 +115,16 @@ test('abort: no error line for the aborted tool, no footer, next turn is normal'
115
115
  const { effects } = replay({ events: loadFixture('abort.events.jsonl') })
116
116
  expect(effectLines(effects)).toMatchInlineSnapshot(`
117
117
  [
118
- "» **user:** Run \`sleep 30 && echo never\` with bash, then summarize.",
118
+ "-# » **user:** Run \`sleep 30 && echo never\` with bash, then summarize.",
119
119
  "[typing on]",
120
120
  "-# *using openai/gpt-6-luna low ⋅ build*",
121
121
  "-# ┣ shell _sleep 30 && echo never_",
122
122
  "[typing off]",
123
- "» **user:** Say only: resumed after abort.",
123
+ "-# » **user:** Say only: resumed after abort.",
124
124
  "[typing on]",
125
125
  "resumed after abort",
126
126
  "[typing off]",
127
- "-# *project ⋅ main ⋅ 1s ⋅ 6% ⋅ gpt-6-luna*",
127
+ "-# *main ⋅ 1s ⋅ 6% ⋅ gpt-6-luna*",
128
128
  ]
129
129
  `)
130
130
  })
@@ -185,23 +185,25 @@ test('queue-plain: acks with positions while busy, silent footer before each del
185
185
  const { effects, view } = replay({ events: loadFixture('queue-plain.events.jsonl') })
186
186
  expect(effectLines(effects)).toMatchInlineSnapshot(`
187
187
  [
188
- "» **user:** Run \`sleep 6 && echo slow-done\` with bash, then say done.",
188
+ "-# » **user:** Run \`sleep 6 && echo slow-done\` with bash, then say done.",
189
189
  "[typing on]",
190
190
  "-# *using openai/gpt-6-luna low ⋅ build*",
191
191
  "-# ┣ shell _sleep 6 && echo slow-done_",
192
- "[show queue:msg_0f20347e1001nsJ7W4hOvhBpQb] -# Queued at position 1. Delete the original message to remove it, or use /clear-queue position:1",
193
- "[show queue:msg_0f20347e500196F4o0voD7LGMV] -# Queued at position 2. Delete the original message to remove it, or use /clear-queue position:2",
192
+ "[show queue:msg_0f20347e1001nsJ7W4hOvhBpQb] -# » **user:** QUEUED-1: reply with the word apple.
193
+ -# Queued at position 1. Remove it with /clear-queue position:1",
194
+ "[show queue:msg_0f20347e500196F4o0voD7LGMV] -# » **user:** QUEUED-2: reply with the word cherry.
195
+ -# Queued at position 2. Remove it with /clear-queue position:2",
194
196
  "\\nDone.",
195
- "[silent] -# *project ⋅ main ⋅ 10s ⋅ 6% ⋅ gpt-6-luna*",
196
- "[edit queue:msg_0f20347e1001nsJ7W4hOvhBpQb] -# Queued message sent",
197
- "» **user:** QUEUED-1: reply with the word apple.",
197
+ "[silent] -# *main ⋅ 10s ⋅ 6% ⋅ gpt-6-luna*",
198
+ "[edit queue:msg_0f20347e1001nsJ7W4hOvhBpQb] -# » **user:** QUEUED-1: reply with the word apple.",
199
+ "[reply to queue:msg_0f20347e1001nsJ7W4hOvhBpQb] -# Queued message sent",
198
200
  "apple",
199
- "[silent] -# *project ⋅ main ⋅ 2s ⋅ 6% ⋅ gpt-6-luna*",
200
- "[edit queue:msg_0f20347e500196F4o0voD7LGMV] -# Queued message sent",
201
- "» **user:** QUEUED-2: reply with the word cherry.",
201
+ "[silent] -# *main ⋅ 2s ⋅ 6% ⋅ gpt-6-luna*",
202
+ "[edit queue:msg_0f20347e500196F4o0voD7LGMV] -# » **user:** QUEUED-2: reply with the word cherry.",
203
+ "[reply to queue:msg_0f20347e500196F4o0voD7LGMV] -# Queued message sent",
202
204
  "cherry",
203
205
  "[typing off]",
204
- "-# *project ⋅ main ⋅ 2s ⋅ 6% ⋅ gpt-6-luna*",
206
+ "-# *main ⋅ 2s ⋅ 6% ⋅ gpt-6-luna*",
205
207
  ]
206
208
  `)
207
209
  expect(view.inbox).toMatchInlineSnapshot(`[]`)
@@ -211,21 +213,25 @@ test('steer-queue: cancelled item edits its ack, interrupted run has no footer',
211
213
  const { effects, view } = replay({ events: loadFixture('steer-queue.events.jsonl') })
212
214
  expect(effectLines(effects)).toMatchInlineSnapshot(`
213
215
  [
214
- "» **user:** Run \`sleep 8 && echo slow-done\` with bash, then say done.",
216
+ "-# » **user:** Run \`sleep 8 && echo slow-done\` with bash, then say done.",
215
217
  "[typing on]",
216
218
  "-# *using openai/gpt-6-luna low ⋅ build*",
217
219
  "-# ┣ shell _sleep 8 && echo slow-done_",
218
- "[show queue:msg_0f200f884001tIpWlHhNB8Y5bv] -# Queued at position 1. Delete the original message to remove it, or use /clear-queue position:1",
219
- "[show queue:msg_kimaki_queued_b_test1] -# Queued at position 2. Delete the original message to remove it, or use /clear-queue position:2",
220
- "[show queue:msg_0f200f88a001SXny4FyIMuMPS6] -# Queued at position 3. Delete the original message to remove it, or use /clear-queue position:3",
221
- "[edit queue:msg_kimaki_queued_b_test1] -# Removed from queue",
222
- "» **user:** STEER: actually also tell me the current date using bash \`date\`.",
220
+ "[show queue:msg_0f200f884001tIpWlHhNB8Y5bv] -# » **user:** QUEUED-A: after everything, reply with the word apple.
221
+ -# Queued at position 1. Remove it with /clear-queue position:1",
222
+ "[show queue:msg_kimaki_queued_b_test1] -# » **user:** QUEUED-B: reply with the word banana.
223
+ -# Queued at position 2. Remove it with /clear-queue position:2",
224
+ "[show queue:msg_0f200f88a001SXny4FyIMuMPS6] -# » **user:** QUEUED-C: reply with the word cherry.
225
+ -# Queued at position 3. Remove it with /clear-queue position:3",
226
+ "[edit queue:msg_kimaki_queued_b_test1] -# » **user:** QUEUED-B: reply with the word banana.
227
+ -# Removed from queue",
228
+ "-# » **user:** STEER: actually also tell me the current date using bash \`date\`.",
223
229
  "[typing off]",
224
230
  "[typing on]",
225
231
  "-# ┣ shell _date_",
226
232
  "\\nDone. The current date is Wed Sep 30 2026.",
227
233
  "[typing off]",
228
- "-# *project ⋅ main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
234
+ "-# *main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
229
235
  ]
230
236
  `)
231
237
  // The recorded order (prompt, then interrupt with resume) parks queued items.
@@ -241,21 +247,22 @@ test('queue-parked: parked item runs and echoes after the next prompt, with a fo
241
247
  const { effects } = replay({ events: loadFixture('queue-parked.events.jsonl') })
242
248
  expect(effectLines(effects)).toMatchInlineSnapshot(`
243
249
  [
244
- "» **user:** Run \`sleep 6 && echo slow-done\` with bash, then say done.",
250
+ "-# » **user:** Run \`sleep 6 && echo slow-done\` with bash, then say done.",
245
251
  "[typing on]",
246
252
  "-# *using openai/gpt-6-luna low ⋅ build*",
247
253
  "-# ┣ shell _sleep 6 && echo slow-done_",
248
- "[show queue:msg_0f2039909001oR0GJKPGqxOjml] -# Queued at position 1. Delete the original message to remove it, or use /clear-queue position:1",
254
+ "[show queue:msg_0f2039909001oR0GJKPGqxOjml] -# » **user:** PARKED-1: reply with the word apple.
255
+ -# Queued at position 1. Remove it with /clear-queue position:1",
249
256
  "[typing off]",
250
- "» **user:** NEW: reply with the word kiwi.",
257
+ "-# » **user:** NEW: reply with the word kiwi.",
251
258
  "[typing on]",
252
259
  "kiwi",
253
- "[silent] -# *project ⋅ main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
254
- "[edit queue:msg_0f2039909001oR0GJKPGqxOjml] -# Queued message sent",
255
- "» **user:** PARKED-1: reply with the word apple.",
260
+ "[silent] -# *main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
261
+ "[edit queue:msg_0f2039909001oR0GJKPGqxOjml] -# » **user:** PARKED-1: reply with the word apple.",
262
+ "[reply to queue:msg_0f2039909001oR0GJKPGqxOjml] -# Queued message sent",
256
263
  "apple",
257
264
  "[typing off]",
258
- "-# *project ⋅ main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
265
+ "-# *main ⋅ 3s ⋅ 6% ⋅ gpt-6-luna*",
259
266
  ]
260
267
  `)
261
268
  })
@@ -264,7 +271,7 @@ test('question: one dropdown per question, typing off while waiting, edited on r
264
271
  const { effects, view } = replay({ events: loadFixture('question.events.jsonl') })
265
272
  expect(effectLines(effects)).toMatchInlineSnapshot(`
266
273
  [
267
- "» **user:** Use the question tool to ask me which color I prefer, with options red, green, blue (single choice), and in the same question tool call add a second question asking which fruits I like with options apple, pear, kiwi (multiple choice). After my answer, reply with one line.",
274
+ "-# » **user:** Use the question tool to ask me which color I prefer, with options red, green, blue (single choice), and in the same question tool call add a second question asking which fruits I like with options apple, pear, kiwi (multiple choice). After my answer, reply with one line.",
268
275
  "[typing on]",
269
276
  "-# *using openai/gpt-6-luna low ⋅ build*",
270
277
  "[typing off]",
@@ -279,7 +286,7 @@ test('question: one dropdown per question, typing off while waiting, edited on r
279
286
  ✓ _apple, kiwi_",
280
287
  "You prefer green, and you like apples and kiwi.",
281
288
  "[typing off]",
282
- "-# *project ⋅ main ⋅ 5s ⋅ 6% ⋅ gpt-6-luna*",
289
+ "-# *main ⋅ 5s ⋅ 6% ⋅ gpt-6-luna*",
283
290
  ]
284
291
  `)
285
292
  expect(view.forms).toMatchInlineSnapshot(`{}`)
@@ -289,7 +296,7 @@ test('permission: two requests, the rejected one fails its tool call', () => {
289
296
  const { effects, view } = replay({ events: loadFixture('permission.events.jsonl') })
290
297
  expect(effectLines(effects)).toMatchInlineSnapshot(`
291
298
  [
292
- "» **user:** Run \`echo first\` with bash. Then run \`echo second\` with bash. Then reply with one line describing what happened.",
299
+ "-# » **user:** Run \`echo first\` with bash. Then run \`echo second\` with bash. Then reply with one line describing what happened.",
293
300
  "[typing on]",
294
301
  "-# *using openai/gpt-6-luna low ⋅ build*",
295
302
  "-# ┣ shell _echo first_",
@@ -315,7 +322,7 @@ test('permission: two requests, the rejected one fails its tool call', () => {
315
322
  "-# ⨯ shell _Unable to execute command: echo second_",
316
323
  "\\n\`echo first\` printed \`first\`; \`echo second\` failed to execute.",
317
324
  "[typing off]",
318
- "-# *project ⋅ main ⋅ 5s ⋅ 6% ⋅ gpt-6-luna*",
325
+ "-# *main ⋅ 5s ⋅ 6% ⋅ gpt-6-luna*",
319
326
  ]
320
327
  `)
321
328
  expect(view.permissions).toMatchInlineSnapshot(`{}`)
@@ -323,26 +330,26 @@ test('permission: two requests, the rejected one fails its tool call', () => {
323
330
 
324
331
  test('shell-background: background shell line even without side effects, finished line when its job ends', () => {
325
332
  const events = loadFixture('shell-background.events.jsonl')
326
- const view = emptyView({ sessionId: 'ses_ee56d6caeffech0rMI5NTnkky1', channelId: 'channel', directory: '/project', isNew: false })
327
- const { effects, view: end } = replay({ events, view, prefs: { verbosity: 'tools', contextLimits: {} } })
333
+ const view = emptyView({ sessionId: 'ses_ee56d6caeffech0rMI5NTnkky1', channelId: 'channel', directory: '/project', isNew: false, coordinator: false })
334
+ const { effects, view: end } = replay({ events, view, prefs: { verbosity: 'tools', models: {} } })
328
335
  expect(effectLines(effects)).toMatchInlineSnapshot(`
329
336
  [
330
- "» **user:** start a task in background that runs sleep 100",
337
+ "-# » **user:** start a task in background that runs sleep 100",
331
338
  "[typing on]",
332
339
  "-# ┣ shell _Run sleep 100 in background_ (background)",
333
340
  "\\nI started \`sleep 100\` in the background (shell ID \`sh_11a929cf5001mAukdL6rQFIbLg\`). It ends in about 100 seconds, and I get a notification when it does.",
334
341
  "[typing off]",
335
- "-# *project ⋅ main ⋅ 4s ⋅ claude-opus-5-5 ⋅ opus*",
336
- "» **user:** is it still running",
342
+ "-# *main ⋅ 4s ⋅ claude-opus-5-5 ⋅ opus*",
343
+ "-# » **user:** is it still running",
337
344
  "[typing on]",
338
345
  "Yes, it is still running (PID 47861). I have not had the completion notification yet.",
339
346
  "[typing off]",
340
- "-# *project ⋅ main ⋅ 5s ⋅ claude-opus-5-5 ⋅ opus*",
341
- "-# ⬦ background shell finished: _Run sleep 100 in background_",
347
+ "-# *main ⋅ 5s ⋅ claude-opus-5-5 ⋅ opus*",
348
+ "-# background shell finished: _Run sleep 100 in background_",
342
349
  "[typing on]",
343
350
  "\\nThe background \`sleep 100\` has finished. It printed no output.",
344
351
  "[typing off]",
345
- "-# *project ⋅ main ⋅ 2s ⋅ claude-opus-5-5 ⋅ opus*",
352
+ "-# *main ⋅ 2s ⋅ claude-opus-5-5 ⋅ opus*",
346
353
  ]
347
354
  `)
348
355
  expect(end.shells).toMatchInlineSnapshot(`{}`)
@@ -357,8 +364,8 @@ test('shell: user !cmd output while idle and while busy, synthetic items render
357
364
  user-shell-idle
358
365
  05de62d feat: make external opencode session sync opt-in via --enable-sync
359
366
  \`\`\`",
360
- "» **user:** CONTEXT ONLY: the deploy tag is v9.9.9",
361
- "» **user:** What did my shell command print, and what is the deploy tag? Also run \`sleep 5\` with bash first.",
367
+ "-# » **user:** CONTEXT ONLY: the deploy tag is v9.9.9",
368
+ "-# » **user:** What did my shell command print, and what is the deploy tag? Also run \`sleep 5\` with bash first.",
362
369
  "[typing on]",
363
370
  "-# *using openai/gpt-6-luna low ⋅ build*",
364
371
  "-# ┣ shell _sleep 5_",
@@ -368,7 +375,7 @@ test('shell: user !cmd output while idle and while busy, synthetic items render
368
375
  \`\`\`",
369
376
  "\\nYour shell command printed \`user-shell-idle\` and \`05de62d feat: make external opencode session sync opt-in via --enable-sync\`. The deploy tag is \`v9.9.9\`.",
370
377
  "[typing off]",
371
- "-# *project ⋅ main ⋅ 9s ⋅ 6% ⋅ gpt-6-luna*",
378
+ "-# *main ⋅ 9s ⋅ 6% ⋅ gpt-6-luna*",
372
379
  ]
373
380
  `)
374
381
  })