kimaki 1.3.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 (346) 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 +13 -6
  15. package/dist/bot.js +17 -15
  16. package/dist/bot.js.map +1 -1
  17. package/dist/btw.e2e.test.js +3 -3
  18. package/dist/builtin-skills.d.ts +3 -0
  19. package/dist/builtin-skills.js +29 -0
  20. package/dist/builtin-skills.js.map +1 -1
  21. package/dist/builtin-skills.test.js +72 -1
  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.d.ts +1 -1
  25. package/dist/cli/bot.js +33 -18
  26. package/dist/cli/bot.js.map +1 -1
  27. package/dist/cli/discord.js +32 -6
  28. package/dist/cli/discord.js.map +1 -1
  29. package/dist/cli/schedule.js +23 -9
  30. package/dist/cli/schedule.js.map +1 -1
  31. package/dist/cli/session.js +37 -12
  32. package/dist/cli/session.js.map +1 -1
  33. package/dist/cli/shared.d.ts +5 -3
  34. package/dist/cli/shared.js +45 -10
  35. package/dist/cli/shared.js.map +1 -1
  36. package/dist/cli-discord.e2e.test.js +13 -8
  37. package/dist/cli-discord.e2e.test.js.map +1 -1
  38. package/dist/cli-lock.e2e.test.js +87 -20
  39. package/dist/cli-lock.e2e.test.js.map +1 -1
  40. package/dist/cli-read.e2e.test.js +18 -9
  41. package/dist/cli-read.e2e.test.js.map +1 -1
  42. package/dist/cli-sessions.e2e.test.js +4 -3
  43. package/dist/cli-sessions.e2e.test.js.map +1 -1
  44. package/dist/cli.js +2 -0
  45. package/dist/cli.js.map +1 -1
  46. package/dist/commands/preference-commands.d.ts +4 -0
  47. package/dist/commands/preference-commands.js +38 -4
  48. package/dist/commands/preference-commands.js.map +1 -1
  49. package/dist/commands/session-commands.js +11 -8
  50. package/dist/commands/session-commands.js.map +1 -1
  51. package/dist/commands/thread-commands.js +94 -24
  52. package/dist/commands/thread-commands.js.map +1 -1
  53. package/dist/commands/worktree-commands.d.ts +5 -6
  54. package/dist/commands/worktree-commands.js +8 -7
  55. package/dist/commands/worktree-commands.js.map +1 -1
  56. package/dist/commands.e2e.test.js +25 -21
  57. package/dist/commands.e2e.test.js.map +1 -1
  58. package/dist/coordinator-agents.e2e.test.d.ts +1 -0
  59. package/dist/coordinator-agents.e2e.test.js +113 -0
  60. package/dist/coordinator-agents.e2e.test.js.map +1 -0
  61. package/dist/coordinator-idle.e2e.test.d.ts +1 -0
  62. package/dist/coordinator-idle.e2e.test.js +100 -0
  63. package/dist/coordinator-idle.e2e.test.js.map +1 -0
  64. package/dist/coordinator-queue.e2e.test.d.ts +1 -0
  65. package/dist/coordinator-queue.e2e.test.js +244 -0
  66. package/dist/coordinator-queue.e2e.test.js.map +1 -0
  67. package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
  68. package/dist/coordinator-warmup.e2e.test.js +131 -0
  69. package/dist/coordinator-warmup.e2e.test.js.map +1 -0
  70. package/dist/coordinator.d.ts +6 -0
  71. package/dist/coordinator.e2e.test.d.ts +1 -0
  72. package/dist/coordinator.e2e.test.js +505 -0
  73. package/dist/coordinator.e2e.test.js.map +1 -0
  74. package/dist/coordinator.js +164 -0
  75. package/dist/coordinator.js.map +1 -0
  76. package/dist/db.d.ts +78 -3
  77. package/dist/db.test.js +1 -0
  78. package/dist/db.test.js.map +1 -1
  79. package/dist/diagram.d.ts +11 -0
  80. package/dist/diagram.js +232 -0
  81. package/dist/diagram.js.map +1 -0
  82. package/dist/diagram.test.d.ts +1 -0
  83. package/dist/diagram.test.js +52 -0
  84. package/dist/diagram.test.js.map +1 -0
  85. package/dist/duration.js +13 -5
  86. package/dist/duration.js.map +1 -1
  87. package/dist/effects.d.ts +6 -1
  88. package/dist/effects.js +97 -26
  89. package/dist/effects.js.map +1 -1
  90. package/dist/event-loop.d.ts +2 -3
  91. package/dist/event-loop.js +74 -44
  92. package/dist/event-loop.js.map +1 -1
  93. package/dist/format-parts.d.ts +21 -4
  94. package/dist/format-parts.js +40 -23
  95. package/dist/format-parts.js.map +1 -1
  96. package/dist/gateway-replay.e2e.test.js +4 -4
  97. package/dist/harness.e2e.test.js +2 -2
  98. package/dist/harness.e2e.test.js.map +1 -1
  99. package/dist/hello-thread.e2e.test.js +56 -5
  100. package/dist/hello-thread.e2e.test.js.map +1 -1
  101. package/dist/ingress.js +47 -12
  102. package/dist/ingress.js.map +1 -1
  103. package/dist/interaction-context.d.ts +4 -4
  104. package/dist/interaction-context.js +9 -7
  105. package/dist/interaction-context.js.map +1 -1
  106. package/dist/interrupt.e2e.test.js +8 -5
  107. package/dist/interrupt.e2e.test.js.map +1 -1
  108. package/dist/legacy.e2e.test.js +1 -1
  109. package/dist/lock-routes.d.ts +83 -31
  110. package/dist/lock-routes.js +68 -8
  111. package/dist/lock-routes.js.map +1 -1
  112. package/dist/lock-server.d.ts +3 -2
  113. package/dist/lock-server.js +23 -7
  114. package/dist/lock-server.js.map +1 -1
  115. package/dist/main.d.ts +6 -3
  116. package/dist/main.js +14 -4
  117. package/dist/main.js.map +1 -1
  118. package/dist/mainmast-idle.e2e.test.d.ts +1 -0
  119. package/dist/mainmast-idle.e2e.test.js +99 -0
  120. package/dist/mainmast-idle.e2e.test.js.map +1 -0
  121. package/dist/mainmast-queue.e2e.test.d.ts +1 -0
  122. package/dist/mainmast-queue.e2e.test.js +244 -0
  123. package/dist/mainmast-queue.e2e.test.js.map +1 -0
  124. package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
  125. package/dist/mainmast-warmup.e2e.test.js +130 -0
  126. package/dist/mainmast-warmup.e2e.test.js.map +1 -0
  127. package/dist/mainmast.d.ts +6 -0
  128. package/dist/mainmast.e2e.test.d.ts +1 -0
  129. package/dist/mainmast.e2e.test.js +428 -0
  130. package/dist/mainmast.e2e.test.js.map +1 -0
  131. package/dist/mainmast.js +156 -0
  132. package/dist/mainmast.js.map +1 -0
  133. package/dist/markdown/markdown.test.js +14 -4
  134. package/dist/markdown/markdown.test.js.map +1 -1
  135. package/dist/markdown/render-markdown.js +9 -6
  136. package/dist/markdown/render-markdown.js.map +1 -1
  137. package/dist/onboarding-cli.e2e.test.js +7 -6
  138. package/dist/onboarding-cli.e2e.test.js.map +1 -1
  139. package/dist/onboarding.d.ts +3 -10
  140. package/dist/onboarding.e2e.test.js +8 -10
  141. package/dist/onboarding.e2e.test.js.map +1 -1
  142. package/dist/onboarding.js +14 -24
  143. package/dist/onboarding.js.map +1 -1
  144. package/dist/permissions.e2e.test.js +3 -3
  145. package/dist/plugin/coordinator.d.ts +4 -0
  146. package/dist/plugin/coordinator.js +389 -0
  147. package/dist/plugin/coordinator.js.map +1 -0
  148. package/dist/plugin/index.js +93 -61
  149. package/dist/plugin/index.js.map +1 -1
  150. package/dist/plugin/mainmast.d.ts +4 -0
  151. package/dist/plugin/mainmast.js +379 -0
  152. package/dist/plugin/mainmast.js.map +1 -0
  153. package/dist/plugin.e2e.test.js +1 -1
  154. package/dist/preference-commands.e2e.test.js +103 -24
  155. package/dist/preference-commands.e2e.test.js.map +1 -1
  156. package/dist/project.d.ts +1 -1
  157. package/dist/prompt.d.ts +22 -9
  158. package/dist/prompt.js +64 -48
  159. package/dist/prompt.js.map +1 -1
  160. package/dist/questions.e2e.test.js +13 -13
  161. package/dist/queue-sources.e2e.test.d.ts +1 -0
  162. package/dist/queue-sources.e2e.test.js +168 -0
  163. package/dist/queue-sources.e2e.test.js.map +1 -0
  164. package/dist/queue.d.ts +9 -1
  165. package/dist/queue.e2e.test.js +48 -30
  166. package/dist/queue.e2e.test.js.map +1 -1
  167. package/dist/queue.js +97 -24
  168. package/dist/queue.js.map +1 -1
  169. package/dist/queue.test.d.ts +1 -0
  170. package/dist/queue.test.js +57 -0
  171. package/dist/queue.test.js.map +1 -0
  172. package/dist/remote-send.e2e.test.js +6 -6
  173. package/dist/remote-send.js +2 -1
  174. package/dist/remote-send.js.map +1 -1
  175. package/dist/renderer.e2e.test.js +7 -7
  176. package/dist/restart.e2e.test.js +2 -2
  177. package/dist/routines.e2e.test.js +2 -2
  178. package/dist/scheduler.d.ts +3 -3
  179. package/dist/scheduler.e2e.test.js +43 -20
  180. package/dist/scheduler.e2e.test.js.map +1 -1
  181. package/dist/scheduler.js +4 -2
  182. package/dist/scheduler.js.map +1 -1
  183. package/dist/schema-sql.d.ts +1 -1
  184. package/dist/schema-sql.js +1 -1
  185. package/dist/schema-sql.js.map +1 -1
  186. package/dist/schema.d.ts +152 -6
  187. package/dist/schema.js +12 -0
  188. package/dist/schema.js.map +1 -1
  189. package/dist/session-commands.e2e.test.js +36 -11
  190. package/dist/session-commands.e2e.test.js.map +1 -1
  191. package/dist/session-events.d.ts +18 -6
  192. package/dist/session-events.js +175 -27
  193. package/dist/session-events.js.map +1 -1
  194. package/dist/session-ops.e2e.test.js +63 -12
  195. package/dist/session-ops.e2e.test.js.map +1 -1
  196. package/dist/sessions.d.ts +44 -21
  197. package/dist/sessions.js +177 -65
  198. package/dist/sessions.js.map +1 -1
  199. package/dist/shell.e2e.test.js +4 -4
  200. package/dist/slash-commands.js +6 -4
  201. package/dist/slash-commands.js.map +1 -1
  202. package/dist/slash-commands.test.js +2 -0
  203. package/dist/slash-commands.test.js.map +1 -1
  204. package/dist/sleep.e2e.test.js +29 -14
  205. package/dist/sleep.e2e.test.js.map +1 -1
  206. package/dist/sleeps.d.ts +2 -1
  207. package/dist/sleeps.js +21 -3
  208. package/dist/sleeps.js.map +1 -1
  209. package/dist/store.d.ts +1 -0
  210. package/dist/store.js +3 -2
  211. package/dist/store.js.map +1 -1
  212. package/dist/system-prompt.d.ts +5 -15
  213. package/dist/system-prompt.js +180 -131
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/test/generate-log-plugin/index.d.ts +3 -0
  216. package/dist/test/generate-log-plugin/index.js +15 -0
  217. package/dist/test/generate-log-plugin/index.js.map +1 -0
  218. package/dist/test/harness.d.ts +20 -2
  219. package/dist/test/harness.js +51 -11
  220. package/dist/test/harness.js.map +1 -1
  221. package/dist/test/replay.js +11 -8
  222. package/dist/test/replay.js.map +1 -1
  223. package/dist/thread-reducer.d.ts +29 -6
  224. package/dist/thread-reducer.fixtures.test.js +58 -51
  225. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  226. package/dist/thread-reducer.js +109 -50
  227. package/dist/thread-reducer.js.map +1 -1
  228. package/dist/thread-reducer.test.js +190 -48
  229. package/dist/thread-reducer.test.js.map +1 -1
  230. package/dist/tunnel.d.ts +9 -0
  231. package/dist/tunnel.js +117 -28
  232. package/dist/tunnel.js.map +1 -1
  233. package/dist/tunnel.test.js +81 -5
  234. package/dist/tunnel.test.js.map +1 -1
  235. package/dist/voice-attachments.e2e.test.js +108 -18
  236. package/dist/voice-attachments.e2e.test.js.map +1 -1
  237. package/dist/voice-call.e2e.test.js +9 -9
  238. package/dist/voice-call.js +6 -6
  239. package/dist/voice-call.js.map +1 -1
  240. package/dist/voice-openai.e2e.test.js +59 -23
  241. package/dist/voice-openai.e2e.test.js.map +1 -1
  242. package/dist/voice.d.ts +1 -1
  243. package/dist/voice.js +72 -24
  244. package/dist/voice.js.map +1 -1
  245. package/dist/working-directory.e2e.test.js +11 -11
  246. package/dist/working-directory.e2e.test.js.map +1 -1
  247. package/package.json +11 -5
  248. package/skills/kimaki-tunnel/SKILL.md +3 -2
  249. package/src/agent-ui.e2e.test.ts +27 -13
  250. package/src/agent-ui.ts +2 -1
  251. package/src/analytics.e2e.test.ts +1 -1
  252. package/src/autostart.ts +55 -10
  253. package/src/bot.ts +36 -19
  254. package/src/btw.e2e.test.ts +3 -3
  255. package/src/builtin-skills.test.ts +73 -1
  256. package/src/builtin-skills.ts +33 -0
  257. package/src/catalog-refresh.e2e.test.ts +2 -2
  258. package/src/cli/bot.ts +30 -19
  259. package/src/cli/discord.ts +30 -6
  260. package/src/cli/schedule.ts +23 -9
  261. package/src/cli/session.ts +35 -12
  262. package/src/cli/shared.ts +43 -8
  263. package/src/cli-discord.e2e.test.ts +13 -8
  264. package/src/cli-lock.e2e.test.ts +88 -20
  265. package/src/cli-read.e2e.test.ts +17 -8
  266. package/src/cli-sessions.e2e.test.ts +4 -3
  267. package/src/cli.ts +2 -0
  268. package/src/commands/preference-commands.ts +38 -4
  269. package/src/commands/session-commands.ts +12 -7
  270. package/src/commands/thread-commands.ts +92 -24
  271. package/src/commands/worktree-commands.ts +10 -9
  272. package/src/commands.e2e.test.ts +25 -21
  273. package/src/coordinator-agents.e2e.test.ts +145 -0
  274. package/src/coordinator-idle.e2e.test.ts +122 -0
  275. package/src/coordinator-queue.e2e.test.ts +279 -0
  276. package/src/coordinator-warmup.e2e.test.ts +158 -0
  277. package/src/coordinator.e2e.test.ts +545 -0
  278. package/src/coordinator.ts +168 -0
  279. package/src/db.test.ts +1 -0
  280. package/src/diagram.test.ts +56 -0
  281. package/src/diagram.ts +243 -0
  282. package/src/duration.ts +13 -5
  283. package/src/effects.ts +99 -27
  284. package/src/event-loop.ts +71 -45
  285. package/src/format-parts.ts +53 -22
  286. package/src/gateway-replay.e2e.test.ts +4 -4
  287. package/src/harness.e2e.test.ts +2 -2
  288. package/src/hello-thread.e2e.test.ts +59 -5
  289. package/src/ingress.ts +44 -11
  290. package/src/interaction-context.ts +12 -11
  291. package/src/interrupt.e2e.test.ts +8 -5
  292. package/src/legacy.e2e.test.ts +1 -1
  293. package/src/lock-routes.ts +62 -8
  294. package/src/lock-server.ts +23 -7
  295. package/src/main.ts +26 -10
  296. package/src/markdown/markdown.test.ts +15 -4
  297. package/src/markdown/render-markdown.ts +9 -6
  298. package/src/onboarding-cli.e2e.test.ts +7 -6
  299. package/src/onboarding.e2e.test.ts +8 -10
  300. package/src/onboarding.ts +13 -29
  301. package/src/permissions.e2e.test.ts +3 -3
  302. package/src/plugin/coordinator.ts +401 -0
  303. package/src/plugin/index.ts +86 -59
  304. package/src/plugin.e2e.test.ts +1 -1
  305. package/src/preference-commands.e2e.test.ts +103 -23
  306. package/src/prompt.ts +87 -43
  307. package/src/questions.e2e.test.ts +13 -13
  308. package/src/queue-sources.e2e.test.ts +197 -0
  309. package/src/queue.e2e.test.ts +48 -30
  310. package/src/queue.test.ts +59 -0
  311. package/src/queue.ts +104 -24
  312. package/src/remote-send.e2e.test.ts +6 -6
  313. package/src/remote-send.ts +2 -1
  314. package/src/renderer.e2e.test.ts +7 -7
  315. package/src/restart.e2e.test.ts +2 -2
  316. package/src/routines.e2e.test.ts +2 -2
  317. package/src/scheduler.e2e.test.ts +45 -19
  318. package/src/scheduler.ts +4 -2
  319. package/src/schema-sql.ts +1 -1
  320. package/src/schema.ts +13 -0
  321. package/src/session-commands.e2e.test.ts +38 -11
  322. package/src/session-events.ts +168 -25
  323. package/src/session-ops.e2e.test.ts +64 -11
  324. package/src/sessions.ts +217 -67
  325. package/src/shell.e2e.test.ts +4 -4
  326. package/src/slash-commands.test.ts +2 -0
  327. package/src/slash-commands.ts +6 -4
  328. package/src/sleep.e2e.test.ts +30 -13
  329. package/src/sleeps.ts +20 -3
  330. package/src/store.ts +5 -2
  331. package/src/system-prompt.ts +190 -134
  332. package/src/test/generate-log-plugin/index.ts +16 -0
  333. package/src/test/harness.ts +77 -17
  334. package/src/test/replay.ts +12 -7
  335. package/src/thread-reducer.fixtures.test.ts +58 -51
  336. package/src/thread-reducer.test.ts +199 -49
  337. package/src/thread-reducer.ts +135 -66
  338. package/src/tunnel.test.ts +81 -5
  339. package/src/tunnel.ts +124 -25
  340. package/src/voice-attachments.e2e.test.ts +110 -18
  341. package/src/voice-call.e2e.test.ts +9 -9
  342. package/src/voice-call.ts +6 -6
  343. package/src/voice-openai.e2e.test.ts +64 -24
  344. package/src/voice.ts +93 -26
  345. package/src/working-directory.e2e.test.ts +11 -11
  346. package/src/message-tool.e2e.test.ts +0 -45
package/dist/sessions.js CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import crypto from 'node:crypto';
6
6
  import { setTimeout as sleep } from 'node:timers/promises';
7
7
  import * as orm from 'drizzle-orm';
8
- import { cliContext, globalModel, markerIsCurrent, oc, parseModel, projectOf, readMarker, rootSession, routeText, sessionDirectory, textChannel, } from "./bot.js";
8
+ import { cliContext, globalModel, oc, parseModel, projectChannelOf, projectOf, readMarker, rootSession, routeText, sessionDirectory, textChannel, } from "./bot.js";
9
9
  import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableError } from "./errors.js";
10
10
  import { SILENT_MESSAGE_FLAGS } from "./format-parts.js";
11
11
  import { createLogger } from "./logger.js";
@@ -34,17 +34,17 @@ export function btwPrompt({ text, parentSessionId, sourceThreadId, sessionId, th
34
34
  text,
35
35
  ].join('\n');
36
36
  }
37
- // Project channel, project directory and current cwd of a session thread.
38
- export async function threadProject(bot, thread) {
39
- const channelId = thread.parentId;
37
+ // Project channel, project directory and current cwd of a conversation's root session.
38
+ export async function conversationProject(bot, conversation) {
39
+ const channelId = projectChannelOf(conversation);
40
40
  if (!channelId)
41
- return new DiscordError({ operation: `find the channel of thread ${thread.id}` });
41
+ return new DiscordError({ operation: `find the channel of conversation ${conversation.id}` });
42
42
  const row = await projectOf(bot, channelId);
43
43
  if (row instanceof Error)
44
44
  return row;
45
45
  if (!row)
46
46
  return new DiscordError({ operation: `find the project of channel ${channelId}` });
47
- const sessionId = rootSession(bot, thread.id);
47
+ const sessionId = rootSession(bot, conversation.id);
48
48
  if (sessionId instanceof Error)
49
49
  return sessionId;
50
50
  const directory = await sessionDirectory(bot, sessionId);
@@ -54,8 +54,8 @@ export async function threadProject(bot, thread) {
54
54
  }
55
55
  // /cwd and `kimaki session cwd`: show the cwd, or move the session to a
56
56
  // project subfolder or linked worktree.
57
- export async function sessionCwd(bot, { thread, directory }) {
58
- const project = await threadProject(bot, thread);
57
+ export async function sessionCwd(bot, { conversation, directory }) {
58
+ const project = await conversationProject(bot, conversation);
59
59
  if (project instanceof Error)
60
60
  return project;
61
61
  if (!directory)
@@ -69,7 +69,7 @@ export async function sessionCwd(bot, { thread, directory }) {
69
69
  const plugin = await bot.features.waitForPlugin(destination);
70
70
  if (plugin instanceof Error)
71
71
  return plugin;
72
- const sessionId = rootSession(bot, thread.id);
72
+ const sessionId = rootSession(bot, conversation.id);
73
73
  if (sessionId instanceof Error)
74
74
  return sessionId;
75
75
  const moved = await oc(bot, 'session.move', (client) => client.session.move({ sessionID: sessionId, directory: destination, delivery: 'steer' }));
@@ -95,7 +95,8 @@ export async function catalogReady(bot, directory) {
95
95
  if (ready instanceof Error)
96
96
  return ready;
97
97
  }
98
- // Agents a session may switch to (not subagents, not hidden ones).
98
+ // Agents a session may switch to (not subagents, not hidden ones). OpenCode lists
99
+ // its default agent (`default_agent`, else build) first: core/src/agent.ts `list`.
99
100
  export async function primaryAgents(bot, directory) {
100
101
  const ready = await catalogReady(bot, directory);
101
102
  if (ready instanceof Error)
@@ -105,6 +106,22 @@ export async function primaryAgents(bot, directory) {
105
106
  return agents;
106
107
  return agents.data.filter((agent) => agent.mode !== 'subagent' && !agent.hidden);
107
108
  }
109
+ // What a new session of the channel starts with: its project folder, and the channel agent or OpenCode's default agent.
110
+ export async function channelDefaults(bot, channelId) {
111
+ const row = await bot.db.query.channel_directories
112
+ .findFirst({ where: { channel_id: channelId }, with: { channel_agent: true } })
113
+ .catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
114
+ if (row instanceof Error)
115
+ return row;
116
+ if (!row)
117
+ return null;
118
+ if (row.channel_agent)
119
+ return { projectDirectory: row.directory, defaultAgent: row.channel_agent.agent_name };
120
+ const agents = await primaryAgents(bot, row.directory);
121
+ if (agents instanceof Error)
122
+ return agents;
123
+ return { projectDirectory: row.directory, defaultAgent: agents[0]?.id ?? null };
124
+ }
108
125
  export async function agentSelection(bot, { directory, agent }) {
109
126
  const agents = await primaryAgents(bot, directory);
110
127
  if (agents instanceof Error)
@@ -116,10 +133,17 @@ export async function agentSelection(bot, { directory, agent }) {
116
133
  return { agent: selected.id, model };
117
134
  }
118
135
  // OpenCode switches the agent and model separately, including the configured variant.
119
- export async function switchAgent(bot, { sessionId, directory, agent, model }) {
136
+ export async function switchAgent(bot, { sessionId, directory, agent, model, reselect = false, }) {
120
137
  const selected = await agentSelection(bot, { directory, agent });
121
138
  if (selected instanceof Error)
122
139
  return selected;
140
+ // naming the session's current agent again (agents pass --agent <current_agent>
141
+ // on every send) keeps the model the session runs on, e.g. one set with /model.
142
+ if (!model && !reselect) {
143
+ const info = await oc(bot, 'session.get', (client) => client.session.get({ sessionID: sessionId }));
144
+ if (!(info instanceof Error) && info.agent === selected.agent)
145
+ return { ...selected, model: null };
146
+ }
123
147
  const choice = model ?? selected.model;
124
148
  if (choice) {
125
149
  const switched = await oc(bot, 'session.switchModel', (client) => client.session.switchModel({
@@ -135,8 +159,8 @@ export async function switchAgent(bot, { sessionId, directory, agent, model }) {
135
159
  return { ...selected, model: choice };
136
160
  }
137
161
  // The one durable system instruction of a session (spec 5.4).
138
- async function putInstructions(bot, { sessionId, thread, channel, directory, userId, parentSessionId = null, scheduledTask = null, }) {
139
- // The agent list is optional prompt context: a failure must not strand the thread.
162
+ async function putInstructions(bot, { sessionId, conversation, channel, directory, userId, parentSessionId = null, scheduledTask = null, }) {
163
+ // The agent list is optional prompt context: a failure must not strand the conversation.
140
164
  const found = await primaryAgents(bot, directory);
141
165
  if (found instanceof Error)
142
166
  logger.warn(`agent list for instructions failed`, found);
@@ -144,7 +168,7 @@ async function putInstructions(bot, { sessionId, thread, channel, directory, use
144
168
  const agents = found instanceof Error ? [] : found.map((agent) => ({ name: agent.id, description: agent.description ?? '' }));
145
169
  const value = baseInstructions({
146
170
  sessionId,
147
- threadId: thread.id,
171
+ threadId: conversation.id,
148
172
  channelId: channel.id,
149
173
  guildId: channel.guildId,
150
174
  userId,
@@ -153,6 +177,9 @@ async function putInstructions(bot, { sessionId, thread, channel, directory, use
153
177
  agents,
154
178
  parentSessionId,
155
179
  scheduledTask,
180
+ // Derived from where the session lives, never from bindings that can be stale.
181
+ coordinator: conversation.id === channel.id,
182
+ disabled: bot.disabledPrompt,
156
183
  });
157
184
  const put = await oc(bot, 'instructions.entry.put', (client) => client.session.instructions.entry.put({ sessionID: sessionId, key: INSTRUCTION_KEY, value }));
158
185
  if (put instanceof Error)
@@ -171,6 +198,7 @@ export function createPluginWait(bot) {
171
198
  return;
172
199
  const location = { directory };
173
200
  const deadline = Date.now() + 10_000;
201
+ let loadError;
174
202
  while (Date.now() < deadline) {
175
203
  const endpoint = bot.opencode.endpoint;
176
204
  if (!endpoint)
@@ -190,8 +218,18 @@ export function createPluginWait(bot) {
190
218
  active.set(endpoint, (active.get(endpoint) ?? new Set()).add(directory));
191
219
  return;
192
220
  }
221
+ // A plugin that fails to load has no ID: find the shim directory by its path.
222
+ const failed = plugins.data.find((plugin) => plugin.source.type === 'local' && path.basename(plugin.source.path) === 'kimaki');
223
+ loadError = failed?.state.status === 'failed' ? (failed.state.ref ?? failed.state.error) : undefined;
193
224
  await sleep(200);
194
225
  }
226
+ if (loadError) {
227
+ // TODO: remove the restart hint once OpenCode's Bun evicts failed ESM loads; Bun 1.4.4 keeps
228
+ // them forever, so a reload during `pnpm build` stays broken after the build ends.
229
+ return new ConfigError({
230
+ reason: `Kimaki plugin failed to load in OpenCode (${loadError}); the OpenCode log has the cause. If it names code that no longer exists, OpenCode kept a stale failed load: run \`opencode service restart\``,
231
+ });
232
+ }
195
233
  return new ConfigError({
196
234
  reason: `Kimaki plugin is not active in OpenCode for ${directory}. Kimaki writes it to <OpenCode config dir>/plugins/kimaki/ on start; check \`opencode plugin list\` and the OpenCode logs`,
197
235
  });
@@ -202,7 +240,7 @@ export function createPluginWait(bot) {
202
240
  // entry; sessions of an older bot run point at its old lock port. Checked
203
241
  // on each input instead of for every binding at startup (a V1 install has
204
242
  // thousands of bindings).
205
- export async function ensureSessionMarker(bot, { sessionId, thread, channelId, directory, userId, }) {
243
+ export async function ensureSessionMarker(bot, { sessionId, conversation, channelId, directory, userId, }) {
206
244
  const plugin = await bot.features.waitForPlugin(directory);
207
245
  if (plugin instanceof Error)
208
246
  return plugin;
@@ -210,19 +248,19 @@ export async function ensureSessionMarker(bot, { sessionId, thread, channelId, d
210
248
  if (info instanceof Error)
211
249
  return info;
212
250
  const marker = readMarker(info.metadata);
213
- if (marker && markerIsCurrent(bot, marker))
251
+ if (marker && marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort)
214
252
  return;
215
253
  // Instructions first: the marker means "set up", so a failure here is retried on the next input.
216
254
  if (!marker) {
217
- logger.log(`adopting legacy session ${sessionId} of thread ${thread.id}`);
255
+ logger.log(`adopting legacy session ${sessionId} of conversation ${conversation.id}`);
218
256
  const channel = await textChannel(bot, channelId);
219
257
  if (channel instanceof Error)
220
258
  return channel;
221
- const instructions = await putInstructions(bot, { sessionId, thread, channel, directory, userId });
259
+ const instructions = await putInstructions(bot, { sessionId, conversation, channel, directory, userId });
222
260
  if (instructions instanceof Error)
223
261
  return instructions;
224
262
  }
225
- const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId: thread.id, channelId };
263
+ const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId: conversation.id, channelId };
226
264
  const updated = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: sessionId, metadata: { ...info.metadata, kimaki } }));
227
265
  if (updated instanceof Error)
228
266
  return updated;
@@ -243,7 +281,7 @@ export async function refreshCliContext(bot) {
243
281
  const marker = readMarker(info.metadata);
244
282
  if (!marker)
245
283
  continue;
246
- if (markerIsCurrent(bot, marker))
284
+ if (marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort)
247
285
  continue;
248
286
  const metadata = { ...info.metadata, kimaki: { ...marker.fields, ...cliContext(bot) } };
249
287
  const result = await oc(bot, 'refresh Kimaki CLI context', (client) => client.session.update({ sessionID: sessionId, metadata }));
@@ -251,7 +289,24 @@ export async function refreshCliContext(bot) {
251
289
  return result;
252
290
  }
253
291
  }
254
- export async function startSession(bot, { channelId, directory: requestedDirectory, route, author, messageId, startMessageId = messageId, showInput = startMessageId === null, threadName: explicitName, files = [], model: explicitModel, permissions, parentSessionId, task, worktree, baseBranch, run = true, }) {
292
+ // Agent and model of a new session: an agent the input asked for, else the
293
+ // channel agent; the model from the input, that agent, the channel, the global default.
294
+ async function agentAndModel(bot, { defaults, route, directory, explicitModel }) {
295
+ const requestedAgent = (route.kind === 'steer' && route.agent) || defaults?.channel_agent?.agent_name;
296
+ const selected = requestedAgent ? await agentSelection(bot, { directory, agent: requestedAgent }) : null;
297
+ if (selected instanceof Error)
298
+ return selected;
299
+ const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null;
300
+ const global = await globalModel(bot);
301
+ if (global instanceof Error)
302
+ return global;
303
+ const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global;
304
+ const model = choice
305
+ ? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
306
+ : undefined;
307
+ return { model, agent: selected?.agent };
308
+ }
309
+ export async function startSession(bot, { channelId, directory: requestedDirectory, route, author, members = [], messageId, startMessageId = messageId, showInput = startMessageId === null, threadName: explicitName, files = [], model: explicitModel, permissions, parentSessionId, sender, task, worktree, baseBranch, run = true, }) {
255
310
  const defaults = await bot.db.query.channel_directories
256
311
  .findFirst({ where: { channel_id: channelId }, with: { channel_model: true, channel_agent: true, channel_worktree: true } })
257
312
  .catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
@@ -262,7 +317,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
262
317
  const useWorktree = typeof worktree === 'string' || (worktree !== false && channelWorktrees);
263
318
  const textSlug = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40).replace(/^-|-$/g, '') || 'session';
264
319
  const slug = typeof worktree === 'string' && worktree ? worktree : `${textSlug}-${crypto.randomBytes(4).toString('hex')}`;
265
- const threadName = explicitName ?? (useWorktree ? `⬦ ${slug}` : text.replace(/\s+/g, ' ').slice(0, 80) || 'Kimaki session');
320
+ const threadName = explicitName ?? (useWorktree ? slug : text.replace(/\s+/g, ' ').slice(0, 80) || 'Kimaki session');
266
321
  // The thread shows in Discord while the checkout, plugin, agent and model are resolved.
267
322
  const creating = (async () => {
268
323
  const channel = await textChannel(bot, channelId);
@@ -273,9 +328,12 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
273
328
  .catch((cause) => new DiscordError({ operation: 'create thread', cause }));
274
329
  if (thread instanceof Error)
275
330
  return thread;
276
- // Discord only adds the thread creator (the bot); the user must be added to see it in the sidebar.
277
- if (author.id !== bot.discord.user?.id)
278
- await thread.members.add(author.id).catch((e) => logger.warn(`add member`, e));
331
+ // Discord only adds the thread creator (the bot); users must be added to see it in the sidebar.
332
+ // Not awaited: the session does not need them, and each add is a Discord round trip.
333
+ for (const id of new Set([author.id, ...members])) {
334
+ if (id !== bot.discord.user?.id)
335
+ void thread.members.add(id).catch((e) => logger.warn(`add member ${id}`, e));
336
+ }
279
337
  return { channel, thread };
280
338
  })();
281
339
  const setup = await (async () => {
@@ -288,19 +346,10 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
288
346
  const plugin = await bot.features.waitForPlugin(directory);
289
347
  if (plugin instanceof Error)
290
348
  return plugin;
291
- const requestedAgent = (route.kind === 'steer' && route.agent) || defaults?.channel_agent?.agent_name;
292
- const selected = requestedAgent ? await agentSelection(bot, { directory, agent: requestedAgent }) : null;
293
- if (selected instanceof Error)
294
- return selected;
295
- const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null;
296
- const global = await globalModel(bot);
297
- if (global instanceof Error)
298
- return global;
299
- const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global;
300
- const model = choice
301
- ? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
302
- : undefined;
303
- return { checkout, directory, model, agent: selected?.agent };
349
+ const chosen = await agentAndModel(bot, { defaults, route, directory, explicitModel });
350
+ if (chosen instanceof Error)
351
+ return chosen;
352
+ return { checkout, directory, ...chosen };
304
353
  })();
305
354
  const created = await creating;
306
355
  if (created instanceof Error)
@@ -316,6 +365,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
316
365
  channelId,
317
366
  source: task ? 'task' : 'discord',
318
367
  ...(task && { taskId: task.id, task }),
368
+ ...(bot.warmupTimers && { warmup: bot.warmupTimers }),
319
369
  ...cliContext(bot),
320
370
  ...(parentSessionId && { parentSessionId }),
321
371
  };
@@ -332,7 +382,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
332
382
  bot.analytics.track('session_created', { has_worktree: Boolean(checkout), source: 'discord' });
333
383
  const instructions = await putInstructions(bot, {
334
384
  sessionId: session.id,
335
- thread,
385
+ conversation: thread,
336
386
  channel,
337
387
  directory,
338
388
  userId: author.id,
@@ -345,9 +395,10 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
345
395
  if (bound instanceof Error)
346
396
  return bound;
347
397
  if (!run) {
398
+ const ready = checkout ? `Worktree ready: \`${directory}\`\nBranch: \`${checkout.branch ?? 'detached'}\`\n` : '';
348
399
  const posted = await thread
349
400
  .send({
350
- content: `Worktree ready: \`${directory}\`\nBranch: \`${checkout?.branch ?? 'detached'}\`\nSend a message to start working.`,
401
+ content: `${ready}Send a message to start working.`,
351
402
  allowedMentions: { parse: [] },
352
403
  flags: SILENT_MESSAGE_FLAGS,
353
404
  })
@@ -359,26 +410,78 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
359
410
  if (showInput) {
360
411
  const names = files.map((file) => file.name).join(', ');
361
412
  const shown = names ? `${text}\nFiles: ${names}` : text;
362
- const echo = await thread
363
- .send({ content: formatEcho({ username: author.username, text: shown }), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
364
- .catch((cause) => new DiscordError({ operation: 'send first input', cause }));
365
- if (echo instanceof Error)
366
- return echo;
413
+ // Through the thread's effect queue, so the prompt does not wait for Discord,
414
+ // the echo comes before the session output and a long prompt is split.
415
+ bot.effects.run(thread.id, [{ type: 'send', text: formatEcho({ username: author.username, text: shown }), echo: true }]);
367
416
  }
368
- const first = route.kind === 'steer' ? { kind: 'steer', text: route.text } : route;
417
+ const base = { sessionId: session.id, threadId: thread.id, threadName, author, messageId, files, sender };
418
+ // A new session has no run, question or permission to cancel: prompt directly instead of steer().
419
+ const sent = route.kind === 'steer'
420
+ ? await prompt(bot, { ...base, text: route.text, delivery: 'steer' })
421
+ : await runInSession(bot, { ...base, directory, route });
422
+ if (sent instanceof Error)
423
+ return sent;
424
+ return { threadId: thread.id, sessionId: session.id };
425
+ }
426
+ // The first message of a coordinator channel: one long session that lives in the
427
+ // channel itself, bound like a thread with the channel ID as its conversation.
428
+ export async function startCoordinator(bot, { channel, route, author, messageId, files = [], }) {
429
+ const defaults = await bot.db.query.channel_directories
430
+ .findFirst({ where: { channel_id: channel.id }, with: { channel_model: true, channel_agent: true } })
431
+ .catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
432
+ if (defaults instanceof Error)
433
+ return defaults;
434
+ if (!defaults)
435
+ return new ConfigError({ reason: 'This channel is not configured with a project directory' });
436
+ const directory = defaults.directory;
437
+ const plugin = await bot.features.waitForPlugin(directory);
438
+ if (plugin instanceof Error)
439
+ return plugin;
440
+ const chosen = await agentAndModel(bot, { defaults, route, directory });
441
+ if (chosen instanceof Error)
442
+ return chosen;
443
+ const title = `coordinator: ${channel.name}`;
444
+ const session = await oc(bot, 'session.create', (client) => client.session.create({
445
+ title,
446
+ location: { directory },
447
+ ...(chosen.model && { model: chosen.model }),
448
+ ...(chosen.agent && { agent: chosen.agent }),
449
+ permissions: [NO_QUESTION_TOOL, ...disabledSkillRules(bot.disabledSkills)],
450
+ metadata: { kimaki: { threadId: channel.id, channelId: channel.id, source: 'discord', ...(bot.warmupTimers && { warmup: bot.warmupTimers }), ...cliContext(bot) } },
451
+ }));
452
+ if (session instanceof Error)
453
+ return session;
454
+ bot.analytics.track('session_created', { has_worktree: false, source: 'discord' });
455
+ const instructions = await putInstructions(bot, { sessionId: session.id, conversation: channel, channel, directory, userId: author.id });
456
+ if (instructions instanceof Error)
457
+ return instructions;
458
+ const bound = await bindThread(bot, { threadId: channel.id, sessionId: session.id, channelId: channel.id, directory, isNew: true });
459
+ if (bound instanceof Error)
460
+ return bound;
369
461
  const sent = await runInSession(bot, {
370
462
  sessionId: session.id,
371
- threadId: thread.id,
372
- threadName,
463
+ threadId: channel.id,
464
+ threadName: channel.name,
373
465
  directory,
374
- route: first,
466
+ route,
375
467
  author,
376
468
  messageId,
377
469
  files,
378
470
  });
379
471
  if (sent instanceof Error)
380
472
  return sent;
381
- return { threadId: thread.id, sessionId: session.id };
473
+ return { threadId: channel.id, sessionId: session.id };
474
+ }
475
+ // /reset-coordinator: the channel forgets its coordinator. The next message starts a fresh
476
+ // one; the old session stays in OpenCode for /resume.
477
+ export async function endCoordinator(bot, channelId) {
478
+ const deleted = await bot.db
479
+ .delete(schema.thread_sessions)
480
+ .where(orm.eq(schema.thread_sessions.thread_id, channelId))
481
+ .catch((cause) => new DbError({ operation: 'delete coordinator binding', cause }));
482
+ if (deleted instanceof Error)
483
+ return deleted;
484
+ bot.eventLoop.unbind(channelId);
382
485
  }
383
486
  // --- Sessions that get a new thread: /resume, /fork, /fork-subagent, btw.
384
487
  // Binds an existing session to a new thread. Order matters:
@@ -415,7 +518,7 @@ async function adoptSession(bot, { channel, session, threadName, intro, note, au
415
518
  return marked;
416
519
  const instructions = await putInstructions(bot, {
417
520
  sessionId,
418
- thread,
521
+ conversation: thread,
419
522
  channel,
420
523
  directory: session.location.directory,
421
524
  userId: author.id,
@@ -519,8 +622,8 @@ export async function resume(bot, { channelId, sessionId, author }) {
519
622
  }
520
623
  // Forks the thread's session (or one of its subagent sessions) into a new
521
624
  // thread. `before`: a user message ID; the fork ends right before it.
522
- export async function fork(bot, { sourceThread, sessionId, before, subagent, name, author, worktree, }) {
523
- const root = rootSession(bot, sourceThread.id);
625
+ export async function fork(bot, { source, sessionId, before, subagent, name, author, worktree, }) {
626
+ const root = rootSession(bot, source.id);
524
627
  if (root instanceof Error)
525
628
  return root;
526
629
  if (sessionId !== root) {
@@ -531,7 +634,7 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
531
634
  return new ConfigError({ reason: "This session is not the thread's root or a direct subagent. Run /fork or /fork-subagent in the session's own thread." });
532
635
  }
533
636
  }
534
- const project = await threadProject(bot, sourceThread);
637
+ const project = await conversationProject(bot, source);
535
638
  if (project instanceof Error)
536
639
  return project;
537
640
  const channel = await textChannel(bot, project.channelId);
@@ -579,13 +682,13 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
579
682
  }
580
683
  const intro = subagent
581
684
  ? `**Forked subagent session created!**\nAgent: \`${subagent.agent}\`\nTask: ${subagent.description || 'No description'}\nFrom: \`${sessionId}\`\nNew session: \`${forked.id}\``
582
- : `**Forked session created!**\nFrom: <#${sourceThread.id}> (\`${sessionId}\`)\nNew session: \`${forked.id}\``;
685
+ : `**Forked session created!**\nFrom: <#${source.id}> (\`${sessionId}\`)\nNew session: \`${forked.id}\``;
583
686
  return adoptSession(bot, {
584
687
  channel,
585
688
  session: relocated,
586
689
  discard,
587
690
  // OpenCode titles forks "<title> (fork #1)".
588
- threadName: name ?? (forked.title || `Fork: ${subagent?.description || sourceThread.name}`),
691
+ threadName: name ?? (forked.title || `Fork: ${subagent?.description || source.name}`),
589
692
  intro,
590
693
  note: 'You can now continue the conversation from this point.',
591
694
  author,
@@ -594,18 +697,18 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
594
697
  // Fork the whole session into a new "btw:" thread that answers one side
595
698
  // question (V1 /btw). The source session keeps running. The fork inherits
596
699
  // messages, agent, model and instructions, so its prompt cache is warm.
597
- export async function forkBtw(bot, { sourceThread, text, author, messageId, files = [], agent, }) {
598
- const parentSessionId = rootSession(bot, sourceThread.id);
599
- if (parentSessionId instanceof Error)
600
- return parentSessionId;
601
- const project = await threadProject(bot, sourceThread);
700
+ export async function forkBtw(bot, { source, text, author, messageId, files = [], agent, parentSessionId, sender, }) {
701
+ const sourceSessionId = rootSession(bot, source.id);
702
+ if (sourceSessionId instanceof Error)
703
+ return sourceSessionId;
704
+ const project = await conversationProject(bot, source);
602
705
  if (project instanceof Error)
603
706
  return project;
604
707
  const channel = await textChannel(bot, project.channelId);
605
708
  if (channel instanceof Error)
606
709
  return channel;
607
710
  const [forked, thread] = await Promise.all([
608
- oc(bot, 'session.fork', (client) => client.session.fork({ sessionID: parentSessionId })),
711
+ oc(bot, 'session.fork', (client) => client.session.fork({ sessionID: sourceSessionId })),
609
712
  channel.threads
610
713
  .create({ name: `btw: ${text.replace(/\s+/g, ' ')}`.slice(0, 100), autoArchiveDuration: 1440 })
611
714
  .catch((cause) => new DiscordError({ operation: 'create btw thread', cause })),
@@ -622,10 +725,18 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
622
725
  logger.warn(`discard btw fork ${forked.id}`, removed);
623
726
  return thread;
624
727
  }
625
- const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot) };
728
+ const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot), ...(parentSessionId && { parentSessionId }) };
729
+ const fromCoordinator = !source.isThread();
626
730
  const marked = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: forked.id, metadata: { kimaki } }));
627
731
  if (marked instanceof Error)
628
732
  return marked;
733
+ // A fork of a thread keeps its copied instructions (prompt cache); a fork of a coordinator gets thread
734
+ // ones. The plugin makes it a worker (plugin/coordinator.ts).
735
+ if (fromCoordinator) {
736
+ const instructions = await putInstructions(bot, { sessionId: forked.id, conversation: thread, channel, directory: project.directory, userId: author.id, parentSessionId: sourceSessionId });
737
+ if (instructions instanceof Error)
738
+ return instructions;
739
+ }
629
740
  const bound = await bindThread(bot, {
630
741
  threadId: thread.id,
631
742
  sessionId: forked.id,
@@ -637,7 +748,7 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
637
748
  return bound;
638
749
  await thread.members.add(author.id).catch((e) => logger.warn(`add btw member`, e));
639
750
  const intro = await thread
640
- .send({ content: `Reusing context from <#${sourceThread.id}> to answer prompt...\n${text}`.slice(0, 2_000), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
751
+ .send({ content: `Reusing context from <#${source.id}> to answer prompt...\n${text}`.slice(0, 2_000), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
641
752
  .catch((cause) => new DiscordError({ operation: 'send btw intro', cause }));
642
753
  if (intro instanceof Error)
643
754
  return intro;
@@ -650,11 +761,12 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
650
761
  sessionId: forked.id,
651
762
  threadId: thread.id,
652
763
  threadName: thread.name,
653
- text: btwPrompt({ text, parentSessionId, sourceThreadId: sourceThread.id, sessionId: forked.id, threadId: thread.id }),
764
+ text: btwPrompt({ text, parentSessionId: sourceSessionId, sourceThreadId: source.id, sessionId: forked.id, threadId: thread.id }),
654
765
  author,
655
766
  messageId,
656
767
  delivery: 'steer',
657
768
  files,
769
+ sender,
658
770
  });
659
771
  if (sent instanceof Error)
660
772
  return sent;