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
@@ -35,6 +35,76 @@ export function getEventBufferSessionId(event: EventBufferEvent): string | undef
35
35
  type AssistantMessage = Extract<OpenCodeMessage, { role: 'assistant' }>
36
36
  type UserMessage = Extract<OpenCodeMessage, { role: 'user' }>
37
37
 
38
+ function isCompactionContinuePart(part: Part): boolean {
39
+ if (part.type !== 'text') {
40
+ return false
41
+ }
42
+ return part.synthetic === true && part.metadata?.compaction_continue === true
43
+ }
44
+
45
+ function isInternalOpenCodeUserMessageId({
46
+ events,
47
+ sessionId,
48
+ messageId,
49
+ upToIndex,
50
+ }: {
51
+ events: EventBufferEntry[]
52
+ sessionId: string
53
+ messageId: string
54
+ upToIndex?: number
55
+ }): boolean {
56
+ const end = upToIndex ?? events.length - 1
57
+ for (let i = 0; i <= end; i++) {
58
+ const event = events[i]?.event
59
+ if (event?.type !== 'message.part.updated') {
60
+ continue
61
+ }
62
+ const part = event.properties.part
63
+ if (part.sessionID !== sessionId || part.messageID !== messageId) {
64
+ continue
65
+ }
66
+ if (part.type === 'compaction') {
67
+ return true
68
+ }
69
+ if (isCompactionContinuePart(part)) {
70
+ return true
71
+ }
72
+ }
73
+ return false
74
+ }
75
+
76
+ function isUserFacingAssistantMessage(message: AssistantMessage): boolean {
77
+ return message.summary !== true
78
+ }
79
+
80
+ export type AssistantMessageKind = 'user-facing' | 'summary' | 'unknown'
81
+
82
+ export function getAssistantMessageKind({
83
+ events,
84
+ sessionId,
85
+ messageId,
86
+ upToIndex,
87
+ }: {
88
+ events: EventBufferEntry[]
89
+ sessionId: string
90
+ messageId: string
91
+ upToIndex?: number
92
+ }): AssistantMessageKind {
93
+ const end = upToIndex ?? events.length - 1
94
+ for (let i = end; i >= 0; i--) {
95
+ const event = events[i]?.event
96
+ if (event?.type !== 'message.updated') {
97
+ continue
98
+ }
99
+ const info = event.properties.info
100
+ if (info.sessionID !== sessionId || info.role !== 'assistant' || info.id !== messageId) {
101
+ continue
102
+ }
103
+ return info.summary === true ? 'summary' : 'user-facing'
104
+ }
105
+ return 'unknown'
106
+ }
107
+
38
108
  function getTaskChildSessionId({
39
109
  part,
40
110
  }: {
@@ -165,6 +235,119 @@ export function didQuestionQueueHandoffSinceLatestQuestionAsked({
165
235
  return false
166
236
  }
167
237
 
238
+ export type DerivedUnansweredQuestion = {
239
+ id: string
240
+ questions: Array<{
241
+ question: string
242
+ header: string
243
+ options: Array<{
244
+ label: string
245
+ description: string
246
+ }>
247
+ multiple?: boolean
248
+ }>
249
+ tool?: {
250
+ messageID: string
251
+ callID: string
252
+ }
253
+ }
254
+
255
+ // OpenCode emits question.asked when the tool starts, often before the
256
+ // preceding text part gets time.end. Discord must wait for that end event
257
+ // or the question UI posts first and the text dumps later.
258
+ export function isAssistantTextReadyForQuestion({
259
+ events,
260
+ sessionId,
261
+ messageId,
262
+ upToIndex,
263
+ }: {
264
+ events: EventBufferEntry[]
265
+ sessionId: string
266
+ messageId: string
267
+ upToIndex?: number
268
+ }): boolean {
269
+ const end = upToIndex ?? events.length - 1
270
+ for (let i = end; i >= 0; i--) {
271
+ const event = events[i]?.event
272
+ if (!event || event.type !== 'message.part.updated') {
273
+ continue
274
+ }
275
+ const part = event.properties.part
276
+ if (part.sessionID !== sessionId) {
277
+ continue
278
+ }
279
+ if (part.messageID !== messageId) {
280
+ continue
281
+ }
282
+ if (part.type !== 'text') {
283
+ continue
284
+ }
285
+ return Boolean(part.time?.end)
286
+ }
287
+ return true
288
+ }
289
+
290
+ export function deriveLatestUnansweredQuestion({
291
+ events,
292
+ sessionId,
293
+ upToIndex,
294
+ }: {
295
+ events: EventBufferEntry[]
296
+ sessionId: string
297
+ upToIndex?: number
298
+ }): DerivedUnansweredQuestion | undefined {
299
+ const end = upToIndex ?? events.length - 1
300
+ for (let i = end; i >= 0; i--) {
301
+ const entry = events[i]
302
+ if (!entry) {
303
+ continue
304
+ }
305
+ const event = entry.event
306
+ if (getEventBufferSessionId(event) !== sessionId) {
307
+ continue
308
+ }
309
+ if (event.type === 'question.replied' || event.type === 'question.rejected') {
310
+ return undefined
311
+ }
312
+ if (event.type === 'message.part.updated') {
313
+ const part = event.properties.part
314
+ if (
315
+ part.type === 'tool'
316
+ && part.tool === 'question'
317
+ && (part.state.status === 'error' || part.state.status === 'completed')
318
+ ) {
319
+ return undefined
320
+ }
321
+ }
322
+ if (event.type === 'question.asked') {
323
+ const messageId = event.properties.tool?.messageID
324
+ const latestUserMessage = getLatestUserMessage({
325
+ events,
326
+ sessionId,
327
+ upToIndex: end,
328
+ })
329
+ if (
330
+ messageId
331
+ && latestUserMessage
332
+ && !isAssistantMessageInLatestUserTurn({
333
+ events,
334
+ sessionId,
335
+ messageId,
336
+ upToIndex: end,
337
+ })
338
+ ) {
339
+ return undefined
340
+ }
341
+ return {
342
+ id: event.properties.id,
343
+ questions: event.properties.questions,
344
+ tool: event.properties.tool,
345
+ }
346
+ }
347
+ }
348
+ return undefined
349
+ }
350
+
168
351
  export function derivePendingPermissionRequests({
169
352
  events,
170
353
  sessionId,
@@ -199,6 +382,9 @@ export function isAssistantMessageNaturalCompletion({
199
382
  }: {
200
383
  message: AssistantMessage
201
384
  }): boolean {
385
+ if (!isUserFacingAssistantMessage(message)) {
386
+ return false
387
+ }
202
388
  if (typeof message.time.completed !== 'number') {
203
389
  return false
204
390
  }
@@ -269,6 +455,14 @@ export function getLatestUserMessage({
269
455
  if (info.sessionID !== sessionId || info.role !== 'user') {
270
456
  continue
271
457
  }
458
+ if (isInternalOpenCodeUserMessageId({
459
+ events,
460
+ sessionId,
461
+ messageId: info.id,
462
+ upToIndex: end,
463
+ })) {
464
+ continue
465
+ }
272
466
  if (!latestUserMessage) {
273
467
  latestUserMessage = info
274
468
  continue
@@ -337,73 +531,162 @@ function emptyTurnTokenUsage(): TurnTokenUsage {
337
531
  }
338
532
  }
339
533
 
340
- // Latest billed token snapshot for the current user turn.
341
- // Sums the last message.updated tokens per assistant message id so streaming
342
- // updates are not double-counted. Scoped to sessionId so subagent idles
343
- // report their own usage.
344
- export function getLatestTurnTokenUsage({
534
+ function addAssistantTokens({
535
+ usage,
536
+ message,
537
+ }: {
538
+ usage: TurnTokenUsage
539
+ message: AssistantMessage
540
+ }): void {
541
+ if (message.tokens) {
542
+ usage.input += message.tokens.input
543
+ usage.output += message.tokens.output
544
+ usage.reasoning += message.tokens.reasoning
545
+ usage.cacheRead += message.tokens.cache.read
546
+ usage.cacheWrite += message.tokens.cache.write
547
+ usage.total += message.tokens.total ?? getTokenTotal(message.tokens)
548
+ }
549
+ usage.cost += message.cost
550
+ usage.model = message.modelID
551
+ usage.providerID = message.providerID
552
+ }
553
+
554
+ function sumAssistantMessages({
555
+ messages,
556
+ userMessageId,
557
+ }: {
558
+ messages: Map<string, AssistantMessage>
559
+ userMessageId?: string
560
+ }): TurnTokenUsage {
561
+ if (messages.size === 0) {
562
+ return {
563
+ ...emptyTurnTokenUsage(),
564
+ userMessageId,
565
+ }
566
+ }
567
+ const usage = emptyTurnTokenUsage()
568
+ usage.userMessageId = userMessageId
569
+ usage.assistantMessageCount = messages.size
570
+ for (const message of messages.values()) {
571
+ addAssistantTokens({ usage, message })
572
+ }
573
+ return usage
574
+ }
575
+
576
+ function collectAssistantMessages({
345
577
  events,
346
578
  sessionId,
347
579
  upToIndex,
580
+ parentID,
348
581
  }: {
349
582
  events: EventBufferEntry[]
350
583
  sessionId: string
351
- upToIndex?: number
352
- }): TurnTokenUsage {
353
- const latestUserMessage = getLatestUserMessage({
354
- events,
355
- sessionId,
356
- upToIndex,
357
- })
358
- if (!latestUserMessage) {
359
- return emptyTurnTokenUsage()
360
- }
361
- const end = upToIndex ?? events.length - 1
584
+ upToIndex: number
585
+ parentID?: string
586
+ }): Map<string, AssistantMessage> {
362
587
  const latestByMessageId = new Map<string, AssistantMessage>()
363
-
364
- for (let i = 0; i <= end; i++) {
365
- const entry = events[i]
366
- if (!entry) {
367
- continue
368
- }
369
- const event = entry.event
370
- if (event.type !== 'message.updated') {
588
+ for (let i = 0; i <= upToIndex; i++) {
589
+ const event = events[i]?.event
590
+ if (event?.type !== 'message.updated') {
371
591
  continue
372
592
  }
373
593
  const info = event.properties.info
374
594
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
375
595
  continue
376
596
  }
377
- if (info.parentID !== latestUserMessage.id) {
597
+ if (parentID && info.parentID !== parentID) {
378
598
  continue
379
599
  }
380
600
  latestByMessageId.set(info.id, info)
381
601
  }
602
+ return latestByMessageId
603
+ }
382
604
 
383
- if (latestByMessageId.size === 0) {
384
- return {
385
- ...emptyTurnTokenUsage(),
386
- userMessageId: latestUserMessage?.id,
605
+ function getSessionInfoTokenUsage({
606
+ events,
607
+ sessionId,
608
+ upToIndex,
609
+ }: {
610
+ events: EventBufferEntry[]
611
+ sessionId: string
612
+ upToIndex: number
613
+ }): TurnTokenUsage | undefined {
614
+ for (let i = upToIndex; i >= 0; i--) {
615
+ const event = events[i]?.event
616
+ if (event?.type !== 'session.updated' && event?.type !== 'session.created') {
617
+ continue
387
618
  }
619
+ const info = event.properties.info
620
+ if (info.id !== sessionId) {
621
+ continue
622
+ }
623
+ if (!info.tokens) {
624
+ continue
625
+ }
626
+ const usage = emptyTurnTokenUsage()
627
+ usage.input = info.tokens.input
628
+ usage.output = info.tokens.output
629
+ usage.reasoning = info.tokens.reasoning
630
+ usage.cacheRead = info.tokens.cache.read
631
+ usage.cacheWrite = info.tokens.cache.write
632
+ usage.total = getTokenTotal(info.tokens)
633
+ usage.cost = info.cost ?? 0
634
+ usage.model = info.model?.id
635
+ usage.providerID = info.model?.providerID
636
+ return usage.total > 0 || usage.cost > 0 ? usage : undefined
388
637
  }
638
+ return undefined
639
+ }
389
640
 
390
- const usage = emptyTurnTokenUsage()
391
- usage.userMessageId = latestUserMessage.id
392
- usage.assistantMessageCount = latestByMessageId.size
393
- for (const message of latestByMessageId.values()) {
394
- if (message.tokens) {
395
- usage.input += message.tokens.input
396
- usage.output += message.tokens.output
397
- usage.reasoning += message.tokens.reasoning
398
- usage.cacheRead += message.tokens.cache.read
399
- usage.cacheWrite += message.tokens.cache.write
400
- usage.total += message.tokens.total ?? getTokenTotal(message.tokens)
401
- }
402
- usage.cost += message.cost
403
- usage.model = message.modelID
404
- usage.providerID = message.providerID
641
+ // Latest billed token snapshot for the current user turn.
642
+ // Sums the last message.updated tokens per assistant message id so streaming
643
+ // updates are not double-counted. Scoped to sessionId so subagent idles
644
+ // report their own usage. Child task sessions often have no user message in
645
+ // the buffer; fall back to all assistant messages, then Session.tokens on
646
+ // session.updated (OpenCode projects per-session usage there).
647
+ export function getLatestTurnTokenUsage({
648
+ events,
649
+ sessionId,
650
+ upToIndex,
651
+ }: {
652
+ events: EventBufferEntry[]
653
+ sessionId: string
654
+ upToIndex?: number
655
+ }): TurnTokenUsage {
656
+ const end = upToIndex ?? events.length - 1
657
+ const latestUserMessage = getLatestUserMessage({
658
+ events,
659
+ sessionId,
660
+ upToIndex,
661
+ })
662
+ if (latestUserMessage) {
663
+ return sumAssistantMessages({
664
+ messages: collectAssistantMessages({
665
+ events,
666
+ sessionId,
667
+ upToIndex: end,
668
+ parentID: latestUserMessage.id,
669
+ }),
670
+ userMessageId: latestUserMessage.id,
671
+ })
405
672
  }
406
- return usage
673
+
674
+ const sessionAssistants = sumAssistantMessages({
675
+ messages: collectAssistantMessages({
676
+ events,
677
+ sessionId,
678
+ upToIndex: end,
679
+ }),
680
+ })
681
+ if (sessionAssistants.total > 0 || sessionAssistants.assistantMessageCount > 0) {
682
+ return sessionAssistants
683
+ }
684
+
685
+ return getSessionInfoTokenUsage({
686
+ events,
687
+ sessionId,
688
+ upToIndex: end,
689
+ }) ?? emptyTurnTokenUsage()
407
690
  }
408
691
 
409
692
  function findFirstUserMessageIndex({
@@ -469,8 +752,31 @@ function subtractTokenUsage({
469
752
  }
470
753
  }
471
754
 
755
+ function findFirstSessionEventIndex({
756
+ events,
757
+ sessionId,
758
+ upToIndex,
759
+ }: {
760
+ events: EventBufferEntry[]
761
+ sessionId: string
762
+ upToIndex: number
763
+ }): number {
764
+ for (let i = 0; i <= upToIndex; i++) {
765
+ const event = events[i]?.event
766
+ if (!event) {
767
+ continue
768
+ }
769
+ if (getEventBufferSessionId(event) === sessionId) {
770
+ return i
771
+ }
772
+ }
773
+ return 0
774
+ }
775
+
472
776
  // Tokens billed since the previous session.idle in this user turn.
473
777
  // Survives process restart because both idles stay in the event buffer.
778
+ // Child task sessions may have no user message.updated; scope from the first
779
+ // event for that sessionId instead so their tokens still emit.
474
780
  export function getIdleTokenUsageDelta({
475
781
  events,
476
782
  sessionId,
@@ -485,16 +791,22 @@ export function getIdleTokenUsageDelta({
485
791
  sessionId,
486
792
  upToIndex: idleEventIndex,
487
793
  })
488
- if (!current.userMessageId) {
794
+ if (current.total <= 0) {
489
795
  return undefined
490
796
  }
491
- const firstUserMessageIndex = findFirstUserMessageIndex({
492
- events,
493
- userMessageId: current.userMessageId,
494
- upToIndex: idleEventIndex,
495
- })
797
+ const firstUserMessageIndex = current.userMessageId
798
+ ? findFirstUserMessageIndex({
799
+ events,
800
+ userMessageId: current.userMessageId,
801
+ upToIndex: idleEventIndex,
802
+ })
803
+ : findFirstSessionEventIndex({
804
+ events,
805
+ sessionId,
806
+ upToIndex: idleEventIndex,
807
+ })
496
808
  if (firstUserMessageIndex === undefined) {
497
- return undefined
809
+ return current
498
810
  }
499
811
  const previousIdleIndex = findPreviousIdleIndexInTurn({
500
812
  events,
@@ -503,7 +815,7 @@ export function getIdleTokenUsageDelta({
503
815
  beforeIndex: idleEventIndex,
504
816
  })
505
817
  if (previousIdleIndex === undefined) {
506
- return current.total > 0 ? current : undefined
818
+ return current
507
819
  }
508
820
  const previous = getLatestTurnTokenUsage({
509
821
  events,
@@ -553,6 +865,9 @@ export function getLatestRunInfo({
553
865
  if (msg.sessionID !== sessionId || msg.role !== 'assistant') {
554
866
  continue
555
867
  }
868
+ if (!isUserFacingAssistantMessage(msg)) {
869
+ continue
870
+ }
556
871
  return {
557
872
  model: msg.modelID,
558
873
  providerID: msg.providerID,
@@ -597,6 +912,9 @@ export function getAssistantMessageIdsForLatestUserTurn({
597
912
  if (msg.sessionID !== sessionId || msg.role !== 'assistant') {
598
913
  continue
599
914
  }
915
+ if (!isUserFacingAssistantMessage(msg)) {
916
+ continue
917
+ }
600
918
  if (msg.parentID === latestUserMessage.id) {
601
919
  assistantMessageIds.add(msg.id)
602
920
  }
@@ -604,6 +922,33 @@ export function getAssistantMessageIdsForLatestUserTurn({
604
922
  return assistantMessageIds
605
923
  }
606
924
 
925
+ export function didLatestUserTurnUseSleepTool({
926
+ events,
927
+ sessionId,
928
+ upToIndex,
929
+ }: {
930
+ events: EventBufferEntry[]
931
+ sessionId: string
932
+ upToIndex?: number
933
+ }): boolean {
934
+ const assistantMessageIds = getAssistantMessageIdsForLatestUserTurn({
935
+ events,
936
+ sessionId,
937
+ upToIndex,
938
+ })
939
+ const end = upToIndex ?? events.length - 1
940
+ for (let i = end; i >= 0; i--) {
941
+ const event = events[i]?.event
942
+ if (event?.type !== 'message.part.updated') continue
943
+ const part = event.properties.part
944
+ if (part.sessionID !== sessionId || part.type !== 'tool') continue
945
+ if (part.tool === 'kimaki_sleep' && assistantMessageIds.has(part.messageID)) {
946
+ return true
947
+ }
948
+ }
949
+ return false
950
+ }
951
+
607
952
  export function getLatestAssistantMessageIdForLatestUserTurn({
608
953
  events,
609
954
  sessionId,
@@ -638,6 +983,9 @@ export function getLatestAssistantMessageIdForLatestUserTurn({
638
983
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
639
984
  continue
640
985
  }
986
+ if (!isUserFacingAssistantMessage(info)) {
987
+ continue
988
+ }
641
989
  if (info.parentID !== latestUserMessage.id) {
642
990
  continue
643
991
  }
@@ -816,6 +1164,25 @@ export function isAssistantMessageInLatestUserTurn({
816
1164
  return assistantMessageIds.has(messageId)
817
1165
  }
818
1166
 
1167
+ export function isSummaryAssistantMessage({
1168
+ events,
1169
+ sessionId,
1170
+ messageId,
1171
+ upToIndex,
1172
+ }: {
1173
+ events: EventBufferEntry[]
1174
+ sessionId: string
1175
+ messageId: string
1176
+ upToIndex?: number
1177
+ }): boolean {
1178
+ return getAssistantMessageKind({
1179
+ events,
1180
+ sessionId,
1181
+ messageId,
1182
+ upToIndex,
1183
+ }) === 'summary'
1184
+ }
1185
+
819
1186
  // Returns a stable 1-based subtask index for candidateSessionId.
820
1187
  // Indexing scope is the parent assistant message that spawned the task tool calls,
821
1188
  // so numbering restarts at 1 for each assistant message.
@@ -945,3 +1312,151 @@ export function getDerivedSubagentSessions({
945
1312
 
946
1313
  return sessions
947
1314
  }
1315
+
1316
+ function getParentIdFromSessionEvent(event: EventBufferEvent): {
1317
+ sessionId: string
1318
+ parentID: string
1319
+ } | undefined {
1320
+ if (event.type !== 'session.created' && event.type !== 'session.updated') {
1321
+ return undefined
1322
+ }
1323
+ const parentID = event.properties.info.parentID
1324
+ if (typeof parentID !== 'string' || parentID.length === 0) {
1325
+ return undefined
1326
+ }
1327
+ return {
1328
+ sessionId: event.properties.info.id,
1329
+ parentID,
1330
+ }
1331
+ }
1332
+
1333
+ // Global SSE is broadcast to every thread. Buffer only this thread's session
1334
+ // plus its task/subagent children. /btw fork clones history onto a new
1335
+ // sessionId; those events must not evict the parent's latest-turn buffer.
1336
+ export function shouldBufferSessionEvent({
1337
+ event,
1338
+ mainSessionId,
1339
+ isKnownChildSession,
1340
+ }: {
1341
+ event: EventBufferEvent
1342
+ mainSessionId?: string
1343
+ isKnownChildSession: (sessionId: string) => boolean
1344
+ }): boolean {
1345
+ if (event.type === 'session.diff' || event.type.endsWith('.delta')) {
1346
+ return false
1347
+ }
1348
+ if (event.type === 'tui.toast.show') {
1349
+ return true
1350
+ }
1351
+
1352
+ const eventSessionId = getEventBufferSessionId(event)
1353
+ if (!eventSessionId) {
1354
+ return true
1355
+ }
1356
+ // A new /btw thread registers the SSE listener before ensureSession.
1357
+ // Until the child session id is bound, drop every scoped event so the
1358
+ // parent clone flood cannot fill this buffer first.
1359
+ if (!mainSessionId) {
1360
+ return false
1361
+ }
1362
+ if (eventSessionId === mainSessionId) {
1363
+ return true
1364
+ }
1365
+ if (isKnownChildSession(eventSessionId)) {
1366
+ return true
1367
+ }
1368
+
1369
+ const parented = getParentIdFromSessionEvent(event)
1370
+ if (!parented) {
1371
+ return false
1372
+ }
1373
+ return parented.parentID === mainSessionId || isKnownChildSession(parented.parentID)
1374
+ }
1375
+
1376
+ // Child sessions of the main thread: task tool metadata.sessionId, plus
1377
+ // session.created/updated parentID (available before task metadata lands).
1378
+ export function getDerivedChildSessionIds({
1379
+ events,
1380
+ mainSessionId,
1381
+ upToIndex,
1382
+ }: {
1383
+ events: EventBufferEntry[]
1384
+ mainSessionId: string
1385
+ upToIndex?: number
1386
+ }): Set<string> {
1387
+ const end = upToIndex ?? events.length - 1
1388
+ const ids = new Set<string>()
1389
+ for (const session of getDerivedSubagentSessions({
1390
+ events,
1391
+ mainSessionId,
1392
+ upToIndex,
1393
+ })) {
1394
+ ids.add(session.childSessionId)
1395
+ }
1396
+
1397
+ let grew = true
1398
+ while (grew) {
1399
+ grew = false
1400
+ for (let i = 0; i <= end; i++) {
1401
+ const event = events[i]?.event
1402
+ if (!event) {
1403
+ continue
1404
+ }
1405
+ const parented = getParentIdFromSessionEvent(event)
1406
+ if (!parented || ids.has(parented.sessionId)) {
1407
+ continue
1408
+ }
1409
+ if (parented.parentID !== mainSessionId && !ids.has(parented.parentID)) {
1410
+ continue
1411
+ }
1412
+ ids.add(parented.sessionId)
1413
+ grew = true
1414
+ }
1415
+ }
1416
+ return ids
1417
+ }
1418
+
1419
+ export function isDerivedChildSession({
1420
+ events,
1421
+ mainSessionId,
1422
+ candidateSessionId,
1423
+ upToIndex,
1424
+ }: {
1425
+ events: EventBufferEntry[]
1426
+ mainSessionId: string
1427
+ candidateSessionId: string
1428
+ upToIndex?: number
1429
+ }): boolean {
1430
+ if (candidateSessionId === mainSessionId) {
1431
+ return false
1432
+ }
1433
+ return getDerivedChildSessionIds({
1434
+ events,
1435
+ mainSessionId,
1436
+ upToIndex,
1437
+ }).has(candidateSessionId)
1438
+ }
1439
+
1440
+ export function getTokenUsageSessionIdsForIdle({
1441
+ events,
1442
+ mainSessionId,
1443
+ idleSessionId,
1444
+ upToIndex,
1445
+ }: {
1446
+ events: EventBufferEntry[]
1447
+ mainSessionId: string
1448
+ idleSessionId: string
1449
+ upToIndex?: number
1450
+ }): string[] {
1451
+ if (idleSessionId !== mainSessionId) {
1452
+ return [idleSessionId]
1453
+ }
1454
+ return [
1455
+ mainSessionId,
1456
+ ...getDerivedChildSessionIds({
1457
+ events,
1458
+ mainSessionId,
1459
+ upToIndex,
1460
+ }),
1461
+ ]
1462
+ }