@ironbee-ai/cli 0.14.1 → 0.16.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 (500) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -95
  3. package/dist/analytics/{emit.d.ts → claude/emit.d.ts} +1 -1
  4. package/dist/analytics/claude/emit.d.ts.map +1 -0
  5. package/dist/analytics/{emit.js → claude/emit.js} +34 -7
  6. package/dist/analytics/claude/emit.js.map +1 -0
  7. package/dist/analytics/{hook-trigger.d.ts → claude/hook-trigger.d.ts} +1 -1
  8. package/dist/analytics/claude/hook-trigger.d.ts.map +1 -0
  9. package/dist/analytics/{hook-trigger.js → claude/hook-trigger.js} +2 -2
  10. package/dist/analytics/claude/hook-trigger.js.map +1 -0
  11. package/dist/analytics/claude/log.d.ts.map +1 -0
  12. package/dist/analytics/{log.js → claude/log.js} +1 -1
  13. package/dist/analytics/claude/log.js.map +1 -0
  14. package/dist/analytics/{merge.d.ts → claude/merge.d.ts} +2 -1
  15. package/dist/analytics/claude/merge.d.ts.map +1 -0
  16. package/dist/analytics/{merge.js → claude/merge.js} +13 -1
  17. package/dist/analytics/claude/merge.js.map +1 -0
  18. package/dist/analytics/{pricing.d.ts → claude/pricing.d.ts} +1 -13
  19. package/dist/analytics/claude/pricing.d.ts.map +1 -0
  20. package/dist/analytics/{pricing.js → claude/pricing.js} +6 -14
  21. package/dist/analytics/claude/pricing.js.map +1 -0
  22. package/dist/analytics/{projection.d.ts → claude/projection.d.ts} +31 -7
  23. package/dist/analytics/claude/projection.d.ts.map +1 -0
  24. package/dist/analytics/{projection.js → claude/projection.js} +631 -327
  25. package/dist/analytics/claude/projection.js.map +1 -0
  26. package/dist/analytics/{spawn.d.ts → claude/spawn.d.ts} +4 -4
  27. package/dist/analytics/claude/spawn.d.ts.map +1 -0
  28. package/dist/analytics/{spawn.js → claude/spawn.js} +4 -3
  29. package/dist/analytics/claude/spawn.js.map +1 -0
  30. package/dist/analytics/{state.d.ts → claude/state.d.ts} +1 -1
  31. package/dist/analytics/claude/state.d.ts.map +1 -0
  32. package/dist/analytics/{state.js → claude/state.js} +2 -2
  33. package/dist/analytics/claude/state.js.map +1 -0
  34. package/dist/analytics/claude/transcript.d.ts.map +1 -0
  35. package/dist/analytics/{transcript.js → claude/transcript.js} +1 -1
  36. package/dist/analytics/claude/transcript.js.map +1 -0
  37. package/dist/analytics/codex/api-request.d.ts +108 -0
  38. package/dist/analytics/codex/api-request.d.ts.map +1 -0
  39. package/dist/analytics/codex/api-request.js +155 -0
  40. package/dist/analytics/codex/api-request.js.map +1 -0
  41. package/dist/analytics/codex/apply-patch.d.ts +21 -0
  42. package/dist/analytics/codex/apply-patch.d.ts.map +1 -0
  43. package/dist/analytics/codex/apply-patch.js +49 -0
  44. package/dist/analytics/codex/apply-patch.js.map +1 -0
  45. package/dist/analytics/codex/classifier.d.ts +28 -0
  46. package/dist/analytics/codex/classifier.d.ts.map +1 -0
  47. package/dist/analytics/codex/classifier.js +111 -0
  48. package/dist/analytics/codex/classifier.js.map +1 -0
  49. package/dist/analytics/codex/emit.d.ts +47 -0
  50. package/dist/analytics/codex/emit.d.ts.map +1 -0
  51. package/dist/analytics/codex/emit.js +158 -0
  52. package/dist/analytics/codex/emit.js.map +1 -0
  53. package/dist/analytics/codex/events-emit.d.ts +62 -0
  54. package/dist/analytics/codex/events-emit.d.ts.map +1 -0
  55. package/dist/analytics/codex/events-emit.js +555 -0
  56. package/dist/analytics/codex/events-emit.js.map +1 -0
  57. package/dist/analytics/codex/pricing.d.ts +57 -0
  58. package/dist/analytics/codex/pricing.d.ts.map +1 -0
  59. package/dist/analytics/codex/pricing.js +125 -0
  60. package/dist/analytics/codex/pricing.js.map +1 -0
  61. package/dist/analytics/codex/projection.d.ts +51 -0
  62. package/dist/analytics/codex/projection.d.ts.map +1 -0
  63. package/dist/analytics/codex/projection.js +1477 -0
  64. package/dist/analytics/codex/projection.js.map +1 -0
  65. package/dist/analytics/codex/spawn.d.ts +27 -0
  66. package/dist/analytics/codex/spawn.d.ts.map +1 -0
  67. package/dist/analytics/codex/spawn.js +64 -0
  68. package/dist/analytics/codex/spawn.js.map +1 -0
  69. package/dist/analytics/codex/status-snapshot.d.ts +80 -0
  70. package/dist/analytics/codex/status-snapshot.d.ts.map +1 -0
  71. package/dist/analytics/codex/status-snapshot.js +206 -0
  72. package/dist/analytics/codex/status-snapshot.js.map +1 -0
  73. package/dist/analytics/codex/transcript.d.ts +51 -0
  74. package/dist/analytics/codex/transcript.d.ts.map +1 -0
  75. package/dist/analytics/codex/transcript.js +134 -0
  76. package/dist/analytics/codex/transcript.js.map +1 -0
  77. package/dist/analytics/codex/types.d.ts +253 -0
  78. package/dist/analytics/codex/types.d.ts.map +1 -0
  79. package/dist/analytics/codex/types.js +29 -0
  80. package/dist/analytics/codex/types.js.map +1 -0
  81. package/dist/analytics/shared/classifier.d.ts.map +1 -0
  82. package/dist/analytics/{classifier.js → shared/classifier.js} +9 -0
  83. package/dist/analytics/shared/classifier.js.map +1 -0
  84. package/dist/analytics/shared/errors.d.ts.map +1 -0
  85. package/dist/analytics/shared/errors.js.map +1 -0
  86. package/dist/analytics/shared/tokens.d.ts +14 -0
  87. package/dist/analytics/shared/tokens.d.ts.map +1 -0
  88. package/dist/analytics/shared/tokens.js +17 -0
  89. package/dist/analytics/shared/tokens.js.map +1 -0
  90. package/dist/analytics/{types.d.ts → shared/types.d.ts} +42 -9
  91. package/dist/analytics/shared/types.d.ts.map +1 -0
  92. package/dist/analytics/shared/types.js.map +1 -0
  93. package/dist/clients/base.d.ts +9 -0
  94. package/dist/clients/base.d.ts.map +1 -1
  95. package/dist/clients/claude/hooks/activity-end.js +1 -1
  96. package/dist/clients/claude/hooks/activity-end.js.map +1 -1
  97. package/dist/clients/claude/hooks/activity-start.js +1 -1
  98. package/dist/clients/claude/hooks/activity-start.js.map +1 -1
  99. package/dist/clients/claude/hooks/clear-verdict.d.ts.map +1 -1
  100. package/dist/clients/claude/hooks/clear-verdict.js +14 -0
  101. package/dist/clients/claude/hooks/clear-verdict.js.map +1 -1
  102. package/dist/clients/claude/hooks/session-end.d.ts.map +1 -1
  103. package/dist/clients/claude/hooks/session-end.js +7 -1
  104. package/dist/clients/claude/hooks/session-end.js.map +1 -1
  105. package/dist/clients/claude/hooks/session-start.d.ts.map +1 -1
  106. package/dist/clients/claude/hooks/session-start.js +7 -1
  107. package/dist/clients/claude/hooks/session-start.js.map +1 -1
  108. package/dist/clients/claude/hooks/session-status.d.ts.map +1 -1
  109. package/dist/clients/claude/hooks/session-status.js +13 -9
  110. package/dist/clients/claude/hooks/session-status.js.map +1 -1
  111. package/dist/clients/claude/hooks/track-action.d.ts.map +1 -1
  112. package/dist/clients/claude/hooks/track-action.js +26 -1
  113. package/dist/clients/claude/hooks/track-action.js.map +1 -1
  114. package/dist/clients/claude/hooks/verify-gate.d.ts.map +1 -1
  115. package/dist/clients/claude/hooks/verify-gate.js +8 -1
  116. package/dist/clients/claude/hooks/verify-gate.js.map +1 -1
  117. package/dist/clients/claude/index.d.ts +1 -0
  118. package/dist/clients/claude/index.d.ts.map +1 -1
  119. package/dist/clients/claude/index.js +18 -14
  120. package/dist/clients/claude/index.js.map +1 -1
  121. package/dist/clients/claude/util.d.ts.map +1 -1
  122. package/dist/clients/claude/util.js +55 -0
  123. package/dist/clients/claude/util.js.map +1 -1
  124. package/dist/clients/codex/commands/ironbee-verify/SKILL.md +58 -0
  125. package/dist/clients/codex/hooks/activity-end.d.ts +9 -0
  126. package/dist/clients/codex/hooks/activity-end.d.ts.map +1 -0
  127. package/dist/clients/codex/hooks/activity-end.js +65 -0
  128. package/dist/clients/codex/hooks/activity-end.js.map +1 -0
  129. package/dist/clients/codex/hooks/activity-start.d.ts +17 -0
  130. package/dist/clients/codex/hooks/activity-start.d.ts.map +1 -0
  131. package/dist/clients/codex/hooks/activity-start.js +38 -0
  132. package/dist/clients/codex/hooks/activity-start.js.map +1 -0
  133. package/dist/clients/codex/hooks/clear-verdict.d.ts +55 -0
  134. package/dist/clients/codex/hooks/clear-verdict.d.ts.map +1 -0
  135. package/dist/clients/codex/hooks/clear-verdict.js +299 -0
  136. package/dist/clients/codex/hooks/clear-verdict.js.map +1 -0
  137. package/dist/clients/codex/hooks/require-verdict.d.ts +30 -0
  138. package/dist/clients/codex/hooks/require-verdict.d.ts.map +1 -0
  139. package/dist/clients/codex/hooks/require-verdict.js +109 -0
  140. package/dist/clients/codex/hooks/require-verdict.js.map +1 -0
  141. package/dist/clients/codex/hooks/require-verification.d.ts +12 -0
  142. package/dist/clients/codex/hooks/require-verification.d.ts.map +1 -0
  143. package/dist/clients/codex/hooks/require-verification.js +136 -0
  144. package/dist/clients/codex/hooks/require-verification.js.map +1 -0
  145. package/dist/clients/codex/hooks/session-start.d.ts +10 -0
  146. package/dist/clients/codex/hooks/session-start.d.ts.map +1 -0
  147. package/dist/clients/codex/hooks/session-start.js +94 -0
  148. package/dist/clients/codex/hooks/session-start.js.map +1 -0
  149. package/dist/clients/codex/hooks/track-action-monitor.d.ts +10 -0
  150. package/dist/clients/codex/hooks/track-action-monitor.d.ts.map +1 -0
  151. package/dist/clients/codex/hooks/track-action-monitor.js +168 -0
  152. package/dist/clients/codex/hooks/track-action-monitor.js.map +1 -0
  153. package/dist/clients/codex/hooks/track-action-pre.d.ts +18 -0
  154. package/dist/clients/codex/hooks/track-action-pre.d.ts.map +1 -0
  155. package/dist/clients/codex/hooks/track-action-pre.js +35 -0
  156. package/dist/clients/codex/hooks/track-action-pre.js.map +1 -0
  157. package/dist/clients/codex/hooks/track-action.d.ts +22 -0
  158. package/dist/clients/codex/hooks/track-action.d.ts.map +1 -0
  159. package/dist/clients/codex/hooks/track-action.js +350 -0
  160. package/dist/clients/codex/hooks/track-action.js.map +1 -0
  161. package/dist/clients/codex/hooks/verify-gate.d.ts +15 -0
  162. package/dist/clients/codex/hooks/verify-gate.d.ts.map +1 -0
  163. package/dist/clients/codex/hooks/verify-gate.js +105 -0
  164. package/dist/clients/codex/hooks/verify-gate.js.map +1 -0
  165. package/dist/clients/codex/index.d.ts +42 -0
  166. package/dist/clients/codex/index.d.ts.map +1 -0
  167. package/dist/clients/codex/index.js +427 -0
  168. package/dist/clients/codex/index.js.map +1 -0
  169. package/dist/clients/codex/platforms/command-verify.backend.md +108 -0
  170. package/dist/clients/codex/platforms/command-verify.browser.md +108 -0
  171. package/dist/clients/codex/platforms/command-verify.node.md +61 -0
  172. package/dist/clients/codex/platforms/rule.backend.md +32 -0
  173. package/dist/clients/codex/platforms/rule.browser.md +17 -0
  174. package/dist/clients/codex/platforms/rule.node.md +28 -0
  175. package/dist/clients/codex/platforms/skill.backend.md +95 -0
  176. package/dist/clients/codex/platforms/skill.browser.md +28 -0
  177. package/dist/clients/codex/platforms/skill.node.md +62 -0
  178. package/dist/clients/codex/rules/ironbee-verification.md +48 -0
  179. package/dist/clients/codex/skills/ironbee-verification.md +80 -0
  180. package/dist/clients/codex/util.d.ts +193 -0
  181. package/dist/clients/codex/util.d.ts.map +1 -0
  182. package/dist/clients/codex/util.js +784 -0
  183. package/dist/clients/codex/util.js.map +1 -0
  184. package/dist/clients/cursor/hooks/activity-end.js +1 -1
  185. package/dist/clients/cursor/hooks/activity-end.js.map +1 -1
  186. package/dist/clients/cursor/hooks/clear-verdict.d.ts +5 -2
  187. package/dist/clients/cursor/hooks/clear-verdict.d.ts.map +1 -1
  188. package/dist/clients/cursor/hooks/clear-verdict.js +12 -3
  189. package/dist/clients/cursor/hooks/clear-verdict.js.map +1 -1
  190. package/dist/clients/cursor/hooks/session-end.js +1 -1
  191. package/dist/clients/cursor/hooks/session-end.js.map +1 -1
  192. package/dist/clients/cursor/hooks/session-start.js +1 -1
  193. package/dist/clients/cursor/hooks/session-start.js.map +1 -1
  194. package/dist/clients/cursor/hooks/verify-gate.d.ts.map +1 -1
  195. package/dist/clients/cursor/hooks/verify-gate.js +6 -1
  196. package/dist/clients/cursor/hooks/verify-gate.js.map +1 -1
  197. package/dist/clients/cursor/index.d.ts +1 -0
  198. package/dist/clients/cursor/index.d.ts.map +1 -1
  199. package/dist/clients/cursor/index.js +22 -13
  200. package/dist/clients/cursor/index.js.map +1 -1
  201. package/dist/clients/registry.d.ts.map +1 -1
  202. package/dist/clients/registry.js +2 -1
  203. package/dist/clients/registry.js.map +1 -1
  204. package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
  205. package/dist/commands/claude/index.d.ts.map +1 -0
  206. package/dist/commands/{claude.js → claude/index.js} +12 -6
  207. package/dist/commands/claude/index.js.map +1 -0
  208. package/dist/commands/{otel.d.ts → claude/otel.d.ts} +5 -1
  209. package/dist/commands/claude/otel.d.ts.map +1 -0
  210. package/dist/commands/{otel.js → claude/otel.js} +9 -5
  211. package/dist/commands/claude/otel.js.map +1 -0
  212. package/dist/commands/claude/process-analytics.d.ts +19 -0
  213. package/dist/commands/claude/process-analytics.d.ts.map +1 -0
  214. package/dist/commands/{process-analytics.js → claude/process-analytics.js} +16 -15
  215. package/dist/commands/claude/process-analytics.js.map +1 -0
  216. package/dist/commands/{statusline-toggle.d.ts → claude/statusline-toggle.d.ts} +2 -2
  217. package/dist/commands/claude/statusline-toggle.d.ts.map +1 -0
  218. package/dist/commands/{statusline-toggle.js → claude/statusline-toggle.js} +8 -8
  219. package/dist/commands/claude/statusline-toggle.js.map +1 -0
  220. package/dist/commands/{statusline.d.ts → claude/statusline.d.ts} +1 -1
  221. package/dist/commands/claude/statusline.d.ts.map +1 -0
  222. package/dist/commands/{statusline.js → claude/statusline.js} +4 -4
  223. package/dist/commands/claude/statusline.js.map +1 -0
  224. package/dist/commands/codex/index.d.ts +11 -0
  225. package/dist/commands/codex/index.d.ts.map +1 -0
  226. package/dist/commands/codex/index.js +17 -0
  227. package/dist/commands/codex/index.js.map +1 -0
  228. package/dist/commands/codex/process-analytics.d.ts +14 -0
  229. package/dist/commands/codex/process-analytics.d.ts.map +1 -0
  230. package/dist/commands/codex/process-analytics.js +111 -0
  231. package/dist/commands/codex/process-analytics.js.map +1 -0
  232. package/dist/commands/hook.js +12 -0
  233. package/dist/commands/hook.js.map +1 -1
  234. package/dist/commands/import.js +3 -3
  235. package/dist/commands/import.js.map +1 -1
  236. package/dist/commands/queue.js +3 -1
  237. package/dist/commands/queue.js.map +1 -1
  238. package/dist/commands/status.js +1 -1
  239. package/dist/commands/verify.d.ts.map +1 -1
  240. package/dist/commands/verify.js +1 -2
  241. package/dist/commands/verify.js.map +1 -1
  242. package/dist/hooks/core/actions.d.ts +17 -1
  243. package/dist/hooks/core/actions.d.ts.map +1 -1
  244. package/dist/hooks/core/actions.js +13 -0
  245. package/dist/hooks/core/actions.js.map +1 -1
  246. package/dist/hooks/core/activity-end.d.ts.map +1 -1
  247. package/dist/hooks/core/activity-end.js +4 -0
  248. package/dist/hooks/core/activity-end.js.map +1 -1
  249. package/dist/hooks/core/session-state.d.ts +15 -1
  250. package/dist/hooks/core/session-state.d.ts.map +1 -1
  251. package/dist/hooks/core/session-state.js +102 -7
  252. package/dist/hooks/core/session-state.js.map +1 -1
  253. package/dist/import/claude/analytics-runner.d.ts +1 -1
  254. package/dist/import/claude/analytics-runner.d.ts.map +1 -1
  255. package/dist/import/claude/analytics-runner.js +5 -5
  256. package/dist/import/claude/analytics-runner.js.map +1 -1
  257. package/dist/import/claude/auth-mode.d.ts +1 -1
  258. package/dist/import/claude/auth-mode.d.ts.map +1 -1
  259. package/dist/import/claude/discovery.js +1 -1
  260. package/dist/import/claude/discovery.js.map +1 -1
  261. package/dist/import/claude/encoding.js +1 -1
  262. package/dist/import/claude/encoding.js.map +1 -1
  263. package/dist/import/claude/events/file-change.d.ts +10 -1
  264. package/dist/import/claude/events/file-change.d.ts.map +1 -1
  265. package/dist/import/claude/events/file-change.js +79 -5
  266. package/dist/import/claude/events/file-change.js.map +1 -1
  267. package/dist/import/claude/events/tool-call.d.ts +16 -1
  268. package/dist/import/claude/events/tool-call.d.ts.map +1 -1
  269. package/dist/import/claude/events/tool-call.js +122 -15
  270. package/dist/import/claude/events/tool-call.js.map +1 -1
  271. package/dist/import/claude/runner.d.ts.map +1 -1
  272. package/dist/import/claude/runner.js +45 -3
  273. package/dist/import/claude/runner.js.map +1 -1
  274. package/dist/import/claude/summary.js +1 -1
  275. package/dist/import/claude/summary.js.map +1 -1
  276. package/dist/import/claude/transcript-walk.d.ts +1 -1
  277. package/dist/import/claude/transcript-walk.d.ts.map +1 -1
  278. package/dist/import/claude/transcript-walk.js +11 -4
  279. package/dist/import/claude/transcript-walk.js.map +1 -1
  280. package/dist/import/codex/analytics-runner.d.ts +46 -0
  281. package/dist/import/codex/analytics-runner.d.ts.map +1 -0
  282. package/dist/import/codex/analytics-runner.js +116 -0
  283. package/dist/import/codex/analytics-runner.js.map +1 -0
  284. package/dist/import/codex/discovery.d.ts +33 -0
  285. package/dist/import/codex/discovery.d.ts.map +1 -0
  286. package/dist/import/codex/discovery.js +202 -0
  287. package/dist/import/codex/discovery.js.map +1 -0
  288. package/dist/import/codex/events/file-change.d.ts +42 -0
  289. package/dist/import/codex/events/file-change.d.ts.map +1 -0
  290. package/dist/import/codex/events/file-change.js +125 -0
  291. package/dist/import/codex/events/file-change.js.map +1 -0
  292. package/dist/import/codex/events/tool-call.d.ts +49 -0
  293. package/dist/import/codex/events/tool-call.d.ts.map +1 -0
  294. package/dist/import/codex/events/tool-call.js +151 -0
  295. package/dist/import/codex/events/tool-call.js.map +1 -0
  296. package/dist/import/codex/runner.d.ts +34 -0
  297. package/dist/import/codex/runner.d.ts.map +1 -0
  298. package/dist/import/codex/runner.js +456 -0
  299. package/dist/import/codex/runner.js.map +1 -0
  300. package/dist/import/codex/summary.d.ts +20 -0
  301. package/dist/import/codex/summary.d.ts.map +1 -0
  302. package/dist/import/codex/summary.js +206 -0
  303. package/dist/import/codex/summary.js.map +1 -0
  304. package/dist/import/events/activity.d.ts.map +1 -1
  305. package/dist/import/events/activity.js +17 -2
  306. package/dist/import/events/activity.js.map +1 -1
  307. package/dist/import/events/session.d.ts +11 -1
  308. package/dist/import/events/session.d.ts.map +1 -1
  309. package/dist/import/events/session.js +19 -1
  310. package/dist/import/events/session.js.map +1 -1
  311. package/dist/import/ids.js +3 -3
  312. package/dist/import/ids.js.map +1 -1
  313. package/dist/import/pipeline.d.ts +22 -15
  314. package/dist/import/pipeline.d.ts.map +1 -1
  315. package/dist/import/pipeline.js +99 -18
  316. package/dist/import/pipeline.js.map +1 -1
  317. package/dist/import/types.d.ts +4 -0
  318. package/dist/import/types.d.ts.map +1 -1
  319. package/dist/import/types.js.map +1 -1
  320. package/dist/index.js +9 -14
  321. package/dist/index.js.map +1 -1
  322. package/dist/lib/collector.d.ts +2 -1
  323. package/dist/lib/collector.d.ts.map +1 -1
  324. package/dist/lib/collector.js +28 -3
  325. package/dist/lib/collector.js.map +1 -1
  326. package/dist/lib/config.d.ts.map +1 -1
  327. package/dist/lib/config.js.map +1 -1
  328. package/dist/lib/event.d.ts +18 -1
  329. package/dist/lib/event.d.ts.map +1 -1
  330. package/dist/lib/event.js +25 -1
  331. package/dist/lib/event.js.map +1 -1
  332. package/dist/lib/fs-prune.d.ts +1 -1
  333. package/dist/lib/fs-prune.js +1 -1
  334. package/dist/lib/platform-section.d.ts.map +1 -1
  335. package/dist/lib/platform-section.js +8 -0
  336. package/dist/lib/platform-section.js.map +1 -1
  337. package/dist/otel/{context → claude/context}/build.d.ts +1 -1
  338. package/dist/otel/claude/context/build.d.ts.map +1 -0
  339. package/dist/otel/{context → claude/context}/build.js +3 -7
  340. package/dist/otel/claude/context/build.js.map +1 -0
  341. package/dist/otel/claude/context/classify.d.ts.map +1 -0
  342. package/dist/otel/claude/context/classify.js.map +1 -0
  343. package/dist/otel/{context → claude/context}/extract.d.ts +1 -1
  344. package/dist/otel/claude/context/extract.d.ts.map +1 -0
  345. package/dist/otel/claude/context/extract.js.map +1 -0
  346. package/dist/otel/claude/context/markers.d.ts.map +1 -0
  347. package/dist/otel/{context → claude/context}/markers.js +22 -3
  348. package/dist/otel/claude/context/markers.js.map +1 -0
  349. package/dist/otel/claude/context/util.d.ts.map +1 -0
  350. package/dist/otel/claude/context/util.js.map +1 -0
  351. package/dist/otel/{daemon → claude/daemon}/ensure.d.ts +1 -1
  352. package/dist/otel/claude/daemon/ensure.d.ts.map +1 -0
  353. package/dist/otel/{daemon → claude/daemon}/ensure.js +6 -6
  354. package/dist/otel/claude/daemon/ensure.js.map +1 -0
  355. package/dist/otel/{daemon → claude/daemon}/forward.d.ts +1 -1
  356. package/dist/otel/claude/daemon/forward.d.ts.map +1 -0
  357. package/dist/otel/{daemon → claude/daemon}/forward.js +0 -0
  358. package/dist/otel/claude/daemon/forward.js.map +1 -0
  359. package/dist/otel/claude/daemon/paths.d.ts.map +1 -0
  360. package/dist/otel/claude/daemon/paths.js.map +1 -0
  361. package/dist/otel/{daemon → claude/daemon}/process.d.ts +1 -1
  362. package/dist/otel/claude/daemon/process.d.ts.map +1 -0
  363. package/dist/otel/{daemon → claude/daemon}/process.js +1 -1
  364. package/dist/otel/claude/daemon/process.js.map +1 -0
  365. package/dist/otel/claude/daemon/reprocess.d.ts.map +1 -0
  366. package/dist/otel/{daemon → claude/daemon}/reprocess.js +2 -2
  367. package/dist/otel/claude/daemon/reprocess.js.map +1 -0
  368. package/dist/otel/claude/log-handler.d.ts.map +1 -0
  369. package/dist/otel/{log-handler.js → claude/log-handler.js} +1 -1
  370. package/dist/otel/claude/log-handler.js.map +1 -0
  371. package/dist/otel/collector.js +4 -4
  372. package/dist/otel/collector.js.map +1 -1
  373. package/dist/queue/flush.d.ts +23 -0
  374. package/dist/queue/flush.d.ts.map +1 -1
  375. package/dist/queue/flush.js +44 -0
  376. package/dist/queue/flush.js.map +1 -1
  377. package/dist/queue/handlers/send-event.d.ts.map +1 -1
  378. package/dist/queue/handlers/send-event.js +5 -4
  379. package/dist/queue/handlers/send-event.js.map +1 -1
  380. package/dist/queue/index.d.ts +2 -2
  381. package/dist/queue/index.d.ts.map +1 -1
  382. package/dist/queue/index.js +4 -1
  383. package/dist/queue/index.js.map +1 -1
  384. package/dist/queue/spawn.d.ts +20 -0
  385. package/dist/queue/spawn.d.ts.map +1 -1
  386. package/dist/queue/spawn.js +37 -0
  387. package/dist/queue/spawn.js.map +1 -1
  388. package/dist/tui/import/area.js +3 -3
  389. package/dist/tui/import/area.js.map +1 -1
  390. package/dist/tui/sessions/area.d.ts.map +1 -1
  391. package/dist/tui/sessions/area.js +2 -45
  392. package/dist/tui/sessions/area.js.map +1 -1
  393. package/package.json +2 -1
  394. package/dist/analysis/code-changes.d.ts +0 -22
  395. package/dist/analysis/code-changes.d.ts.map +0 -1
  396. package/dist/analysis/code-changes.js +0 -141
  397. package/dist/analysis/code-changes.js.map +0 -1
  398. package/dist/analysis/cross-session.d.ts +0 -34
  399. package/dist/analysis/cross-session.d.ts.map +0 -1
  400. package/dist/analysis/cross-session.js +0 -230
  401. package/dist/analysis/cross-session.js.map +0 -1
  402. package/dist/analysis/fix-effectiveness.d.ts +0 -16
  403. package/dist/analysis/fix-effectiveness.d.ts.map +0 -1
  404. package/dist/analysis/fix-effectiveness.js +0 -99
  405. package/dist/analysis/fix-effectiveness.js.map +0 -1
  406. package/dist/analysis/scoring.d.ts +0 -15
  407. package/dist/analysis/scoring.d.ts.map +0 -1
  408. package/dist/analysis/scoring.js +0 -59
  409. package/dist/analysis/scoring.js.map +0 -1
  410. package/dist/analysis/time-analysis.d.ts +0 -22
  411. package/dist/analysis/time-analysis.d.ts.map +0 -1
  412. package/dist/analysis/time-analysis.js +0 -174
  413. package/dist/analysis/time-analysis.js.map +0 -1
  414. package/dist/analysis/verdict-details.d.ts +0 -23
  415. package/dist/analysis/verdict-details.d.ts.map +0 -1
  416. package/dist/analysis/verdict-details.js +0 -59
  417. package/dist/analysis/verdict-details.js.map +0 -1
  418. package/dist/analysis/verification-quality.d.ts +0 -20
  419. package/dist/analysis/verification-quality.d.ts.map +0 -1
  420. package/dist/analysis/verification-quality.js +0 -145
  421. package/dist/analysis/verification-quality.js.map +0 -1
  422. package/dist/analytics/classifier.d.ts.map +0 -1
  423. package/dist/analytics/classifier.js.map +0 -1
  424. package/dist/analytics/emit.d.ts.map +0 -1
  425. package/dist/analytics/emit.js.map +0 -1
  426. package/dist/analytics/errors.d.ts.map +0 -1
  427. package/dist/analytics/errors.js.map +0 -1
  428. package/dist/analytics/hook-trigger.d.ts.map +0 -1
  429. package/dist/analytics/hook-trigger.js.map +0 -1
  430. package/dist/analytics/log.d.ts.map +0 -1
  431. package/dist/analytics/log.js.map +0 -1
  432. package/dist/analytics/merge.d.ts.map +0 -1
  433. package/dist/analytics/merge.js.map +0 -1
  434. package/dist/analytics/pricing.d.ts.map +0 -1
  435. package/dist/analytics/pricing.js.map +0 -1
  436. package/dist/analytics/projection.d.ts.map +0 -1
  437. package/dist/analytics/projection.js.map +0 -1
  438. package/dist/analytics/spawn.d.ts.map +0 -1
  439. package/dist/analytics/spawn.js.map +0 -1
  440. package/dist/analytics/state.d.ts.map +0 -1
  441. package/dist/analytics/state.js.map +0 -1
  442. package/dist/analytics/transcript.d.ts.map +0 -1
  443. package/dist/analytics/transcript.js.map +0 -1
  444. package/dist/analytics/types.d.ts.map +0 -1
  445. package/dist/analytics/types.js.map +0 -1
  446. package/dist/clients/claude/commands/ironbee-analyze.md +0 -42
  447. package/dist/clients/cursor/commands/ironbee-analyze/SKILL.md +0 -48
  448. package/dist/commands/analyze.d.ts +0 -3
  449. package/dist/commands/analyze.d.ts.map +0 -1
  450. package/dist/commands/analyze.js +0 -329
  451. package/dist/commands/analyze.js.map +0 -1
  452. package/dist/commands/claude.d.ts.map +0 -1
  453. package/dist/commands/claude.js.map +0 -1
  454. package/dist/commands/otel.d.ts.map +0 -1
  455. package/dist/commands/otel.js.map +0 -1
  456. package/dist/commands/process-analytics.d.ts +0 -18
  457. package/dist/commands/process-analytics.d.ts.map +0 -1
  458. package/dist/commands/process-analytics.js.map +0 -1
  459. package/dist/commands/statusline-toggle.d.ts.map +0 -1
  460. package/dist/commands/statusline-toggle.js.map +0 -1
  461. package/dist/commands/statusline.d.ts.map +0 -1
  462. package/dist/commands/statusline.js.map +0 -1
  463. package/dist/otel/context/build.d.ts.map +0 -1
  464. package/dist/otel/context/build.js.map +0 -1
  465. package/dist/otel/context/classify.d.ts.map +0 -1
  466. package/dist/otel/context/classify.js.map +0 -1
  467. package/dist/otel/context/extract.d.ts.map +0 -1
  468. package/dist/otel/context/extract.js.map +0 -1
  469. package/dist/otel/context/markers.d.ts.map +0 -1
  470. package/dist/otel/context/markers.js.map +0 -1
  471. package/dist/otel/context/util.d.ts.map +0 -1
  472. package/dist/otel/context/util.js.map +0 -1
  473. package/dist/otel/daemon/ensure.d.ts.map +0 -1
  474. package/dist/otel/daemon/ensure.js.map +0 -1
  475. package/dist/otel/daemon/forward.d.ts.map +0 -1
  476. package/dist/otel/daemon/forward.js.map +0 -1
  477. package/dist/otel/daemon/paths.d.ts.map +0 -1
  478. package/dist/otel/daemon/paths.js.map +0 -1
  479. package/dist/otel/daemon/process.d.ts.map +0 -1
  480. package/dist/otel/daemon/process.js.map +0 -1
  481. package/dist/otel/daemon/reprocess.d.ts.map +0 -1
  482. package/dist/otel/daemon/reprocess.js.map +0 -1
  483. package/dist/otel/log-handler.d.ts.map +0 -1
  484. package/dist/otel/log-handler.js.map +0 -1
  485. /package/dist/analytics/{log.d.ts → claude/log.d.ts} +0 -0
  486. /package/dist/analytics/{transcript.d.ts → claude/transcript.d.ts} +0 -0
  487. /package/dist/analytics/{classifier.d.ts → shared/classifier.d.ts} +0 -0
  488. /package/dist/analytics/{errors.d.ts → shared/errors.d.ts} +0 -0
  489. /package/dist/analytics/{errors.js → shared/errors.js} +0 -0
  490. /package/dist/analytics/{types.js → shared/types.js} +0 -0
  491. /package/dist/otel/{context → claude/context}/classify.d.ts +0 -0
  492. /package/dist/otel/{context → claude/context}/classify.js +0 -0
  493. /package/dist/otel/{context → claude/context}/extract.js +0 -0
  494. /package/dist/otel/{context → claude/context}/markers.d.ts +0 -0
  495. /package/dist/otel/{context → claude/context}/util.d.ts +0 -0
  496. /package/dist/otel/{context → claude/context}/util.js +0 -0
  497. /package/dist/otel/{daemon → claude/daemon}/paths.d.ts +0 -0
  498. /package/dist/otel/{daemon → claude/daemon}/paths.js +0 -0
  499. /package/dist/otel/{daemon → claude/daemon}/reprocess.d.ts +0 -0
  500. /package/dist/otel/{log-handler.d.ts → claude/log-handler.d.ts} +0 -0
@@ -0,0 +1,1477 @@
1
+ "use strict";
2
+ /**
3
+ * Codex session analytics projection.
4
+ *
5
+ * Turns a parsed rollout (`CodexRolloutLine[]`) into a single
6
+ * `SessionAnalytics` delta that's POSTed to the collector with a
7
+ * deterministic event id (one id per session — backend latest-wins).
8
+ *
9
+ * Privacy fence preserved: every emitted field is a count, label,
10
+ * byte size, or timestamp. No raw user prompts / assistant text / tool
11
+ * arguments on the wire. The projection READS content for keyword
12
+ * matching (classifier) + byte counting + apply_patch parsing, but only
13
+ * structural signals leave the projection.
14
+ *
15
+ * Scope note: per-turn `session_turn_analytics` events are intentionally
16
+ * OUT OF SCOPE on Codex. Session-level aggregates (turn counts, category
17
+ * breakdown, code_changes, per-tool maps) still ship via the master
18
+ * `session_analytics` record. Turn boundary lifecycle was an order of
19
+ * magnitude more code than the master event itself — Codex's turn shape
20
+ * (per `task_started`) was rich enough to support it, but the per-turn
21
+ * wire records added complexity that didn't pay off in observability.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.formatHexAsUuid = void 0;
25
+ exports.deriveCodexSessionAnalyticsEventId = deriveCodexSessionAnalyticsEventId;
26
+ exports.projectCodexDelta = projectCodexDelta;
27
+ const crypto_1 = require("crypto");
28
+ const event_1 = require("../../lib/event");
29
+ Object.defineProperty(exports, "formatHexAsUuid", { enumerable: true, get: function () { return event_1.formatHexAsUuid; } });
30
+ const types_1 = require("../shared/types");
31
+ const classifier_1 = require("../shared/classifier");
32
+ const tokens_1 = require("../shared/tokens");
33
+ const errors_1 = require("../shared/errors");
34
+ const apply_patch_1 = require("./apply-patch");
35
+ const pricing_1 = require("./pricing");
36
+ const classifier_2 = require("./classifier");
37
+ const util_1 = require("../../clients/codex/util");
38
+ const types_2 = require("./types");
39
+ function deriveCodexSessionAnalyticsEventId(sessionId) {
40
+ const hex = (0, crypto_1.createHash)("sha256")
41
+ .update(`session_analytics:${sessionId}`)
42
+ .digest("hex");
43
+ return (0, event_1.formatHexAsUuid)(hex.slice(0, 32));
44
+ }
45
+ function freshAccumulator() {
46
+ return {
47
+ firstSeenTs: null, lastSeenTs: null, sessionMetaTs: null,
48
+ sessionSource: null, systemPromptBytes: 0,
49
+ turnDurationMsTotal: 0, turnDurationCount: 0,
50
+ firstTokenMsTotal: 0, firstTokenMsCount: 0,
51
+ turnCount: 0,
52
+ userMsgCount: 0, userMsgBytes: 0,
53
+ assistantMsgCount: 0,
54
+ toolCallCount: 0,
55
+ sawEditPendingBash: false, sawBashAfterEdit: false, retries: 0,
56
+ interruptionCount: 0,
57
+ currentTurnHadEdit: false, currentTurnHadRetry: false,
58
+ turnsWithEdit: 0, turnsWithRetry: 0, oneShotTurns: 0,
59
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, costUsd: 0,
60
+ currentModel: "gpt-5.5",
61
+ models: {}, tools: {}, mcpServers: {}, bashBinaries: {}, bashSubcommands: {}, skills: {}, subAgents: {},
62
+ callIdToToolName: {}, callIdToBashBinary: {}, callIdToSubAgentType: {},
63
+ pendingPatchEntries: {}, mcpErrorRecordedCallIds: new Set(),
64
+ usesSubAgent: false, usesSkill: false, usesMcp: false, usesWebSearch: false, usesWebFetch: false,
65
+ kw: { debug: false, feature: false, refactor: false, brainstorm: false, research: false, file_pattern: false, script_pattern: false, url: false },
66
+ toolBuckets: (0, classifier_1.emptyToolBucketFlags)(),
67
+ bashFlags: (0, classifier_1.emptyBashCmdFlags)(),
68
+ fileChangeCounts: {}, languages: {}, linesAdded: 0, linesRemoved: 0,
69
+ toolErrors: 0, toolErrorCategories: {},
70
+ contextLatest: 0, contextPeak: 0,
71
+ assistantTurnIndex: 0, contextTokensBuckets: {},
72
+ processErrors: {},
73
+ userMsgsByHour: {}, userMsgsByDate: {}, userMsgsByWeekday: {},
74
+ lastAgentMessageMs: null, responseTimeBuckets: {},
75
+ priorLineTsMs: null, idleMs: 0,
76
+ };
77
+ }
78
+ // ─────────────────────────────────────────────────────────────────────────
79
+ // Small helpers
80
+ // ─────────────────────────────────────────────────────────────────────────
81
+ function utf8Bytes(s) {
82
+ if (!s) {
83
+ return 0;
84
+ }
85
+ try {
86
+ return Buffer.byteLength(s, "utf8");
87
+ }
88
+ catch {
89
+ return 0;
90
+ }
91
+ }
92
+ function emptyToolUsage() {
93
+ return { count: 0, errors: 0, input_size: 0, output_size: 0, approximated_input_tokens: 0, approximated_output_tokens: 0 };
94
+ }
95
+ function emptyModelUsage() {
96
+ return { count: 0, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, cost_usd: 0 };
97
+ }
98
+ function bumpToolInputSize(map, key, inputBytes) {
99
+ if (!map[key]) {
100
+ map[key] = emptyToolUsage();
101
+ }
102
+ map[key].count += 1;
103
+ if (inputBytes > 0) {
104
+ map[key].input_size += inputBytes;
105
+ // Raw float (no Math.round) — mirrors Claude's `slot.approximated_input_tokens
106
+ // = slot.input_size / BYTES_PER_TOKEN` (projection.ts:1912). Rounding here
107
+ // would silently drift values cross-client: a tool with input_size=10 emits
108
+ // Codex=3 vs Claude=2.5, breaking backend aggregations summing per-tool
109
+ // approximated_*_tokens across both clients.
110
+ map[key].approximated_input_tokens = map[key].input_size / tokens_1.BYTES_PER_TOKEN;
111
+ }
112
+ }
113
+ function addToolOutputBytes(map, key, bytes) {
114
+ if (!map[key]) {
115
+ map[key] = emptyToolUsage();
116
+ }
117
+ map[key].output_size += bytes;
118
+ map[key].approximated_output_tokens = map[key].output_size / tokens_1.BYTES_PER_TOKEN;
119
+ }
120
+ function localHour(iso) {
121
+ const d = new Date(iso);
122
+ // Return null on invalid timestamp instead of silently bucketing into
123
+ // hour 0. The call site previously got `0` from this helper and dumped
124
+ // every malformed-timestamp user_message into `messages_by_hour["0"]`
125
+ // (midnight bucket) — while the sister helpers `localDate` /
126
+ // `localWeekday` correctly returned "" and were guarded. The three
127
+ // histograms then disagreed by the malformed count: hour gained N
128
+ // phantom entries at "0", date and weekday gained 0. Backend cohort
129
+ // dashboards that pivot on time-of-day misattribute the count.
130
+ return Number.isFinite(d.getTime()) ? d.getHours() : null;
131
+ }
132
+ function localDate(iso) {
133
+ const d = new Date(iso);
134
+ if (!Number.isFinite(d.getTime())) {
135
+ return "";
136
+ }
137
+ const yyyy = d.getFullYear();
138
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
139
+ const dd = String(d.getDate()).padStart(2, "0");
140
+ return `${yyyy}-${mm}-${dd}`;
141
+ }
142
+ function localWeekday(iso) {
143
+ const names = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
144
+ const d = new Date(iso);
145
+ return Number.isFinite(d.getTime()) ? names[d.getDay()] : "";
146
+ }
147
+ /**
148
+ * Bash subcommand extraction — privacy-safe (mirrors `extractBashSubcommand`
149
+ * in `analytics/claude/projection.ts`; keep the lists in sync).
150
+ *
151
+ * Without this guard, `tool_meta.bash_subcommands` would carry the raw
152
+ * second token of every command — including secrets / args / inline JSON
153
+ * (`echo '{"session_id":"...","status":"fail",...}'` → leaked verdict body,
154
+ * `rg "isValidCreditCard|cardNumber"` → leaked search pattern). Both are
155
+ * actual cases observed in production Codex sessions.
156
+ *
157
+ * Policy: emit `<binary> <subcommand>` ONLY for the small set of CLIs that
158
+ * actually have well-known subcommand verbs (git/npm/cargo/...), AND only
159
+ * when the second token has a safe identifier shape (alphanumeric + `.-_`).
160
+ * Everything else returns just the binary name (`echo`, `cat`, `find`, ...).
161
+ */
162
+ const KNOWN_MULTI_SUBCOMMAND_BINARIES = new Set([
163
+ "git", "npm", "yarn", "pnpm", "bun",
164
+ "cargo", "go", "uv", "pipx", "poetry",
165
+ "docker", "kubectl", "helm",
166
+ "brew", "apt", "apt-get",
167
+ "aws", "gcloud", "az",
168
+ "make", "deno", "rustup",
169
+ ]);
170
+ const SUBCOMMAND_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
171
+ function extractBashSubcommand(cmd) {
172
+ const tokens = cmd.trim().split(/\s+/);
173
+ const stripped = [];
174
+ for (const t of tokens) {
175
+ // Skip leading env-var assignments (e.g. `FOO=bar npm test` → strip FOO=bar).
176
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
177
+ continue;
178
+ }
179
+ stripped.push(t);
180
+ }
181
+ if (stripped.length === 0) {
182
+ return undefined;
183
+ }
184
+ // Path-strip to match `extractBashBinary` (clients/codex/util.ts): same
185
+ // command source must produce the same key shape across `bash_binaries`
186
+ // and `bash_subcommands` so backend joins line up. Without the strip,
187
+ // `/usr/bin/git status` shipped `bash_binaries["git"]` (stripped) AND
188
+ // `bash_subcommands["/usr/bin/git"]` (raw) in the SAME payload, AND the
189
+ // `KNOWN_MULTI_SUBCOMMAND_BINARIES` lookup missed the absolute-path token
190
+ // → `git status` granularity dropped entirely whenever the agent typed
191
+ // an absolute path or `env … /usr/bin/git status`. Internal projection
192
+ // divergence with secret-shaped pollution downstream of the same fence
193
+ // round 73 closed for `bash_binaries`.
194
+ const rawBinary = stripped[0];
195
+ const binary = rawBinary.split(/[\\/]/).pop() ?? rawBinary;
196
+ // Defense-in-depth: empty/whitespace-only commands fall through with
197
+ // `binary = ""`. The current Codex caller filters via `if (sub)` so the
198
+ // empty key never reaches the wire, but defending at function level
199
+ // matches the Claude-side fix and protects future callers.
200
+ if (binary.length === 0) {
201
+ return undefined;
202
+ }
203
+ if (stripped.length === 1) {
204
+ return binary;
205
+ }
206
+ if (KNOWN_MULTI_SUBCOMMAND_BINARIES.has(binary)
207
+ && SUBCOMMAND_TOKEN_RE.test(stripped[1])) {
208
+ return `${binary} ${stripped[1]}`;
209
+ }
210
+ return binary;
211
+ }
212
+ // Same `[2s, 3600s]` admission window as the Claude side — filters out
213
+ // auto-dispatched ticks at the low end and "left the laptop overnight"
214
+ // stretches at the high end so the histogram reflects real human
215
+ // response cadence.
216
+ const RESPONSE_TIME_MIN_SEC = 2;
217
+ const RESPONSE_TIME_MAX_SEC = 3600;
218
+ /**
219
+ * Bucket an `agent_message → next user_message` gap (ms) into one of seven
220
+ * human-readable response-time buckets. Mirrors `responseTimeBucket` in
221
+ * `analytics/claude/projection.ts` — keep bucket boundaries in sync.
222
+ */
223
+ function responseTimeBucket(ms) {
224
+ const sec = ms / 1000;
225
+ // Strict boundaries (matches Claude's caller-side guard at projection.ts:2311
226
+ // `gapSec > MIN && gapSec < MAX` — so 2.0s and 3600.0s exact gaps are both
227
+ // EXCLUDED, not included). Without strict bounds Codex over-counts boundary
228
+ // values vs Claude in cross-client dashboards.
229
+ if (sec <= RESPONSE_TIME_MIN_SEC || sec >= RESPONSE_TIME_MAX_SEC) {
230
+ return null;
231
+ }
232
+ if (sec < 10) {
233
+ return "2-10s";
234
+ }
235
+ if (sec < 30) {
236
+ return "10-30s";
237
+ }
238
+ if (sec < 60) {
239
+ return "30s-1m";
240
+ }
241
+ if (sec < 120) {
242
+ return "1-2m";
243
+ }
244
+ if (sec < 300) {
245
+ return "2-5m";
246
+ }
247
+ if (sec < 900) {
248
+ return "5-15m";
249
+ }
250
+ return ">15m";
251
+ }
252
+ /**
253
+ * Detect a Codex skill activation in a `response_item.message` (role=user)
254
+ * content text. Codex framework injects a synthetic user message of shape
255
+ * `<skill>\n<name>NAME</name>\n<path>...</path>\n---\n<rest of SKILL.md>` when
256
+ * the model invokes a skill (live-verified across 2026/05-06 sessions).
257
+ * Returns the skill name (e.g. "ironbee-verify") or null if the content
258
+ * doesn't match the injection shape.
259
+ *
260
+ * Defensive on the content shape — payload is typed `unknown` because Codex
261
+ * has multiple content-array variants (input_text / output_text / etc).
262
+ */
263
+ function extractCodexSkillName(content) {
264
+ // content shape: [{ type: "input_text", text: "<skill>\n<name>X</name>..." }, ...]
265
+ if (!Array.isArray(content) || content.length === 0) {
266
+ return null;
267
+ }
268
+ const first = content[0];
269
+ if (typeof first !== "object" || first === null) {
270
+ return null;
271
+ }
272
+ const text = first.text;
273
+ if (typeof text !== "string") {
274
+ return null;
275
+ }
276
+ if (!text.startsWith("<skill>")) {
277
+ return null;
278
+ }
279
+ // <name>NAME</name> — non-greedy, line-aware (NAME can't contain </name>).
280
+ const m = /<name>([^<]+)<\/name>/.exec(text);
281
+ return m ? m[1].trim() : null;
282
+ }
283
+ /**
284
+ * Bucket an assistant-message index into one of the six `ContextTurnBucket`
285
+ * keys. Mirrors `turnBucket` in `analytics/claude/projection.ts` — keep in sync.
286
+ */
287
+ function contextTurnBucket(turnIndex) {
288
+ if (turnIndex <= 3) {
289
+ return "1-3";
290
+ }
291
+ if (turnIndex <= 10) {
292
+ return "4-10";
293
+ }
294
+ if (turnIndex <= 25) {
295
+ return "11-25";
296
+ }
297
+ if (turnIndex <= 50) {
298
+ return "26-50";
299
+ }
300
+ if (turnIndex <= 100) {
301
+ return "51-100";
302
+ }
303
+ return "100+";
304
+ }
305
+ /**
306
+ * Classify a Codex tool-output string into one of the closed-list error
307
+ * categories. Mirrors Claude's `classifyToolError` taxonomy (see
308
+ * `analytics/claude/projection.ts`) so the wire shape stays uniform across
309
+ * clients — same `ToolErrorCategory` keys appear under
310
+ * `errors.tool_error_categories` regardless of client.
311
+ *
312
+ * Codex doesn't have a structured `is_error: true` signal (unlike Anthropic's
313
+ * tool_result blocks), so detection is pattern-based on the output text.
314
+ * Order matters — first match wins; check exit codes / explicit rejection
315
+ * before the broader edit/file patterns so e.g. an `Exit code: 1` from a
316
+ * failing edit lands in `command_failed`, not `edit_failed`.
317
+ */
318
+ function classifyOutputError(output) {
319
+ if (!output) {
320
+ return { isError: false, category: null };
321
+ }
322
+ // 1. User interrupt — explicit cancel signal beats everything else.
323
+ // Anchored to (?:^|\n) so a tool output that narratively contains
324
+ // "[Request interrupted by user]" as a substring (test fixtures,
325
+ // log replays, copy-pasted instructions) doesn't false-positive
326
+ // into user_rejected. Cross-pipeline parity: live `detectFailure`
327
+ // (clients/codex/hooks/track-action.ts) and import `looksLikeError`
328
+ // (import/codex/runner.ts) both anchor identically — without this
329
+ // `session_analytics.errors.tool_error_categories.user_rejected`
330
+ // increments on substring matches while `tool_call.error` stays
331
+ // undefined on the same call.
332
+ if (/(?:^|\n)\[Request interrupted by user\]/.test(output)) {
333
+ return { isError: true, category: "user_rejected" };
334
+ }
335
+ // 2. Non-zero exit code (both legacy `Process exited with code N` and
336
+ // the `Exit code: N` shell-tool format Codex emits for exec_command
337
+ // AND apply_patch). Both patterns are LINE-ANCHORED so a tool whose
338
+ // legitimate stdout contains "Process exited with code N" in
339
+ // narrative / log text doesn't false-positive into command_failed.
340
+ // Cross-pipeline parity: live `detectFailure` (clients/codex/hooks/
341
+ // track-action.ts) and import `looksLikeError` (import/codex/
342
+ // runner.ts) both anchor to (?:^|\n) — projection must match or
343
+ // `session_analytics.errors.tool_error_categories` diverges from
344
+ // `tool_call.error` on the same call (analytics flags it, but
345
+ // tool_call.error stays undefined).
346
+ const procExit = /(?:^|\n)Process exited with code (\d+)/i.exec(output);
347
+ if (procExit && procExit[1] !== "0") {
348
+ return { isError: true, category: "command_failed" };
349
+ }
350
+ const exitCode = /^Exit code:\s*(\d+)/m.exec(output);
351
+ if (exitCode && exitCode[1] !== "0") {
352
+ return { isError: true, category: "command_failed" };
353
+ }
354
+ // 3. file_changed — Claude-parity. Tightly scoped so common git diff
355
+ // output (`1 file changed, 2 insertions(+)`) doesn't match.
356
+ if (/modified since (?:last )?read|stale read/i.test(output)) {
357
+ return { isError: true, category: "file_changed" };
358
+ }
359
+ // 4. file_too_large — Claude-parity. Triggered when a Codex tool refuses
360
+ // an oversized file or hits a size cap.
361
+ if (/file (?:is )?too large|exceeds (?:maximum|max|the (?:size )?limit)/i.test(output)) {
362
+ return { isError: true, category: "file_too_large" };
363
+ }
364
+ // 5. file_not_found — tight phrases that come from tool-side wrappers,
365
+ // not arbitrary grep / ls noise. "No such file" alone would catch
366
+ // `ls: /nope: No such file or directory` even when the user wanted
367
+ // that output; the explicit phrasings below are safer.
368
+ if (/file not found|No such file or directory|does not exist/i.test(output)) {
369
+ return { isError: true, category: "file_not_found" };
370
+ }
371
+ // 6. edit_failed — apply_patch verification failures (Codex's most
372
+ // common edit-error shape: `apply_patch verification failed: Failed
373
+ // to find expected lines in ...`), plus the legacy patch/cannot-apply
374
+ // patterns the prior regex caught and Claude's "string to replace not
375
+ // found" phrasing for cross-client parity.
376
+ // The leading `Error` branch uses `/^\s*Error\b/` (no `/m` flag, no
377
+ // colon requirement) for cross-pipeline parity with live `detectFailure`
378
+ // and import `looksLikeError`. Both anchor to the START OF THE WHOLE
379
+ // OUTPUT (not each line) and require the word `Error\b` — NOT a colon
380
+ // suffix. The prior `/^(?:error|Error|ERROR):/m` form diverged two ways:
381
+ // (a) missed bare `Error fetching ...` / `Error executing ...` outputs
382
+ // that live + import correctly flag (no colon); (b) matched mid-output
383
+ // `Error:` lines that live + import don't (no `/m` flag there). Net:
384
+ // `session_analytics.errors.tool_error_categories.edit_failed` diverged
385
+ // from `tool_call.error` non-null counts on the same call.
386
+ if (/^\s*Error\b/.test(output)
387
+ || /apply_patch verification failed/i.test(output)
388
+ || /failed to find expected lines/i.test(output)
389
+ || /string to replace not found/i.test(output)
390
+ || /patch failed|patch could not|cannot apply/i.test(output)) {
391
+ return { isError: true, category: "edit_failed" };
392
+ }
393
+ return { isError: false, category: null };
394
+ }
395
+ /**
396
+ * Map a file extension to a canonical language label. PascalCase / acronym
397
+ * casing mirrors Claude's `EXTENSION_TO_LANGUAGE` (projection.ts:39) so the
398
+ * `code_changes.languages` map is cross-client comparable — a backend
399
+ * aggregating Claude + Codex sessions sees a single key `"TypeScript"`
400
+ * regardless of source, not `"TypeScript"` + `"typescript"` as distinct
401
+ * buckets. Codex covers more extensions than Claude's narrower list, which
402
+ * is fine (more granular labels just appear under canonical casing).
403
+ */
404
+ function languageFromPath(path) {
405
+ const ext = (/\.[a-z0-9]+$/i.exec(path)?.[0] ?? "").toLowerCase();
406
+ const map = {
407
+ ".ts": "TypeScript", ".tsx": "TypeScript",
408
+ ".js": "JavaScript", ".jsx": "JavaScript", ".mjs": "JavaScript", ".cjs": "JavaScript",
409
+ ".py": "Python", ".rb": "Ruby", ".go": "Go", ".rs": "Rust",
410
+ ".java": "Java", ".kt": "Kotlin", ".scala": "Scala",
411
+ ".swift": "Swift", ".m": "Objective-C", ".mm": "Objective-C",
412
+ ".c": "C", ".h": "C", ".cpp": "C++", ".cc": "C++", ".hpp": "C++",
413
+ ".cs": "C#", ".vb": "VB",
414
+ ".php": "PHP", ".html": "HTML", ".htm": "HTML",
415
+ ".css": "CSS", ".scss": "CSS", ".sass": "CSS", ".less": "CSS",
416
+ ".md": "Markdown", ".mdx": "Markdown",
417
+ ".json": "JSON", ".yaml": "YAML", ".yml": "YAML", ".toml": "TOML", ".xml": "XML",
418
+ ".sh": "Shell", ".bash": "Shell", ".zsh": "Shell",
419
+ ".sql": "SQL", ".graphql": "GraphQL", ".proto": "Protocol Buffer",
420
+ ".ex": "Elixir", ".exs": "Elixir", ".erl": "Erlang",
421
+ ".lua": "Lua", ".dart": "Dart", ".clj": "Clojure",
422
+ };
423
+ return map[ext] ?? null;
424
+ }
425
+ /**
426
+ * Shared per-tool-call accounting. Both `function_call` and `custom_tool_call`
427
+ * route here so the per-tool maps, MCP detection, Bash extraction, and
428
+ * code_changes derivation work uniformly.
429
+ *
430
+ * `inputRaw` is the raw input text used for size attribution:
431
+ * - function_call: `arguments` (JSON-encoded; JSON-parsed when extracting Bash cmd or apply_patch body).
432
+ * - custom_tool_call: `input` (raw patch text for apply_patch).
433
+ *
434
+ * `applyPatchBodyOverride` short-circuits the JSON parse path for custom_tool_call
435
+ * — pass the raw patch body and we use it directly. Pass `null` for function_call
436
+ * and we fall back to `extractApplyPatchBody(inputRaw)`.
437
+ */
438
+ function processToolCall(acc, rawToolName, inputRaw, callId, applyPatchBodyOverride,
439
+ /**
440
+ * Codex's `function_call.namespace` value — used to disambiguate
441
+ * sub-agent detection from third-party MCP servers that might collide
442
+ * with built-in tool names (e.g. a custom MCP exposing its own
443
+ * `spawn_agent`). When set to `"multi_agent_v1"`, treat `spawn_agent`
444
+ * as the canonical sub-agent orchestrator. When unset / different, the
445
+ * tool name match still works as a fallback (legacy 0.131 rollouts
446
+ * omitted the field entirely).
447
+ */
448
+ namespace = undefined) {
449
+ acc.toolCallCount += 1;
450
+ // Canonicalize the bare devtools tool name (`bdt_content_take_screenshot`
451
+ // → `bdt_content_take-screenshot`) so the `tools` map key matches the
452
+ // wire shape emitted by `tool_call` events. No-op on non-devtools names.
453
+ const toolName = (0, util_1.canonicalizeCodexToolName)(rawToolName);
454
+ const inputSize = utf8Bytes(inputRaw);
455
+ // Map Codex tool name → Claude classifier vocab (so the shared classifier's
456
+ // keyword logic + tool-bucket booleans work).
457
+ //
458
+ // ALSO used as the `tools` map key — without this, every Codex session
459
+ // shipped `session_analytics.tools["exec_command" | "apply_patch" |
460
+ // "read_file" | "web_search" | "web_fetch" | "update_plan"]`, while the
461
+ // live `tool_call` events from `hooks/track-action.ts` ship
462
+ // `tool_name: "Bash"` (via `normalizeCodexToolName`) for the SAME logical
463
+ // shell call. Cross-event-type backend joins (per-tool aggregates ↔
464
+ // per-call tool_call events) broke on Codex sessions because the names
465
+ // never matched. Same fix aligns Codex `session_analytics.tools` with
466
+ // Claude `session_analytics.tools` so cross-client aggregations on
467
+ // `tools["Bash"]` / `tools["Read"]` / etc. include both sources. Devtools
468
+ // / sub-agent / unknown tool names pass through unchanged.
469
+ const claudeAlias = toolName === "apply_patch"
470
+ ? "Edit"
471
+ : toolName === "exec_command" || toolName === "Bash"
472
+ ? "Bash"
473
+ : toolName === "update_plan"
474
+ ? "TodoWrite"
475
+ : toolName === "read_file"
476
+ ? "Read"
477
+ : toolName === "web_search"
478
+ ? "WebSearch"
479
+ : toolName === "web_fetch"
480
+ ? "WebFetch"
481
+ : toolName;
482
+ (0, classifier_1.applyToolBucketFlags)(acc.toolBuckets, claudeAlias);
483
+ // Track call_id → wire-vocab tool_name for the output branch's bucket
484
+ // lookup. Storing `claudeAlias` here (not raw `toolName`) keeps
485
+ // `acc.tools[key]` writes from `processToolCallOutput` on the same key
486
+ // that `processToolCall`'s input write used — without this the output
487
+ // bytes would land in a `tools["exec_command"]` slot that the input
488
+ // write never created, splitting input/output across two map entries.
489
+ if (toolName.length > 0 && callId.length > 0) {
490
+ acc.callIdToToolName[callId] = claudeAlias;
491
+ }
492
+ bumpToolInputSize(acc.tools, claudeAlias, inputSize);
493
+ // MCP server extraction — handles BOTH `mcp__<server>__<tool>` (Claude wire
494
+ // shape, kept for forward-compat) AND bare `bdt_*` / `ndt_*` / `bedt_*`
495
+ // (Codex rollout shape; the dominant case in practice).
496
+ const mcpServer = (0, util_1.extractCodexMcpServer)(toolName);
497
+ if (mcpServer !== null) {
498
+ acc.usesMcp = true;
499
+ // applyToolBucketFlags only sets the mcp bucket for the `mcp__*` shape —
500
+ // OR-set it here so classifyTurn sees a consistent signal regardless of surface.
501
+ acc.toolBuckets.mcp = true;
502
+ bumpToolInputSize(acc.mcpServers, mcpServer, inputSize);
503
+ }
504
+ const cat = (0, classifier_2.classifyCodexToolName)(toolName);
505
+ if (cat === "research") {
506
+ if (toolName === "web_search") {
507
+ acc.usesWebSearch = true;
508
+ }
509
+ if (toolName === "web_fetch") {
510
+ acc.usesWebFetch = true;
511
+ }
512
+ }
513
+ // Sub-agent detection — Codex's `multi_agent_v1` namespace exposes
514
+ // `spawn_agent` (with arguments.agent_type ∈ {explorer, worker, default,
515
+ // custom}). The two coordination tools `wait_agent` / `close_agent` are
516
+ // tracked under their bare names in `tools[*]` like any other function_call
517
+ // — they're not per-sub-agent buckets. Only `spawn_agent` populates the
518
+ // `subAgents[agent_type]` map (Claude-parity `Agent`/`Task.input.subagent_type`).
519
+ // Namespace-guarded sub-agent detection. Codex's `multi_agent_v1`
520
+ // namespace exclusively owns `spawn_agent` / `wait_agent` / `close_agent`.
521
+ // A third-party MCP server could legally expose its own tool named
522
+ // `spawn_agent` under a different namespace (e.g. `mcp__custom-orchestrator`);
523
+ // we should NOT route THAT into sub_agents. Legacy fallback: 0.131-alpha
524
+ // rollouts omit the namespace field entirely → fall back to name-only
525
+ // detection (the dominant case for IronBee users today).
526
+ const isMultiAgentNs = namespace === "multi_agent_v1"
527
+ || namespace === undefined // legacy / missing field — name-only fallback
528
+ || namespace === ""; // some Codex versions emit "" instead of omitting
529
+ if (toolName === "spawn_agent" && isMultiAgentNs) {
530
+ let agentType = null;
531
+ try {
532
+ const obj = JSON.parse(inputRaw || "{}");
533
+ if (obj !== null && typeof obj === "object") {
534
+ const t = obj.agent_type;
535
+ if (typeof t === "string" && t.length > 0) {
536
+ agentType = t;
537
+ }
538
+ }
539
+ }
540
+ catch { /* ignore — malformed arguments */ }
541
+ if (agentType !== null) {
542
+ bumpToolInputSize(acc.subAgents, agentType, inputSize);
543
+ acc.usesSubAgent = true;
544
+ acc.toolBuckets.task = true;
545
+ if (callId.length > 0) {
546
+ acc.callIdToSubAgentType[callId] = agentType;
547
+ }
548
+ }
549
+ }
550
+ // Bash: classify subcommand + first-token binary. Only function_call carries
551
+ // Bash; custom_tool_call is apply_patch-only, so the JSON parse will fail
552
+ // harmlessly there.
553
+ if (toolName === "exec_command" || toolName === "Bash") {
554
+ const bashCmd = (() => {
555
+ try {
556
+ const obj = JSON.parse(inputRaw || "{}");
557
+ if (obj !== null && typeof obj === "object") {
558
+ const cmd = obj.cmd
559
+ ?? obj.command;
560
+ if (typeof cmd === "string") {
561
+ return cmd;
562
+ }
563
+ }
564
+ }
565
+ catch { /* ignore */ }
566
+ return "";
567
+ })();
568
+ // Reuse `extractBashBinary` from `clients/codex/util.ts` so the
569
+ // `bash_binaries` aggregate key matches the per-event
570
+ // `tool_call.tool_input.binary` value byte-for-byte. Without this,
571
+ // `/usr/bin/git status` shipped `tool_input.binary="git"` (path
572
+ // stripped) AND `bash_binaries["/usr/bin/git"]` (raw token) on the
573
+ // same wire payload — backend couldn't join per-event labels to the
574
+ // session-level aggregate. Same string source, two divergent keys.
575
+ const bin = (0, util_1.extractBashBinary)(bashCmd) || "";
576
+ if (bin) {
577
+ bumpToolInputSize(acc.bashBinaries, bin, inputSize);
578
+ // Remember which binary this call_id ran so processToolCallOutput
579
+ // can mirror outBytes to the same bashBinaries bucket. Without
580
+ // this, every binary's output_size stays at 0.
581
+ if (callId.length > 0) {
582
+ acc.callIdToBashBinary[callId] = bin;
583
+ }
584
+ // Privacy-safe subcommand — emits `<binary> <subcommand>` only
585
+ // for known multi-subcommand CLIs (git/npm/cargo/...) AND when
586
+ // the second token has a safe identifier shape. Otherwise just
587
+ // the binary name; raw arguments NEVER reach the wire.
588
+ const sub = extractBashSubcommand(bashCmd);
589
+ if (sub) {
590
+ acc.bashSubcommands[sub] = (acc.bashSubcommands[sub] ?? 0) + 1;
591
+ }
592
+ const bc = (0, classifier_1.emptyBashCmdFlags)();
593
+ (0, classifier_1.applyBashCmdFlags)(bc, bashCmd);
594
+ acc.bashFlags.test = acc.bashFlags.test || bc.test;
595
+ acc.bashFlags.build = acc.bashFlags.build || bc.build;
596
+ acc.bashFlags.install = acc.bashFlags.install || bc.install;
597
+ acc.bashFlags.git = acc.bashFlags.git || bc.git;
598
+ }
599
+ }
600
+ // Edit→Bash→Edit retry detection. `acc.retries` is the session-level
601
+ // count (raw retry events); `currentTurnHadEdit` / `currentTurnHadRetry`
602
+ // are per-turn flags that drive Claude-parity COUNTS on
603
+ // `turns.with_retry` / `turns.one_shot` (folded by closeCurrentTurnIfAny
604
+ // on each new task_started + at finalize).
605
+ if (toolName === "apply_patch") {
606
+ acc.currentTurnHadEdit = true;
607
+ if (acc.sawEditPendingBash && acc.sawBashAfterEdit) {
608
+ acc.retries += 1;
609
+ acc.currentTurnHadRetry = true;
610
+ acc.sawEditPendingBash = true;
611
+ acc.sawBashAfterEdit = false;
612
+ }
613
+ else {
614
+ acc.sawEditPendingBash = true;
615
+ acc.sawBashAfterEdit = false;
616
+ }
617
+ }
618
+ else if (toolName === "exec_command" || toolName === "Bash") {
619
+ if (acc.sawEditPendingBash) {
620
+ acc.sawBashAfterEdit = true;
621
+ }
622
+ }
623
+ // apply_patch → code_changes derivation. custom_tool_call passes the body
624
+ // directly via the override; function_call routes through extractApplyPatchBody.
625
+ //
626
+ // LAZY-COMMIT: parse here but STASH in `pendingPatchEntries[callId]`
627
+ // rather than committing to code_changes. The authoritative success
628
+ // signal is `event_msg.patch_apply_end.success` (paired by call_id) —
629
+ // commit only when that fires with success=true. Without lazy-commit,
630
+ // a rejected apply_patch (e.g. "verification failed: Failed to find
631
+ // expected lines") would still inflate `lines_added` / `lines_removed`
632
+ // / `files_modified` / `languages` for code that never landed on disk.
633
+ if (toolName === "apply_patch") {
634
+ const body = applyPatchBodyOverride
635
+ ?? (0, apply_patch_1.extractApplyPatchBody)(inputRaw || "{}");
636
+ if (body !== null && callId.length > 0) {
637
+ let files = [];
638
+ try {
639
+ files = (0, apply_patch_1.parseApplyPatchBody)(body);
640
+ }
641
+ catch {
642
+ files = [];
643
+ }
644
+ if (files.length > 0) {
645
+ acc.pendingPatchEntries[callId] = files;
646
+ }
647
+ }
648
+ }
649
+ }
650
+ /** Commit a single apply_patch invocation's parsed entries into the final
651
+ * code_changes aggregates. Called when `patch_apply_end.success === true`
652
+ * AND defensively at end-of-walker for any still-pending entries (legacy
653
+ * rollouts without patch_apply_end / mid-write truncation). */
654
+ function commitPatchEntries(acc, entries) {
655
+ for (const f of entries) {
656
+ acc.fileChangeCounts[f.path] = (acc.fileChangeCounts[f.path] ?? 0) + 1;
657
+ acc.linesAdded += f.linesAdded;
658
+ acc.linesRemoved += f.linesRemoved;
659
+ const lang = languageFromPath(f.path);
660
+ if (lang) {
661
+ acc.languages[lang] = (acc.languages[lang] ?? 0) + 1;
662
+ }
663
+ }
664
+ }
665
+ /**
666
+ * Shared `function_call_output` / `custom_tool_call_output` handler — attributes
667
+ * the response bytes (and any error) to the originating tool via the
668
+ * `callIdToToolName` map. Falls back to a sentinel `[orphan]` bucket when the
669
+ * call_id was never seen — defensive only; the bucket should stay at 0 in
670
+ * well-formed rollouts.
671
+ */
672
+ function processToolCallOutput(acc, callId, output) {
673
+ const outBytes = utf8Bytes(output ?? "");
674
+ const { isError, category } = classifyOutputError(output);
675
+ // Wire-order guard: `mcp_tool_call_end` fires BEFORE function_call_output
676
+ // for MCP calls (verified Codex 0.135). If this output's call_id was
677
+ // already recorded as an MCP error there, suppress the error increment
678
+ // here to prevent double-counting. Output bytes are still attributed
679
+ // normally below.
680
+ const alreadyMcpErrored = acc.mcpErrorRecordedCallIds.has(callId);
681
+ const effectiveIsError = isError && !alreadyMcpErrored;
682
+ if (effectiveIsError) {
683
+ acc.toolErrors += 1;
684
+ if (category !== null) {
685
+ acc.toolErrorCategories[category] = (acc.toolErrorCategories[category] ?? 0) + 1;
686
+ }
687
+ // Mark this call_id in the same set the mcp_tool_call_end branch
688
+ // uses so the symmetric guard there (reverse-wire-order case) can
689
+ // skip via call-id match instead of the broken tool-name-scoped
690
+ // check. Without this, the reverse-order protection relied on a
691
+ // tool-NAME-scoped guard that silently dropped every legitimate
692
+ // subsequent error on the same MCP tool after the first.
693
+ acc.mcpErrorRecordedCallIds.add(callId);
694
+ // Note: `interruptionCount` is sourced EXCLUSIVELY from
695
+ // `event_msg.turn_aborted` (the canonical Codex interrupt signal —
696
+ // verified across all 2026/* sessions). The `user_rejected` output
697
+ // pattern is retained for category labeling, but no longer doubles
698
+ // as an interruption signal — that risked double-counting on
699
+ // sessions where both fire (rare today but defensive against
700
+ // future Codex schema additions).
701
+ }
702
+ const key = acc.callIdToToolName[callId] ?? ORPHAN_OUTPUT_BUCKET;
703
+ addToolOutputBytes(acc.tools, key, outBytes);
704
+ if (effectiveIsError) {
705
+ acc.tools[key].errors += 1;
706
+ }
707
+ // Mirror outBytes to the matching sub-buckets so per-server / per-binary
708
+ // output_size lines up with the per-tool view. Without this, mcp_servers[*]
709
+ // and bash_binaries[*] would only ever show input bytes (every call
710
+ // populates input on processToolCall, but output here was tools-only).
711
+ const mcpServer = (0, util_1.extractCodexMcpServer)(key);
712
+ if (mcpServer !== null) {
713
+ addToolOutputBytes(acc.mcpServers, mcpServer, outBytes);
714
+ if (effectiveIsError) {
715
+ acc.mcpServers[mcpServer].errors += 1;
716
+ }
717
+ }
718
+ const bashBinary = acc.callIdToBashBinary[callId];
719
+ if (bashBinary !== undefined) {
720
+ addToolOutputBytes(acc.bashBinaries, bashBinary, outBytes);
721
+ if (effectiveIsError) {
722
+ acc.bashBinaries[bashBinary].errors += 1;
723
+ }
724
+ }
725
+ const subAgentType = acc.callIdToSubAgentType[callId];
726
+ if (subAgentType !== undefined) {
727
+ addToolOutputBytes(acc.subAgents, subAgentType, outBytes);
728
+ if (effectiveIsError) {
729
+ acc.subAgents[subAgentType].errors += 1;
730
+ }
731
+ }
732
+ // apply_patch failure fallback: if patch_apply_end never arrived (or
733
+ // arrives AFTER this output line for some rollout shapes) but the output
734
+ // text classified as `edit_failed`, drop the stashed PatchFileEntry[]
735
+ // — same effect as `patch_apply_end.success: false`. Primary path is
736
+ // still patch_apply_end (more authoritative + handles silent failures);
737
+ // this is the secondary safety net.
738
+ if (isError && category === "edit_failed") {
739
+ delete acc.pendingPatchEntries[callId];
740
+ }
741
+ }
742
+ /**
743
+ * Sentinel key used when a `function_call_output` / `custom_tool_call_output`
744
+ * arrives for a `call_id` we never saw in a corresponding tool-call event.
745
+ * Should stay at 0 in well-formed rollouts; non-zero counts here usually point
746
+ * at a Codex schema change (new response_item type we're not parsing) or
747
+ * mid-session truncation in the test harness.
748
+ */
749
+ const ORPHAN_OUTPUT_BUCKET = "[orphan]";
750
+ /**
751
+ * Close the current task_started turn — fold its per-turn flags into the
752
+ * closed-turn aggregates. Called on every NEW `task_started` event (to close
753
+ * the previous turn) AND at finalization (to close the in-flight last turn).
754
+ * No-op when no turn has been opened yet (first call before any task_started).
755
+ */
756
+ function closeCurrentTurnIfAny(acc) {
757
+ // Skip when no turn was open AND no flags accumulated — first
758
+ // `task_started` of the session has nothing to close. But on a truncated
759
+ // rollout (no `task_started` at all, just orphan tool calls), the flags
760
+ // may have accumulated against the implicit "turn 0"; fold those into
761
+ // the aggregates so `category_breakdown.turns_with_edit` / `turns.one_shot`
762
+ // / `turns.with_retry` reflect real edits even on degenerate rollouts.
763
+ if (acc.turnCount === 0 && !acc.currentTurnHadEdit && !acc.currentTurnHadRetry) {
764
+ return;
765
+ }
766
+ if (acc.currentTurnHadEdit) {
767
+ acc.turnsWithEdit += 1;
768
+ }
769
+ if (acc.currentTurnHadRetry) {
770
+ acc.turnsWithRetry += 1;
771
+ }
772
+ // Claude-parity `one_shot`: the turn produced edits AND had no retry.
773
+ // Bash-only / read-only turns don't count.
774
+ if (acc.currentTurnHadEdit && !acc.currentTurnHadRetry) {
775
+ acc.oneShotTurns += 1;
776
+ }
777
+ acc.currentTurnHadEdit = false;
778
+ acc.currentTurnHadRetry = false;
779
+ // Reset the Edit→Bash→Edit state-machine flags too. These are
780
+ // session-level scratch state that drives `currentTurnHadRetry`, and
781
+ // without per-turn reset, an Edit in turn 1 + Bash + Edit in turn 2 (a
782
+ // legitimate fresh prompt with a build-test-fix shape) is mis-classified
783
+ // as a retry of turn 1's work. Same logical scope as `currentTurnHad*`,
784
+ // so they reset together.
785
+ acc.sawEditPendingBash = false;
786
+ acc.sawBashAfterEdit = false;
787
+ }
788
+ function projectCodexDelta(input) {
789
+ const { sessionId, projectName, userEmail, lines, endReason, isFinal } = input;
790
+ const acc = freshAccumulator();
791
+ for (const ln of lines) {
792
+ try {
793
+ processLine(acc, ln);
794
+ }
795
+ catch (e) {
796
+ // Claude-parity process_errors capture: a single malformed line
797
+ // (schema drift, corrupt JSON post-parse, unexpected payload
798
+ // shape) is recorded with dedup + sample_context but does NOT
799
+ // crash the projection. sample_context = top-level type +
800
+ // payload.type for downstream diagnostics.
801
+ const payloadType = (typeof ln.payload === "object" && ln.payload !== null
802
+ && typeof ln.payload.type === "string")
803
+ ? ln.payload.type
804
+ : "(unknown)";
805
+ (0, errors_1.recordProcessError)(acc.processErrors, e, ln.timestamp ?? new Date(0).toISOString(), `${ln.type}.${payloadType}`);
806
+ }
807
+ }
808
+ const delta = buildSessionAnalytics(acc, sessionId, projectName, userEmail, endReason, isFinal === true);
809
+ return { delta };
810
+ }
811
+ /**
812
+ * Per-line walker body, extracted so the outer loop's try/catch can wrap
813
+ * a single function call. Any thrown error here lands in `process_errors`
814
+ * via `recordProcessError`, and projection continues with the next line —
815
+ * Claude-parity defensive posture.
816
+ */
817
+ function processLine(acc, ln) {
818
+ const ts = ln.timestamp;
819
+ if (acc.firstSeenTs === null) {
820
+ acc.firstSeenTs = ts;
821
+ }
822
+ acc.lastSeenTs = ts;
823
+ // Idle attribution (Claude-parity): the gap BEFORE a user_message
824
+ // event is attributed to "user thinking / away" idle time; every
825
+ // other gap is active (agent + tools + model time). We compute the
826
+ // gap here against the prior line's timestamp, then advance the
827
+ // cursor unconditionally. Negative / zero gaps are ignored (line
828
+ // ordering should be monotonic in practice).
829
+ const lnMs = new Date(ts).getTime();
830
+ if (acc.priorLineTsMs !== null && Number.isFinite(lnMs)
831
+ && (0, types_2.isEventMsg)(ln, "user_message")) {
832
+ const gap = lnMs - acc.priorLineTsMs;
833
+ if (gap > 0) {
834
+ acc.idleMs += gap;
835
+ }
836
+ }
837
+ if (Number.isFinite(lnMs)) {
838
+ acc.priorLineTsMs = lnMs;
839
+ }
840
+ // session_meta — set model + record session start
841
+ if (ln.type === "session_meta") {
842
+ const meta = ln.payload;
843
+ if (acc.sessionMetaTs === null) {
844
+ acc.sessionMetaTs = meta.timestamp ?? ts;
845
+ }
846
+ if (meta.model_provider === "openai") {
847
+ acc.currentModel = "gpt-5.5";
848
+ }
849
+ // Capture session_meta.source (surface origin) + system-prompt bytes
850
+ // for forward-compat — wire emission deferred (see SessionAccumulator
851
+ // docstrings for sessionSource / systemPromptBytes).
852
+ if (acc.sessionSource === null && typeof meta.source === "string" && meta.source.length > 0) {
853
+ acc.sessionSource = meta.source;
854
+ }
855
+ const baseText = meta.base_instructions?.text;
856
+ if (typeof baseText === "string" && acc.systemPromptBytes === 0) {
857
+ acc.systemPromptBytes = utf8Bytes(baseText);
858
+ }
859
+ return;
860
+ }
861
+ if (ln.type === "turn_context") {
862
+ const tc = ln.payload;
863
+ if (typeof tc.model === "string" && tc.model.length > 0) {
864
+ // Strip bracketed runtime suffixes (e.g. `gpt-5.5-codex[1m]` →
865
+ // `gpt-5.5-codex`) so `session_analytics.models[<key>]` map keys
866
+ // ship the canonical id consistent with `api_request.model`
867
+ // (api-request.ts strips identically at its own capture site).
868
+ // Without this, cross-event joins on `model` see distinct
869
+ // strings for the same physical model.
870
+ acc.currentModel = tc.model.replace(/\[[^\]]*\]/g, "").trim();
871
+ }
872
+ return;
873
+ }
874
+ // ── event_msg variants ──────────────────────────────────────────────
875
+ if ((0, types_2.isEventMsg)(ln, "task_started")) {
876
+ const _p = ln.payload;
877
+ void _p;
878
+ // Close the PRIOR turn (if any) before opening the new one — so
879
+ // `turnsWithRetry` / `oneShotTurns` accumulate correctly across
880
+ // multi-turn sessions. The in-flight last turn is closed in
881
+ // buildSessionAnalytics.
882
+ closeCurrentTurnIfAny(acc);
883
+ acc.turnCount += 1;
884
+ return;
885
+ }
886
+ if ((0, types_2.isEventMsg)(ln, "user_message")) {
887
+ const p = ln.payload;
888
+ const msg = p.message ?? "";
889
+ acc.userMsgCount += 1;
890
+ acc.userMsgBytes += utf8Bytes(msg);
891
+ // Symmetric guard with localDate / localWeekday below — skip the
892
+ // whole user_msg's time-bucket attribution when the timestamp is
893
+ // malformed (rather than silently dumping it into hour-0 while the
894
+ // date/weekday buckets correctly drop it).
895
+ const hr = localHour(ts);
896
+ if (hr !== null) {
897
+ const hrKey = String(hr);
898
+ acc.userMsgsByHour[hrKey] = (acc.userMsgsByHour[hrKey] ?? 0) + 1;
899
+ }
900
+ const d = localDate(ts);
901
+ if (d) {
902
+ acc.userMsgsByDate[d] = (acc.userMsgsByDate[d] ?? 0) + 1;
903
+ }
904
+ const w = localWeekday(ts);
905
+ if (w) {
906
+ acc.userMsgsByWeekday[w] = (acc.userMsgsByWeekday[w] ?? 0) + 1;
907
+ }
908
+ // Response-time histogram: time elapsed between the previous
909
+ // agent reply and this user prompt — proxy for "how quickly did
910
+ // the human come back to the conversation."
911
+ const userMs = new Date(ts).getTime();
912
+ if (acc.lastAgentMessageMs !== null && Number.isFinite(userMs)) {
913
+ const bucket = responseTimeBucket(userMs - acc.lastAgentMessageMs);
914
+ if (bucket !== null) {
915
+ acc.responseTimeBuckets[bucket] = (acc.responseTimeBuckets[bucket] ?? 0) + 1;
916
+ }
917
+ }
918
+ // Merge keyword flags from this user message (session-level OR).
919
+ const kw = (0, classifier_1.extractUserMsgKeywords)(msg);
920
+ acc.kw.debug = acc.kw.debug || kw.debug;
921
+ acc.kw.feature = acc.kw.feature || kw.feature;
922
+ acc.kw.refactor = acc.kw.refactor || kw.refactor;
923
+ acc.kw.brainstorm = acc.kw.brainstorm || kw.brainstorm;
924
+ acc.kw.research = acc.kw.research || kw.research;
925
+ acc.kw.file_pattern = acc.kw.file_pattern || kw.file_pattern;
926
+ acc.kw.script_pattern = acc.kw.script_pattern || kw.script_pattern;
927
+ acc.kw.url = acc.kw.url || kw.url;
928
+ return;
929
+ }
930
+ if ((0, types_2.isEventMsg)(ln, "agent_message")) {
931
+ const _p = ln.payload;
932
+ void _p;
933
+ // Mark the wall-clock at which the agent finished a VISIBLE reply
934
+ // so the next user_message can compute its response_time_buckets
935
+ // gap. Stays here (not on token_count) because the gap we want is
936
+ // "after the agent stopped TALKING, how long until the user came
937
+ // back" — tool-only API calls don't count.
938
+ const ms = new Date(ts).getTime();
939
+ if (Number.isFinite(ms)) {
940
+ acc.lastAgentMessageMs = ms;
941
+ }
942
+ return;
943
+ }
944
+ if ((0, types_2.isEventMsg)(ln, "token_count")) {
945
+ const p = ln.payload;
946
+ const last = p.info?.last_token_usage;
947
+ if (!last) {
948
+ return;
949
+ }
950
+ // ONE token_count event per API call — including the ones whose
951
+ // assistant message contained only function_calls (no visible
952
+ // text). Claude-parity: `turns.assistant_count` and
953
+ // `models[*].count` are API-call counts, NOT text-response counts.
954
+ // (event_msg.agent_message only fires when the assistant emits
955
+ // visible text, so it undercounts tool-heavy responses ~5x in
956
+ // practice.) `assistantTurnIndex` follows the same semantic so
957
+ // context_tokens bucketing aligns 1:1 with API calls.
958
+ acc.assistantMsgCount += 1;
959
+ acc.assistantTurnIndex += 1;
960
+ // Anthropic/Claude convention: `usage.input_tokens` is EXCLUSIVE of
961
+ // cached tokens (cache_read_tokens shipped as a separate field so
962
+ // `input + cache_read = total context`). Codex/OpenAI's payload uses
963
+ // the inverse — `last.input_tokens` is INCLUSIVE of cached. Subtract
964
+ // here so the wire shape stays cross-client comparable; without this,
965
+ // `input + cache_read` would double-count the cached portion on every
966
+ // Codex session (e.g. 13.8M + 13.3M = 27.1M for a real session whose
967
+ // actual context churn was ~530K fresh + 13.3M reuse).
968
+ const freshInput = Math.max(0, last.input_tokens - last.cached_input_tokens);
969
+ acc.inputTokens += freshInput;
970
+ // OpenAI's Responses API rollout payload uses `output_tokens` as the
971
+ // BILLED output total (already includes `reasoning_output_tokens`
972
+ // as a subset — verified across the real corpus: `total_tokens ===
973
+ // input_tokens + output_tokens` in every token_count event sampled).
974
+ // Adding `reasoning_output_tokens` separately here would double-count
975
+ // reasoning into `session_analytics.usage.output_tokens` and
976
+ // `models[*].output_tokens` on the wire. `acc.reasoningTokens` keeps
977
+ // the subset breakdown for any future per-stream split, but the
978
+ // session-level total stays single-sourced from `output_tokens`.
979
+ acc.outputTokens += last.output_tokens;
980
+ acc.cacheReadTokens += last.cached_input_tokens;
981
+ acc.reasoningTokens += last.reasoning_output_tokens;
982
+ const cost = (0, pricing_1.computeCodexCost)(acc.currentModel, last);
983
+ acc.costUsd += cost;
984
+ // Per-model tokens + count (same fresh-exclusive convention, same
985
+ // no-double-counting rule for reasoning).
986
+ if (!acc.models[acc.currentModel]) {
987
+ acc.models[acc.currentModel] = emptyModelUsage();
988
+ }
989
+ acc.models[acc.currentModel].count += 1;
990
+ acc.models[acc.currentModel].input_tokens += freshInput;
991
+ acc.models[acc.currentModel].output_tokens += last.output_tokens;
992
+ acc.models[acc.currentModel].cache_read_tokens += last.cached_input_tokens;
993
+ acc.models[acc.currentModel].cost_usd += cost;
994
+ // Context tokens — point-in-time size of THIS API call's prompt window.
995
+ // Codex exposes two `total_tokens` fields per token_count event:
996
+ // - `total_token_usage.total_tokens`: CUMULATIVE session total
997
+ // (sums every prior call's input+output → grows ~N² over a long
998
+ // session; meaningless as a "context size" signal).
999
+ // - `last_token_usage.total_tokens`: per-call total = input +
1000
+ // output + reasoning + cached_input — the actual prompt size
1001
+ // for the most recent API response.
1002
+ // Use the per-call value (matches Claude's
1003
+ // `msgInput + msgCacheCreation + msgCacheRead` per-assistant-line
1004
+ // derivation) so `latest` / `peak` / bucket samples reflect real
1005
+ // context-window growth, not cumulative billing.
1006
+ //
1007
+ // Use `last.input_tokens` rather than `last.total_tokens` — the latter
1008
+ // is `input + output [+ reasoning]` which would over-state context
1009
+ // window size by including the model's RESPONSE bytes. Claude's
1010
+ // analog (`msgInput + msgCacheCreation + msgCacheRead`) excludes
1011
+ // output; same exclusion here. On OpenAI's payload `input_tokens`
1012
+ // is INCLUSIVE of `cached_input_tokens`, so this single field already
1013
+ // captures "total bytes the model saw as input on this call."
1014
+ const ctxTokens = last.input_tokens;
1015
+ if (ctxTokens > 0) {
1016
+ acc.contextLatest = ctxTokens;
1017
+ if (ctxTokens > acc.contextPeak) {
1018
+ acc.contextPeak = ctxTokens;
1019
+ }
1020
+ // Per-Claude-parity turn-index bucketing. `token_count` fires
1021
+ // AFTER `agent_message`, so `assistantTurnIndex` already points
1022
+ // at the bucket this sample belongs to. Clamp to 1 for the rare
1023
+ // pre-first-agent_message token_count (e.g. system-prompt-only
1024
+ // accounting line).
1025
+ const bucketKey = contextTurnBucket(Math.max(1, acc.assistantTurnIndex));
1026
+ const slot = acc.contextTokensBuckets[bucketKey]
1027
+ ?? { sum: 0, count: 0 };
1028
+ slot.sum += ctxTokens;
1029
+ slot.count += 1;
1030
+ acc.contextTokensBuckets[bucketKey] = slot;
1031
+ }
1032
+ return;
1033
+ }
1034
+ if ((0, types_2.isEventMsg)(ln, "turn_aborted")) {
1035
+ acc.interruptionCount += 1;
1036
+ // Discard any partial work the aborted turn accumulated so it doesn't
1037
+ // get folded into `turns_with_edit` / `turns_with_retry` / `oneShotTurns`
1038
+ // when the NEXT `task_started` closes it. A turn that the user
1039
+ // interrupted shouldn't count as a successful edit cycle.
1040
+ // turnCount is NOT decremented — the aborted turn is still a turn
1041
+ // (just one that didn't complete its work).
1042
+ acc.currentTurnHadEdit = false;
1043
+ acc.currentTurnHadRetry = false;
1044
+ acc.sawEditPendingBash = false;
1045
+ acc.sawBashAfterEdit = false;
1046
+ // Drop any pending apply_patch entries whose patch_apply_end was
1047
+ // preempted by the abort. Without this clear, the end-of-walker
1048
+ // defensive fold (line ~1370) would commit them as successful →
1049
+ // `code_changes.lines_added/removed/files_modified` would inflate
1050
+ // with edits the user explicitly cancelled. Each pending entry by
1051
+ // definition lost its outcome event, so dropping them all is the
1052
+ // conservative call (matches `patch_apply_end.success === false`
1053
+ // semantic).
1054
+ acc.pendingPatchEntries = {};
1055
+ return;
1056
+ }
1057
+ if ((0, types_2.isEventMsg)(ln, "task_complete")) {
1058
+ // Codex's authoritative turn-completion signal — duration_ms is the
1059
+ // host-measured wall-clock for the turn (including tool execution),
1060
+ // time_to_first_token_ms is the latency to first model token.
1061
+ // Capture for future wire emission (deferred per accumulator
1062
+ // docstrings). Empirically 65 events / 69 rollouts; turns aborted
1063
+ // mid-flight produce turn_aborted INSTEAD, so this is a clean
1064
+ // success-path signal.
1065
+ const p = ln.payload;
1066
+ if (typeof p.duration_ms === "number" && Number.isFinite(p.duration_ms)) {
1067
+ acc.turnDurationMsTotal += p.duration_ms;
1068
+ acc.turnDurationCount += 1;
1069
+ }
1070
+ if (typeof p.time_to_first_token_ms === "number" && Number.isFinite(p.time_to_first_token_ms)) {
1071
+ acc.firstTokenMsTotal += p.time_to_first_token_ms;
1072
+ acc.firstTokenMsCount += 1;
1073
+ }
1074
+ return;
1075
+ }
1076
+ if ((0, types_2.isEventMsg)(ln, "mcp_tool_call_end")) {
1077
+ // Authoritative MCP tool outcome — fires per MCP function_call,
1078
+ // carrying the Rust `Result<Ok|Err>` from the MCP server.
1079
+ //
1080
+ // EMPIRICAL WIRE ORDER (verified Codex 0.135.0 round-12 audit):
1081
+ // function_call T0
1082
+ // mcp_tool_call_end T0 + Δ ← FIRES BEFORE OUTPUT
1083
+ // function_call_output T0 + Δ + ε
1084
+ //
1085
+ // The cancellation output text ("user cancelled MCP tool call") is
1086
+ // NOT currently matched by `classifyOutputError` — so the output
1087
+ // branch silently classifies as non-error, and we don't double-count
1088
+ // today by accident. To make this robust against future Codex output
1089
+ // text changes (e.g. adding "[Request interrupted by user]" wrapper),
1090
+ // we mark the call_id in `mcpErrorRecordedCallIds` here, and
1091
+ // `processToolCallOutput` skips its error increment if the cid is
1092
+ // in the set. (Output bytes are still attributed normally.)
1093
+ const p = ln.payload;
1094
+ // Defensive: Codex Rust `Result::Err` is typed as `string` today; if a
1095
+ // future Codex version emits a struct variant, fall through gracefully.
1096
+ const errMsg = typeof p.result?.Err === "string"
1097
+ ? p.result.Err
1098
+ : undefined;
1099
+ const cid = p.call_id ?? "";
1100
+ if (errMsg !== undefined && cid.length > 0) {
1101
+ const toolName = acc.callIdToToolName[cid];
1102
+ // Symmetric guard with `processToolCallOutput` — both branches
1103
+ // check `mcpErrorRecordedCallIds.has(cid)` to avoid double-
1104
+ // counting the SAME call_id across reverse wire orderings.
1105
+ // Earlier this guard was tool-NAME-scoped (`tools[toolName].errors > 0`),
1106
+ // which silently dropped every legitimate subsequent MCP error
1107
+ // with a different call_id on the same tool name (e.g., two
1108
+ // separate `bdt_navigation_go-to` cancellations in one session
1109
+ // → only the first one counted). Wire impact: under-counted
1110
+ // `errors.tool_errors_total`, `tools[*].errors`, and
1111
+ // `mcp_servers[*].errors` for any session with multi-call MCP
1112
+ // errors. Call-id-scoped guard fixes the under-count while
1113
+ // preserving reverse-order double-counting protection.
1114
+ const alreadyIncremented = acc.mcpErrorRecordedCallIds.has(cid);
1115
+ if (toolName !== undefined && !alreadyIncremented) {
1116
+ acc.toolErrors += 1;
1117
+ // Categorize as `user_rejected` for cancellation, `other` for
1118
+ // remaining MCP-side errors (transport, server crash, etc).
1119
+ const cat = /cancel/i.test(errMsg) ? "user_rejected" : "other";
1120
+ acc.toolErrorCategories[cat] = (acc.toolErrorCategories[cat] ?? 0) + 1;
1121
+ if (acc.tools[toolName]) {
1122
+ acc.tools[toolName].errors += 1;
1123
+ }
1124
+ // Mirror to per-server bucket — same pattern as processToolCallOutput.
1125
+ const mcpServer = (0, util_1.extractCodexMcpServer)(toolName)
1126
+ ?? (typeof p.invocation?.server === "string" ? p.invocation.server : null);
1127
+ if (mcpServer !== null && acc.mcpServers[mcpServer]) {
1128
+ acc.mcpServers[mcpServer].errors += 1;
1129
+ }
1130
+ // Mark the cid so the later function_call_output's error
1131
+ // classifier doesn't double-count.
1132
+ acc.mcpErrorRecordedCallIds.add(cid);
1133
+ }
1134
+ }
1135
+ return;
1136
+ }
1137
+ if ((0, types_2.isEventMsg)(ln, "patch_apply_end")) {
1138
+ // Authoritative apply_patch outcome — pairs with the originating
1139
+ // custom_tool_call via call_id. On success, commit the eagerly-
1140
+ // parsed PatchFileEntry[] into code_changes. On failure, drop the
1141
+ // entries (they would otherwise inflate lines_added/removed for a
1142
+ // patch that never landed on disk).
1143
+ const p = ln.payload;
1144
+ const cid = p.call_id ?? "";
1145
+ if (cid.length > 0) {
1146
+ const pending = acc.pendingPatchEntries[cid];
1147
+ if (pending !== undefined) {
1148
+ if (p.success) {
1149
+ commitPatchEntries(acc, pending);
1150
+ }
1151
+ // On failure: drop. delete() runs either way so end-of-walker
1152
+ // doesn't double-commit.
1153
+ delete acc.pendingPatchEntries[cid];
1154
+ }
1155
+ }
1156
+ return;
1157
+ }
1158
+ // ── response_item variants ──────────────────────────────────────────
1159
+ if ((0, types_2.isResponseItem)(ln, "function_call")) {
1160
+ const p = ln.payload;
1161
+ // Pass `namespace` (live wire field on Codex 0.135+) so processToolCall
1162
+ // can authoritatively route `spawn_agent` only when it's truly
1163
+ // `multi_agent_v1` (vs a future third-party MCP server's same-named tool).
1164
+ processToolCall(acc, p.name ?? "", p.arguments ?? "", p.call_id, /* applyPatchBodyOverride */ null, p.namespace);
1165
+ return;
1166
+ }
1167
+ if ((0, types_2.isResponseItem)(ln, "custom_tool_call")) {
1168
+ const p = ln.payload;
1169
+ // Live-verified on Codex 0.135.x: apply_patch is the only emitter of
1170
+ // `custom_tool_call`. `input` carries the RAW patch text (no JSON wrapper).
1171
+ // Pass it as the `applyPatchBodyOverride` so the helper skips the JSON
1172
+ // parse path that `function_call` uses.
1173
+ processToolCall(acc, p.name ?? "", p.input ?? "", p.call_id, p.input ?? null);
1174
+ return;
1175
+ }
1176
+ if ((0, types_2.isResponseItem)(ln, "function_call_output")) {
1177
+ const p = ln.payload;
1178
+ processToolCallOutput(acc, p.call_id, p.output);
1179
+ return;
1180
+ }
1181
+ if ((0, types_2.isResponseItem)(ln, "custom_tool_call_output")) {
1182
+ const p = ln.payload;
1183
+ processToolCallOutput(acc, p.call_id, p.output);
1184
+ return;
1185
+ }
1186
+ // Codex web search — emitted as `response_item.web_search_call`, NOT as a
1187
+ // `function_call name="web_search"`. Live-verified: 80+ web_search_call
1188
+ // events in a single session, 0 function_call(web_search). Without this
1189
+ // branch every Codex web search ships `uses_web_search: false` and
1190
+ // `tools.WebSearch` map missing — silent data loss. Each web_search_call
1191
+ // is a discrete event (no paired output line; results land in the next
1192
+ // assistant message); input_size = JSON of the action payload (query
1193
+ // string + queries[] when present). Key is `WebSearch` to align with the
1194
+ // Claude-vocab canonicalization in processToolCall — without this the
1195
+ // direct response_item branch would split `web_search` (here) vs
1196
+ // `WebSearch` (function_call path) across two map entries on sessions
1197
+ // that mix both surfaces.
1198
+ if ((0, types_2.isResponseItem)(ln, "web_search_call")) {
1199
+ const p = ln.payload;
1200
+ const inputBytes = utf8Bytes(JSON.stringify(p.action ?? {}));
1201
+ acc.toolCallCount += 1;
1202
+ bumpToolInputSize(acc.tools, "WebSearch", inputBytes);
1203
+ acc.usesWebSearch = true;
1204
+ // Set the shared classifier's search bucket so classifyTurn sees
1205
+ // research/exploration signals correctly.
1206
+ acc.toolBuckets.search = true;
1207
+ return;
1208
+ }
1209
+ // Skill detection — Codex injects a synthetic role=user message
1210
+ // wrapping the SKILL.md body when the agent invokes a skill. The
1211
+ // wire signal is `<skill>\n<name>NAME</name>...` — see
1212
+ // `extractCodexSkillName`. Counted into `acc.skills` with input_size
1213
+ // = the full injected text bytes (the skill prompt entering context).
1214
+ // No output_size — skills don't have a return value the way tools do.
1215
+ if ((0, types_2.isResponseItem)(ln, "message")) {
1216
+ const p = ln.payload;
1217
+ if (p.role === "user") {
1218
+ const skillName = extractCodexSkillName(p.content);
1219
+ if (skillName !== null) {
1220
+ // Bytes = full content[0].text length (the SKILL.md body
1221
+ // entering context, the meaningful cost-attributable signal).
1222
+ let bytes = 0;
1223
+ if (Array.isArray(p.content) && p.content.length > 0) {
1224
+ const c0 = p.content[0];
1225
+ if (c0 !== null && typeof c0 === "object") {
1226
+ const t = c0.text;
1227
+ if (typeof t === "string") {
1228
+ bytes = utf8Bytes(t);
1229
+ }
1230
+ }
1231
+ }
1232
+ bumpToolInputSize(acc.skills, skillName, bytes);
1233
+ acc.usesSkill = true;
1234
+ acc.toolBuckets.skill = true;
1235
+ }
1236
+ }
1237
+ return;
1238
+ }
1239
+ }
1240
+ // ─────────────────────────────────────────────────────────────────────────
1241
+ // Build wire event
1242
+ // ─────────────────────────────────────────────────────────────────────────
1243
+ function buildSessionAnalytics(acc, sessionId, projectName, userEmail, endReason, isFinal) {
1244
+ // Detect "no real session data" — happens on empty rollouts. We fall back
1245
+ // to the epoch ISO so downstream Date parsing succeeds, but the hour
1246
+ // derivation must be aware that this is synthetic (otherwise
1247
+ // `localHour(epoch_iso)` returns the user's TZ offset modulo 24 — e.g. 2
1248
+ // in UTC+2, 0 in UTC, breaking cross-machine reproducibility for empty
1249
+ // sessions).
1250
+ const hasRealActivity = acc.sessionMetaTs !== null || acc.firstSeenTs !== null;
1251
+ const firstSeen = acc.sessionMetaTs ?? acc.firstSeenTs ?? new Date(0).toISOString();
1252
+ const lastSeen = acc.lastSeenTs ?? firstSeen;
1253
+ const startMs = new Date(firstSeen).getTime();
1254
+ const endMs = new Date(lastSeen).getTime();
1255
+ const durationMin = Number.isFinite(startMs) && Number.isFinite(endMs)
1256
+ ? Math.max(0, Math.round((endMs - startMs) / 60000))
1257
+ : 0;
1258
+ // Session-level category classification (single TurnCategory bucket
1259
+ // since there's no per-turn classification anymore; backend can derive
1260
+ // a "primary topic" from this).
1261
+ const category = (0, classifier_1.classifyTurn)({
1262
+ user_msg_keywords: acc.kw,
1263
+ tool_buckets: acc.toolBuckets,
1264
+ bash_cmd_flags: acc.bashFlags,
1265
+ });
1266
+ // Defensive end-of-walker fold for apply_patches that had no paired
1267
+ // patch_apply_end signal (legacy Codex rollouts pre-0.131, mid-write
1268
+ // truncation, or any failure to emit the outcome event). Commit any
1269
+ // still-pending entries so we don't silently drop code_changes for
1270
+ // valid edits. patch_apply_end / output-text-error already cleared
1271
+ // the entries for known-failed patches.
1272
+ for (const cid of Object.keys(acc.pendingPatchEntries)) {
1273
+ commitPatchEntries(acc, acc.pendingPatchEntries[cid]);
1274
+ }
1275
+ acc.pendingPatchEntries = {};
1276
+ // Close the LAST in-flight turn before reading aggregates — its
1277
+ // currentTurnHadEdit/Retry flags would otherwise stay un-folded.
1278
+ closeCurrentTurnIfAny(acc);
1279
+ // Claude-parity COUNTS (not 0/1 booleans). For a 5-turn session where
1280
+ // 3 turns had retries and 2 were one-shot edits, this emits
1281
+ // `with_retry: 3, one_shot: 2`. Defensive zero-turn fallback: empty
1282
+ // rollout → both 0.
1283
+ const oneShot = acc.oneShotTurns;
1284
+ const withRetry = acc.turnsWithRetry;
1285
+ const category_breakdown = {};
1286
+ if (acc.turnCount > 0 || acc.assistantMsgCount > 0) {
1287
+ category_breakdown[category] = {
1288
+ turns: Math.max(acc.turnCount, 1),
1289
+ // Claude-parity COUNT (was 0/1). For a session where 3 turns
1290
+ // produced edits and 2 didn't, this emits `turns_with_edit: 3`.
1291
+ turns_with_edit: acc.turnsWithEdit,
1292
+ turns_with_retry: withRetry,
1293
+ total_retries: acc.retries,
1294
+ cost_usd: acc.costUsd,
1295
+ };
1296
+ }
1297
+ // hot_files: top-5 by change_count desc; mirror Claude's tiebreaker
1298
+ // (`topKHotFiles` in merge.ts) so equal-count files have a deterministic
1299
+ // path-alphabetical order — important for diff-against-prior-snapshot.
1300
+ const sortedHot = Object.entries(acc.fileChangeCounts)
1301
+ .sort((a, b) => {
1302
+ if (b[1] !== a[1]) {
1303
+ return b[1] - a[1];
1304
+ }
1305
+ return a[0].localeCompare(b[0]);
1306
+ })
1307
+ .slice(0, 5)
1308
+ .map(([path, change_count]) => ({ path, change_count }));
1309
+ // idle_minutes: sum of gap-before-user_message intervals, rounded to
1310
+ // minutes (Claude-parity — same gap-before-human-user attribution).
1311
+ // active_minutes = duration - idle, clamped >= 0 (defensive against
1312
+ // rounding edges).
1313
+ const idleMin = Math.round(acc.idleMs / 60000);
1314
+ const activeMin = Math.max(0, durationMin - idleMin);
1315
+ const time = {
1316
+ duration_minutes: durationMin,
1317
+ active_minutes: activeMin,
1318
+ idle_minutes: idleMin,
1319
+ // Deterministic 0 when no real activity (firstSeen is the epoch
1320
+ // fallback). Otherwise local-TZ hour. Keeps empty-session output
1321
+ // identical across machines regardless of timezone.
1322
+ // `?? 0` preserves the deterministic-0 fallback for malformed
1323
+ // first/last timestamps (matches the no-activity branch).
1324
+ start_hour: hasRealActivity ? (localHour(firstSeen) ?? 0) : 0,
1325
+ last_activity_hour: hasRealActivity ? (localHour(lastSeen) ?? 0) : 0,
1326
+ };
1327
+ const turns = {
1328
+ user_count: acc.userMsgCount,
1329
+ assistant_count: acc.assistantMsgCount,
1330
+ with_retry: withRetry,
1331
+ one_shot: oneShot,
1332
+ };
1333
+ const usage = {
1334
+ input_tokens: acc.inputTokens,
1335
+ output_tokens: acc.outputTokens,
1336
+ cache_creation_tokens: 0,
1337
+ cache_read_tokens: acc.cacheReadTokens,
1338
+ cost_usd: acc.costUsd,
1339
+ };
1340
+ const userMessages = {
1341
+ count: acc.userMsgCount,
1342
+ size: acc.userMsgBytes,
1343
+ // Raw float — same Claude-parity rationale as bumpToolInputSize.
1344
+ approximated_tokens: acc.userMsgBytes / tokens_1.BYTES_PER_TOKEN,
1345
+ };
1346
+ const codeChanges = {
1347
+ files_modified: Object.keys(acc.fileChangeCounts).length,
1348
+ lines_added: acc.linesAdded,
1349
+ lines_removed: acc.linesRemoved,
1350
+ hot_files: sortedHot,
1351
+ languages: { ...acc.languages },
1352
+ };
1353
+ const errors = {
1354
+ tool_errors_total: acc.toolErrors,
1355
+ tool_error_categories: { ...acc.toolErrorCategories },
1356
+ user_interruptions: acc.interruptionCount,
1357
+ };
1358
+ const userActivity = {
1359
+ response_time_buckets: { ...acc.responseTimeBuckets },
1360
+ messages_by_hour: acc.userMsgsByHour,
1361
+ messages_by_date: acc.userMsgsByDate,
1362
+ messages_by_weekday: acc.userMsgsByWeekday,
1363
+ };
1364
+ // Bucket sum/count → sum/count/avg (avg recomputed here so it stays in
1365
+ // sync with any future merge step that re-aggregates sum/count).
1366
+ const bucketsWithAvg = {};
1367
+ for (const [k, v] of Object.entries(acc.contextTokensBuckets)) {
1368
+ bucketsWithAvg[k] = {
1369
+ sum: v.sum,
1370
+ count: v.count,
1371
+ avg: v.count > 0 ? v.sum / v.count : 0,
1372
+ };
1373
+ }
1374
+ // Filter the defensive `[orphan]` sentinel out of the wire `tools` map
1375
+ // when its `count` AND `errors` are both 0 — `processToolCallOutput`
1376
+ // creates the entry with count=0 when output arrives for an unknown
1377
+ // call_id. If the orphan entry has accumulated errors, we MUST keep it
1378
+ // on the wire — otherwise `Σ wire.tools[*].errors < errors.tool_errors_total`
1379
+ // (orphan errors would be invisible per-tool but counted in the
1380
+ // session total, breaking the invariant). Non-zero count would similarly
1381
+ // signal real activity worth surfacing.
1382
+ const wireTools = {};
1383
+ for (const [k, v] of Object.entries(acc.tools)) {
1384
+ if (k === ORPHAN_OUTPUT_BUCKET && v.count === 0 && v.errors === 0) {
1385
+ continue;
1386
+ }
1387
+ wireTools[k] = v;
1388
+ }
1389
+ const contextTokens = {
1390
+ latest: acc.contextLatest,
1391
+ peak: acc.contextPeak,
1392
+ buckets: bucketsWithAvg,
1393
+ };
1394
+ const processErrors = {
1395
+ has: Object.keys(acc.processErrors).length > 0,
1396
+ items: { ...acc.processErrors },
1397
+ };
1398
+ const toolMeta = {
1399
+ bash_subcommands: Object.keys(acc.bashSubcommands).length > 0 ? { ...acc.bashSubcommands } : undefined,
1400
+ uses_sub_agent: acc.usesSubAgent,
1401
+ uses_skill: acc.usesSkill,
1402
+ uses_mcp: acc.usesMcp,
1403
+ uses_web_search: acc.usesWebSearch,
1404
+ uses_web_fetch: acc.usesWebFetch,
1405
+ };
1406
+ const sessionType = deriveSessionType(category_breakdown, durationMin, acc.userMsgCount, Object.keys(acc.fileChangeCounts).length, sortedHot);
1407
+ return {
1408
+ session_id: sessionId,
1409
+ project_name: projectName,
1410
+ user_email: userEmail,
1411
+ schema_version: types_1.SCHEMA_VERSION,
1412
+ transcript_source: "codex",
1413
+ is_final: isFinal,
1414
+ snapshot_at: new Date().toISOString(),
1415
+ offset: 0,
1416
+ end_reason: endReason,
1417
+ start_time: firstSeen,
1418
+ last_activity_time: lastSeen,
1419
+ time,
1420
+ turns,
1421
+ classification: { category_breakdown, session_type: sessionType },
1422
+ usage,
1423
+ models: acc.models,
1424
+ user_messages: userMessages,
1425
+ tools: wireTools,
1426
+ mcp_servers: acc.mcpServers,
1427
+ skills: acc.skills,
1428
+ sub_agents: acc.subAgents,
1429
+ bash_binaries: acc.bashBinaries,
1430
+ tool_meta: toolMeta,
1431
+ code_changes: codeChanges,
1432
+ errors,
1433
+ user_activity: userActivity,
1434
+ context_tokens: contextTokens,
1435
+ process_errors: processErrors,
1436
+ };
1437
+ }
1438
+ /**
1439
+ * Heuristic mirrored from Claude's `deriveSessionType` (`analytics/claude/merge.ts`)
1440
+ * so the two clients produce the same labels for equivalent shapes. Order matters —
1441
+ * earlier branches take precedence.
1442
+ */
1443
+ function deriveSessionType(categories, durationMin, userCount, filesModified, hotFiles) {
1444
+ const totalTurns = Object.values(categories).reduce((s, c) => s + c.turns, 0);
1445
+ const editTurns = Object.values(categories).reduce((s, c) => s + c.turns_with_edit, 0);
1446
+ const explorationTurns = (categories["exploration"]?.turns ?? 0)
1447
+ + (categories["brainstorming"]?.turns ?? 0);
1448
+ // 1. Quick question
1449
+ if (durationMin < 5 && userCount <= 2 && filesModified === 0) {
1450
+ return "quick_question";
1451
+ }
1452
+ // 2. Exploration-dominant
1453
+ if (totalTurns >= 2 && editTurns === 0) {
1454
+ return "exploration";
1455
+ }
1456
+ if (totalTurns >= 3 && explorationTurns / totalTurns >= 0.6) {
1457
+ return "exploration";
1458
+ }
1459
+ // 3. Iterative refinement — hot_files concentration
1460
+ if (hotFiles.length > 0) {
1461
+ const top = hotFiles[0];
1462
+ const totalChanges = hotFiles.reduce((s, f) => s + f.change_count, 0);
1463
+ if (top.change_count >= 5 && totalChanges > 0 && top.change_count / totalChanges >= 0.5) {
1464
+ return "iterative_refinement";
1465
+ }
1466
+ }
1467
+ // 4. Multi-task
1468
+ if (userCount >= 3) {
1469
+ return "multi_task";
1470
+ }
1471
+ // 5. Single task — has edit
1472
+ if (editTurns > 0) {
1473
+ return "single_task";
1474
+ }
1475
+ return "general";
1476
+ }
1477
+ //# sourceMappingURL=projection.js.map