kimaki 0.26.0 → 0.27.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 (214) hide show
  1. package/dist/agent-model.e2e.test.js +25 -122
  2. package/dist/anthropic-account-identity.js +1 -0
  3. package/dist/anthropic-auth-plugin.js +1 -0
  4. package/dist/anthropic-auth-state.js +1 -0
  5. package/dist/bin.js +73 -19
  6. package/dist/cli-commands/maintenance.js +46 -7
  7. package/dist/cli-commands/send.js +24 -0
  8. package/dist/cli-commands/session.js +183 -8
  9. package/dist/cli-commands/task.js +13 -1
  10. package/dist/cli-parsing.test.js +59 -3
  11. package/dist/cli-runner.js +28 -4
  12. package/dist/cli-runner.test.js +27 -0
  13. package/dist/cli-send-thread.e2e.test.js +1 -1
  14. package/dist/cli.js +32 -0
  15. package/dist/commands/abort.js +5 -1
  16. package/dist/commands/agent.js +6 -8
  17. package/dist/commands/ask-question.js +21 -32
  18. package/dist/commands/ask-question.test.js +43 -1
  19. package/dist/commands/login.js +6 -0
  20. package/dist/commands/merge-worktree.js +36 -10
  21. package/dist/commands/model-variant.js +7 -1
  22. package/dist/commands/model.js +14 -8
  23. package/dist/commands/multioauth.js +1 -0
  24. package/dist/commands/new-worktree.js +170 -102
  25. package/dist/commands/user-command.js +6 -8
  26. package/dist/commands/worktrees.js +5 -13
  27. package/dist/config.js +16 -1
  28. package/dist/database.js +124 -3
  29. package/dist/db.js +22 -0
  30. package/dist/db.test.js +167 -1
  31. package/dist/discord-bot.js +34 -32
  32. package/dist/discord-command-registration.js +9 -1
  33. package/dist/discord-utils.js +13 -5
  34. package/dist/discord-utils.test.js +17 -1
  35. package/dist/errors.js +5 -0
  36. package/dist/exec-async.js +17 -5
  37. package/dist/external-directory-permissions.e2e.test.js +2 -2
  38. package/dist/file-edit-log.js +197 -0
  39. package/dist/file-edit-log.test.js +258 -0
  40. package/dist/format-tables.js +52 -0
  41. package/dist/format-tables.test.js +93 -0
  42. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  43. package/dist/gateway-proxy.e2e.test.js +5 -5
  44. package/dist/git-worktree-core.js +182 -17
  45. package/dist/ipc-tools-plugin.js +63 -3
  46. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  47. package/dist/kimaki-opencode-plugin.js +7 -3
  48. package/dist/kimaki-workspace-adaptor.js +33 -10
  49. package/dist/logger.js +10 -1
  50. package/dist/markdown.js +3 -1
  51. package/dist/markdown.test.js +11 -3
  52. package/dist/message-formatting.js +18 -7
  53. package/dist/message-formatting.test.js +56 -9
  54. package/dist/oauth-rotation-shared.js +10 -0
  55. package/dist/openai-auth-plugin.js +1 -0
  56. package/dist/openai-auth-state.js +1 -0
  57. package/dist/opencode-command.test.js +60 -0
  58. package/dist/opencode.js +53 -12
  59. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  60. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  61. package/dist/queue-advanced-abort.e2e.test.js +1 -1
  62. package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
  63. package/dist/queue-advanced-e2e-setup.js +78 -5
  64. package/dist/queue-advanced-footer.e2e.test.js +40 -246
  65. package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
  66. package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
  67. package/dist/queue-advanced-question.e2e.test.js +31 -26
  68. package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
  69. package/dist/queue-delete-message.e2e.test.js +1 -1
  70. package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
  71. package/dist/queue-interrupt-drain.e2e.test.js +112 -2
  72. package/dist/queue-question-select-drain.e2e.test.js +30 -128
  73. package/dist/runtime-lifecycle.e2e.test.js +10 -37
  74. package/dist/schema.js +34 -0
  75. package/dist/session-handler/event-stream-state.js +267 -50
  76. package/dist/session-handler/event-stream-state.test.js +415 -8
  77. package/dist/session-handler/model-utils.js +159 -5
  78. package/dist/session-handler/model-utils.test.js +345 -0
  79. package/dist/session-handler/thread-session-runtime.js +252 -182
  80. package/dist/session-sleep.test.js +224 -0
  81. package/dist/session-title-rename.test.js +7 -39
  82. package/dist/sleep.e2e.test.js +491 -0
  83. package/dist/startup-time.e2e.test.js +2 -1
  84. package/dist/store.js +3 -0
  85. package/dist/system-message.js +59 -6
  86. package/dist/system-message.test.js +76 -6
  87. package/dist/task-runner.js +94 -4
  88. package/dist/task-schedule.js +56 -0
  89. package/dist/task-schedule.test.js +71 -1
  90. package/dist/thread-message-queue.e2e.test.js +10 -169
  91. package/dist/undo-redo.e2e.test.js +2 -2
  92. package/dist/voice-attachment.js +23 -1
  93. package/dist/voice-message.e2e.test.js +10 -10
  94. package/dist/voice.test.js +55 -29
  95. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  96. package/dist/worktree-lifecycle.e2e.test.js +29 -10
  97. package/dist/worktrees.js +226 -443
  98. package/dist/worktrees.test.js +435 -58
  99. package/dist/xai-auth-plugin.js +1 -0
  100. package/dist/xai-auth-state.js +1 -0
  101. package/package.json +8 -6
  102. package/skills/errore/SKILL.md +18 -0
  103. package/skills/goke/SKILL.md +150 -10
  104. package/skills/holocron/SKILL.md +37 -0
  105. package/skills/npm-package/SKILL.md +55 -35
  106. package/skills/profano/SKILL.md +17 -2
  107. package/skills/spiceflow/SKILL.md +39 -3
  108. package/skills/strada/SKILL.md +39 -0
  109. package/skills/zele/SKILL.md +19 -0
  110. package/src/agent-model.e2e.test.ts +25 -151
  111. package/src/anthropic-account-identity.ts +1 -0
  112. package/src/anthropic-auth-plugin.ts +1 -0
  113. package/src/anthropic-auth-state.ts +1 -0
  114. package/src/bin.ts +78 -26
  115. package/src/cli-commands/maintenance.ts +59 -13
  116. package/src/cli-commands/send.ts +25 -0
  117. package/src/cli-commands/session.ts +203 -9
  118. package/src/cli-commands/task.ts +13 -1
  119. package/src/cli-parsing.test.ts +69 -3
  120. package/src/cli-runner.test.ts +49 -0
  121. package/src/cli-runner.ts +28 -4
  122. package/src/cli-send-thread.e2e.test.ts +1 -1
  123. package/src/cli.ts +53 -0
  124. package/src/commands/abort.ts +6 -1
  125. package/src/commands/agent.ts +6 -6
  126. package/src/commands/ask-question.test.ts +48 -1
  127. package/src/commands/ask-question.ts +21 -32
  128. package/src/commands/login.ts +6 -0
  129. package/src/commands/merge-worktree.ts +43 -12
  130. package/src/commands/model-variant.ts +8 -1
  131. package/src/commands/model.ts +18 -8
  132. package/src/commands/multioauth.ts +1 -0
  133. package/src/commands/new-worktree.ts +138 -55
  134. package/src/commands/user-command.ts +6 -6
  135. package/src/commands/worktrees.ts +5 -12
  136. package/src/config.ts +18 -1
  137. package/src/database.ts +195 -3
  138. package/src/db.test.ts +188 -0
  139. package/src/db.ts +26 -0
  140. package/src/discord-bot.ts +35 -29
  141. package/src/discord-command-registration.ts +15 -1
  142. package/src/discord-utils.test.ts +19 -0
  143. package/src/discord-utils.ts +21 -6
  144. package/src/errors.ts +5 -0
  145. package/src/exec-async.ts +20 -11
  146. package/src/external-directory-permissions.e2e.test.ts +2 -2
  147. package/src/file-edit-log.test.ts +287 -0
  148. package/src/file-edit-log.ts +283 -0
  149. package/src/format-tables.test.ts +101 -0
  150. package/src/format-tables.ts +75 -0
  151. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  152. package/src/gateway-proxy.e2e.test.ts +5 -5
  153. package/src/git-worktree-core.ts +247 -20
  154. package/src/ipc-tools-plugin.ts +75 -5
  155. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  156. package/src/kimaki-opencode-plugin.ts +7 -3
  157. package/src/kimaki-workspace-adaptor.ts +40 -11
  158. package/src/logger.ts +13 -4
  159. package/src/markdown.test.ts +13 -3
  160. package/src/markdown.ts +4 -1
  161. package/src/message-formatting.test.ts +86 -12
  162. package/src/message-formatting.ts +19 -7
  163. package/src/oauth-rotation-shared.ts +10 -0
  164. package/src/openai-auth-plugin.ts +1 -0
  165. package/src/openai-auth-state.ts +1 -0
  166. package/src/opencode-command.test.ts +81 -0
  167. package/src/opencode.ts +71 -11
  168. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  169. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  170. package/src/queue-advanced-abort.e2e.test.ts +1 -1
  171. package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
  172. package/src/queue-advanced-e2e-setup.ts +83 -4
  173. package/src/queue-advanced-footer.e2e.test.ts +47 -294
  174. package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
  175. package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
  176. package/src/queue-advanced-question.e2e.test.ts +35 -29
  177. package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
  178. package/src/queue-delete-message.e2e.test.ts +1 -1
  179. package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
  180. package/src/queue-interrupt-drain.e2e.test.ts +123 -2
  181. package/src/queue-question-select-drain.e2e.test.ts +32 -157
  182. package/src/runtime-lifecycle.e2e.test.ts +10 -41
  183. package/src/schema.sql +14 -1
  184. package/src/schema.ts +36 -0
  185. package/src/session-handler/event-stream-state.test.ts +437 -7
  186. package/src/session-handler/event-stream-state.ts +389 -53
  187. package/src/session-handler/model-utils.test.ts +415 -0
  188. package/src/session-handler/model-utils.ts +249 -5
  189. package/src/session-handler/thread-session-runtime.ts +300 -215
  190. package/src/session-sleep.test.ts +294 -0
  191. package/src/session-title-rename.test.ts +9 -48
  192. package/src/sleep.e2e.test.ts +562 -0
  193. package/src/startup-time.e2e.test.ts +5 -1
  194. package/src/store.ts +21 -0
  195. package/src/system-message.test.ts +86 -6
  196. package/src/system-message.ts +66 -7
  197. package/src/task-runner.ts +136 -2
  198. package/src/task-schedule.test.ts +76 -0
  199. package/src/task-schedule.ts +95 -0
  200. package/src/thread-message-queue.e2e.test.ts +10 -202
  201. package/src/undo-redo.e2e.test.ts +2 -2
  202. package/src/voice-attachment.ts +29 -1
  203. package/src/voice-message.e2e.test.ts +10 -10
  204. package/src/voice.test.ts +63 -29
  205. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  206. package/src/worktree-lifecycle.e2e.test.ts +29 -9
  207. package/src/worktrees.test.ts +505 -64
  208. package/src/worktrees.ts +257 -603
  209. package/src/xai-auth-plugin.ts +1 -0
  210. package/src/xai-auth-state.ts +1 -0
  211. package/dist/message-finish-field.e2e.test.js +0 -169
  212. package/dist/subagent-rate-limit-plugin.js +0 -184
  213. package/src/message-finish-field.e2e.test.ts +0 -196
  214. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -165,6 +165,119 @@ export function didQuestionQueueHandoffSinceLatestQuestionAsked({
165
165
  return false
166
166
  }
167
167
 
168
+ export type DerivedUnansweredQuestion = {
169
+ id: string
170
+ questions: Array<{
171
+ question: string
172
+ header: string
173
+ options: Array<{
174
+ label: string
175
+ description: string
176
+ }>
177
+ multiple?: boolean
178
+ }>
179
+ tool?: {
180
+ messageID: string
181
+ callID: string
182
+ }
183
+ }
184
+
185
+ // OpenCode emits question.asked when the tool starts, often before the
186
+ // preceding text part gets time.end. Discord must wait for that end event
187
+ // or the question UI posts first and the text dumps later.
188
+ export function isAssistantTextReadyForQuestion({
189
+ events,
190
+ sessionId,
191
+ messageId,
192
+ upToIndex,
193
+ }: {
194
+ events: EventBufferEntry[]
195
+ sessionId: string
196
+ messageId: string
197
+ upToIndex?: number
198
+ }): boolean {
199
+ const end = upToIndex ?? events.length - 1
200
+ for (let i = end; i >= 0; i--) {
201
+ const event = events[i]?.event
202
+ if (!event || event.type !== 'message.part.updated') {
203
+ continue
204
+ }
205
+ const part = event.properties.part
206
+ if (part.sessionID !== sessionId) {
207
+ continue
208
+ }
209
+ if (part.messageID !== messageId) {
210
+ continue
211
+ }
212
+ if (part.type !== 'text') {
213
+ continue
214
+ }
215
+ return Boolean(part.time?.end)
216
+ }
217
+ return true
218
+ }
219
+
220
+ export function deriveLatestUnansweredQuestion({
221
+ events,
222
+ sessionId,
223
+ upToIndex,
224
+ }: {
225
+ events: EventBufferEntry[]
226
+ sessionId: string
227
+ upToIndex?: number
228
+ }): DerivedUnansweredQuestion | undefined {
229
+ const end = upToIndex ?? events.length - 1
230
+ for (let i = end; i >= 0; i--) {
231
+ const entry = events[i]
232
+ if (!entry) {
233
+ continue
234
+ }
235
+ const event = entry.event
236
+ if (getEventBufferSessionId(event) !== sessionId) {
237
+ continue
238
+ }
239
+ if (event.type === 'question.replied' || event.type === 'question.rejected') {
240
+ return undefined
241
+ }
242
+ if (event.type === 'message.part.updated') {
243
+ const part = event.properties.part
244
+ if (
245
+ part.type === 'tool'
246
+ && part.tool === 'question'
247
+ && (part.state.status === 'error' || part.state.status === 'completed')
248
+ ) {
249
+ return undefined
250
+ }
251
+ }
252
+ if (event.type === 'question.asked') {
253
+ const messageId = event.properties.tool?.messageID
254
+ const latestUserMessage = getLatestUserMessage({
255
+ events,
256
+ sessionId,
257
+ upToIndex: end,
258
+ })
259
+ if (
260
+ messageId
261
+ && latestUserMessage
262
+ && !isAssistantMessageInLatestUserTurn({
263
+ events,
264
+ sessionId,
265
+ messageId,
266
+ upToIndex: end,
267
+ })
268
+ ) {
269
+ return undefined
270
+ }
271
+ return {
272
+ id: event.properties.id,
273
+ questions: event.properties.questions,
274
+ tool: event.properties.tool,
275
+ }
276
+ }
277
+ }
278
+ return undefined
279
+ }
280
+
168
281
  export function derivePendingPermissionRequests({
169
282
  events,
170
283
  sessionId,
@@ -337,73 +450,162 @@ function emptyTurnTokenUsage(): TurnTokenUsage {
337
450
  }
338
451
  }
339
452
 
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({
453
+ function addAssistantTokens({
454
+ usage,
455
+ message,
456
+ }: {
457
+ usage: TurnTokenUsage
458
+ message: AssistantMessage
459
+ }): void {
460
+ if (message.tokens) {
461
+ usage.input += message.tokens.input
462
+ usage.output += message.tokens.output
463
+ usage.reasoning += message.tokens.reasoning
464
+ usage.cacheRead += message.tokens.cache.read
465
+ usage.cacheWrite += message.tokens.cache.write
466
+ usage.total += message.tokens.total ?? getTokenTotal(message.tokens)
467
+ }
468
+ usage.cost += message.cost
469
+ usage.model = message.modelID
470
+ usage.providerID = message.providerID
471
+ }
472
+
473
+ function sumAssistantMessages({
474
+ messages,
475
+ userMessageId,
476
+ }: {
477
+ messages: Map<string, AssistantMessage>
478
+ userMessageId?: string
479
+ }): TurnTokenUsage {
480
+ if (messages.size === 0) {
481
+ return {
482
+ ...emptyTurnTokenUsage(),
483
+ userMessageId,
484
+ }
485
+ }
486
+ const usage = emptyTurnTokenUsage()
487
+ usage.userMessageId = userMessageId
488
+ usage.assistantMessageCount = messages.size
489
+ for (const message of messages.values()) {
490
+ addAssistantTokens({ usage, message })
491
+ }
492
+ return usage
493
+ }
494
+
495
+ function collectAssistantMessages({
345
496
  events,
346
497
  sessionId,
347
498
  upToIndex,
499
+ parentID,
348
500
  }: {
349
501
  events: EventBufferEntry[]
350
502
  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
503
+ upToIndex: number
504
+ parentID?: string
505
+ }): Map<string, AssistantMessage> {
362
506
  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') {
507
+ for (let i = 0; i <= upToIndex; i++) {
508
+ const event = events[i]?.event
509
+ if (event?.type !== 'message.updated') {
371
510
  continue
372
511
  }
373
512
  const info = event.properties.info
374
513
  if (info.sessionID !== sessionId || info.role !== 'assistant') {
375
514
  continue
376
515
  }
377
- if (info.parentID !== latestUserMessage.id) {
516
+ if (parentID && info.parentID !== parentID) {
378
517
  continue
379
518
  }
380
519
  latestByMessageId.set(info.id, info)
381
520
  }
521
+ return latestByMessageId
522
+ }
382
523
 
383
- if (latestByMessageId.size === 0) {
384
- return {
385
- ...emptyTurnTokenUsage(),
386
- userMessageId: latestUserMessage?.id,
524
+ function getSessionInfoTokenUsage({
525
+ events,
526
+ sessionId,
527
+ upToIndex,
528
+ }: {
529
+ events: EventBufferEntry[]
530
+ sessionId: string
531
+ upToIndex: number
532
+ }): TurnTokenUsage | undefined {
533
+ for (let i = upToIndex; i >= 0; i--) {
534
+ const event = events[i]?.event
535
+ if (event?.type !== 'session.updated' && event?.type !== 'session.created') {
536
+ continue
387
537
  }
538
+ const info = event.properties.info
539
+ if (info.id !== sessionId) {
540
+ continue
541
+ }
542
+ if (!info.tokens) {
543
+ continue
544
+ }
545
+ const usage = emptyTurnTokenUsage()
546
+ usage.input = info.tokens.input
547
+ usage.output = info.tokens.output
548
+ usage.reasoning = info.tokens.reasoning
549
+ usage.cacheRead = info.tokens.cache.read
550
+ usage.cacheWrite = info.tokens.cache.write
551
+ usage.total = getTokenTotal(info.tokens)
552
+ usage.cost = info.cost ?? 0
553
+ usage.model = info.model?.id
554
+ usage.providerID = info.model?.providerID
555
+ return usage.total > 0 || usage.cost > 0 ? usage : undefined
388
556
  }
557
+ return undefined
558
+ }
389
559
 
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
560
+ // Latest billed token snapshot for the current user turn.
561
+ // Sums the last message.updated tokens per assistant message id so streaming
562
+ // updates are not double-counted. Scoped to sessionId so subagent idles
563
+ // report their own usage. Child task sessions often have no user message in
564
+ // the buffer; fall back to all assistant messages, then Session.tokens on
565
+ // session.updated (OpenCode projects per-session usage there).
566
+ export function getLatestTurnTokenUsage({
567
+ events,
568
+ sessionId,
569
+ upToIndex,
570
+ }: {
571
+ events: EventBufferEntry[]
572
+ sessionId: string
573
+ upToIndex?: number
574
+ }): TurnTokenUsage {
575
+ const end = upToIndex ?? events.length - 1
576
+ const latestUserMessage = getLatestUserMessage({
577
+ events,
578
+ sessionId,
579
+ upToIndex,
580
+ })
581
+ if (latestUserMessage) {
582
+ return sumAssistantMessages({
583
+ messages: collectAssistantMessages({
584
+ events,
585
+ sessionId,
586
+ upToIndex: end,
587
+ parentID: latestUserMessage.id,
588
+ }),
589
+ userMessageId: latestUserMessage.id,
590
+ })
405
591
  }
406
- return usage
592
+
593
+ const sessionAssistants = sumAssistantMessages({
594
+ messages: collectAssistantMessages({
595
+ events,
596
+ sessionId,
597
+ upToIndex: end,
598
+ }),
599
+ })
600
+ if (sessionAssistants.total > 0 || sessionAssistants.assistantMessageCount > 0) {
601
+ return sessionAssistants
602
+ }
603
+
604
+ return getSessionInfoTokenUsage({
605
+ events,
606
+ sessionId,
607
+ upToIndex: end,
608
+ }) ?? emptyTurnTokenUsage()
407
609
  }
408
610
 
409
611
  function findFirstUserMessageIndex({
@@ -469,8 +671,31 @@ function subtractTokenUsage({
469
671
  }
470
672
  }
471
673
 
674
+ function findFirstSessionEventIndex({
675
+ events,
676
+ sessionId,
677
+ upToIndex,
678
+ }: {
679
+ events: EventBufferEntry[]
680
+ sessionId: string
681
+ upToIndex: number
682
+ }): number {
683
+ for (let i = 0; i <= upToIndex; i++) {
684
+ const event = events[i]?.event
685
+ if (!event) {
686
+ continue
687
+ }
688
+ if (getEventBufferSessionId(event) === sessionId) {
689
+ return i
690
+ }
691
+ }
692
+ return 0
693
+ }
694
+
472
695
  // Tokens billed since the previous session.idle in this user turn.
473
696
  // Survives process restart because both idles stay in the event buffer.
697
+ // Child task sessions may have no user message.updated; scope from the first
698
+ // event for that sessionId instead so their tokens still emit.
474
699
  export function getIdleTokenUsageDelta({
475
700
  events,
476
701
  sessionId,
@@ -485,16 +710,22 @@ export function getIdleTokenUsageDelta({
485
710
  sessionId,
486
711
  upToIndex: idleEventIndex,
487
712
  })
488
- if (!current.userMessageId) {
713
+ if (current.total <= 0) {
489
714
  return undefined
490
715
  }
491
- const firstUserMessageIndex = findFirstUserMessageIndex({
492
- events,
493
- userMessageId: current.userMessageId,
494
- upToIndex: idleEventIndex,
495
- })
716
+ const firstUserMessageIndex = current.userMessageId
717
+ ? findFirstUserMessageIndex({
718
+ events,
719
+ userMessageId: current.userMessageId,
720
+ upToIndex: idleEventIndex,
721
+ })
722
+ : findFirstSessionEventIndex({
723
+ events,
724
+ sessionId,
725
+ upToIndex: idleEventIndex,
726
+ })
496
727
  if (firstUserMessageIndex === undefined) {
497
- return undefined
728
+ return current
498
729
  }
499
730
  const previousIdleIndex = findPreviousIdleIndexInTurn({
500
731
  events,
@@ -503,7 +734,7 @@ export function getIdleTokenUsageDelta({
503
734
  beforeIndex: idleEventIndex,
504
735
  })
505
736
  if (previousIdleIndex === undefined) {
506
- return current.total > 0 ? current : undefined
737
+ return current
507
738
  }
508
739
  const previous = getLatestTurnTokenUsage({
509
740
  events,
@@ -945,3 +1176,108 @@ export function getDerivedSubagentSessions({
945
1176
 
946
1177
  return sessions
947
1178
  }
1179
+
1180
+ function getParentIdFromSessionEvent(event: EventBufferEvent): {
1181
+ sessionId: string
1182
+ parentID: string
1183
+ } | undefined {
1184
+ if (event.type !== 'session.created' && event.type !== 'session.updated') {
1185
+ return undefined
1186
+ }
1187
+ const parentID = event.properties.info.parentID
1188
+ if (typeof parentID !== 'string' || parentID.length === 0) {
1189
+ return undefined
1190
+ }
1191
+ return {
1192
+ sessionId: event.properties.info.id,
1193
+ parentID,
1194
+ }
1195
+ }
1196
+
1197
+ // Child sessions of the main thread: task tool metadata.sessionId, plus
1198
+ // session.created/updated parentID (available before task metadata lands).
1199
+ export function getDerivedChildSessionIds({
1200
+ events,
1201
+ mainSessionId,
1202
+ upToIndex,
1203
+ }: {
1204
+ events: EventBufferEntry[]
1205
+ mainSessionId: string
1206
+ upToIndex?: number
1207
+ }): Set<string> {
1208
+ const end = upToIndex ?? events.length - 1
1209
+ const ids = new Set<string>()
1210
+ for (const session of getDerivedSubagentSessions({
1211
+ events,
1212
+ mainSessionId,
1213
+ upToIndex,
1214
+ })) {
1215
+ ids.add(session.childSessionId)
1216
+ }
1217
+
1218
+ let grew = true
1219
+ while (grew) {
1220
+ grew = false
1221
+ for (let i = 0; i <= end; i++) {
1222
+ const event = events[i]?.event
1223
+ if (!event) {
1224
+ continue
1225
+ }
1226
+ const parented = getParentIdFromSessionEvent(event)
1227
+ if (!parented || ids.has(parented.sessionId)) {
1228
+ continue
1229
+ }
1230
+ if (parented.parentID !== mainSessionId && !ids.has(parented.parentID)) {
1231
+ continue
1232
+ }
1233
+ ids.add(parented.sessionId)
1234
+ grew = true
1235
+ }
1236
+ }
1237
+ return ids
1238
+ }
1239
+
1240
+ export function isDerivedChildSession({
1241
+ events,
1242
+ mainSessionId,
1243
+ candidateSessionId,
1244
+ upToIndex,
1245
+ }: {
1246
+ events: EventBufferEntry[]
1247
+ mainSessionId: string
1248
+ candidateSessionId: string
1249
+ upToIndex?: number
1250
+ }): boolean {
1251
+ if (candidateSessionId === mainSessionId) {
1252
+ return false
1253
+ }
1254
+ return getDerivedChildSessionIds({
1255
+ events,
1256
+ mainSessionId,
1257
+ upToIndex,
1258
+ }).has(candidateSessionId)
1259
+ }
1260
+
1261
+ export function getTokenUsageSessionIdsForIdle({
1262
+ events,
1263
+ mainSessionId,
1264
+ idleSessionId,
1265
+ upToIndex,
1266
+ }: {
1267
+ events: EventBufferEntry[]
1268
+ mainSessionId: string
1269
+ idleSessionId: string
1270
+ upToIndex?: number
1271
+ }): string[] {
1272
+ if (idleSessionId !== mainSessionId) {
1273
+ return [idleSessionId]
1274
+ }
1275
+ return [
1276
+ mainSessionId,
1277
+ ...getDerivedChildSessionIds({
1278
+ events,
1279
+ mainSessionId,
1280
+ upToIndex,
1281
+ }),
1282
+ ]
1283
+ }