kimaki 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -12,6 +12,7 @@ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectM
12
12
  import crypto from 'node:crypto';
13
13
  import { initializeOpencodeForDirectory, getOpencodeServerPort, getOpencodeServerAuthHeaders, } from '../opencode.js';
14
14
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
15
+ import { clearModelListCache } from '../session-handler/model-utils.js';
15
16
  import { createLogger, LogPrefix } from '../logger.js';
16
17
  import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
17
18
  const loginLogger = createLogger(LogPrefix.LOGIN);
@@ -31,7 +32,9 @@ function createContextHash(context) {
31
32
  // ── Provider popularity order ───────────────────────────────────
32
33
  // Discord select menus cap at 25 options, so we show popular ones first.
33
34
  // IDs sourced from opencode's provider.list() API (scripts/list-providers.ts).
35
+ // `subrouter` leads: it pools every subscription and cycles across them.
34
36
  const PROVIDER_POPULARITY_ORDER = [
37
+ 'subrouter',
35
38
  'anthropic',
36
39
  'openai',
37
40
  'google',
@@ -691,6 +694,7 @@ export async function handleOAuthCodeModalSubmit(interaction) {
691
694
  return;
692
695
  }
693
696
  await getClient().instance.dispose({ directory: ctx.dir });
697
+ clearModelListCache();
694
698
  pendingLoginContexts.delete(hash);
695
699
  await interaction.editReply({
696
700
  content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
@@ -736,6 +740,7 @@ export async function handleApiKeyModalSubmit(interaction) {
736
740
  });
737
741
  // Dispose to refresh provider state so new credentials are recognized
738
742
  await getClient().instance.dispose({ directory: ctx.dir });
743
+ clearModelListCache();
739
744
  await interaction.editReply({
740
745
  content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
741
746
  });
@@ -870,6 +875,7 @@ async function startOAuthFlow(interaction, ctx, hash) {
870
875
  return;
871
876
  }
872
877
  await getClient().instance.dispose({ directory: ctx.dir });
878
+ clearModelListCache();
873
879
  pendingLoginContexts.delete(hash);
874
880
  await interaction.editReply({
875
881
  content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
@@ -1,23 +1,30 @@
1
- // /merge-worktree command - Merge worktree commits into default branch.
2
- // Pipeline: rebase worktree commits onto target -> local fast-forward push.
3
- // Preserves all commits (no squash). On rebase conflicts, asks the AI model
4
- // in the thread to resolve them.
1
+ // /merge-worktree command - Rebase or squash worktree commits into a target branch.
2
+ // On rebase conflicts, asks the AI model in the thread to resolve them.
5
3
  import {} from 'discord.js';
6
4
  import { getThreadWorktreeOrWorkspace, getThreadSession, getChannelDirectory, } from '../database.js';
7
5
  import { createLogger, LogPrefix } from '../logger.js';
8
6
  import { notifyError } from '../sentry.js';
9
- import { mergeWorktree, listBranchesByLastCommit, validateBranchRef } from '../worktrees.js';
7
+ import { mergeWorktree, listBranchesByLastCommit, validateBranchRef, formatMergeWorktreeError, } from '../worktrees.js';
10
8
  import { sendThreadMessage, resolveWorkingDirectory, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
11
9
  import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
12
10
  import { RebaseConflictError, DirtyWorktreeError, TargetDirtyWorktreeError, NothingToMergeError, } from '../errors.js';
11
+ import { WORKTREE_PREFIX } from '../message-formatting.js';
13
12
  const logger = createLogger(LogPrefix.WORKTREE);
14
- /** Worktree thread title prefix - indicates unmerged worktree */
15
- export const WORKTREE_PREFIX = '⬦ ';
13
+ function quoteShellArg(value) {
14
+ return `'${value.replaceAll("'", "'\\''")}'`;
15
+ }
16
+ export { WORKTREE_PREFIX };
17
+ function worktreePrefixLength(name) {
18
+ if (name.startsWith(WORKTREE_PREFIX))
19
+ return WORKTREE_PREFIX.length;
20
+ return 0;
21
+ }
16
22
  async function removeWorktreePrefixFromTitle(thread) {
17
- if (!thread.name.startsWith(WORKTREE_PREFIX)) {
23
+ const prefixLength = worktreePrefixLength(thread.name);
24
+ if (!prefixLength) {
18
25
  return;
19
26
  }
20
- const newName = thread.name.slice(WORKTREE_PREFIX.length);
27
+ const newName = thread.name.slice(prefixLength);
21
28
  const timeoutMs = 5000;
22
29
  await Promise.race([
23
30
  thread.setName(newName).catch((e) => {
@@ -73,6 +80,14 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
73
80
  await command.editReply(`Worktree is not ready (status: ${info.status})${info.error_message ? `: ${info.error_message}` : ''}`);
74
81
  return;
75
82
  }
83
+ const strategyOption = command.options.getString('strategy') || 'rebase';
84
+ if (strategyOption !== 'rebase' && strategyOption !== 'squash') {
85
+ await command.editReply(`Invalid merge strategy: \`${strategyOption}\``);
86
+ return;
87
+ }
88
+ const strategyName = strategyOption === 'squash'
89
+ ? 'Squash into one commit'
90
+ : 'Keep commits (rebase)';
76
91
  const rawTargetBranch = command.options.getString('target-branch') || undefined;
77
92
  let targetBranch = rawTargetBranch;
78
93
  if (targetBranch) {
@@ -91,6 +106,7 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
91
106
  mainRepoDir: info.project_directory,
92
107
  worktreeName: info.workspace_name,
93
108
  targetBranch,
109
+ strategy: strategyOption,
94
110
  onProgress: (msg) => {
95
111
  logger.log(`[merge] ${msg}`);
96
112
  },
@@ -106,11 +122,22 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
106
122
  }
107
123
  if (result instanceof NothingToMergeError) {
108
124
  void removeWorktreePrefixFromTitle(thread);
109
- await command.editReply(`Merge failed: ${result.message}`);
125
+ await command.editReply(formatMergeWorktreeError(result));
110
126
  return;
111
127
  }
112
128
  if (result instanceof RebaseConflictError) {
113
- await command.editReply('Rebase conflict detected. Asking the model to resolve...');
129
+ const prefixLength = worktreePrefixLength(thread.name);
130
+ const mergedThreadName = prefixLength
131
+ ? thread.name.slice(prefixLength)
132
+ : thread.name;
133
+ const mergeCommand = [
134
+ 'kimaki merge-worktree',
135
+ `--strategy ${strategyOption}`,
136
+ `--target-branch ${quoteShellArg(String(result.target))}`,
137
+ `--thread ${quoteShellArg(thread.id)}`,
138
+ `--thread-name ${quoteShellArg(mergedThreadName)}`,
139
+ ].join(' ');
140
+ await command.editReply(`Rebase conflict detected. Asking the model to resolve it and retry the merge with **${strategyName}**.`);
114
141
  await sendPromptToModel({
115
142
  prompt: [
116
143
  `A rebase conflict occurred while merging this worktree into \`${result.target}\`.`,
@@ -124,7 +151,8 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
124
151
  '6. Stage resolved files with `git add`',
125
152
  '7. Continue the rebase with `git rebase --continue`',
126
153
  '8. If git reports more conflicts, repeat steps 1-7 until the rebase finishes (no more rebase in progress, `git status` is clean)',
127
- '9. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
154
+ `9. Once the rebase is fully complete, run \`${mergeCommand}\` yourself`,
155
+ '10. If that command fails because the target changed or is not ready, stop and report the failure. The user can run it again later.',
128
156
  ].join('\n'),
129
157
  thread,
130
158
  projectDirectory: info.project_directory,
@@ -133,11 +161,16 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
133
161
  });
134
162
  return;
135
163
  }
136
- await command.editReply(`Merge failed: ${result.message}`);
164
+ const formatted = formatMergeWorktreeError(result);
165
+ logger.error(formatted);
166
+ await command.editReply(formatted);
137
167
  return;
138
168
  }
139
169
  void removeWorktreePrefixFromTitle(thread);
140
- await command.editReply(`Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})\nWorktree now at detached HEAD.`);
170
+ const mergeSummary = strategyOption === 'squash'
171
+ ? `Squashed \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} source commit${result.commitCount === 1 ? '' : 's'} → 1)`
172
+ : `Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})`;
173
+ await command.editReply(`${mergeSummary}\nWorktree now at detached HEAD.`);
141
174
  }
142
175
  /**
143
176
  * Autocomplete handler for /merge-worktree target-branch option.
@@ -13,6 +13,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
13
13
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
14
14
  import { getCurrentModelInfo, ensureSessionPreferencesSnapshot, applyToCurrentSession, } from './model.js';
15
15
  import { getThinkingValuesForModel } from '../thinking-utils.js';
16
+ import { resolveDisplayedModelId } from '../session-handler/model-utils.js';
16
17
  import { createLogger, LogPrefix } from '../logger.js';
17
18
  const logger = createLogger(LogPrefix.MODEL);
18
19
  const pendingVariantContexts = new Map();
@@ -137,6 +138,12 @@ export async function showModelVariantPicker({ channel, appId, editReply, }) {
137
138
  const { providerID, modelID, model: fullModelId } = currentModelInfo;
138
139
  const sourceLabel = formatSourceLabel(currentModelInfo);
139
140
  const variantLabel = cascadeVariant ? ` (${cascadeVariant})` : '';
141
+ const displayedModelId = (await resolveDisplayedModelId({
142
+ providers: providersResponse.data.all,
143
+ providerID,
144
+ modelID,
145
+ sessionID: sessionId,
146
+ })) ?? fullModelId;
140
147
  const provider = providersResponse.data.all.find((p) => {
141
148
  return p.id === providerID;
142
149
  });
@@ -146,7 +153,7 @@ export async function showModelVariantPicker({ channel, appId, editReply, }) {
146
153
  providerId: providerID,
147
154
  modelId: modelID,
148
155
  });
149
- const statusText = `**Current model:** \`${fullModelId}\`${variantLabel} — ${sourceLabel}`;
156
+ const statusText = `**Current model:** \`${displayedModelId}\`${variantLabel} — ${sourceLabel}`;
150
157
  if (variants.length === 0) {
151
158
  await editReply({
152
159
  content: `${statusText}\nThis model doesn't support thinking level variants.`,
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
4
4
  import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
5
5
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
6
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
- import { getDefaultModel } from '../session-handler/model-utils.js';
7
+ import { getDefaultModel, resolveDisplayedModelId, } from '../session-handler/model-utils.js';
8
8
  import { getRuntime } from '../session-handler/thread-session-runtime.js';
9
9
  import { getThinkingValuesForModel } from '../thinking-utils.js';
10
10
  import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
@@ -321,22 +321,29 @@ export async function handleModelCommand({ interaction, appId, }) {
321
321
  });
322
322
  return;
323
323
  }
324
- const currentModelText = (() => {
324
+ const currentModelText = await (async () => {
325
+ if (currentModelInfo.type === 'none') {
326
+ return '**Current:** none';
327
+ }
328
+ const displayed = (await resolveDisplayedModelId({
329
+ providers: allProviders,
330
+ providerID: currentModelInfo.providerID,
331
+ modelID: currentModelInfo.modelID,
332
+ sessionID: sessionId,
333
+ })) ?? currentModelInfo.model;
325
334
  switch (currentModelInfo.type) {
326
335
  case 'session':
327
- return `**Current (this thread):** \`${currentModelInfo.model}\``;
336
+ return `**Current (this thread):** \`${displayed}\``;
328
337
  case 'agent':
329
- return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
338
+ return `**Current (agent "${currentModelInfo.agentName}"):** \`${displayed}\``;
330
339
  case 'channel':
331
- return `**Current (channel override):** \`${currentModelInfo.model}\``;
340
+ return `**Current (channel override):** \`${displayed}\``;
332
341
  case 'global':
333
- return `**Current (global default):** \`${currentModelInfo.model}\``;
342
+ return `**Current (global default):** \`${displayed}\``;
334
343
  case 'opencode-config':
335
344
  case 'opencode-recent':
336
345
  case 'opencode-provider-default':
337
- return `**Current (opencode default):** \`${currentModelInfo.model}\``;
338
- case 'none':
339
- return '**Current:** none';
346
+ return `**Current (opencode default):** \`${displayed}\``;
340
347
  }
341
348
  })();
342
349
  const variantText = (() => {
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * CLI commands for multi-provider OAuth account management.
3
4
  * Mounted via goke .use() in cli.ts under the `multioauth` namespace.
@@ -2,25 +2,26 @@
2
2
  // Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
3
3
  // Creates thread immediately, then worktree in background so user can type
4
4
  import { ChannelType, REST, } from 'discord.js';
5
+ import crypto from 'node:crypto';
5
6
  import fs from 'node:fs';
6
7
  import { OpenCodeSdkError } from '../errors.js';
7
- import { createPendingWorkspace, setWorkspaceReady, setWorkspaceError, getChannelDirectory, getThreadSession, getThreadWorktreeOrWorkspace, setThreadSession, } from '../database.js';
8
+ import { createPendingWorkspace, setWorkspaceReady, setWorkspaceError, getChannelDirectory, getThreadSession, setThreadSession, } from '../database.js';
8
9
  import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
9
10
  import { createLogger, LogPrefix } from '../logger.js';
10
11
  import { notifyError } from '../sentry.js';
11
- import { execAsync, listBranchesByLastCommit, resolveBestBaseRef, validateBranchRef, } from '../worktrees.js';
12
+ import { execAsync, getManagedWorktreeDirectory, listBranchesByLastCommit, resolveBestBaseRef, validateBranchRef, } from '../worktrees.js';
13
+ import { KIMAKI_WORKTREE_ADAPTER_TYPE, removeWorktreeFromOwnRepository, resolveGitCommit, validateWorktreeIdentity, } from '../git-worktree-core.js';
12
14
  import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
13
15
  import { buildSessionPermissions, initializeOpencodeForDirectory, } from '../opencode.js';
14
16
  import { WORKTREE_PREFIX } from './merge-worktree.js';
15
17
  import * as errore from 'errore';
16
18
  import { copyCurrentSessionModel } from './model.js';
17
19
  const logger = createLogger(LogPrefix.WORKTREE);
18
- const DEFAULT_WORKTREE_BASE_REF = 'HEAD';
19
20
  async function resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch, }) {
20
21
  if (!rawBaseBranch) {
21
22
  // Default to the current local HEAD so worktrees can branch from
22
23
  // unpublished commits in the main checkout.
23
- return DEFAULT_WORKTREE_BASE_REF;
24
+ return resolveGitCommit({ directory: projectDirectory, ref: 'HEAD' });
24
25
  }
25
26
  const validated = await validateBranchRef({
26
27
  directory: projectDirectory,
@@ -37,7 +38,7 @@ async function resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch
37
38
  if (bestRef !== validated) {
38
39
  logger.log(`Base branch resolved: ${validated} → ${bestRef} (remote is ahead)`);
39
40
  }
40
- return bestRef;
41
+ return resolveGitCommit({ directory: projectDirectory, ref: bestRef });
41
42
  }
42
43
  /** Status message shown while a worktree is being created. */
43
44
  export function worktreeCreatingMessage(worktreeName) {
@@ -153,28 +154,83 @@ async function getProjectDirectoryFromChannel(channel) {
153
154
  /**
154
155
  * Try creating a worktree via the OpenCode workspace SDK.
155
156
  * Returns the workspace directory on success, or an Error if the workspace
156
- * feature is not available or the creation fails. Callers fall back to the
157
- * direct git path on Error.
157
+ * feature is not available or the creation fails.
158
158
  */
159
- async function tryWorkspaceCreate({ threadId, worktreeName, projectDirectory, baseBranch, }) {
159
+ export async function tryWorkspaceCreate({ worktreeName, projectDirectory, baseCommit, }) {
160
160
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
161
161
  if (getClient instanceof Error)
162
162
  return getClient;
163
163
  const client = getClient();
164
+ const workspaceId = `wrk_${crypto.randomUUID()}`;
165
+ const managedDirectory = getManagedWorktreeDirectory({
166
+ directory: projectDirectory,
167
+ name: worktreeName,
168
+ });
169
+ const cleanupFailedWorkspace = async (worktreeDirectory) => {
170
+ const removeResponse = await client.experimental.workspace.remove({
171
+ id: workspaceId,
172
+ directory: projectDirectory,
173
+ }).catch((e) => new OpenCodeSdkError({ operation: 'workspace.remove', cause: e }));
174
+ const sdkCleanupError = (() => {
175
+ if (removeResponse instanceof Error)
176
+ return removeResponse;
177
+ if (removeResponse.error) {
178
+ return new Error(`Workspace cleanup failed: ${JSON.stringify(removeResponse.error)}`);
179
+ }
180
+ return undefined;
181
+ })();
182
+ const gitCleanupError = fs.existsSync(worktreeDirectory)
183
+ ? await removeWorktreeFromOwnRepository({
184
+ worktreeDirectory,
185
+ branchName: worktreeName,
186
+ })
187
+ : undefined;
188
+ return sdkCleanupError ?? gitCleanupError;
189
+ };
164
190
  const response = await client.experimental.workspace.create({
191
+ id: workspaceId,
165
192
  directory: projectDirectory,
166
- type: 'kimaki-worktree',
193
+ type: KIMAKI_WORKTREE_ADAPTER_TYPE,
167
194
  branch: worktreeName,
168
- extra: baseBranch ? { baseBranch } : null,
195
+ extra: {
196
+ projectDirectory,
197
+ baseCommit,
198
+ },
169
199
  }).catch((e) => new OpenCodeSdkError({ operation: 'workspace.create', cause: e }));
170
- if (response instanceof Error)
171
- return response;
172
- if (response.error) {
173
- return new Error(`Workspace creation failed: ${JSON.stringify(response.error)}`);
200
+ if (response instanceof Error || response.error) {
201
+ const creationError = response instanceof Error
202
+ ? response
203
+ : new Error(`Workspace creation failed: ${JSON.stringify(response.error)}`);
204
+ const cleanupError = await cleanupFailedWorkspace(managedDirectory);
205
+ if (cleanupError instanceof Error) {
206
+ return new Error(`${creationError.message}; cleanup failed: ${cleanupError.message}`, {
207
+ cause: creationError,
208
+ });
209
+ }
210
+ return creationError;
174
211
  }
175
212
  const workspace = response.data;
176
213
  if (!workspace?.directory || !workspace.id) {
177
- return new Error('Workspace SDK returned no directory or ID');
214
+ const creationError = new Error('Workspace SDK returned no directory or ID');
215
+ const cleanupError = await cleanupFailedWorkspace(managedDirectory);
216
+ if (cleanupError instanceof Error) {
217
+ return new Error(`${creationError.message}; cleanup failed: ${cleanupError.message}`, {
218
+ cause: creationError,
219
+ });
220
+ }
221
+ return creationError;
222
+ }
223
+ const identityResult = await validateWorktreeIdentity({
224
+ projectDirectory,
225
+ worktreeDirectory: workspace.directory,
226
+ baseCommit,
227
+ });
228
+ if (identityResult instanceof Error) {
229
+ const cleanupError = await cleanupFailedWorkspace(workspace.directory);
230
+ if (cleanupError instanceof Error) {
231
+ return new Error(`${identityResult.message}; cleanup failed: ${cleanupError.message}`, { cause: identityResult });
232
+ }
233
+ return identityResult;
178
234
  }
179
235
  return { directory: workspace.directory, workspaceId: workspace.id };
180
236
  }
@@ -188,13 +244,13 @@ async function tryWorkspaceCreate({ threadId, worktreeName, projectDirectory, ba
188
244
  * starterMessage is optional — if omitted, status edits are skipped (creation
189
245
  * still proceeds). This keeps worktree creation independent of Discord message
190
246
  * delivery, so a transient send failure never silently skips the worktree.
247
+ * beforeReady can bind a session before incoming messages may use the checkout.
191
248
  *
192
249
  * Returns the worktree directory on success, or an Error on failure.
193
250
  * Never throws — all internal errors are caught and returned as Error values.
194
251
  */
195
- export async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseBranch, rest, }) {
252
+ export async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseCommit, rest, beforeReady, }) {
196
253
  return (async () => {
197
- logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
198
254
  // Serialize status message edits so onProgress can't overwrite the
199
255
  // final success/error edit even if Discord's API is slow.
200
256
  let editChain = Promise.resolve();
@@ -213,11 +269,23 @@ export async function createWorktreeInBackground({ thread, starterMessage, workt
213
269
  workspaceName: worktreeName,
214
270
  projectDirectory,
215
271
  });
272
+ const resolvedBaseCommit = baseCommit
273
+ ? baseCommit
274
+ : await resolveGitCommit({ directory: projectDirectory, ref: 'HEAD' });
275
+ if (resolvedBaseCommit instanceof Error) {
276
+ await setWorkspaceError({
277
+ threadId: thread.id,
278
+ errorMessage: resolvedBaseCommit.message,
279
+ });
280
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${resolvedBaseCommit.message}`);
281
+ await editChain;
282
+ return resolvedBaseCommit;
283
+ }
284
+ logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory} from ${resolvedBaseCommit}`);
216
285
  const workspaceResult = await tryWorkspaceCreate({
217
- threadId: thread.id,
218
286
  worktreeName,
219
287
  projectDirectory,
220
- baseBranch,
288
+ baseCommit: resolvedBaseCommit,
221
289
  });
222
290
  if (workspaceResult instanceof Error) {
223
291
  const errorMsg = workspaceResult.message;
@@ -227,6 +295,14 @@ export async function createWorktreeInBackground({ thread, starterMessage, workt
227
295
  await editChain;
228
296
  return workspaceResult;
229
297
  }
298
+ const preparationResult = beforeReady
299
+ ? await beforeReady(workspaceResult).catch((e) => new WorktreeError('Failed to prepare worktree session', { cause: e }))
300
+ : undefined;
301
+ if (preparationResult instanceof Error) {
302
+ logger.error('[WORKTREE] Session preparation failed:', preparationResult);
303
+ void notifyError(preparationResult, 'Worktree session preparation failed');
304
+ await sendThreadMessage(thread, `Worktree is ready, but failed to prepare its session: ${preparationResult.message}`);
305
+ }
230
306
  await setWorkspaceReady({
231
307
  threadId: thread.id,
232
308
  workspaceId: workspaceResult.workspaceId,
@@ -308,11 +384,11 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
308
384
  return;
309
385
  }
310
386
  // Parallelize: base branch validation and existing worktree check are independent
311
- const [baseBranch, existingWorktree] = await Promise.all([
387
+ const [baseCommit, existingWorktree] = await Promise.all([
312
388
  resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch }),
313
389
  findExistingWorktreePath({ projectDirectory, worktreeName }),
314
390
  ]);
315
- if (baseBranch instanceof Error) {
391
+ if (baseCommit instanceof Error) {
316
392
  await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``);
317
393
  return;
318
394
  }
@@ -354,7 +430,7 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
354
430
  starterMessage,
355
431
  worktreeName,
356
432
  projectDirectory,
357
- baseBranch,
433
+ baseCommit,
358
434
  rest: command.client.rest,
359
435
  }).catch((e) => {
360
436
  logger.error('[NEW-WORKTREE] Background error:', e);
@@ -391,12 +467,12 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
391
467
  // Parallelize: base branch validation, existing worktree check, and parent channel
392
468
  // resolve are all independent. resolveTextChannel fetches the parent from Discord
393
469
  // cache/API which can overlap with the git operations.
394
- const [baseBranch, existingWorktreePath, textChannel] = await Promise.all([
470
+ const [baseCommit, existingWorktreePath, textChannel] = await Promise.all([
395
471
  resolveRequestedWorktreeBaseRef({ projectDirectory, rawBaseBranch }),
396
472
  findExistingWorktreePath({ projectDirectory, worktreeName }),
397
473
  resolveTextChannel(thread),
398
474
  ]);
399
- if (baseBranch instanceof Error) {
475
+ if (baseCommit instanceof Error) {
400
476
  await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``);
401
477
  return;
402
478
  }
@@ -440,86 +516,78 @@ async function handleWorktreeInThread({ command, thread, appId, }) {
440
516
  starterMessage: statusMessage,
441
517
  worktreeName,
442
518
  projectDirectory,
443
- baseBranch,
519
+ baseCommit,
444
520
  rest: command.client.rest,
445
- })
446
- .then(async (result) => {
447
- if (result instanceof Error)
448
- return;
449
- const sourceSessionId = await getThreadSession(thread.id);
450
- if (!sourceSessionId) {
451
- await sendThreadMessage(worktreeThread, 'Worktree is ready. Send a message here to start a fresh session in this checkout.');
452
- return;
453
- }
454
- const workspace = await getThreadWorktreeOrWorkspace(worktreeThread.id);
455
- if (!workspace?.workspace_id) {
456
- await sendThreadMessage(worktreeThread, '✗ Worktree is ready, but OpenCode returned no workspace ID for context reuse.');
457
- return;
458
- }
459
- const getClient = await initializeOpencodeForDirectory(result, {
460
- originalRepoDirectory: projectDirectory,
461
- channelId: parent.id,
462
- });
463
- if (getClient instanceof Error) {
464
- await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to initialize OpenCode for context reuse: ${getClient.message}`);
465
- return;
466
- }
467
- const forkResponse = await getClient().session.fork({
468
- sessionID: sourceSessionId,
469
- directory: result,
470
- workspace: workspace.workspace_id,
471
- }).catch((e) => new OpenCodeSdkError({ operation: 'session.fork', cause: e }));
472
- if (forkResponse instanceof Error) {
473
- logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', forkResponse);
474
- void notifyError(forkResponse, 'Failed to fork session into worktree');
475
- await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${forkResponse.message}`);
476
- return;
477
- }
478
- const forkedSession = forkResponse.data;
479
- if (!forkedSession) {
480
- const error = new Error('OpenCode did not return a forked session');
481
- logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', error);
482
- void notifyError(error, 'Failed to fork session into worktree');
483
- await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${error.message}`);
484
- return;
485
- }
486
- await copyCurrentSessionModel({
487
- sourceSessionId,
488
- targetSessionId: forkedSession.id,
489
- channelId: parent.id,
490
- appId,
491
- getClient,
492
- directory: projectDirectory,
493
- });
494
- const permissionResponse = await getClient().session.update({
495
- sessionID: forkedSession.id,
496
- directory: result,
497
- permission: buildSessionPermissions({
498
- directory: result,
521
+ beforeReady: async ({ directory, workspaceId }) => {
522
+ const sourceSessionId = await getThreadSession(thread.id);
523
+ if (!sourceSessionId) {
524
+ await sendThreadMessage(worktreeThread, 'Worktree is ready. Send a message here to start a fresh session in this checkout.');
525
+ return;
526
+ }
527
+ const getClient = await initializeOpencodeForDirectory(directory, {
499
528
  originalRepoDirectory: projectDirectory,
500
- }),
501
- }).catch((e) => new OpenCodeSdkError({ operation: 'session.update', cause: e }));
502
- if (permissionResponse instanceof Error || permissionResponse.error) {
503
- const error = permissionResponse instanceof Error
504
- ? permissionResponse
505
- : new Error('OpenCode rejected forked session permission update');
506
- logger.error('[NEW-WORKTREE] Failed to update forked session permissions:', error);
507
- void notifyError(error, 'Failed to update forked session permissions');
508
- await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to update forked session permissions: ${error.message}`);
509
- return;
510
- }
511
- await setThreadSession(worktreeThread.id, forkedSession.id);
512
- getOrCreateRuntime({
513
- threadId: worktreeThread.id,
514
- thread: worktreeThread,
515
- projectDirectory,
516
- sdkDirectory: result,
517
- channelId: parent.id,
518
- appId,
519
- });
520
- await sendThreadMessage(worktreeThread, `Reusing context from <#${thread.id}> in worktree session \`${forkedSession.id}\`.`);
521
- })
522
- .catch((e) => {
529
+ channelId: parent.id,
530
+ });
531
+ if (getClient instanceof Error) {
532
+ await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to initialize OpenCode for context reuse: ${getClient.message}`);
533
+ return;
534
+ }
535
+ const forkResponse = await getClient().session.fork({
536
+ sessionID: sourceSessionId,
537
+ directory,
538
+ workspace: workspaceId,
539
+ }).catch((e) => new OpenCodeSdkError({ operation: 'session.fork', cause: e }));
540
+ if (forkResponse instanceof Error) {
541
+ logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', forkResponse);
542
+ void notifyError(forkResponse, 'Failed to fork session into worktree');
543
+ await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${forkResponse.message}`);
544
+ return;
545
+ }
546
+ const forkedSession = forkResponse.data;
547
+ if (!forkedSession) {
548
+ const error = new Error('OpenCode did not return a forked session');
549
+ logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', error);
550
+ void notifyError(error, 'Failed to fork session into worktree');
551
+ await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${error.message}`);
552
+ return;
553
+ }
554
+ await copyCurrentSessionModel({
555
+ sourceSessionId,
556
+ targetSessionId: forkedSession.id,
557
+ channelId: parent.id,
558
+ appId,
559
+ getClient,
560
+ directory: projectDirectory,
561
+ });
562
+ const permissionResponse = await getClient().session.update({
563
+ sessionID: forkedSession.id,
564
+ directory,
565
+ permission: buildSessionPermissions({
566
+ directory,
567
+ originalRepoDirectory: projectDirectory,
568
+ }),
569
+ }).catch((e) => new OpenCodeSdkError({ operation: 'session.update', cause: e }));
570
+ if (permissionResponse instanceof Error || permissionResponse.error) {
571
+ const error = permissionResponse instanceof Error
572
+ ? permissionResponse
573
+ : new Error('OpenCode rejected forked session permission update');
574
+ logger.error('[NEW-WORKTREE] Failed to update forked session permissions:', error);
575
+ void notifyError(error, 'Failed to update forked session permissions');
576
+ await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to update forked session permissions: ${error.message}`);
577
+ return;
578
+ }
579
+ await setThreadSession(worktreeThread.id, forkedSession.id);
580
+ getOrCreateRuntime({
581
+ threadId: worktreeThread.id,
582
+ thread: worktreeThread,
583
+ projectDirectory,
584
+ sdkDirectory: directory,
585
+ channelId: parent.id,
586
+ appId,
587
+ });
588
+ await sendThreadMessage(worktreeThread, `Reusing context from <#${thread.id}> in worktree session \`${forkedSession.id}\`.`);
589
+ },
590
+ }).catch((e) => {
523
591
  logger.error('[NEW-WORKTREE] Background error:', e);
524
592
  void notifyError(e, 'Background worktree creation failed (in-thread)');
525
593
  });
@@ -6,6 +6,7 @@ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils
6
6
  import { getOrCreateRuntime, getRuntime, } from '../session-handler/thread-session-runtime.js';
7
7
  import { buildHtmlActionCustomId, registerHtmlAction, } from '../html-actions.js';
8
8
  import { createLogger, LogPrefix } from '../logger.js';
9
+ import { QUEUE_PREFIX } from '../message-formatting.js';
9
10
  import { store } from '../store.js';
10
11
  const logger = createLogger(LogPrefix.QUEUE);
11
12
  function buildQueueRemoveRow({ threadId, queueId, position, }) {
@@ -108,7 +109,7 @@ export async function handleQueueCommand({ command, appId, }) {
108
109
  });
109
110
  return;
110
111
  }
111
- const responseText = `» **${command.user.displayName}:** ${message.slice(0, 1000)}${message.length > 1000 ? '...' : ''}`;
112
+ const responseText = `${QUEUE_PREFIX}**${command.user.displayName}:** ${message.slice(0, 1000)}${message.length > 1000 ? '...' : ''}`;
112
113
  await command.reply({
113
114
  content: responseText,
114
115
  flags: SILENT_MESSAGE_FLAGS,
@@ -264,7 +265,7 @@ export async function handleQueueCommandCommand({ command, appId, }) {
264
265
  logger.log(`[QUEUE] User ${command.user.displayName} queued command /${commandName} in thread ${channel.id}`);
265
266
  return;
266
267
  }
267
- const responseText = `» **${command.user.displayName}:** ${displayText}`;
268
+ const responseText = `${QUEUE_PREFIX}**${command.user.displayName}:** ${displayText}`;
268
269
  await command.reply({
269
270
  content: responseText,
270
271
  flags: SILENT_MESSAGE_FLAGS,