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
@@ -9,6 +9,50 @@ export function getEventBufferSessionId(event) {
9
9
  }
10
10
  return getOpencodeEventSessionId(event);
11
11
  }
12
+ function isCompactionContinuePart(part) {
13
+ if (part.type !== 'text') {
14
+ return false;
15
+ }
16
+ return part.synthetic === true && part.metadata?.compaction_continue === true;
17
+ }
18
+ function isInternalOpenCodeUserMessageId({ events, sessionId, messageId, upToIndex, }) {
19
+ const end = upToIndex ?? events.length - 1;
20
+ for (let i = 0; i <= end; i++) {
21
+ const event = events[i]?.event;
22
+ if (event?.type !== 'message.part.updated') {
23
+ continue;
24
+ }
25
+ const part = event.properties.part;
26
+ if (part.sessionID !== sessionId || part.messageID !== messageId) {
27
+ continue;
28
+ }
29
+ if (part.type === 'compaction') {
30
+ return true;
31
+ }
32
+ if (isCompactionContinuePart(part)) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ function isUserFacingAssistantMessage(message) {
39
+ return message.summary !== true;
40
+ }
41
+ export function getAssistantMessageKind({ events, sessionId, messageId, upToIndex, }) {
42
+ const end = upToIndex ?? events.length - 1;
43
+ for (let i = end; i >= 0; i--) {
44
+ const event = events[i]?.event;
45
+ if (event?.type !== 'message.updated') {
46
+ continue;
47
+ }
48
+ const info = event.properties.info;
49
+ if (info.sessionID !== sessionId || info.role !== 'assistant' || info.id !== messageId) {
50
+ continue;
51
+ }
52
+ return info.summary === true ? 'summary' : 'user-facing';
53
+ }
54
+ return 'unknown';
55
+ }
12
56
  function getTaskChildSessionId({ part, }) {
13
57
  // Event-shape reference:
14
58
  // - cli/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl
@@ -93,6 +137,78 @@ export function didQuestionQueueHandoffSinceLatestQuestionAsked({ events, sessio
93
137
  }
94
138
  return false;
95
139
  }
140
+ // OpenCode emits question.asked when the tool starts, often before the
141
+ // preceding text part gets time.end. Discord must wait for that end event
142
+ // or the question UI posts first and the text dumps later.
143
+ export function isAssistantTextReadyForQuestion({ events, sessionId, messageId, upToIndex, }) {
144
+ const end = upToIndex ?? events.length - 1;
145
+ for (let i = end; i >= 0; i--) {
146
+ const event = events[i]?.event;
147
+ if (!event || event.type !== 'message.part.updated') {
148
+ continue;
149
+ }
150
+ const part = event.properties.part;
151
+ if (part.sessionID !== sessionId) {
152
+ continue;
153
+ }
154
+ if (part.messageID !== messageId) {
155
+ continue;
156
+ }
157
+ if (part.type !== 'text') {
158
+ continue;
159
+ }
160
+ return Boolean(part.time?.end);
161
+ }
162
+ return true;
163
+ }
164
+ export function deriveLatestUnansweredQuestion({ events, sessionId, upToIndex, }) {
165
+ const end = upToIndex ?? events.length - 1;
166
+ for (let i = end; i >= 0; i--) {
167
+ const entry = events[i];
168
+ if (!entry) {
169
+ continue;
170
+ }
171
+ const event = entry.event;
172
+ if (getEventBufferSessionId(event) !== sessionId) {
173
+ continue;
174
+ }
175
+ if (event.type === 'question.replied' || event.type === 'question.rejected') {
176
+ return undefined;
177
+ }
178
+ if (event.type === 'message.part.updated') {
179
+ const part = event.properties.part;
180
+ if (part.type === 'tool'
181
+ && part.tool === 'question'
182
+ && (part.state.status === 'error' || part.state.status === 'completed')) {
183
+ return undefined;
184
+ }
185
+ }
186
+ if (event.type === 'question.asked') {
187
+ const messageId = event.properties.tool?.messageID;
188
+ const latestUserMessage = getLatestUserMessage({
189
+ events,
190
+ sessionId,
191
+ upToIndex: end,
192
+ });
193
+ if (messageId
194
+ && latestUserMessage
195
+ && !isAssistantMessageInLatestUserTurn({
196
+ events,
197
+ sessionId,
198
+ messageId,
199
+ upToIndex: end,
200
+ })) {
201
+ return undefined;
202
+ }
203
+ return {
204
+ id: event.properties.id,
205
+ questions: event.properties.questions,
206
+ tool: event.properties.tool,
207
+ };
208
+ }
209
+ }
210
+ return undefined;
211
+ }
96
212
  export function derivePendingPermissionRequests({ events, sessionId, }) {
97
213
  const permissions = new Set();
98
214
  for (const entry of events) {
@@ -112,6 +228,9 @@ export function derivePendingPermissionRequests({ events, sessionId, }) {
112
228
  return [...permissions];
113
229
  }
114
230
  export function isAssistantMessageNaturalCompletion({ message, }) {
231
+ if (!isUserFacingAssistantMessage(message)) {
232
+ return false;
233
+ }
115
234
  if (typeof message.time.completed !== 'number') {
116
235
  return false;
117
236
  }
@@ -162,6 +281,14 @@ export function getLatestUserMessage({ events, sessionId, upToIndex, }) {
162
281
  if (info.sessionID !== sessionId || info.role !== 'user') {
163
282
  continue;
164
283
  }
284
+ if (isInternalOpenCodeUserMessageId({
285
+ events,
286
+ sessionId,
287
+ messageId: info.id,
288
+ upToIndex: end,
289
+ })) {
290
+ continue;
291
+ }
165
292
  if (!latestUserMessage) {
166
293
  latestUserMessage = info;
167
294
  continue;
@@ -199,62 +326,118 @@ function emptyTurnTokenUsage() {
199
326
  userMessageId: undefined,
200
327
  };
201
328
  }
202
- // Latest billed token snapshot for the current user turn.
203
- // Sums the last message.updated tokens per assistant message id so streaming
204
- // updates are not double-counted. Scoped to sessionId so subagent idles
205
- // report their own usage.
206
- export function getLatestTurnTokenUsage({ events, sessionId, upToIndex, }) {
207
- const latestUserMessage = getLatestUserMessage({
208
- events,
209
- sessionId,
210
- upToIndex,
211
- });
212
- if (!latestUserMessage) {
213
- return emptyTurnTokenUsage();
329
+ function addAssistantTokens({ usage, message, }) {
330
+ if (message.tokens) {
331
+ usage.input += message.tokens.input;
332
+ usage.output += message.tokens.output;
333
+ usage.reasoning += message.tokens.reasoning;
334
+ usage.cacheRead += message.tokens.cache.read;
335
+ usage.cacheWrite += message.tokens.cache.write;
336
+ usage.total += message.tokens.total ?? getTokenTotal(message.tokens);
337
+ }
338
+ usage.cost += message.cost;
339
+ usage.model = message.modelID;
340
+ usage.providerID = message.providerID;
341
+ }
342
+ function sumAssistantMessages({ messages, userMessageId, }) {
343
+ if (messages.size === 0) {
344
+ return {
345
+ ...emptyTurnTokenUsage(),
346
+ userMessageId,
347
+ };
214
348
  }
215
- const end = upToIndex ?? events.length - 1;
349
+ const usage = emptyTurnTokenUsage();
350
+ usage.userMessageId = userMessageId;
351
+ usage.assistantMessageCount = messages.size;
352
+ for (const message of messages.values()) {
353
+ addAssistantTokens({ usage, message });
354
+ }
355
+ return usage;
356
+ }
357
+ function collectAssistantMessages({ events, sessionId, upToIndex, parentID, }) {
216
358
  const latestByMessageId = new Map();
217
- for (let i = 0; i <= end; i++) {
218
- const entry = events[i];
219
- if (!entry) {
220
- continue;
221
- }
222
- const event = entry.event;
223
- if (event.type !== 'message.updated') {
359
+ for (let i = 0; i <= upToIndex; i++) {
360
+ const event = events[i]?.event;
361
+ if (event?.type !== 'message.updated') {
224
362
  continue;
225
363
  }
226
364
  const info = event.properties.info;
227
365
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
228
366
  continue;
229
367
  }
230
- if (info.parentID !== latestUserMessage.id) {
368
+ if (parentID && info.parentID !== parentID) {
231
369
  continue;
232
370
  }
233
371
  latestByMessageId.set(info.id, info);
234
372
  }
235
- if (latestByMessageId.size === 0) {
236
- return {
237
- ...emptyTurnTokenUsage(),
238
- userMessageId: latestUserMessage?.id,
239
- };
373
+ return latestByMessageId;
374
+ }
375
+ function getSessionInfoTokenUsage({ events, sessionId, upToIndex, }) {
376
+ for (let i = upToIndex; i >= 0; i--) {
377
+ const event = events[i]?.event;
378
+ if (event?.type !== 'session.updated' && event?.type !== 'session.created') {
379
+ continue;
380
+ }
381
+ const info = event.properties.info;
382
+ if (info.id !== sessionId) {
383
+ continue;
384
+ }
385
+ if (!info.tokens) {
386
+ continue;
387
+ }
388
+ const usage = emptyTurnTokenUsage();
389
+ usage.input = info.tokens.input;
390
+ usage.output = info.tokens.output;
391
+ usage.reasoning = info.tokens.reasoning;
392
+ usage.cacheRead = info.tokens.cache.read;
393
+ usage.cacheWrite = info.tokens.cache.write;
394
+ usage.total = getTokenTotal(info.tokens);
395
+ usage.cost = info.cost ?? 0;
396
+ usage.model = info.model?.id;
397
+ usage.providerID = info.model?.providerID;
398
+ return usage.total > 0 || usage.cost > 0 ? usage : undefined;
240
399
  }
241
- const usage = emptyTurnTokenUsage();
242
- usage.userMessageId = latestUserMessage.id;
243
- usage.assistantMessageCount = latestByMessageId.size;
244
- for (const message of latestByMessageId.values()) {
245
- if (message.tokens) {
246
- usage.input += message.tokens.input;
247
- usage.output += message.tokens.output;
248
- usage.reasoning += message.tokens.reasoning;
249
- usage.cacheRead += message.tokens.cache.read;
250
- usage.cacheWrite += message.tokens.cache.write;
251
- usage.total += message.tokens.total ?? getTokenTotal(message.tokens);
252
- }
253
- usage.cost += message.cost;
254
- usage.model = message.modelID;
255
- usage.providerID = message.providerID;
400
+ return undefined;
401
+ }
402
+ // Latest billed token snapshot for the current user turn.
403
+ // Sums the last message.updated tokens per assistant message id so streaming
404
+ // updates are not double-counted. Scoped to sessionId so subagent idles
405
+ // report their own usage. Child task sessions often have no user message in
406
+ // the buffer; fall back to all assistant messages, then Session.tokens on
407
+ // session.updated (OpenCode projects per-session usage there).
408
+ export function getLatestTurnTokenUsage({ events, sessionId, upToIndex, }) {
409
+ const end = upToIndex ?? events.length - 1;
410
+ const latestUserMessage = getLatestUserMessage({
411
+ events,
412
+ sessionId,
413
+ upToIndex,
414
+ });
415
+ if (latestUserMessage) {
416
+ return sumAssistantMessages({
417
+ messages: collectAssistantMessages({
418
+ events,
419
+ sessionId,
420
+ upToIndex: end,
421
+ parentID: latestUserMessage.id,
422
+ }),
423
+ userMessageId: latestUserMessage.id,
424
+ });
256
425
  }
257
- return usage;
426
+ const sessionAssistants = sumAssistantMessages({
427
+ messages: collectAssistantMessages({
428
+ events,
429
+ sessionId,
430
+ upToIndex: end,
431
+ }),
432
+ });
433
+ if (sessionAssistants.total > 0 || sessionAssistants.assistantMessageCount > 0) {
434
+ return sessionAssistants;
435
+ }
436
+ return getSessionInfoTokenUsage({
437
+ events,
438
+ sessionId,
439
+ upToIndex: end,
440
+ }) ?? emptyTurnTokenUsage();
258
441
  }
259
442
  function findFirstUserMessageIndex({ events, userMessageId, upToIndex, }) {
260
443
  for (let i = 0; i <= upToIndex; i++) {
@@ -292,24 +475,44 @@ function subtractTokenUsage({ current, previous, }) {
292
475
  userMessageId: current.userMessageId,
293
476
  };
294
477
  }
478
+ function findFirstSessionEventIndex({ events, sessionId, upToIndex, }) {
479
+ for (let i = 0; i <= upToIndex; i++) {
480
+ const event = events[i]?.event;
481
+ if (!event) {
482
+ continue;
483
+ }
484
+ if (getEventBufferSessionId(event) === sessionId) {
485
+ return i;
486
+ }
487
+ }
488
+ return 0;
489
+ }
295
490
  // Tokens billed since the previous session.idle in this user turn.
296
491
  // Survives process restart because both idles stay in the event buffer.
492
+ // Child task sessions may have no user message.updated; scope from the first
493
+ // event for that sessionId instead so their tokens still emit.
297
494
  export function getIdleTokenUsageDelta({ events, sessionId, idleEventIndex, }) {
298
495
  const current = getLatestTurnTokenUsage({
299
496
  events,
300
497
  sessionId,
301
498
  upToIndex: idleEventIndex,
302
499
  });
303
- if (!current.userMessageId) {
500
+ if (current.total <= 0) {
304
501
  return undefined;
305
502
  }
306
- const firstUserMessageIndex = findFirstUserMessageIndex({
307
- events,
308
- userMessageId: current.userMessageId,
309
- upToIndex: idleEventIndex,
310
- });
503
+ const firstUserMessageIndex = current.userMessageId
504
+ ? findFirstUserMessageIndex({
505
+ events,
506
+ userMessageId: current.userMessageId,
507
+ upToIndex: idleEventIndex,
508
+ })
509
+ : findFirstSessionEventIndex({
510
+ events,
511
+ sessionId,
512
+ upToIndex: idleEventIndex,
513
+ });
311
514
  if (firstUserMessageIndex === undefined) {
312
- return undefined;
515
+ return current;
313
516
  }
314
517
  const previousIdleIndex = findPreviousIdleIndexInTurn({
315
518
  events,
@@ -318,7 +521,7 @@ export function getIdleTokenUsageDelta({ events, sessionId, idleEventIndex, }) {
318
521
  beforeIndex: idleEventIndex,
319
522
  });
320
523
  if (previousIdleIndex === undefined) {
321
- return current.total > 0 ? current : undefined;
524
+ return current;
322
525
  }
323
526
  const previous = getLatestTurnTokenUsage({
324
527
  events,
@@ -354,6 +557,9 @@ export function getLatestRunInfo({ events, sessionId, upToIndex, }) {
354
557
  if (msg.sessionID !== sessionId || msg.role !== 'assistant') {
355
558
  continue;
356
559
  }
560
+ if (!isUserFacingAssistantMessage(msg)) {
561
+ continue;
562
+ }
357
563
  return {
358
564
  model: msg.modelID,
359
565
  providerID: msg.providerID,
@@ -389,12 +595,35 @@ export function getAssistantMessageIdsForLatestUserTurn({ events, sessionId, upT
389
595
  if (msg.sessionID !== sessionId || msg.role !== 'assistant') {
390
596
  continue;
391
597
  }
598
+ if (!isUserFacingAssistantMessage(msg)) {
599
+ continue;
600
+ }
392
601
  if (msg.parentID === latestUserMessage.id) {
393
602
  assistantMessageIds.add(msg.id);
394
603
  }
395
604
  }
396
605
  return assistantMessageIds;
397
606
  }
607
+ export function didLatestUserTurnUseSleepTool({ events, sessionId, upToIndex, }) {
608
+ const assistantMessageIds = getAssistantMessageIdsForLatestUserTurn({
609
+ events,
610
+ sessionId,
611
+ upToIndex,
612
+ });
613
+ const end = upToIndex ?? events.length - 1;
614
+ for (let i = end; i >= 0; i--) {
615
+ const event = events[i]?.event;
616
+ if (event?.type !== 'message.part.updated')
617
+ continue;
618
+ const part = event.properties.part;
619
+ if (part.sessionID !== sessionId || part.type !== 'tool')
620
+ continue;
621
+ if (part.tool === 'kimaki_sleep' && assistantMessageIds.has(part.messageID)) {
622
+ return true;
623
+ }
624
+ }
625
+ return false;
626
+ }
398
627
  export function getLatestAssistantMessageIdForLatestUserTurn({ events, sessionId, upToIndex, }) {
399
628
  const latestUserMessage = getLatestUserMessage({
400
629
  events,
@@ -419,6 +648,9 @@ export function getLatestAssistantMessageIdForLatestUserTurn({ events, sessionId
419
648
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
420
649
  continue;
421
650
  }
651
+ if (!isUserFacingAssistantMessage(info)) {
652
+ continue;
653
+ }
422
654
  if (info.parentID !== latestUserMessage.id) {
423
655
  continue;
424
656
  }
@@ -549,6 +781,14 @@ export function isAssistantMessageInLatestUserTurn({ events, sessionId, messageI
549
781
  });
550
782
  return assistantMessageIds.has(messageId);
551
783
  }
784
+ export function isSummaryAssistantMessage({ events, sessionId, messageId, upToIndex, }) {
785
+ return getAssistantMessageKind({
786
+ events,
787
+ sessionId,
788
+ messageId,
789
+ upToIndex,
790
+ }) === 'summary';
791
+ }
552
792
  // Returns a stable 1-based subtask index for candidateSessionId.
553
793
  // Indexing scope is the parent assistant message that spawned the task tool calls,
554
794
  // so numbering restarts at 1 for each assistant message.
@@ -640,3 +880,104 @@ export function getDerivedSubagentSessions({ events, mainSessionId, upToIndex, }
640
880
  }
641
881
  return sessions;
642
882
  }
883
+ function getParentIdFromSessionEvent(event) {
884
+ if (event.type !== 'session.created' && event.type !== 'session.updated') {
885
+ return undefined;
886
+ }
887
+ const parentID = event.properties.info.parentID;
888
+ if (typeof parentID !== 'string' || parentID.length === 0) {
889
+ return undefined;
890
+ }
891
+ return {
892
+ sessionId: event.properties.info.id,
893
+ parentID,
894
+ };
895
+ }
896
+ // Global SSE is broadcast to every thread. Buffer only this thread's session
897
+ // plus its task/subagent children. /btw fork clones history onto a new
898
+ // sessionId; those events must not evict the parent's latest-turn buffer.
899
+ export function shouldBufferSessionEvent({ event, mainSessionId, isKnownChildSession, }) {
900
+ if (event.type === 'session.diff' || event.type.endsWith('.delta')) {
901
+ return false;
902
+ }
903
+ if (event.type === 'tui.toast.show') {
904
+ return true;
905
+ }
906
+ const eventSessionId = getEventBufferSessionId(event);
907
+ if (!eventSessionId) {
908
+ return true;
909
+ }
910
+ // A new /btw thread registers the SSE listener before ensureSession.
911
+ // Until the child session id is bound, drop every scoped event so the
912
+ // parent clone flood cannot fill this buffer first.
913
+ if (!mainSessionId) {
914
+ return false;
915
+ }
916
+ if (eventSessionId === mainSessionId) {
917
+ return true;
918
+ }
919
+ if (isKnownChildSession(eventSessionId)) {
920
+ return true;
921
+ }
922
+ const parented = getParentIdFromSessionEvent(event);
923
+ if (!parented) {
924
+ return false;
925
+ }
926
+ return parented.parentID === mainSessionId || isKnownChildSession(parented.parentID);
927
+ }
928
+ // Child sessions of the main thread: task tool metadata.sessionId, plus
929
+ // session.created/updated parentID (available before task metadata lands).
930
+ export function getDerivedChildSessionIds({ events, mainSessionId, upToIndex, }) {
931
+ const end = upToIndex ?? events.length - 1;
932
+ const ids = new Set();
933
+ for (const session of getDerivedSubagentSessions({
934
+ events,
935
+ mainSessionId,
936
+ upToIndex,
937
+ })) {
938
+ ids.add(session.childSessionId);
939
+ }
940
+ let grew = true;
941
+ while (grew) {
942
+ grew = false;
943
+ for (let i = 0; i <= end; i++) {
944
+ const event = events[i]?.event;
945
+ if (!event) {
946
+ continue;
947
+ }
948
+ const parented = getParentIdFromSessionEvent(event);
949
+ if (!parented || ids.has(parented.sessionId)) {
950
+ continue;
951
+ }
952
+ if (parented.parentID !== mainSessionId && !ids.has(parented.parentID)) {
953
+ continue;
954
+ }
955
+ ids.add(parented.sessionId);
956
+ grew = true;
957
+ }
958
+ }
959
+ return ids;
960
+ }
961
+ export function isDerivedChildSession({ events, mainSessionId, candidateSessionId, upToIndex, }) {
962
+ if (candidateSessionId === mainSessionId) {
963
+ return false;
964
+ }
965
+ return getDerivedChildSessionIds({
966
+ events,
967
+ mainSessionId,
968
+ upToIndex,
969
+ }).has(candidateSessionId);
970
+ }
971
+ export function getTokenUsageSessionIdsForIdle({ events, mainSessionId, idleSessionId, upToIndex, }) {
972
+ if (idleSessionId !== mainSessionId) {
973
+ return [idleSessionId];
974
+ }
975
+ return [
976
+ mainSessionId,
977
+ ...getDerivedChildSessionIds({
978
+ events,
979
+ mainSessionId,
980
+ upToIndex,
981
+ }),
982
+ ];
983
+ }